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::{
8    format::all_extensions, source::FileSourceResult, FileSource, FileStoredFormat, Format,
9};
10
11/// Describes a file sourced from a file
12#[derive(Clone, Debug)]
13pub struct FileSourceFile {
14    /// Path of configuration file
15    name: PathBuf,
16}
17
18impl FileSourceFile {
19    pub fn new(name: PathBuf) -> Self {
20        Self { name }
21    }
22
23    fn find_file<F>(
24        &self,
25        format_hint: Option<F>,
26    ) -> Result<(PathBuf, Box<dyn Format>), Box<dyn Error + Send + Sync>>
27    where
28        F: FileStoredFormat + Format + 'static,
29    {
30        let filename = if self.name.is_absolute() {
31            self.name.clone()
32        } else {
33            env::current_dir()?.as_path().join(&self.name)
34        };
35
36        // First check for an _exact_ match
37        if filename.is_file() {
38            return if let Some(format) = format_hint {
39                Ok((filename, Box::new(format)))
40            } else {
41                for (format, extensions) in all_extensions().iter() {
42                    if extensions.contains(
43                        &filename
44                            .extension()
45                            .unwrap_or_default()
46                            .to_string_lossy()
47                            .as_ref(),
48                    ) {
49                        return Ok((filename, Box::new(*format)));
50                    }
51                }
52
53                Err(Box::new(io::Error::new(
54                    io::ErrorKind::NotFound,
55                    format!(
56                        "configuration file \"{}\" is not of a registered file format",
57                        filename.to_string_lossy()
58                    ),
59                )))
60            };
61        }
62        // Adding a dummy extension will make sure we will not override secondary extensions, i.e. "file.local"
63        // This will make the following set_extension function calls to append the extension.
64        let mut filename = add_dummy_extension(filename);
65
66        match format_hint {
67            Some(format) => {
68                for ext in format.file_extensions() {
69                    filename.set_extension(ext);
70
71                    if filename.is_file() {
72                        return Ok((filename, Box::new(format)));
73                    }
74                }
75            }
76
77            None => {
78                for format in all_extensions().keys() {
79                    for ext in format.extensions() {
80                        filename.set_extension(ext);
81
82                        if filename.is_file() {
83                            return Ok((filename, Box::new(*format)));
84                        }
85                    }
86                }
87            }
88        }
89
90        Err(Box::new(io::Error::new(
91            io::ErrorKind::NotFound,
92            format!(
93                "configuration file \"{}\" not found",
94                self.name.to_string_lossy()
95            ),
96        )))
97    }
98}
99
100impl<F> FileSource<F> for FileSourceFile
101where
102    F: Format + FileStoredFormat + 'static,
103{
104    fn resolve(
105        &self,
106        format_hint: Option<F>,
107    ) -> Result<FileSourceResult, Box<dyn Error + Send + Sync>> {
108        // Find file
109        let (filename, format) = self.find_file(format_hint)?;
110
111        // Attempt to use a relative path for the URI
112        let uri = env::current_dir()
113            .ok()
114            .and_then(|base| pathdiff::diff_paths(&filename, base))
115            .unwrap_or_else(|| filename.clone());
116
117        // Read contents from file
118        let text = fs::read_to_string(filename)?;
119
120        Ok(FileSourceResult {
121            uri: Some(uri.to_string_lossy().into_owned()),
122            content: text,
123            format,
124        })
125    }
126}
127
128fn add_dummy_extension(mut filename: PathBuf) -> PathBuf {
129    match filename.extension() {
130        Some(extension) => {
131            let mut ext = extension.to_os_string();
132            ext.push(".");
133            ext.push("dummy");
134            filename.set_extension(ext);
135        }
136        None => {
137            filename.set_extension("dummy");
138        }
139    }
140    filename
141}