1use crate::Format;
4use crate::error::Result;
5use cfgmatic_merge::{Merge, MergeBehavior, MergeOptions};
6use cfgmatic_paths::ConfigTier;
7use serde::de::DeserializeOwned;
8use std::fs;
9use std::path::PathBuf;
10
11#[derive(Debug, Clone)]
13pub struct ConfigFile {
14 pub path: PathBuf,
16
17 pub tier: ConfigTier,
19
20 pub format: Format,
22
23 pub content: Option<String>,
25}
26
27impl ConfigFile {
28 pub(crate) fn new(path: PathBuf, tier: ConfigTier) -> Option<Self> {
30 let format = Format::from_path(&path)?;
31 Some(Self {
32 path,
33 tier,
34 format,
35 content: None,
36 })
37 }
38
39 #[must_use]
41 pub fn name(&self) -> Option<&str> {
42 self.path.file_name()?.to_str()
43 }
44
45 pub fn read(&mut self) -> Result<&str> {
55 if let Some(ref content) = self.content {
56 return Ok(content);
57 }
58 let content = fs::read_to_string(&self.path)?;
59 self.content = Some(content);
60 Ok(self.content.as_ref().unwrap()) }
62
63 pub fn parse<T: DeserializeOwned>(&mut self) -> Result<T> {
69 if self.content.is_none() {
70 let content = fs::read_to_string(&self.path)?;
71 self.content = Some(content);
72 }
73 let content = self.content.as_ref().unwrap();
75 self.format.parse(content, &self.path)
76 }
77
78 pub fn parse_uncached<T: DeserializeOwned>(&self) -> Result<T> {
84 let content = fs::read_to_string(&self.path)?;
85 self.format.parse(&content, &self.path)
86 }
87
88 #[must_use]
90 pub fn exists(&self) -> bool {
91 self.path.exists()
92 }
93
94 pub fn modified(&self) -> Result<std::time::SystemTime> {
100 Ok(fs::metadata(&self.path)?.modified()?)
101 }
102}
103
104impl PartialEq for ConfigFile {
105 fn eq(&self, other: &Self) -> bool {
106 self.path == other.path
107 }
108}
109
110impl Eq for ConfigFile {}
111
112impl PartialOrd for ConfigFile {
113 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
114 Some(self.cmp(other))
115 }
116}
117
118impl Ord for ConfigFile {
119 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
120 u8::from(self.tier).cmp(&u8::from(other.tier))
122 }
123}
124
125impl std::fmt::Display for ConfigFile {
126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 write!(
128 f,
129 "{} ({:?}, {})",
130 self.path.display(),
131 self.tier,
132 self.format
133 )
134 }
135}
136
137#[derive(Debug, Clone, Default)]
139pub struct ConfigFiles {
140 files: Vec<ConfigFile>,
141}
142
143impl ConfigFiles {
144 #[must_use]
146 pub const fn new() -> Self {
147 Self { files: Vec::new() }
148 }
149
150 pub fn push(&mut self, file: ConfigFile) {
152 self.files.push(file);
153 self.sort();
154 }
155
156 fn sort(&mut self) {
158 self.files.sort_by(|a, b| b.cmp(a)); }
160
161 #[must_use]
163 pub const fn len(&self) -> usize {
164 self.files.len()
165 }
166
167 #[must_use]
169 pub const fn is_empty(&self) -> bool {
170 self.files.is_empty()
171 }
172
173 #[must_use]
175 pub fn first(&self) -> Option<&ConfigFile> {
176 self.files.first()
177 }
178
179 pub fn first_mut(&mut self) -> Option<&mut ConfigFile> {
181 self.files.first_mut()
182 }
183
184 pub fn iter(&self) -> impl Iterator<Item = &ConfigFile> {
186 self.files.iter()
187 }
188
189 pub fn merge<T>(&mut self) -> Result<T>
197 where
198 T: DeserializeOwned + Mergeable + Default,
199 {
200 let mut result = T::default();
201 for file in self.files.iter_mut().rev() {
202 let value: T = file.parse()?;
203 result = result.merge(value);
204 }
205 Ok(result)
206 }
207}
208
209impl IntoIterator for ConfigFiles {
210 type Item = ConfigFile;
211 type IntoIter = std::vec::IntoIter<ConfigFile>;
212
213 fn into_iter(self) -> Self::IntoIter {
214 self.files.into_iter()
215 }
216}
217
218impl<'a> IntoIterator for &'a ConfigFiles {
219 type Item = &'a ConfigFile;
220 type IntoIter = std::slice::Iter<'a, ConfigFile>;
221
222 fn into_iter(self) -> Self::IntoIter {
223 self.files.iter()
224 }
225}
226
227pub trait Mergeable {
232 #[must_use]
234 fn merge(self, other: Self) -> Self;
235}
236
237impl Mergeable for serde_json::Value {
238 fn merge(self, other: Self) -> Self {
239 let opts = MergeOptions::new().behavior(MergeBehavior::Deep);
240 Merge::merge(self, other.clone(), &opts).unwrap_or(other)
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249 use std::io::Write;
250 use tempfile::NamedTempFile;
251
252 #[test]
253 fn test_config_file_sorting() {
254 let file1 = ConfigFile {
255 path: PathBuf::from("/etc/config.toml"),
256 tier: ConfigTier::System,
257 format: Format::Toml,
258 content: None,
259 };
260 let file2 = ConfigFile {
261 path: PathBuf::from("~/.config.toml"),
262 tier: ConfigTier::User,
263 format: Format::Toml,
264 content: None,
265 };
266
267 assert!(file2 > file1);
269 }
270
271 #[test]
272 fn test_parse_toml() -> Result<()> {
273 #[derive(Debug, serde::Deserialize, PartialEq)]
274 struct Config {
275 timeout: u32,
276 host: String,
277 }
278
279 let mut temp = NamedTempFile::with_suffix(".toml")?;
280 write!(temp, "timeout = 30\nhost = \"localhost\"")?;
281
282 let mut file =
283 ConfigFile::new(temp.path().to_path_buf(), ConfigTier::User).expect("valid toml file");
284
285 let config: Config = file.parse()?;
286 assert_eq!(config.timeout, 30);
287 assert_eq!(config.host, "localhost");
288
289 Ok(())
290 }
291
292 #[test]
293 fn test_parse_json() -> Result<()> {
294 let mut temp = NamedTempFile::with_suffix(".json")?;
295 write!(temp, "{{\"port\": 8080, \"enabled\": true}}")?;
296
297 let mut file =
298 ConfigFile::new(temp.path().to_path_buf(), ConfigTier::User).expect("valid json file");
299
300 let value: serde_json::Value = file.parse()?;
301 assert_eq!(value["port"], 8080);
302 assert_eq!(value["enabled"], true);
303
304 Ok(())
305 }
306
307 #[test]
308 fn test_config_files_collection() {
309 let mut files = ConfigFiles::new();
310 assert!(files.is_empty());
311
312 let file = ConfigFile {
313 path: PathBuf::from("config.toml"),
314 tier: ConfigTier::User,
315 format: Format::Toml,
316 content: None,
317 };
318
319 files.push(file);
320 assert_eq!(files.len(), 1);
321 assert!(files.first().is_some());
322 }
323}