confique/
file.rs

1use std::{ffi::OsStr, fs, io, path::PathBuf};
2
3use crate::{error::ErrorInner, Error, Partial};
4
5
6/// A file as source for configuration.
7///
8/// By default, the file is considered optional, meaning that on [`File::load`],
9/// if the file does not exist, [`Partial::empty()`][crate::Partial::empty] is
10/// returned.
11pub struct File {
12    path: PathBuf,
13    format: FileFormat,
14    required: bool,
15}
16
17impl File {
18    /// Configuration file with the given path. The format is inferred from the
19    /// file extension. If the path does not have an extension or it is
20    /// unknown, an error is returned.
21    pub fn new(path: impl Into<PathBuf>) -> Result<Self, Error> {
22        let path = path.into();
23        let ext = path
24            .extension()
25            .ok_or_else(|| ErrorInner::MissingFileExtension { path: path.clone() })?;
26        let format = FileFormat::from_extension(ext)
27            .ok_or_else(|| ErrorInner::UnsupportedFileFormat { path: path.clone() })?;
28
29        Ok(Self::with_format(path, format))
30    }
31
32    /// Config file with specified file format.
33    pub fn with_format(path: impl Into<PathBuf>, format: FileFormat) -> Self {
34        Self {
35            path: path.into(),
36            format,
37            required: false,
38        }
39    }
40
41    /// Marks this file as required, meaning that [`File::load`] will return an
42    /// error if the file does not exist. Otherwise, an empty layer (all values
43    /// are `None`) is returned.
44    pub fn required(mut self) -> Self {
45        self.required = true;
46        self
47    }
48
49    /// Attempts to load the file into the partial configuration `P`.
50    pub fn load<P: Partial>(&self) -> Result<P, Error> {
51        // Load file contents. If the file does not exist and was not marked as
52        // required, we just return an empty layer.
53        let file_content = match fs::read(&self.path) {
54            Ok(v) => v,
55            Err(e) if e.kind() == io::ErrorKind::NotFound => {
56                if self.required {
57                    return Err(ErrorInner::MissingRequiredFile { path: self.path.clone() }.into());
58                } else {
59                    return Ok(P::empty());
60                }
61            }
62            Err(e) => {
63                return Err(ErrorInner::Io {
64                    path: Some(self.path.clone()),
65                    err: e,
66                }.into());
67            }
68        };
69
70        // Helper closure to create an error.
71        let error = |err| {
72            Error::from(ErrorInner::Deserialization {
73                err,
74                source: Some(format!("file '{}'", self.path.display())),
75            })
76        };
77
78        match self.format {
79            #[cfg(feature = "toml")]
80            FileFormat::Toml => {
81                let s = std::str::from_utf8(&file_content).map_err(|e| error(Box::new(e)))?;
82                toml::from_str(s).map_err(|e| error(Box::new(e)))
83            }
84
85            #[cfg(feature = "yaml")]
86            FileFormat::Yaml => serde_yaml::from_slice(&file_content)
87                .map_err(|e| error(Box::new(e))),
88
89            #[cfg(feature = "json5")]
90            FileFormat::Json5 => {
91                let s = std::str::from_utf8(&file_content).map_err(|e| error(Box::new(e)))?;
92                json5::from_str(s).map_err(|e| error(Box::new(e)))
93            }
94        }
95    }
96}
97
98/// All file formats supported by confique.
99///
100/// All enum variants are `#[cfg]` guarded with the respective crate feature.
101pub enum FileFormat {
102    #[cfg(feature = "toml")]
103    Toml,
104    #[cfg(feature = "yaml")]
105    Yaml,
106    #[cfg(feature = "json5")]
107    Json5,
108}
109
110impl FileFormat {
111    /// Guesses the file format from a file extension, returning `None` if the
112    /// extension is unknown or if the respective crate feature is not enabled.
113    pub fn from_extension(ext: impl AsRef<OsStr>) -> Option<Self> {
114        match ext.as_ref().to_str()? {
115            #[cfg(feature = "toml")]
116            "toml" => Some(Self::Toml),
117
118            #[cfg(feature = "yaml")]
119            "yaml" | "yml" => Some(Self::Yaml),
120
121            #[cfg(feature = "json5")]
122            "json5" | "json" => Some(Self::Json5),
123
124            _ => None,
125        }
126    }
127}