config/file/source/
file.rs

1use std::env;
2use std::error::Error;
3use std::fs;
4use std::io;
5use std::path::PathBuf;
6
7use crate::file::{source::FileSourceResult, FileFormat, FileSource, FileStoredFormat, Format};
8
9/// Describes a file sourced from a file
10#[derive(Clone, Debug)]
11pub struct FileSourceFile {
12    /// Path of configuration file
13    name: PathBuf,
14}
15
16impl FileSourceFile {
17    pub fn new(name: PathBuf) -> Self {
18        Self { name }
19    }
20
21    fn find_file<F>(
22        &self,
23        format_hint: Option<F>,
24    ) -> Result<(PathBuf, Box<dyn Format>), Box<dyn Error + Send + Sync>>
25    where
26        F: FileStoredFormat + Format + 'static,
27    {
28        let path = if self.name.is_absolute() {
29            self.name.clone()
30        } else {
31            env::current_dir()?.as_path().join(&self.name)
32        };
33
34        // First check for an _exact_ match
35        if path.is_file() {
36            if let Some(format) = format_hint {
37                return Ok((path, Box::new(format)));
38            } else {
39                let ext = path.extension().unwrap_or_default().to_string_lossy();
40                for format in FileFormat::all() {
41                    if format.extensions().contains(&ext.as_ref()) {
42                        return Ok((path, Box::new(*format)));
43                    }
44                }
45                return Err(Box::new(io::Error::new(
46                    io::ErrorKind::NotFound,
47                    format!(
48                        "configuration file \"{}\" is not of a supported file format",
49                        path.to_string_lossy()
50                    ),
51                )));
52            };
53        }
54
55        let mut path = path;
56        // Preserve any extension-like text within the provided file stem by appending a fake extension
57        // which will be replaced by `set_extension()` calls (e.g.  `file.local.placeholder` => `file.local.json`)
58        if path.extension().is_some() {
59            path.as_mut_os_string().push(".placeholder");
60        }
61        match format_hint {
62            Some(format) => {
63                for ext in format.file_extensions() {
64                    path.set_extension(ext);
65
66                    if path.is_file() {
67                        return Ok((path, Box::new(format)));
68                    }
69                }
70            }
71            None => {
72                for format in FileFormat::all() {
73                    for ext in format.extensions() {
74                        path.set_extension(ext);
75
76                        if path.is_file() {
77                            return Ok((path, Box::new(*format)));
78                        }
79                    }
80                }
81            }
82        }
83        Err(Box::new(io::Error::new(
84            io::ErrorKind::NotFound,
85            format!(
86                "configuration file \"{}\" not found",
87                self.name.to_string_lossy()
88            ),
89        )))
90    }
91}
92
93impl<F> FileSource<F> for FileSourceFile
94where
95    F: Format + FileStoredFormat + 'static,
96{
97    fn resolve(
98        &self,
99        format_hint: Option<F>,
100    ) -> Result<FileSourceResult, Box<dyn Error + Send + Sync>> {
101        // Find file
102        let (filename, format) = self.find_file(format_hint)?;
103
104        // Attempt to use a relative path for the URI
105        let uri = env::current_dir()
106            .ok()
107            .and_then(|base| pathdiff::diff_paths(&filename, base))
108            .unwrap_or_else(|| filename.clone());
109
110        // Read contents from file
111        let buf = fs::read(filename)?;
112
113        // If it exists, skip the UTF-8 BOM byte sequence: EF BB BF
114        let buf = if buf.len() >= 3 && &buf[0..3] == b"\xef\xbb\xbf" {
115            &buf[3..]
116        } else {
117            &buf
118        };
119
120        let c = String::from_utf8_lossy(buf);
121        let text = c.into_owned();
122
123        Ok(FileSourceResult {
124            uri: Some(uri.to_string_lossy().into_owned()),
125            content: text,
126            format,
127        })
128    }
129}