storm_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
43            .contains(&filename.extension().unwrap_or_default().to_string_lossy().as_ref())
44          {
45            return Ok((filename, Box::new(*format)));
46          }
47        }
48
49        Err(Box::new(io::Error::new(
50          io::ErrorKind::NotFound,
51          format!(
52            "configuration file \"{}\" is not of a registered file format",
53            filename.to_string_lossy()
54          ),
55        )))
56      };
57    }
58    // Adding a dummy extension will make sure we will not override secondary extensions, i.e. "file.local"
59    // This will make the following set_extension function calls to append the extension.
60    let mut filename = add_dummy_extension(filename);
61
62    match format_hint {
63      Some(format) => {
64        for ext in format.file_extensions() {
65          filename.set_extension(ext);
66
67          if filename.is_file() {
68            return Ok((filename, Box::new(format)));
69          }
70        }
71      }
72
73      None => {
74        for format in ALL_EXTENSIONS.keys() {
75          for ext in format.extensions() {
76            filename.set_extension(ext);
77
78            if filename.is_file() {
79              return Ok((filename, Box::new(*format)));
80            }
81          }
82        }
83      }
84    }
85
86    Err(Box::new(io::Error::new(
87      io::ErrorKind::NotFound,
88      format!("configuration file \"{}\" not found", self.name.to_string_lossy()),
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 text = fs::read_to_string(filename)?;
112
113    Ok(FileSourceResult { uri: Some(uri.to_string_lossy().into_owned()), content: text, format })
114  }
115}
116
117fn add_dummy_extension(mut filename: PathBuf) -> PathBuf {
118  match filename.extension() {
119    Some(extension) => {
120      let mut ext = extension.to_os_string();
121      ext.push(".");
122      ext.push("dummy");
123      filename.set_extension(ext);
124    }
125    None => {
126      filename.set_extension("dummy");
127    }
128  }
129  filename
130}