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}