module_util/file/
file.rs

1use std::collections::HashSet;
2use std::fmt;
3use std::fs;
4use std::path::{Path, PathBuf};
5
6use module::{Context, Error, Merge};
7use serde::de::DeserializeOwned;
8
9use super::{Format, Module};
10
11/// An evaluator for files.
12///
13/// This evaluator reads modules from files of a specific format. It uses
14/// [`Module`] as the top-level format of the module and [`serde`] to parse the
15/// contents of the file.
16///
17/// * [`File`] is capable of detecting import-cycles between modules.
18///
19/// * Import paths are resolved relative to the path of the importer module.
20///
21/// # Example
22///
23/// ```rust,no_run
24/// # use module_util::file::File;
25/// use module::Merge;
26/// use serde::Deserialize;
27///
28/// #[derive(Deserialize, Merge)]
29/// struct Config {
30///     key: String,
31///     items: Vec<i32>,
32/// }
33///
34/// let mut file = File::json();
35///
36/// // `config.json`:
37/// // --------------
38/// // {
39/// //   "key": "424242",
40/// //   "items": [1]
41/// // }
42/// assert!(file.read("config.json").is_ok());
43///
44/// // `config-extra.json`:
45/// // --------------------
46/// // {
47/// //   "items": [3, 6, 0]
48/// // }
49/// assert!(file.read("config-extra.json").is_ok());
50///
51/// let config: Config = file.finish().unwrap();
52/// assert_eq!(config.key, "424242");
53/// assert_eq!(config.items, &[1, 3, 6, 0]);
54/// ```
55#[derive(Debug)]
56pub struct File<T, F> {
57    evaluated: HashSet<PathBuf>,
58    value: Option<T>,
59    format: F,
60}
61
62impl<T, F> File<T, F> {
63    /// Create a new [`File`] that reads files according to `format`.
64    pub fn new(format: F) -> Self {
65        Self {
66            evaluated: HashSet::new(),
67            value: None,
68            format,
69        }
70    }
71
72    /// Get a reference to the [`Format`] used.
73    pub fn format(&self) -> &F {
74        &self.format
75    }
76
77    /// Get a mutable reference to the [`Format`] used.
78    pub fn format_mut(&mut self) -> &mut F {
79        &mut self.format
80    }
81
82    /// Finish the evaluation and return the final value.
83    ///
84    /// Returns [`None`] if no file has been [`read()`] successfully. Otherwise,
85    /// it returns [`Some(value)`].
86    ///
87    /// # Example
88    ///
89    /// ```rust,no_run
90    /// # type File = module_util::file::File<i32, module_util::file::Json>;
91    /// let mut file = File::json();
92    /// assert_eq!(file.finish(), None);
93    ///
94    /// let mut file = File::json();
95    /// assert!(file.read("non_existent.json").is_err());
96    /// assert_eq!(file.finish(), None);
97    ///
98    /// let mut file = File::json();
99    /// assert!(file.read("exists.json").is_ok());
100    /// assert!(matches!(file.finish(), Some(_)));
101    /// ```
102    ///
103    /// [`read()`]: File::read
104    /// [`Some(value)`]: Some
105    pub fn finish(self) -> Option<T> {
106        self.value
107    }
108}
109
110impl<T, F> File<T, F>
111where
112    T: Merge + DeserializeOwned,
113    F: Format,
114{
115    /// Read the module at `path`.
116    ///
117    /// See the [type-level docs](File) for more information
118    pub fn read<P>(&mut self, path: P) -> Result<(), Error>
119    where
120        P: AsRef<Path>,
121    {
122        let path = path.as_ref();
123        let path = fs::canonicalize(path).map_err(Error::custom)?;
124        self._read(&path).with_module(|| DisplayPath(path))
125    }
126
127    fn _read(&mut self, path: &Path) -> Result<(), Error> {
128        if self.evaluated.contains(path) {
129            return Err(Error::cycle());
130        }
131
132        let Module { imports, value } = self.format.read(path)?;
133
134        match self.value {
135            Some(ref mut x) => x.merge_ref(value)?,
136            None => self.value = Some(value),
137        }
138
139        let basename = path
140            .parent()
141            .expect("file path should always have an ancestor")
142            .to_path_buf();
143
144        self.evaluated.insert(path.to_path_buf());
145
146        imports
147            .0
148            .into_iter()
149            .map(|x| basename.join(x))
150            .try_for_each(|p| self.read(p))
151    }
152}
153
154/// Read the module at `path` with `format`.
155///
156/// See: [`File`]
157#[expect(clippy::missing_panics_doc)]
158pub fn read<T, F>(path: impl AsRef<Path>, format: F) -> Result<T, Error>
159where
160    T: Merge + DeserializeOwned,
161    F: Format,
162{
163    let mut file = File::new(format);
164    file.read(path)?;
165
166    // SAFETY: `file` must have read at least one module. If it hadn't, the
167    //         above statement should have returned with an error.
168    let value = file
169        .finish()
170        .expect("File should have read at least one module");
171
172    Ok(value)
173}
174
175impl<T, F> Default for File<T, F>
176where
177    F: Default,
178{
179    fn default() -> Self {
180        Self::new(F::default())
181    }
182}
183
184struct DisplayPath(PathBuf);
185
186impl fmt::Display for DisplayPath {
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        self.0.display().fmt(f)
189    }
190}