Skip to main content

configulator/
file.rs

1use crate::error::ConfigulatorError;
2use crate::options::FileOptions;
3use crate::value_map::{ConfigValue, ValueMap};
4
5/// Trait for parsing file contents into configuration values.
6///
7/// Implement this for any configuration format you want to support
8/// (YAML, TOML, JSON, etc.).
9///
10/// For formats supported by [serde](https://serde.rs), use [`serde_loader`]
11/// instead of implementing this trait manually.
12pub trait FileLoader: Send + Sync {
13    /// Parse the raw file contents into configuration values.
14    fn load(&self, contents: &str) -> Result<ValueMap, ConfigulatorError>;
15}
16
17/// A [`FileLoader`] backed by any serde-compatible deserializer.
18///
19/// Created via [`serde_loader`]. Accepts a closure that deserializes
20/// a `&str` into a [`ConfigValue`].
21pub struct SerdeLoader<F>(F);
22
23impl<F, E> FileLoader for SerdeLoader<F>
24where
25    F: Fn(&str) -> Result<ConfigValue, E> + Send + Sync,
26    E: std::fmt::Display,
27{
28    fn load(&self, contents: &str) -> Result<ValueMap, ConfigulatorError> {
29        let value = (self.0)(contents)
30            .map_err(|e| ConfigulatorError::FileError(e.to_string()))?;
31        match value {
32            ConfigValue::Nested(map) => Ok(map),
33            ConfigValue::Scalar(_) => Err(ConfigulatorError::FileError(
34                "config file root must be a mapping/table, got a scalar value".into(),
35            )),
36            ConfigValue::List(_) => Err(ConfigulatorError::FileError(
37                "config file root must be a mapping/table, got a list".into(),
38            )),
39        }
40    }
41}
42
43/// Create a [`FileLoader`] from any serde-compatible deserializer.
44///
45/// This is the easiest way to support a file format. Pass a closure
46/// that calls the format crate's `from_str` function:
47///
48/// ```rust,no_run
49/// use configulator::{serde_loader, FileOptions};
50///
51/// let opts = FileOptions {
52///     paths: vec!["config.yaml".into()],
53///     error_if_not_found: false,
54///     // YAML
55///     loader: serde_loader(|s| serde_yaml_ng::from_str(s)),
56/// };
57/// ```
58///
59/// Works with any format: `serde_json::from_str`, `toml::from_str`, etc.
60pub fn serde_loader<F, E>(f: F) -> Box<dyn FileLoader>
61where
62    F: Fn(&str) -> Result<ConfigValue, E> + Send + Sync + 'static,
63    E: std::fmt::Display + 'static,
64{
65    Box::new(SerdeLoader(f))
66}
67
68/// Load configuration from the first file found in the given paths,
69/// using the loader specified in [`FileOptions`].
70pub fn load_from_file(opts: &FileOptions) -> Result<ValueMap, ConfigulatorError> {
71    let mut contents = None;
72    for path in &opts.paths {
73        match std::fs::read_to_string(path) {
74            Ok(data) => {
75                contents = Some(data);
76                break;
77            }
78            Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
79            Err(e) => {
80                return Err(ConfigulatorError::FileError(format!(
81                    "{}: {e}",
82                    path.display()
83                )));
84            }
85        }
86    }
87
88    let contents = match contents {
89        Some(c) => c,
90        None => {
91            if opts.error_if_not_found {
92                return Err(ConfigulatorError::FileNotFound);
93            }
94            return Ok(ValueMap::new());
95        }
96    };
97
98    opts.loader.load(&contents)
99}