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}