ricecoder_completion/
config.rs1use crate::types::*;
3use ricecoder_storage::manager::PathResolver;
4use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6
7pub struct ConfigLoader;
9
10impl ConfigLoader {
11 pub fn load_from_yaml(path: &Path) -> CompletionResult<CompletionConfig> {
13 let content = std::fs::read_to_string(path)?;
14 let config: CompletionConfig = serde_yaml::from_str(&content)?;
15 Self::validate_config(&config)?;
16 Ok(config)
17 }
18
19 pub fn load_from_json(path: &Path) -> CompletionResult<CompletionConfig> {
21 let content = std::fs::read_to_string(path)?;
22 let config: CompletionConfig = serde_json::from_str(&content)?;
23 Self::validate_config(&config)?;
24 Ok(config)
25 }
26
27 pub fn load_from_string(
29 content: &str,
30 format: ConfigFormat,
31 ) -> CompletionResult<CompletionConfig> {
32 let config = match format {
33 ConfigFormat::Yaml => serde_yaml::from_str(content)?,
34 ConfigFormat::Json => serde_json::from_str(content)?,
35 };
36 Self::validate_config(&config)?;
37 Ok(config)
38 }
39
40 pub fn load_with_hierarchy(language: &str) -> CompletionResult<CompletionConfig> {
50 let project_path = PathResolver::resolve_project_path();
52 let project_completion_path = project_path.join("completion").join("languages");
53
54 if let Ok(config) = Self::load_from_directory(&project_completion_path, language) {
55 return Ok(config);
56 }
57
58 let global_path = PathResolver::resolve_global_path()?;
60 let user_completion_path = global_path.join("completion").join("languages");
61
62 if let Ok(config) = Self::load_from_directory(&user_completion_path, language) {
63 return Ok(config);
64 }
65
66 Ok(Self::default_for_language(language))
68 }
69
70 fn load_from_directory(dir: &Path, language: &str) -> CompletionResult<CompletionConfig> {
72 if !dir.is_dir() {
73 return Err(CompletionError::ConfigError(format!(
74 "Configuration directory not found: {}",
75 dir.display()
76 )));
77 }
78
79 let yaml_path = dir.join(format!("{}.yaml", language));
81 if yaml_path.exists() {
82 return Self::load_from_yaml(&yaml_path);
83 }
84
85 let yml_path = dir.join(format!("{}.yml", language));
86 if yml_path.exists() {
87 return Self::load_from_yaml(&yml_path);
88 }
89
90 let json_path = dir.join(format!("{}.json", language));
92 if json_path.exists() {
93 return Self::load_from_json(&json_path);
94 }
95
96 Err(CompletionError::ConfigError(format!(
97 "No configuration found for language: {}",
98 language
99 )))
100 }
101
102 pub fn get_completion_config_dir() -> CompletionResult<PathBuf> {
104 let global_path = PathResolver::resolve_global_path()?;
105 Ok(global_path.join("completion").join("languages"))
106 }
107
108 pub fn get_project_completion_config_dir() -> PathBuf {
110 let project_path = PathResolver::resolve_project_path();
111 project_path.join("completion").join("languages")
112 }
113
114 fn validate_config(config: &CompletionConfig) -> CompletionResult<()> {
116 if config.language.is_empty() {
117 return Err(CompletionError::ConfigError(
118 "Language name cannot be empty".to_string(),
119 ));
120 }
121
122 if config.ranking_weights.relevance < 0.0
123 || config.ranking_weights.frequency < 0.0
124 || config.ranking_weights.recency < 0.0
125 {
126 return Err(CompletionError::ConfigError(
127 "Ranking weights must be non-negative".to_string(),
128 ));
129 }
130
131 Ok(())
132 }
133
134 pub fn default_for_language(language: &str) -> CompletionConfig {
136 CompletionConfig::new(language.to_string())
137 }
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142pub enum ConfigFormat {
143 Yaml,
144 Json,
145}
146
147pub struct LanguageConfigRegistry {
149 configs: HashMap<String, CompletionConfig>,
150}
151
152impl LanguageConfigRegistry {
153 pub fn new() -> Self {
154 Self {
155 configs: HashMap::new(),
156 }
157 }
158
159 pub fn with_hierarchy() -> CompletionResult<Self> {
161 let mut registry = Self::new();
162
163 let project_dir = ConfigLoader::get_project_completion_config_dir();
165 let _ = registry.load_from_directory(&project_dir);
166
167 if let Ok(user_dir) = ConfigLoader::get_completion_config_dir() {
169 let _ = registry.load_from_directory(&user_dir);
170 }
171
172 Ok(registry)
173 }
174
175 pub fn register(&mut self, config: CompletionConfig) {
176 self.configs.insert(config.language.clone(), config);
177 }
178
179 pub fn get(&self, language: &str) -> Option<&CompletionConfig> {
180 self.configs.get(language)
181 }
182
183 pub fn get_mut(&mut self, language: &str) -> Option<&mut CompletionConfig> {
184 self.configs.get_mut(language)
185 }
186
187 pub fn list_languages(&self) -> Vec<String> {
188 self.configs.keys().cloned().collect()
189 }
190
191 pub fn unregister(&mut self, language: &str) -> Option<CompletionConfig> {
192 self.configs.remove(language)
193 }
194
195 pub fn load_from_directory(&mut self, dir: &Path) -> CompletionResult<()> {
196 if !dir.is_dir() {
197 return Err(CompletionError::ConfigError(format!(
198 "Configuration directory not found: {}",
199 dir.display()
200 )));
201 }
202
203 for entry in std::fs::read_dir(dir)? {
204 let entry = entry?;
205 let path = entry.path();
206
207 if path.is_file() {
208 if let Some(ext) = path.extension() {
209 let config = match ext.to_str() {
210 Some("yaml") | Some("yml") => ConfigLoader::load_from_yaml(&path)?,
211 Some("json") => ConfigLoader::load_from_json(&path)?,
212 _ => continue,
213 };
214 self.register(config);
215 }
216 }
217 }
218
219 Ok(())
220 }
221}
222
223impl Default for LanguageConfigRegistry {
224 fn default() -> Self {
225 Self::new()
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn test_config_loader_validate_empty_language() {
235 let config = CompletionConfig {
236 language: String::new(),
237 keywords: Vec::new(),
238 snippets: Vec::new(),
239 ranking_weights: RankingWeights::default(),
240 provider: None,
241 };
242
243 let result = ConfigLoader::validate_config(&config);
244 assert!(result.is_err());
245 }
246
247 #[test]
248 fn test_config_loader_validate_negative_weights() {
249 let config = CompletionConfig {
250 language: "rust".to_string(),
251 keywords: Vec::new(),
252 snippets: Vec::new(),
253 ranking_weights: RankingWeights {
254 relevance: -0.1,
255 frequency: 0.3,
256 recency: 0.2,
257 },
258 provider: None,
259 };
260
261 let result = ConfigLoader::validate_config(&config);
262 assert!(result.is_err());
263 }
264
265 #[test]
266 fn test_config_loader_validate_valid() {
267 let config = CompletionConfig {
268 language: "rust".to_string(),
269 keywords: vec!["fn".to_string(), "let".to_string()],
270 snippets: Vec::new(),
271 ranking_weights: RankingWeights::default(),
272 provider: None,
273 };
274
275 let result = ConfigLoader::validate_config(&config);
276 assert!(result.is_ok());
277 }
278
279 #[test]
280 fn test_language_config_registry_register() {
281 let mut registry = LanguageConfigRegistry::new();
282 let config = CompletionConfig::new("rust".to_string());
283 registry.register(config);
284
285 assert!(registry.get("rust").is_some());
286 assert_eq!(registry.list_languages().len(), 1);
287 }
288
289 #[test]
290 fn test_language_config_registry_unregister() {
291 let mut registry = LanguageConfigRegistry::new();
292 let config = CompletionConfig::new("rust".to_string());
293 registry.register(config);
294
295 let removed = registry.unregister("rust");
296 assert!(removed.is_some());
297 assert!(registry.get("rust").is_none());
298 }
299
300 #[test]
301 fn test_config_default_for_language() {
302 let config = ConfigLoader::default_for_language("typescript");
303 assert_eq!(config.language, "typescript");
304 assert!(config.keywords.is_empty());
305 }
306
307 #[test]
308 fn test_config_loader_hierarchy_fallback() {
309 let config = ConfigLoader::load_with_hierarchy("unknown_language");
311 assert!(config.is_ok());
312 let cfg = config.unwrap();
313 assert_eq!(cfg.language, "unknown_language");
314 }
315
316 #[test]
317 fn test_language_config_registry_with_hierarchy() {
318 let registry = LanguageConfigRegistry::with_hierarchy();
320 assert!(registry.is_ok());
321 }
322}