module_util/file/
file.rs

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