1use crate::error::{OrchestrationError, Result};
7use crate::models::{RuleType, WorkspaceConfig, WorkspaceRule};
8use serde::{Deserialize, Serialize};
9use serde_json::{json, Value};
10use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12
13#[derive(Debug, Clone)]
20pub struct ConfigManager {
21 workspace_root: PathBuf,
23
24 config: WorkspaceConfig,
26
27 schema: ConfigSchema,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct ConfigSchema {
34 pub required_keys: Vec<String>,
36
37 pub optional_keys: HashMap<String, Value>,
39
40 pub validation_rules: HashMap<String, ValidationRule>,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ValidationRule {
47 pub rule_type: String,
49
50 pub min: Option<f64>,
52
53 pub max: Option<f64>,
55
56 pub allowed_values: Option<Vec<String>>,
58
59 pub pattern: Option<String>,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
65pub enum ConfigSource {
66 Defaults = 0,
68
69 User = 1,
71
72 Project = 2,
74
75 Workspace = 3,
77
78 Runtime = 4,
80}
81
82#[derive(Debug, Clone)]
84pub struct ConfigLoadResult {
85 pub config: WorkspaceConfig,
87
88 pub sources: HashMap<String, ConfigSource>,
90
91 pub warnings: Vec<String>,
93}
94
95impl ConfigManager {
96 pub fn new(workspace_root: PathBuf) -> Self {
98 Self {
99 workspace_root,
100 config: WorkspaceConfig::default(),
101 schema: ConfigSchema::default(),
102 }
103 }
104
105 pub async fn load_configuration(&mut self) -> Result<ConfigLoadResult> {
115 let mut config = WorkspaceConfig::default();
116 let mut sources = HashMap::new();
117 let warnings = Vec::new();
118
119 let defaults = self.load_defaults();
121 config = self.merge_configs(config, defaults.clone());
122 for key in defaults.settings.as_object().unwrap_or(&Default::default()).keys() {
123 sources.insert(key.clone(), ConfigSource::Defaults);
124 }
125
126 if let Ok(user_config) = self.load_user_config().await {
128 config = self.merge_configs(config, user_config.clone());
129 for key in user_config.settings.as_object().unwrap_or(&Default::default()).keys() {
130 sources.insert(key.clone(), ConfigSource::User);
131 }
132 }
133
134 if let Ok(project_config) = self.load_project_config().await {
136 config = self.merge_configs(config, project_config.clone());
137 for key in project_config.settings.as_object().unwrap_or(&Default::default()).keys() {
138 sources.insert(key.clone(), ConfigSource::Project);
139 }
140 }
141
142 if let Ok(workspace_config) = self.load_workspace_config().await {
144 config = self.merge_configs(config, workspace_config.clone());
145 for key in workspace_config.settings.as_object().unwrap_or(&Default::default()).keys() {
146 sources.insert(key.clone(), ConfigSource::Workspace);
147 }
148 }
149
150 self.validate_config(&config)?;
152
153 self.config = config.clone();
154
155 Ok(ConfigLoadResult {
156 config,
157 sources,
158 warnings,
159 })
160 }
161
162 fn load_defaults(&self) -> WorkspaceConfig {
164 WorkspaceConfig {
165 rules: vec![
166 WorkspaceRule {
167 name: "no-circular-deps".to_string(),
168 rule_type: RuleType::DependencyConstraint,
169 enabled: true,
170 },
171 WorkspaceRule {
172 name: "naming-convention".to_string(),
173 rule_type: RuleType::NamingConvention,
174 enabled: true,
175 },
176 ],
177 settings: json!({
178 "max_parallel_operations": 4,
179 "transaction_timeout_ms": 30000,
180 "enable_audit_logging": true,
181 }),
182 }
183 }
184
185 async fn load_user_config(&self) -> Result<WorkspaceConfig> {
187 let user_home = std::env::var("HOME")
188 .or_else(|_| std::env::var("USERPROFILE"))
189 .map_err(|_| crate::error::OrchestrationError::ConfigurationError(
190 "Could not determine user home directory".to_string(),
191 ))?;
192
193 let config_path = PathBuf::from(user_home)
194 .join(".ricecoder")
195 .join("config.yaml");
196
197 if !config_path.exists() {
198 return Err(OrchestrationError::ConfigurationError(
199 format!("User config not found: {}", config_path.display()),
200 ));
201 }
202
203 self.load_config_from_file(&config_path).await
204 }
205
206 async fn load_project_config(&self) -> Result<WorkspaceConfig> {
208 let config_path = self.workspace_root
209 .join(".ricecoder")
210 .join("project.yaml");
211
212 if !config_path.exists() {
213 return Err(crate::error::OrchestrationError::ConfigurationError(
214 format!("Project config not found: {}", config_path.display()),
215 ));
216 }
217
218 self.load_config_from_file(&config_path).await
219 }
220
221 async fn load_workspace_config(&self) -> Result<WorkspaceConfig> {
223 let config_path = self.workspace_root
224 .join(".ricecoder")
225 .join("workspace.yaml");
226
227 if !config_path.exists() {
228 return Err(crate::error::OrchestrationError::ConfigurationError(
229 format!("Workspace config not found: {}", config_path.display()),
230 ));
231 }
232
233 self.load_config_from_file(&config_path).await
234 }
235
236 async fn load_config_from_file(&self, path: &Path) -> Result<WorkspaceConfig> {
238 let content = tokio::fs::read_to_string(path)
239 .await
240 .map_err(crate::error::OrchestrationError::IoError)?;
241
242 let config: WorkspaceConfig = serde_yaml::from_str(&content)?;
243
244 Ok(config)
245 }
246
247 pub fn merge_configs(&self, mut base: WorkspaceConfig, override_config: WorkspaceConfig) -> WorkspaceConfig {
249 for rule in override_config.rules {
251 if let Some(pos) = base.rules.iter().position(|r| r.name == rule.name) {
252 base.rules[pos] = rule;
253 } else {
254 base.rules.push(rule);
255 }
256 }
257
258 if let (Some(base_obj), Some(override_obj)) = (
260 base.settings.as_object_mut(),
261 override_config.settings.as_object(),
262 ) {
263 for (key, value) in override_obj {
264 base_obj.insert(key.clone(), value.clone());
265 }
266 }
267
268 base
269 }
270
271 fn validate_config(&self, config: &WorkspaceConfig) -> Result<()> {
273 for rule in &config.rules {
275 if rule.name.is_empty() {
276 return Err(crate::error::OrchestrationError::ConfigurationError(
277 "Rule name cannot be empty".to_string(),
278 ));
279 }
280 }
281
282 if let Some(settings_obj) = config.settings.as_object() {
284 for (key, value) in settings_obj {
285 if let Some(validation_rule) = self.schema.validation_rules.get(key) {
286 self.validate_value(key, value, validation_rule)?;
287 }
288 }
289 }
290
291 Ok(())
292 }
293
294 pub fn validate_value(&self, key: &str, value: &Value, rule: &ValidationRule) -> Result<()> {
296 match rule.rule_type.as_str() {
297 "number" => {
298 if let Some(num) = value.as_f64() {
299 if let Some(min) = rule.min {
300 if num < min {
301 return Err(crate::error::OrchestrationError::ConfigurationError(
302 format!("{} must be >= {}", key, min),
303 ));
304 }
305 }
306 if let Some(max) = rule.max {
307 if num > max {
308 return Err(crate::error::OrchestrationError::ConfigurationError(
309 format!("{} must be <= {}", key, max),
310 ));
311 }
312 }
313 } else {
314 return Err(crate::error::OrchestrationError::ConfigurationError(
315 format!("{} must be a number", key),
316 ));
317 }
318 }
319 "string" => {
320 if !value.is_string() {
321 return Err(crate::error::OrchestrationError::ConfigurationError(
322 format!("{} must be a string", key),
323 ));
324 }
325 }
326 "boolean" => {
327 if !value.is_boolean() {
328 return Err(crate::error::OrchestrationError::ConfigurationError(
329 format!("{} must be a boolean", key),
330 ));
331 }
332 }
333 "array" => {
334 if !value.is_array() {
335 return Err(crate::error::OrchestrationError::ConfigurationError(
336 format!("{} must be an array", key),
337 ));
338 }
339 }
340 _ => {}
341 }
342
343 Ok(())
344 }
345
346 pub fn get_config(&self) -> &WorkspaceConfig {
348 &self.config
349 }
350
351 pub fn get_setting(&self, key: &str) -> Option<&Value> {
353 self.config.settings.get(key)
354 }
355
356 pub fn set_setting(&mut self, key: String, value: Value) -> Result<()> {
358 if let Some(obj) = self.config.settings.as_object_mut() {
359 obj.insert(key, value);
360 Ok(())
361 } else {
362 Err(crate::error::OrchestrationError::ConfigurationError(
363 "Settings is not an object".to_string(),
364 ))
365 }
366 }
367
368 pub fn get_rules(&self) -> &[WorkspaceRule] {
370 &self.config.rules
371 }
372
373 pub fn get_rule(&self, name: &str) -> Option<&WorkspaceRule> {
375 self.config.rules.iter().find(|r| r.name == name)
376 }
377
378 pub fn enable_rule(&mut self, name: &str) -> Result<()> {
380 if let Some(rule) = self.config.rules.iter_mut().find(|r| r.name == name) {
381 rule.enabled = true;
382 Ok(())
383 } else {
384 Err(crate::error::OrchestrationError::RulesValidationFailed(
385 format!("Rule not found: {}", name),
386 ))
387 }
388 }
389
390 pub fn disable_rule(&mut self, name: &str) -> Result<()> {
392 if let Some(rule) = self.config.rules.iter_mut().find(|r| r.name == name) {
393 rule.enabled = false;
394 Ok(())
395 } else {
396 Err(crate::error::OrchestrationError::RulesValidationFailed(
397 format!("Rule not found: {}", name),
398 ))
399 }
400 }
401}
402
403impl Default for ConfigSchema {
404 fn default() -> Self {
405 let mut validation_rules = HashMap::new();
406
407 validation_rules.insert(
408 "max_parallel_operations".to_string(),
409 ValidationRule {
410 rule_type: "number".to_string(),
411 min: Some(1.0),
412 max: Some(32.0),
413 allowed_values: None,
414 pattern: None,
415 },
416 );
417
418 validation_rules.insert(
419 "transaction_timeout_ms".to_string(),
420 ValidationRule {
421 rule_type: "number".to_string(),
422 min: Some(1000.0),
423 max: Some(300000.0),
424 allowed_values: None,
425 pattern: None,
426 },
427 );
428
429 validation_rules.insert(
430 "enable_audit_logging".to_string(),
431 ValidationRule {
432 rule_type: "boolean".to_string(),
433 min: None,
434 max: None,
435 allowed_values: None,
436 pattern: None,
437 },
438 );
439
440 Self {
441 required_keys: vec![],
442 optional_keys: HashMap::new(),
443 validation_rules,
444 }
445 }
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451
452 #[test]
453 fn test_config_manager_creation() {
454 let manager = ConfigManager::new(PathBuf::from("/workspace"));
455 assert_eq!(manager.workspace_root, PathBuf::from("/workspace"));
456 }
457
458 #[test]
459 fn test_load_defaults() {
460 let manager = ConfigManager::new(PathBuf::from("/workspace"));
461 let defaults = manager.load_defaults();
462
463 assert!(!defaults.rules.is_empty());
464 assert!(defaults.settings.get("max_parallel_operations").is_some());
465 }
466
467 #[test]
468 fn test_merge_configs() {
469 let manager = ConfigManager::new(PathBuf::from("/workspace"));
470
471 let base = WorkspaceConfig {
472 rules: vec![WorkspaceRule {
473 name: "rule1".to_string(),
474 rule_type: RuleType::DependencyConstraint,
475 enabled: true,
476 }],
477 settings: json!({"key1": "value1"}),
478 };
479
480 let override_config = WorkspaceConfig {
481 rules: vec![WorkspaceRule {
482 name: "rule2".to_string(),
483 rule_type: RuleType::NamingConvention,
484 enabled: false,
485 }],
486 settings: json!({"key2": "value2"}),
487 };
488
489 let merged = manager.merge_configs(base, override_config);
490
491 assert_eq!(merged.rules.len(), 2);
492 assert!(merged.settings.get("key1").is_some());
493 assert!(merged.settings.get("key2").is_some());
494 }
495
496 #[test]
497 fn test_validate_config_success() {
498 let manager = ConfigManager::new(PathBuf::from("/workspace"));
499 let config = WorkspaceConfig {
500 rules: vec![WorkspaceRule {
501 name: "test-rule".to_string(),
502 rule_type: RuleType::DependencyConstraint,
503 enabled: true,
504 }],
505 settings: json!({"max_parallel_operations": 4}),
506 };
507
508 assert!(manager.validate_config(&config).is_ok());
509 }
510
511 #[test]
512 fn test_validate_config_empty_rule_name() {
513 let manager = ConfigManager::new(PathBuf::from("/workspace"));
514 let config = WorkspaceConfig {
515 rules: vec![WorkspaceRule {
516 name: "".to_string(),
517 rule_type: RuleType::DependencyConstraint,
518 enabled: true,
519 }],
520 settings: json!({}),
521 };
522
523 assert!(manager.validate_config(&config).is_err());
524 }
525
526 #[test]
527 fn test_get_setting() {
528 let mut manager = ConfigManager::new(PathBuf::from("/workspace"));
529 manager.config = WorkspaceConfig {
530 rules: vec![],
531 settings: json!({"key1": "value1"}),
532 };
533
534 assert_eq!(manager.get_setting("key1"), Some(&Value::String("value1".to_string())));
535 assert_eq!(manager.get_setting("nonexistent"), None);
536 }
537
538 #[test]
539 fn test_set_setting() {
540 let mut manager = ConfigManager::new(PathBuf::from("/workspace"));
541 manager.config = WorkspaceConfig {
542 rules: vec![],
543 settings: json!({}),
544 };
545
546 assert!(manager.set_setting("key1".to_string(), Value::String("value1".to_string())).is_ok());
547 assert_eq!(manager.get_setting("key1"), Some(&Value::String("value1".to_string())));
548 }
549
550 #[test]
551 fn test_get_rules() {
552 let mut manager = ConfigManager::new(PathBuf::from("/workspace"));
553 let rule = WorkspaceRule {
554 name: "test-rule".to_string(),
555 rule_type: RuleType::DependencyConstraint,
556 enabled: true,
557 };
558 manager.config.rules.push(rule);
559
560 assert_eq!(manager.get_rules().len(), 1);
561 }
562
563 #[test]
564 fn test_get_rule_by_name() {
565 let mut manager = ConfigManager::new(PathBuf::from("/workspace"));
566 let rule = WorkspaceRule {
567 name: "test-rule".to_string(),
568 rule_type: RuleType::DependencyConstraint,
569 enabled: true,
570 };
571 manager.config.rules.push(rule);
572
573 assert!(manager.get_rule("test-rule").is_some());
574 assert!(manager.get_rule("nonexistent").is_none());
575 }
576
577 #[test]
578 fn test_enable_rule() {
579 let mut manager = ConfigManager::new(PathBuf::from("/workspace"));
580 let rule = WorkspaceRule {
581 name: "test-rule".to_string(),
582 rule_type: RuleType::DependencyConstraint,
583 enabled: false,
584 };
585 manager.config.rules.push(rule);
586
587 assert!(manager.enable_rule("test-rule").is_ok());
588 assert!(manager.get_rule("test-rule").unwrap().enabled);
589 }
590
591 #[test]
592 fn test_disable_rule() {
593 let mut manager = ConfigManager::new(PathBuf::from("/workspace"));
594 let rule = WorkspaceRule {
595 name: "test-rule".to_string(),
596 rule_type: RuleType::DependencyConstraint,
597 enabled: true,
598 };
599 manager.config.rules.push(rule);
600
601 assert!(manager.disable_rule("test-rule").is_ok());
602 assert!(!manager.get_rule("test-rule").unwrap().enabled);
603 }
604
605 #[test]
606 fn test_config_schema_default() {
607 let schema = ConfigSchema::default();
608 assert!(schema.validation_rules.contains_key("max_parallel_operations"));
609 assert!(schema.validation_rules.contains_key("transaction_timeout_ms"));
610 assert!(schema.validation_rules.contains_key("enable_audit_logging"));
611 }
612
613 #[test]
614 fn test_validate_number_value() {
615 let manager = ConfigManager::new(PathBuf::from("/workspace"));
616 let rule = ValidationRule {
617 rule_type: "number".to_string(),
618 min: Some(1.0),
619 max: Some(10.0),
620 allowed_values: None,
621 pattern: None,
622 };
623
624 assert!(manager.validate_value("test", &Value::Number(5.into()), &rule).is_ok());
625 assert!(manager.validate_value("test", &Value::Number(0.into()), &rule).is_err());
626 assert!(manager.validate_value("test", &Value::Number(11.into()), &rule).is_err());
627 }
628
629 #[test]
630 fn test_validate_string_value() {
631 let manager = ConfigManager::new(PathBuf::from("/workspace"));
632 let rule = ValidationRule {
633 rule_type: "string".to_string(),
634 min: None,
635 max: None,
636 allowed_values: None,
637 pattern: None,
638 };
639
640 assert!(manager.validate_value("test", &Value::String("value".to_string()), &rule).is_ok());
641 assert!(manager.validate_value("test", &Value::Number(5.into()), &rule).is_err());
642 }
643
644 #[test]
645 fn test_validate_boolean_value() {
646 let manager = ConfigManager::new(PathBuf::from("/workspace"));
647 let rule = ValidationRule {
648 rule_type: "boolean".to_string(),
649 min: None,
650 max: None,
651 allowed_values: None,
652 pattern: None,
653 };
654
655 assert!(manager.validate_value("test", &Value::Bool(true), &rule).is_ok());
656 assert!(manager.validate_value("test", &Value::String("true".to_string()), &rule).is_err());
657 }
658}