ronf/
file.rs

1//! File handling
2
3use crate::value::{Map, Value};
4
5/// Supported file formats.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum FileFormat {
8    Ini,
9    Json,
10    Yaml,
11    Toml,
12    Ron,
13}
14
15impl FileFormat {
16    /// Get the file format from the file extension string.
17    pub fn from_extension(extension: &str) -> Option<Self> {
18        match extension {
19            "ini" => Some(FileFormat::Ini),
20            "json" => Some(FileFormat::Json),
21            "yaml" => Some(FileFormat::Yaml),
22            "toml" => Some(FileFormat::Toml),
23            "ron" => Some(FileFormat::Ron),
24            _ => None,
25        }
26    }
27}
28
29impl std::fmt::Display for FileFormat {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        match self {
32            FileFormat::Ini => write!(f, "ini"),
33            FileFormat::Json => write!(f, "json"),
34            FileFormat::Yaml => write!(f, "yaml"),
35            FileFormat::Toml => write!(f, "toml"),
36            FileFormat::Ron => write!(f, "ron"),
37        }
38    }
39}
40
41/// Representation of a configuration file.
42#[derive(Debug, Clone)]
43pub struct File {
44    pub path: String,
45    pub format: FileFormat,
46    pub content: String,
47}
48
49impl File {
50    /// Create a new file with the given path, format, and content.
51    pub fn new(path: String, format: FileFormat, content: String) -> Self {
52        File {
53            path,
54            format,
55            content,
56        }
57    }
58
59    /// Create a new file with the given path, format, and content as a &str.
60    pub fn new_str(path: &str, format: FileFormat, content: &str) -> Self {
61        File {
62            path: path.to_string(),
63            format,
64            content: content.to_string(),
65        }
66    }
67
68    /// Create a new file from a path, reading the content from the file.
69    #[cfg(feature = "read_file")]
70    pub fn from_path(path: String) -> Result<Self, String> {
71        let extension = path
72            .rsplit_once('.')
73            .and_then(|(_, ext)| if ext.is_empty() { None } else { Some(ext) })
74            .ok_or_else(|| format!("Failed to get file extension from {}", path))?;
75        let format = FileFormat::from_extension(extension)
76            .ok_or_else(|| format!("Unsupported file extension: {}", extension))?;
77
78        let content = std::fs::read_to_string(&path)
79            .map_err(|e| format!("Failed to read file {}: {}", path, e))?;
80
81        Ok(File::new(path.clone(), format, content))
82    }
83
84    /// Create a new file from a path and format, reading the content from the file.
85    #[cfg(feature = "read_file")]
86    pub fn from_path_format(path: String, format: FileFormat) -> Result<Self, String> {
87        let content = std::fs::read_to_string(&path)
88            .map_err(|e| format!("Failed to read file {}: {}", path, e))?;
89
90        Ok(File::new(path.clone(), format, content))
91    }
92
93    /// Parse the content of the file to be used in the Config.
94    pub fn parse(&self) -> Result<Map<String, Value>, String> {
95        match self.format {
96            FileFormat::Ini => {
97                #[cfg(feature = "ini")]
98                {
99                    crate::format::ini::deserialize(self.content.clone())
100                }
101
102                #[cfg(not(feature = "ini"))]
103                Err("INI format feature is not enabled".to_string())
104            }
105            FileFormat::Json => {
106                #[cfg(feature = "json")]
107                {
108                    crate::format::json::deserialize(self.content.clone())
109                }
110
111                #[cfg(not(feature = "json"))]
112                Err("JSON format feature is not enabled".to_string())
113            }
114            FileFormat::Yaml => {
115                #[cfg(feature = "yaml")]
116                {
117                    crate::format::yaml::deserialize(self.content.clone())
118                }
119
120                #[cfg(not(feature = "yaml"))]
121                Err("YAML format feature is not enabled".to_string())
122            }
123            FileFormat::Toml => {
124                #[cfg(feature = "toml")]
125                {
126                    crate::format::toml::deserialize(self.content.clone())
127                }
128
129                #[cfg(not(feature = "toml"))]
130                Err("TOML format feature is not enabled".to_string())
131            }
132            FileFormat::Ron => {
133                #[cfg(feature = "ron")]
134                {
135                    crate::format::ron::deserialize(self.content.clone())
136                }
137
138                #[cfg(not(feature = "ron"))]
139                Err("RON format feature is not enabled".to_string())
140            }
141        }
142    }
143}
144
145#[cfg(test)]
146mod test {
147    use super::*;
148
149    #[test]
150    fn test_file_new() {
151        let path = "test.json".to_string();
152        let format = FileFormat::Json;
153        let content = r#"{"key": "value"}"#.to_string();
154        let file = File::new(path.clone(), format.clone(), content.clone());
155        assert_eq!(file.path, path);
156        assert_eq!(file.format, format);
157        assert_eq!(file.content, content);
158    }
159
160    #[test]
161    fn test_file_new_str() {
162        let path = "test.json";
163        let format = FileFormat::Json;
164        let content = r#"{"key": "value"}"#;
165        let file = File::new_str(path, format.clone(), content);
166        assert_eq!(file.path, path);
167        assert_eq!(file.format, format);
168        assert_eq!(file.content, content);
169    }
170
171    #[test]
172    fn test_file_format_from_extension() {
173        assert_eq!(FileFormat::from_extension("ini"), Some(FileFormat::Ini));
174        assert_eq!(FileFormat::from_extension("json"), Some(FileFormat::Json));
175        assert_eq!(FileFormat::from_extension("yaml"), Some(FileFormat::Yaml));
176        assert_eq!(FileFormat::from_extension("toml"), Some(FileFormat::Toml));
177        assert_eq!(FileFormat::from_extension("ron"), Some(FileFormat::Ron));
178        assert_eq!(FileFormat::from_extension("txt"), None);
179    }
180
181    #[test]
182    fn test_file_format_display() {
183        assert_eq!(format!("{}", FileFormat::Ini), "ini");
184        assert_eq!(format!("{}", FileFormat::Json), "json");
185        assert_eq!(format!("{}", FileFormat::Yaml), "yaml");
186        assert_eq!(format!("{}", FileFormat::Toml), "toml");
187        assert_eq!(format!("{}", FileFormat::Ron), "ron");
188    }
189
190    #[test]
191    #[cfg(feature = "read_file")]
192    fn test_file_from_path() {
193        let path = "test.json".to_string();
194        let format = FileFormat::Json;
195        let content = r#"{"key": "value"}"#.to_string();
196        std::fs::write(&path, &content).unwrap();
197        let file = File::from_path(path.clone()).unwrap();
198        assert_eq!(file.path, path);
199        assert_eq!(file.format, format);
200        assert_eq!(file.content, content);
201        std::fs::remove_file(path.clone()).unwrap();
202
203        let file = File::from_path("test.json".to_string());
204        assert!(file.is_err());
205
206        let file = File::from_path("test_yaml.".to_string());
207        assert!(file.is_err());
208
209        let file = File::from_path("test.txt".to_string());
210        assert!(file.is_err());
211    }
212
213    #[test]
214    #[cfg(feature = "read_file")]
215    fn test_file_from_path_format() {
216        let path = "test2.json".to_string();
217        let format = FileFormat::Json;
218        let content = r#"{"key": "value"}"#.to_string();
219        std::fs::write(&path, &content).unwrap();
220        let file = File::from_path_format(path.clone(), format.clone()).unwrap();
221        assert_eq!(file.path, path);
222        assert_eq!(file.format, format);
223        assert_eq!(file.content, content);
224        std::fs::remove_file(path.clone()).unwrap();
225
226        let file = File::from_path_format(path.clone(), FileFormat::Yaml);
227        assert!(file.is_err());
228    }
229
230    mod formats {
231        use super::*;
232
233        #[test]
234        #[cfg(feature = "ini")]
235        fn test_parse_ini() {
236            let path = "test.ini".to_string();
237            let format = FileFormat::Ini;
238            let content = r#"[section]
239key: value"#
240                .to_string();
241            let file = File::new(path.clone(), format.clone(), content.clone());
242            let result = file.parse();
243            assert!(result.is_ok());
244        }
245
246        #[test]
247        #[cfg(not(feature = "ini"))]
248        fn test_parse_ini_fail() {
249            let path = "test.ini".to_string();
250            let format = FileFormat::Ini;
251            let content = r#"[section]
252key: value"#
253                .to_string();
254            let file = File::new(path.clone(), format.clone(), content.clone());
255            let result = file.parse();
256            assert!(result.is_err());
257        }
258
259        #[test]
260        #[cfg(feature = "json")]
261        fn test_parse_json() {
262            let path = "test.json".to_string();
263            let format = FileFormat::Json;
264            let content = r#"{"key": "value"}"#.to_string();
265            let file = File::new(path.clone(), format.clone(), content.clone());
266            let result = file.parse();
267            assert!(result.is_ok());
268        }
269
270        #[test]
271        #[cfg(not(feature = "json"))]
272        fn test_parse_json_fail() {
273            let path = "test.json".to_string();
274            let format = FileFormat::Json;
275            let content = r#"{"key": "value"}"#.to_string();
276            let file = File::new(path.clone(), format.clone(), content.clone());
277            let result = file.parse();
278            assert!(result.is_err());
279        }
280
281        #[test]
282        #[cfg(feature = "yaml")]
283        fn test_parse_yaml() {
284            let path = "test.yaml".to_string();
285            let format = FileFormat::Yaml;
286            let content = r#"key: value"#.to_string();
287            let file = File::new(path.clone(), format.clone(), content.clone());
288            let result = file.parse();
289            assert!(result.is_ok());
290        }
291
292        #[test]
293        #[cfg(not(feature = "yaml"))]
294        fn test_parse_yaml_fail() {
295            let path = "test.yaml".to_string();
296            let format = FileFormat::Yaml;
297            let content = r#"key: value"#.to_string();
298            let file = File::new(path.clone(), format.clone(), content.clone());
299            let result = file.parse();
300            assert!(result.is_err());
301        }
302
303        #[test]
304        #[cfg(feature = "toml")]
305        fn test_parse_toml() {
306            let path = "test.toml".to_string();
307            let format = FileFormat::Toml;
308            let content = r#"key = "value""#.to_string();
309            let file = File::new(path.clone(), format.clone(), content.clone());
310            let result = file.parse();
311            assert!(result.is_ok());
312        }
313
314        #[test]
315        #[cfg(not(feature = "toml"))]
316        fn test_parse_toml_fail() {
317            let path = "test.toml".to_string();
318            let format = FileFormat::Toml;
319            let content = r#"key = "value""#.to_string();
320            let file = File::new(path.clone(), format.clone(), content.clone());
321            let result = file.parse();
322            assert!(result.is_err());
323        }
324
325        #[test]
326        #[cfg(feature = "ron")]
327        fn test_parse_ron() {
328            let path = "test.ron".to_string();
329            let format = FileFormat::Ron;
330            let content = r#"(key: "value")"#.to_string();
331            let file = File::new(path.clone(), format.clone(), content.clone());
332            let result = file.parse();
333            assert!(result.is_ok());
334        }
335
336        #[test]
337        #[cfg(not(feature = "ron"))]
338        fn test_parse_ron_fail() {
339            let path = "test.ron".to_string();
340            let format = FileFormat::Ron;
341            let content = r#"(key: "value")"#.to_string();
342            let file = File::new(path.clone(), format.clone(), content.clone());
343            let result = file.parse();
344            assert!(result.is_err());
345        }
346    }
347}