cfg_rs/source/
file.rs

1//! File config source.
2use std::{
3    marker::PhantomData,
4    path::{Path, PathBuf},
5    time::SystemTime,
6};
7
8use crate::{err::ConfigLock, ConfigError, Mutex};
9
10use super::{
11    memory::{ConfigSourceBuilder, HashSource},
12    ConfigSource, ConfigSourceAdaptor, ConfigSourceParser,
13};
14
15/// FileLoader
16#[derive(Debug)]
17pub(crate) struct FileLoader<L: ConfigSourceParser> {
18    name: String,
19    path: PathBuf,
20    ext: bool,
21    required: bool,
22    modified: Mutex<Option<SystemTime>>,
23    _data: PhantomData<L>,
24}
25
26fn modified_time(path: &Path) -> Option<SystemTime> {
27    path.metadata().and_then(|a| a.modified()).ok()
28}
29
30impl<L: ConfigSourceParser> FileLoader<L> {
31    #[allow(dead_code)]
32    pub(crate) fn new(path: PathBuf, required: bool, ext: bool) -> Self {
33        Self {
34            name: format!(
35                "file:{}.[{}]",
36                path.display(),
37                L::file_extensions().join(",")
38            ),
39            modified: Mutex::new(modified_time(&path)),
40            path,
41            ext,
42            required,
43            _data: PhantomData,
44        }
45    }
46}
47
48fn load_path<L: ConfigSourceParser>(
49    path: PathBuf,
50    flag: &mut bool,
51    builder: &mut ConfigSourceBuilder<'_>,
52) -> Result<(), ConfigError> {
53    if path.exists() {
54        *flag = false;
55        let c = std::fs::read_to_string(&path)?;
56        L::parse_source(&c)?.convert_source(builder)?;
57    }
58    Ok(())
59}
60
61impl<L: ConfigSourceParser> ConfigSource for FileLoader<L> {
62    fn name(&self) -> &str {
63        &self.name
64    }
65
66    fn load(&self, builder: &mut ConfigSourceBuilder<'_>) -> Result<(), ConfigError> {
67        let mut flag = self.required;
68        if self.ext {
69            load_path::<L>(self.path.clone(), &mut flag, builder)?;
70        } else {
71            for ext in L::file_extensions() {
72                let mut path = self.path.clone();
73                path.set_extension(ext);
74                load_path::<L>(path, &mut flag, builder)?;
75            }
76        }
77        if flag {
78            return Err(ConfigError::ConfigFileNotExists(self.path.clone()));
79        }
80        Ok(())
81    }
82
83    fn allow_refresh(&self) -> bool {
84        true
85    }
86
87    fn refreshable(&self) -> Result<bool, ConfigError> {
88        let time = modified_time(&self.path);
89        let mut g = self.modified.lock_c()?;
90        let flag = time == *g;
91        *g = time;
92        Ok(!flag)
93    }
94}
95
96#[doc(hidden)]
97pub fn inline_source_config<S: ConfigSourceParser>(
98    name: String,
99    content: &'static str,
100) -> Result<HashSource, ConfigError> {
101    let v = S::parse_source(content)?;
102    let mut m = HashSource::new(name);
103    v.convert_source(&mut m.prefixed())?;
104    Ok(m)
105}
106
107#[cfg_attr(coverage_nightly, coverage(off))]
108#[cfg(test)]
109mod test {
110    use std::{fs::File, io::Write, path::PathBuf};
111
112    use crate::{
113        source::{ConfigSource, ConfigSourceAdaptor, ConfigSourceBuilder, ConfigSourceParser},
114        ConfigError, Configuration,
115    };
116
117    use super::FileLoader;
118
119    struct Temp;
120
121    impl ConfigSourceAdaptor for Temp {
122        fn convert_source(self, _: &mut ConfigSourceBuilder<'_>) -> Result<(), ConfigError> {
123            Ok(())
124        }
125    }
126    impl ConfigSourceParser for Temp {
127        type Adaptor = Temp;
128
129        fn parse_source(_: &str) -> Result<Self::Adaptor, ConfigError> {
130            Ok(Temp)
131        }
132
133        fn file_extensions() -> Vec<&'static str> {
134            vec!["tmp"]
135        }
136    }
137
138    #[test]
139    fn refresh_file_test() -> Result<(), ConfigError> {
140        let path: PathBuf = "target/file_2.tmp".into();
141        let mut f = File::create(&path)?;
142        let config = <FileLoader<Temp>>::new(path.clone(), false, true);
143        assert!(!config.refreshable()?);
144        update_file(&mut f)?;
145        assert!(config.refreshable()?);
146        std::fs::remove_file(path)?;
147        Ok(())
148    }
149
150    fn update_file(f: &mut File) -> Result<(), ConfigError> {
151        let last = f.metadata()?.modified()?;
152        let mut i = 0;
153        while last == f.metadata()?.modified()? {
154            i += 1;
155            println!("Round: {}", i);
156            f.write_all(b"hello")?;
157            f.flush()?;
158            std::thread::sleep(std::time::Duration::new(0, 1000000));
159        }
160        Ok(())
161    }
162
163    #[test]
164    fn refresh_test() -> Result<(), ConfigError> {
165        let path: PathBuf = "target/file.tmp".into();
166        let mut f = File::create(&path)?;
167        let mut config = Configuration::new().register_source(<FileLoader<Temp>>::new(
168            path.clone(),
169            false,
170            true,
171        ))?;
172        assert!(!config.refresh()?);
173        update_file(&mut f)?;
174        assert!(config.refresh()?);
175        std::fs::remove_file(path)?;
176        Ok(())
177    }
178
179    #[test]
180    fn inline_source_config_success() {
181        // 使用 Temp 解析器,内容无所谓
182        let result = super::inline_source_config::<Temp>("inline".to_string(), "abc");
183        assert!(result.is_ok());
184        let hs = result.unwrap();
185        assert_eq!(hs.name(), "inline");
186    }
187
188    #[test]
189    fn inline_source_config_parse_error() {
190        struct Bad;
191        impl ConfigSourceAdaptor for Bad {
192            fn convert_source(self, _: &mut ConfigSourceBuilder<'_>) -> Result<(), ConfigError> {
193                Ok(())
194            }
195        }
196        impl ConfigSourceParser for Bad {
197            type Adaptor = Bad;
198            fn parse_source(_: &str) -> Result<Self::Adaptor, ConfigError> {
199                Err(ConfigError::ConfigParseError(
200                    "bad".to_string(),
201                    "fail".to_string(),
202                ))
203            }
204            fn file_extensions() -> Vec<&'static str> {
205                vec!["bad"]
206            }
207        }
208        let result = super::inline_source_config::<Bad>("bad".to_string(), "abc");
209        assert!(matches!(result, Err(ConfigError::ConfigParseError(_, _))));
210    }
211
212    #[test]
213    fn file_loader_load_required_not_exists() {
214        // 测试 required=true 且文件不存在时返回 ConfigFileNotExists
215        let path: PathBuf = "target/not_exist_file.tmp".into();
216        let loader = <FileLoader<Temp>>::new(path.clone(), true, true);
217        let mut hash_source = crate::source::memory::HashSource::new("test");
218        let mut builder = hash_source.prefixed();
219        let result = loader.load(&mut builder);
220        assert!(matches!(result, Err(ConfigError::ConfigFileNotExists(p)) if p == path));
221    }
222
223    #[test]
224    fn file_loader_load_ext_false_all_exts() {
225        // 测试 ext=false 时会尝试所有扩展名
226        let path: PathBuf = "target/file_multi_ext".into();
227        // 创建一个带 .tmp 扩展名的文件
228        let mut file_path = path.clone();
229        file_path.set_extension("tmp");
230        let mut f = File::create(&file_path).unwrap();
231        f.write_all(b"abc").unwrap();
232        f.flush().unwrap();
233
234        let mut hash_source = crate::source::memory::HashSource::new("test");
235        let mut builder = hash_source.prefixed();
236        // 创建 loader 实例
237        let loader = <FileLoader<Temp>>::new(path.clone(), true, false);
238        // 应该能加载成功(flag 变为 false,不报错)
239        let result = loader.load(&mut builder);
240        assert!(result.is_ok());
241        assert!(result.is_ok());
242
243        std::fs::remove_file(file_path).unwrap();
244    }
245}