Skip to main content

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::{ConfigError, Mutex, err::ConfigLock};
9
10use super::{
11    ConfigSource, ConfigSourceAdaptor, ConfigSourceParser,
12    memory::{ConfigSourceBuilder, HashSource},
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).map_err(ConfigError::from_cause)?;
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        ConfigError, Configuration,
114        source::{ConfigSource, ConfigSourceAdaptor, ConfigSourceBuilder, ConfigSourceParser},
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).map_err(ConfigError::from_cause)?;
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).map_err(ConfigError::from_cause)?;
147        Ok(())
148    }
149
150    fn update_file(f: &mut File) -> Result<(), ConfigError> {
151        let last = f
152            .metadata()
153            .map_err(ConfigError::from_cause)?
154            .modified()
155            .map_err(ConfigError::from_cause)?;
156        let mut i = 0;
157        while last
158            == f.metadata()
159                .map_err(ConfigError::from_cause)?
160                .modified()
161                .map_err(ConfigError::from_cause)?
162        {
163            i += 1;
164            println!("Round: {}", i);
165            f.write_all(b"hello").map_err(ConfigError::from_cause)?;
166            f.flush().map_err(ConfigError::from_cause)?;
167            std::thread::sleep(std::time::Duration::new(0, 1000000));
168        }
169        Ok(())
170    }
171
172    #[test]
173    fn refresh_test() -> Result<(), ConfigError> {
174        let path: PathBuf = "target/file.tmp".into();
175        let mut f = File::create(&path).map_err(ConfigError::from_cause)?;
176        let mut config = Configuration::new().register_source(<FileLoader<Temp>>::new(
177            path.clone(),
178            false,
179            true,
180        ))?;
181        assert!(!config.refresh()?);
182        update_file(&mut f)?;
183        assert!(config.refresh()?);
184        std::fs::remove_file(path).map_err(ConfigError::from_cause)?;
185        Ok(())
186    }
187
188    #[test]
189    fn inline_source_config_success() {
190        // 使用 Temp 解析器,内容无所谓
191        let result = super::inline_source_config::<Temp>("inline".to_string(), "abc");
192        assert!(result.is_ok());
193        let hs = result.unwrap();
194        assert_eq!(hs.name(), "inline");
195    }
196
197    #[test]
198    fn inline_source_config_parse_error() {
199        struct Bad;
200        impl ConfigSourceAdaptor for Bad {
201            fn convert_source(self, _: &mut ConfigSourceBuilder<'_>) -> Result<(), ConfigError> {
202                Ok(())
203            }
204        }
205        impl ConfigSourceParser for Bad {
206            type Adaptor = Bad;
207            fn parse_source(_: &str) -> Result<Self::Adaptor, ConfigError> {
208                Err(ConfigError::ConfigParseError(
209                    "bad".to_string(),
210                    "fail".to_string(),
211                ))
212            }
213            fn file_extensions() -> Vec<&'static str> {
214                vec!["bad"]
215            }
216        }
217        let result = super::inline_source_config::<Bad>("bad".to_string(), "abc");
218        assert!(matches!(result, Err(ConfigError::ConfigParseError(_, _))));
219    }
220
221    #[test]
222    fn file_loader_load_required_not_exists() {
223        // 测试 required=true 且文件不存在时返回 ConfigFileNotExists
224        let path: PathBuf = "target/not_exist_file.tmp".into();
225        let loader = <FileLoader<Temp>>::new(path.clone(), true, true);
226        let mut hash_source = crate::source::memory::HashSource::new("test");
227        let mut builder = hash_source.prefixed();
228        let result = loader.load(&mut builder);
229        assert!(matches!(result, Err(ConfigError::ConfigFileNotExists(p)) if p == path));
230    }
231
232    #[test]
233    fn file_loader_load_ext_false_all_exts() {
234        // 测试 ext=false 时会尝试所有扩展名
235        let path: PathBuf = "target/file_multi_ext".into();
236        // 创建一个带 .tmp 扩展名的文件
237        let mut file_path = path.clone();
238        file_path.set_extension("tmp");
239        let mut f = File::create(&file_path).unwrap();
240        f.write_all(b"abc").unwrap();
241        f.flush().unwrap();
242
243        let mut hash_source = crate::source::memory::HashSource::new("test");
244        let mut builder = hash_source.prefixed();
245        // 创建 loader 实例
246        let loader = <FileLoader<Temp>>::new(path.clone(), true, false);
247        // 应该能加载成功(flag 变为 false,不报错)
248        let result = loader.load(&mut builder);
249        assert!(result.is_ok());
250        assert!(result.is_ok());
251
252        std::fs::remove_file(file_path).unwrap();
253    }
254}