1use std::collections::HashMap;
8use std::fs;
9use std::path::{Path, PathBuf};
10use serde_json;
11use toml;
12
13use super::ConfigProvider;
14use super::ConfigError;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum FileFormat {
19 Json,
21 Toml,
23 Yaml,
25}
26
27impl FileFormat {
28 pub fn from_extension(path: &Path) -> Option<Self> {
30 path.extension().and_then(|ext| {
31 let ext_str = ext.to_string_lossy().to_lowercase();
32 match ext_str.as_str() {
33 "json" => Some(FileFormat::Json),
34 "toml" => Some(FileFormat::Toml),
35 "yaml" | "yml" => Some(FileFormat::Yaml),
36 _ => None,
37 }
38 })
39 }
40}
41
42#[derive(Debug)]
44pub struct FileConfigProvider {
45 #[allow(dead_code)]
46 path: PathBuf,
47 #[allow(dead_code)]
48 format: FileFormat,
49 data: HashMap<String, serde_json::Value>,
50}
51
52impl FileConfigProvider {
53 pub fn new(path: &str) -> Result<Self, ConfigError> {
55 let path_buf = PathBuf::from(path);
56 let format = FileFormat::from_extension(&path_buf)
57 .ok_or_else(|| ConfigError::provider_error("file", "unsupported file format"))?;
58
59 let data = Self::read_file(&path_buf, format)?;
60
61 Ok(Self {
62 path: path_buf,
63 format,
64 data,
65 })
66 }
67
68 fn read_file(path: &Path, format: FileFormat) -> Result<HashMap<String, serde_json::Value>, ConfigError> {
70 let content = fs::read_to_string(path)
71 .map_err(|e| ConfigError::provider_error("file", format!("failed to read file: {}", e)))?;
72
73 match format {
74 FileFormat::Json => {
75 serde_json::from_str(&content)
76 .map_err(|e| ConfigError::provider_error("file", format!("invalid JSON: {}", e)))
77 },
78 FileFormat::Toml => {
79 let toml_value: toml::Value = toml::from_str(&content)
80 .map_err(|e| ConfigError::provider_error("file", format!("invalid TOML: {}", e)))?;
81
82 let json_value = serde_json::to_value(toml_value)
84 .map_err(|e| ConfigError::provider_error("file", format!("failed to convert TOML: {}", e)))?;
85
86 match json_value {
87 serde_json::Value::Object(map) => {
88 Ok(map.into_iter().collect())
90 },
91 _ => Err(ConfigError::provider_error("file", "root configuration must be an object")),
92 }
93 },
94 FileFormat::Yaml => {
95 {
96 let yaml_value: serde_yaml::Value = serde_yaml::from_str(&content)
97 .map_err(|e| ConfigError::provider_error("file", format!("invalid YAML: {}", e)))?;
98
99 let json_value = serde_json::to_value(yaml_value)
100 .map_err(|e| ConfigError::provider_error("file", format!("failed to convert YAML: {}", e)))?;
101
102 match json_value {
103 serde_json::Value::Object(map) => {
104 Ok(map.into_iter().collect())
105 },
106 _ => Err(ConfigError::provider_error("file", "root configuration must be an object")),
107 }
108 }
109 },
110 }
111 }
112
113 fn get_nested_value(&self, key_path: &str) -> Option<&serde_json::Value> {
115 let parts: Vec<&str> = key_path.split('.').collect();
116
117 let mut current = self.data.get(parts[0])?;
118
119 for part in parts.iter().skip(1) {
120 current = current.get(part)?;
121 }
122
123 Some(current)
124 }
125}
126
127impl ConfigProvider for FileConfigProvider {
128 fn has(&self, key: &str) -> bool {
129 self.get_nested_value(key).is_some()
130 }
131
132 fn provider_name(&self) -> &str {
133 "file"
134 }
135
136 fn get_raw(&self, key: &str) -> Result<Option<serde_json::Value>, ConfigError> {
137 match self.get_nested_value(key) {
138 Some(value) => Ok(Some(value.clone())),
139 None => Ok(None),
140 }
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use std::fs::File;
148 use std::io::Write;
149 use tempfile::tempdir;
150 use crate::config::ConfigProviderExt;
151
152 #[test]
153 fn test_file_format_detection() {
154 assert_eq!(FileFormat::from_extension(Path::new("config.json")), Some(FileFormat::Json));
155 assert_eq!(FileFormat::from_extension(Path::new("config.toml")), Some(FileFormat::Toml));
156 assert_eq!(FileFormat::from_extension(Path::new("config.yaml")), Some(FileFormat::Yaml));
157 assert_eq!(FileFormat::from_extension(Path::new("config.yml")), Some(FileFormat::Yaml));
158 assert_eq!(FileFormat::from_extension(Path::new("config.txt")), None);
159 }
160
161 #[test]
162 fn test_json_config() {
163 let dir = tempdir().unwrap();
164 let file_path = dir.path().join("config.json");
165
166 let content = r#"{
167 "server": {
168 "host": "127.0.0.1",
169 "port": 8080
170 },
171 "timeout": 30
172 }"#;
173
174 let mut file = File::create(&file_path).unwrap();
175 file.write_all(content.as_bytes()).unwrap();
176
177 let provider = FileConfigProvider::new(file_path.to_str().unwrap()).unwrap();
178
179 assert_eq!(provider.has("server.host"), true);
180 assert_eq!(provider.has("server.nonexistent"), false);
181
182 let host: String = provider.get("server.host").unwrap().unwrap();
183 assert_eq!(host, "127.0.0.1");
184
185 let port: u16 = provider.get("server.port").unwrap().unwrap();
186 assert_eq!(port, 8080);
187
188 let timeout: u32 = provider.get("timeout").unwrap().unwrap();
189 assert_eq!(timeout, 30);
190 }
191
192 #[test]
193 fn test_toml_config() {
194 let dir = tempdir().unwrap();
195 let file_path = dir.path().join("config.toml");
196
197 let content = r#"
198 [server]
199 host = "127.0.0.1"
200 port = 8080
201
202 timeout = 30
203 "#;
204
205 let mut file = File::create(&file_path).unwrap();
206 file.write_all(content.as_bytes()).unwrap();
207
208 let provider = FileConfigProvider::new(file_path.to_str().unwrap()).unwrap();
209
210 assert_eq!(provider.has("server.host"), true);
211 let host: String = provider.get("server.host").unwrap().unwrap();
212 assert_eq!(host, "127.0.0.1");
213
214 let port: u16 = provider.get("server.port").unwrap().unwrap();
215 assert_eq!(port, 8080);
216 }
217}