ricecoder_storage/config/
loader.rs

1//! Configuration file loader supporting multiple formats
2//!
3//! This module provides loading of configuration files in YAML, TOML, and JSON formats.
4//! It automatically detects the format based on file extension.
5
6use super::Config;
7use crate::error::{StorageError, StorageResult};
8use crate::types::ConfigFormat;
9use std::path::Path;
10
11/// Configuration loader for multiple formats
12pub struct ConfigLoader;
13
14impl ConfigLoader {
15    /// Load configuration from a file
16    ///
17    /// Automatically detects format based on file extension.
18    /// Supports YAML (.yaml, .yml), TOML (.toml), and JSON (.json) formats.
19    pub fn load_from_file<P: AsRef<Path>>(path: P) -> StorageResult<Config> {
20        let path = path.as_ref();
21        let content = std::fs::read_to_string(path).map_err(|e| {
22            StorageError::io_error(path.to_path_buf(), crate::error::IoOperation::Read, e)
23        })?;
24
25        let extension = path
26            .extension()
27            .and_then(|ext| ext.to_str())
28            .ok_or_else(|| {
29                StorageError::parse_error(path.to_path_buf(), "unknown", "File has no extension")
30            })?;
31
32        let format = ConfigFormat::from_extension(extension).ok_or_else(|| {
33            StorageError::parse_error(
34                path.to_path_buf(),
35                "unknown",
36                format!("Unsupported file format: {}", extension),
37            )
38        })?;
39
40        Self::load_from_string(&content, format, path)
41    }
42
43    /// Load configuration from a string with specified format
44    pub fn load_from_string<P: AsRef<Path>>(
45        content: &str,
46        format: ConfigFormat,
47        path: P,
48    ) -> StorageResult<Config> {
49        let path = path.as_ref();
50        match format {
51            ConfigFormat::Yaml => Self::parse_yaml(content, path),
52            ConfigFormat::Toml => Self::parse_toml(content, path),
53            ConfigFormat::Json => Self::parse_json(content, path),
54        }
55    }
56
57    /// Parse YAML content
58    fn parse_yaml<P: AsRef<Path>>(content: &str, path: P) -> StorageResult<Config> {
59        let path = path.as_ref();
60        serde_yaml::from_str(content)
61            .map_err(|e| StorageError::parse_error(path.to_path_buf(), "YAML", e.to_string()))
62    }
63
64    /// Parse TOML content
65    fn parse_toml<P: AsRef<Path>>(content: &str, path: P) -> StorageResult<Config> {
66        let path = path.as_ref();
67        toml::from_str(content)
68            .map_err(|e| StorageError::parse_error(path.to_path_buf(), "TOML", e.to_string()))
69    }
70
71    /// Parse JSON content
72    fn parse_json<P: AsRef<Path>>(content: &str, path: P) -> StorageResult<Config> {
73        let path = path.as_ref();
74        serde_json::from_str(content)
75            .map_err(|e| StorageError::parse_error(path.to_path_buf(), "JSON", e.to_string()))
76    }
77
78    /// Serialize configuration to string in specified format
79    pub fn serialize(config: &Config, format: ConfigFormat) -> StorageResult<String> {
80        match format {
81            ConfigFormat::Yaml => serde_yaml::to_string(config)
82                .map_err(|e| StorageError::Internal(format!("Failed to serialize to YAML: {}", e))),
83            ConfigFormat::Toml => toml::to_string_pretty(config)
84                .map_err(|e| StorageError::Internal(format!("Failed to serialize to TOML: {}", e))),
85            ConfigFormat::Json => serde_json::to_string_pretty(config)
86                .map_err(|e| StorageError::Internal(format!("Failed to serialize to JSON: {}", e))),
87        }
88    }
89
90    /// Save configuration to a file
91    pub fn save_to_file<P: AsRef<Path>>(
92        config: &Config,
93        path: P,
94        format: ConfigFormat,
95    ) -> StorageResult<()> {
96        let path = path.as_ref();
97        let content = Self::serialize(config, format)?;
98        std::fs::write(path, content).map_err(|e| {
99            StorageError::io_error(path.to_path_buf(), crate::error::IoOperation::Write, e)
100        })
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn test_load_yaml_config() {
110        let yaml_content = r#"
111providers:
112  default_provider: openai
113  api_keys:
114    openai: test-key
115defaults:
116  model: gpt-4
117  temperature: 0.7
118steering: []
119"#;
120        let config = ConfigLoader::load_from_string(yaml_content, ConfigFormat::Yaml, "test.yaml")
121            .expect("Failed to parse YAML");
122        assert_eq!(
123            config.providers.default_provider,
124            Some("openai".to_string())
125        );
126        assert_eq!(config.defaults.model, Some("gpt-4".to_string()));
127    }
128
129    #[test]
130    fn test_load_toml_config() {
131        let toml_content = r#"[providers]
132default_provider = "openai"
133api_keys = { openai = "test-key" }
134endpoints = {}
135
136[defaults]
137model = "gpt-4"
138temperature = 0.7
139
140steering = []
141custom = {}
142"#;
143        let config = ConfigLoader::load_from_string(toml_content, ConfigFormat::Toml, "test.toml")
144            .expect("Failed to parse TOML");
145        assert_eq!(
146            config.providers.default_provider,
147            Some("openai".to_string())
148        );
149        assert_eq!(config.defaults.model, Some("gpt-4".to_string()));
150    }
151
152    #[test]
153    fn test_load_json_config() {
154        let json_content = r#"{
155  "providers": {
156    "default_provider": "openai",
157    "api_keys": {
158      "openai": "test-key"
159    },
160    "endpoints": {}
161  },
162  "defaults": {
163    "model": "gpt-4",
164    "temperature": 0.7
165  },
166  "steering": []
167}"#;
168        let config = ConfigLoader::load_from_string(json_content, ConfigFormat::Json, "test.json")
169            .expect("Failed to parse JSON");
170        assert_eq!(
171            config.providers.default_provider,
172            Some("openai".to_string())
173        );
174        assert_eq!(config.defaults.model, Some("gpt-4".to_string()));
175    }
176
177    #[test]
178    fn test_serialize_yaml() {
179        let config = Config::default();
180        let yaml = ConfigLoader::serialize(&config, ConfigFormat::Yaml)
181            .expect("Failed to serialize to YAML");
182        assert!(yaml.contains("providers:"));
183        assert!(yaml.contains("defaults:"));
184    }
185
186    #[test]
187    fn test_serialize_toml() {
188        let config = Config::default();
189        let toml = ConfigLoader::serialize(&config, ConfigFormat::Toml)
190            .expect("Failed to serialize to TOML");
191        assert!(toml.contains("providers") || toml.contains("[providers]"));
192        assert!(toml.contains("defaults") || toml.contains("[defaults]"));
193    }
194
195    #[test]
196    fn test_serialize_json() {
197        let config = Config::default();
198        let json = ConfigLoader::serialize(&config, ConfigFormat::Json)
199            .expect("Failed to serialize to JSON");
200        assert!(json.contains("\"providers\""));
201        assert!(json.contains("\"defaults\""));
202    }
203
204    #[test]
205    fn test_save_and_load_yaml() {
206        let temp_dir = tempfile::TempDir::new().expect("Failed to create temp dir");
207        let file_path = temp_dir.path().join("config.yaml");
208        let config = Config::default();
209
210        ConfigLoader::save_to_file(&config, &file_path, ConfigFormat::Yaml)
211            .expect("Failed to save config");
212
213        let loaded = ConfigLoader::load_from_file(&file_path).expect("Failed to load config");
214
215        assert_eq!(config, loaded);
216    }
217
218    #[test]
219    fn test_save_and_load_toml() {
220        let temp_dir = tempfile::TempDir::new().expect("Failed to create temp dir");
221        let file_path = temp_dir.path().join("config.toml");
222        let config = Config::default();
223
224        ConfigLoader::save_to_file(&config, &file_path, ConfigFormat::Toml)
225            .expect("Failed to save config");
226
227        let loaded = ConfigLoader::load_from_file(&file_path).expect("Failed to load config");
228
229        assert_eq!(config, loaded);
230    }
231
232    #[test]
233    fn test_save_and_load_json() {
234        let temp_dir = tempfile::TempDir::new().expect("Failed to create temp dir");
235        let file_path = temp_dir.path().join("config.json");
236        let config = Config::default();
237
238        ConfigLoader::save_to_file(&config, &file_path, ConfigFormat::Json)
239            .expect("Failed to save config");
240
241        let loaded = ConfigLoader::load_from_file(&file_path).expect("Failed to load config");
242
243        assert_eq!(config, loaded);
244    }
245}