ricecoder_agents/domain/
config_loader.rs

1//! Configuration loader and validator for domain agents
2//!
3//! This module provides functionality to load domain agent configurations from YAML/JSON files
4//! and validate them against the JSON Schema.
5
6use crate::domain::error::{DomainError, DomainResult};
7use crate::domain::factory::AgentConfig;
8use jsonschema::JSONSchema;
9use serde_json::Value;
10use std::fs;
11use std::path::Path;
12use std::sync::Arc;
13
14/// Configuration loader for domain agents
15///
16/// This struct provides methods to load and validate domain agent configurations
17/// from YAML and JSON files.
18///
19/// # Examples
20///
21/// ```ignore
22/// use ricecoder_agents::domain::ConfigLoader;
23/// use std::path::Path;
24///
25/// let loader = ConfigLoader::new();
26/// let config = loader.load_from_file(Path::new("config/domains/web.yaml"))?;
27/// ```
28#[derive(Debug, Clone)]
29pub struct ConfigLoader {
30    schema: Option<Arc<JSONSchema>>,
31}
32
33impl ConfigLoader {
34    /// Create a new configuration loader
35    ///
36    /// # Returns
37    ///
38    /// Returns a new ConfigLoader instance
39    pub fn new() -> Self {
40        Self { schema: None }
41    }
42
43    /// Create a new configuration loader with schema validation
44    ///
45    /// # Arguments
46    ///
47    /// * `schema_json` - JSON schema as a string
48    ///
49    /// # Returns
50    ///
51    /// Returns a new ConfigLoader instance with schema validation enabled
52    pub fn with_schema(schema_json: &str) -> DomainResult<Self> {
53        let schema_value: Value = serde_json::from_str(schema_json).map_err(|e| {
54            DomainError::config_error(format!("Failed to parse schema: {}", e))
55        })?;
56
57        let schema = JSONSchema::compile(&schema_value).map_err(|e| {
58            DomainError::config_error(format!("Failed to compile schema: {}", e))
59        })?;
60
61        Ok(Self {
62            schema: Some(Arc::new(schema)),
63        })
64    }
65
66    /// Load configuration from a file
67    ///
68    /// Automatically detects file format (YAML or JSON) based on file extension.
69    ///
70    /// # Arguments
71    ///
72    /// * `path` - Path to the configuration file
73    ///
74    /// # Returns
75    ///
76    /// Returns the loaded configuration
77    ///
78    /// # Examples
79    ///
80    /// ```ignore
81    /// let config = loader.load_from_file(Path::new("config/domains/web.yaml"))?;
82    /// ```
83    pub fn load_from_file(&self, path: &Path) -> DomainResult<AgentConfig> {
84        // Read file
85        let content = fs::read_to_string(path).map_err(|e| {
86            DomainError::config_error(format!("Failed to read file: {}", e))
87        })?;
88
89        // Determine format based on extension
90        let extension = path
91            .extension()
92            .and_then(|ext| ext.to_str())
93            .unwrap_or("");
94
95        match extension {
96            "yaml" | "yml" => self.load_from_yaml(&content),
97            "json" => self.load_from_json(&content),
98            _ => Err(DomainError::config_error(
99                "Unsupported file format. Use .yaml, .yml, or .json",
100            )),
101        }
102    }
103
104    /// Load configuration from YAML string
105    ///
106    /// # Arguments
107    ///
108    /// * `yaml` - YAML string containing configuration
109    ///
110    /// # Returns
111    ///
112    /// Returns the loaded configuration
113    pub fn load_from_yaml(&self, yaml: &str) -> DomainResult<AgentConfig> {
114        // Parse YAML to JSON value
115        let value: Value = serde_yaml::from_str(yaml).map_err(|e| {
116            DomainError::config_error(format!("Failed to parse YAML: {}", e))
117        })?;
118
119        // Validate against schema if available
120        if let Some(schema) = &self.schema {
121            schema.validate(&value).map_err(|e| {
122                let errors: Vec<String> = e.map(|err| err.to_string()).collect();
123                DomainError::config_error(format!(
124                    "Configuration validation failed:\n{}",
125                    errors.join("\n")
126                ))
127            })?;
128        }
129
130        // Deserialize to AgentConfig
131        serde_json::from_value(value).map_err(|e| {
132            DomainError::config_error(format!("Failed to deserialize configuration: {}", e))
133        })
134    }
135
136    /// Load configuration from JSON string
137    ///
138    /// # Arguments
139    ///
140    /// * `json` - JSON string containing configuration
141    ///
142    /// # Returns
143    ///
144    /// Returns the loaded configuration
145    pub fn load_from_json(&self, json: &str) -> DomainResult<AgentConfig> {
146        // Parse JSON
147        let value: Value = serde_json::from_str(json).map_err(|e| {
148            DomainError::config_error(format!("Failed to parse JSON: {}", e))
149        })?;
150
151        // Validate against schema if available
152        if let Some(schema) = &self.schema {
153            schema.validate(&value).map_err(|e| {
154                let errors: Vec<String> = e.map(|err| err.to_string()).collect();
155                DomainError::config_error(format!(
156                    "Configuration validation failed:\n{}",
157                    errors.join("\n")
158                ))
159            })?;
160        }
161
162        // Deserialize to AgentConfig
163        serde_json::from_value(value).map_err(|e| {
164            DomainError::config_error(format!("Failed to deserialize configuration: {}", e))
165        })
166    }
167
168    /// Validate configuration against schema
169    ///
170    /// # Arguments
171    ///
172    /// * `config` - Configuration to validate
173    ///
174    /// # Returns
175    ///
176    /// Returns Ok if configuration is valid, otherwise returns an error
177    pub fn validate(&self, config: &AgentConfig) -> DomainResult<()> {
178        if let Some(schema) = &self.schema {
179            let value = serde_json::to_value(config).map_err(|e| {
180                DomainError::config_error(format!("Failed to serialize configuration: {}", e))
181            })?;
182
183            schema.validate(&value).map_err(|e| {
184                let errors: Vec<String> = e.map(|err| err.to_string()).collect();
185                DomainError::config_error(format!(
186                    "Configuration validation failed:\n{}",
187                    errors.join("\n")
188                ))
189            })?;
190        }
191
192        Ok(())
193    }
194
195    /// Load the built-in domain schema
196    ///
197    /// # Returns
198    ///
199    /// Returns a ConfigLoader with the built-in schema
200    pub fn with_builtin_schema() -> DomainResult<Self> {
201        // Try to load schema from file, fall back to embedded schema
202        let schema_json = Self::load_builtin_schema_json()?;
203        Self::with_schema(&schema_json)
204    }
205
206    /// Load the built-in schema JSON
207    fn load_builtin_schema_json() -> DomainResult<String> {
208        // Try multiple possible paths
209        let possible_paths = vec![
210            "config/schemas/domain.schema.json",
211            "../../../config/schemas/domain.schema.json",
212            "../../../../config/schemas/domain.schema.json",
213        ];
214
215        for path in possible_paths {
216            if let Ok(content) = fs::read_to_string(path) {
217                return Ok(content);
218            }
219        }
220
221        // If file not found, return embedded schema
222        Ok(Self::embedded_schema().to_string())
223    }
224
225    /// Embedded domain schema (fallback)
226    fn embedded_schema() -> &'static str {
227        r#"{
228  "$schema": "http://json-schema.org/draft-07/schema#",
229  "title": "Domain Agent Configuration Schema",
230  "description": "Schema for domain-specific agent configuration files",
231  "type": "object",
232  "required": [
233    "domain",
234    "name",
235    "description",
236    "capabilities"
237  ],
238  "properties": {
239    "domain": {
240      "type": "string",
241      "description": "Domain identifier (e.g., 'web', 'backend', 'devops')",
242      "minLength": 1,
243      "maxLength": 100,
244      "pattern": "^[a-z0-9-]+$"
245    },
246    "name": {
247      "type": "string",
248      "description": "Human-readable agent name",
249      "minLength": 1,
250      "maxLength": 200
251    },
252    "description": {
253      "type": "string",
254      "description": "Detailed description of the agent and its purpose",
255      "minLength": 1,
256      "maxLength": 1000
257    },
258    "capabilities": {
259      "type": "array",
260      "description": "List of capabilities this agent provides",
261      "minItems": 1,
262      "items": {
263        "type": "object",
264        "required": [
265          "name",
266          "description",
267          "technologies"
268        ],
269        "properties": {
270          "name": {
271            "type": "string",
272            "description": "Capability name",
273            "minLength": 1,
274            "maxLength": 200
275          },
276          "description": {
277            "type": "string",
278            "description": "Capability description",
279            "minLength": 1,
280            "maxLength": 500
281          },
282          "technologies": {
283            "type": "array",
284            "description": "Technologies supported by this capability",
285            "minItems": 1,
286            "items": {
287              "type": "string",
288              "minLength": 1,
289              "maxLength": 100
290            }
291          }
292        },
293        "additionalProperties": false
294      }
295    },
296    "best_practices": {
297      "type": "array",
298      "description": "List of best practices for this domain",
299      "default": [],
300      "items": {
301        "type": "object",
302        "required": [
303          "title",
304          "description",
305          "technologies",
306          "implementation"
307        ],
308        "properties": {
309          "title": {
310            "type": "string",
311            "description": "Best practice title",
312            "minLength": 1,
313            "maxLength": 200
314          },
315          "description": {
316            "type": "string",
317            "description": "Best practice description",
318            "minLength": 1,
319            "maxLength": 500
320          },
321          "technologies": {
322            "type": "array",
323            "description": "Technologies this practice applies to",
324            "minItems": 1,
325            "items": {
326              "type": "string",
327              "minLength": 1,
328              "maxLength": 100
329            }
330          },
331          "implementation": {
332            "type": "string",
333            "description": "Implementation guidance",
334            "minLength": 1,
335            "maxLength": 1000
336          }
337        },
338        "additionalProperties": false
339      }
340    },
341    "technology_recommendations": {
342      "type": "array",
343      "description": "List of technology recommendations",
344      "default": [],
345      "items": {
346        "type": "object",
347        "required": [
348          "technology",
349          "use_cases",
350          "pros",
351          "cons",
352          "alternatives"
353        ],
354        "properties": {
355          "technology": {
356            "type": "string",
357            "description": "Technology name",
358            "minLength": 1,
359            "maxLength": 100
360          },
361          "use_cases": {
362            "type": "array",
363            "description": "Use cases for this technology",
364            "minItems": 1,
365            "items": {
366              "type": "string",
367              "minLength": 1,
368              "maxLength": 200
369            }
370          },
371          "pros": {
372            "type": "array",
373            "description": "Advantages of this technology",
374            "minItems": 1,
375            "items": {
376              "type": "string",
377              "minLength": 1,
378              "maxLength": 300
379            }
380          },
381          "cons": {
382            "type": "array",
383            "description": "Disadvantages of this technology",
384            "minItems": 1,
385            "items": {
386              "type": "string",
387              "minLength": 1,
388              "maxLength": 300
389            }
390          },
391          "alternatives": {
392            "type": "array",
393            "description": "Alternative technologies",
394            "minItems": 1,
395            "items": {
396              "type": "string",
397              "minLength": 1,
398              "maxLength": 100
399            }
400          }
401        },
402        "additionalProperties": false
403      }
404    },
405    "patterns": {
406      "type": "array",
407      "description": "List of design patterns for this domain",
408      "default": [],
409      "items": {
410        "type": "object",
411        "required": [
412          "name",
413          "description",
414          "technologies",
415          "use_cases"
416        ],
417        "properties": {
418          "name": {
419            "type": "string",
420            "description": "Pattern name",
421            "minLength": 1,
422            "maxLength": 200
423          },
424          "description": {
425            "type": "string",
426            "description": "Pattern description",
427            "minLength": 1,
428            "maxLength": 500
429          },
430          "technologies": {
431            "type": "array",
432            "description": "Technologies this pattern applies to",
433            "minItems": 1,
434            "items": {
435              "type": "string",
436              "minLength": 1,
437              "maxLength": 100
438            }
439          },
440          "use_cases": {
441            "type": "array",
442            "description": "Use cases for this pattern",
443            "minItems": 1,
444            "items": {
445              "type": "string",
446              "minLength": 1,
447              "maxLength": 200
448            }
449          }
450        },
451        "additionalProperties": false
452      }
453    },
454    "anti_patterns": {
455      "type": "array",
456      "description": "List of anti-patterns to avoid",
457      "default": [],
458      "items": {
459        "type": "object",
460        "required": [
461          "name",
462          "description",
463          "why_avoid",
464          "better_alternative"
465        ],
466        "properties": {
467          "name": {
468            "type": "string",
469            "description": "Anti-pattern name",
470            "minLength": 1,
471            "maxLength": 200
472          },
473          "description": {
474            "type": "string",
475            "description": "Anti-pattern description",
476            "minLength": 1,
477            "maxLength": 500
478          },
479          "why_avoid": {
480            "type": "string",
481            "description": "Why this anti-pattern should be avoided",
482            "minLength": 1,
483            "maxLength": 500
484          },
485          "better_alternative": {
486            "type": "string",
487            "description": "Better alternative to use instead",
488            "minLength": 1,
489            "maxLength": 500
490          }
491        },
492        "additionalProperties": false
493      }
494    }
495  },
496  "additionalProperties": false
497}"#
498    }
499}
500
501impl Default for ConfigLoader {
502    fn default() -> Self {
503        Self::new()
504    }
505}
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510
511    fn create_test_yaml() -> String {
512        r#"
513domain: web
514name: "Web Development Agent"
515description: "Specialized agent for web development"
516capabilities:
517  - name: "Frontend Framework Selection"
518    description: "Recommend frontend frameworks based on project needs"
519    technologies: ["React", "Vue", "Angular"]
520best_practices: []
521technology_recommendations: []
522patterns: []
523anti_patterns: []
524"#
525        .to_string()
526    }
527
528    fn create_test_json() -> String {
529        r#"{
530  "domain": "web",
531  "name": "Web Development Agent",
532  "description": "Specialized agent for web development",
533  "capabilities": [
534    {
535      "name": "Frontend Framework Selection",
536      "description": "Recommend frontend frameworks based on project needs",
537      "technologies": ["React", "Vue", "Angular"]
538    }
539  ],
540  "best_practices": [],
541  "technology_recommendations": [],
542  "patterns": [],
543  "anti_patterns": []
544}"#
545        .to_string()
546    }
547
548    #[test]
549    fn test_config_loader_creation() {
550        let loader = ConfigLoader::new();
551        assert!(loader.schema.is_none());
552    }
553
554    #[test]
555    fn test_config_loader_default() {
556        let loader = ConfigLoader::default();
557        assert!(loader.schema.is_none());
558    }
559
560    #[test]
561    fn test_load_from_yaml() {
562        let loader = ConfigLoader::new();
563        let yaml = create_test_yaml();
564
565        let config = loader.load_from_yaml(&yaml).unwrap();
566        assert_eq!(config.domain, "web");
567        assert_eq!(config.name, "Web Development Agent");
568        assert_eq!(config.capabilities.len(), 1);
569    }
570
571    #[test]
572    fn test_load_from_json() {
573        let loader = ConfigLoader::new();
574        let json = create_test_json();
575
576        let config = loader.load_from_json(&json).unwrap();
577        assert_eq!(config.domain, "web");
578        assert_eq!(config.name, "Web Development Agent");
579        assert_eq!(config.capabilities.len(), 1);
580    }
581
582    #[test]
583    fn test_load_from_yaml_invalid() {
584        let loader = ConfigLoader::new();
585        let yaml = "invalid: yaml: content:";
586
587        assert!(loader.load_from_yaml(yaml).is_err());
588    }
589
590    #[test]
591    fn test_load_from_json_invalid() {
592        let loader = ConfigLoader::new();
593        let json = "invalid json";
594
595        assert!(loader.load_from_json(json).is_err());
596    }
597
598    #[test]
599    fn test_with_builtin_schema() {
600        let result = ConfigLoader::with_builtin_schema();
601        assert!(result.is_ok());
602
603        let loader = result.unwrap();
604        assert!(loader.schema.is_some());
605    }
606
607    #[test]
608    fn test_validate_with_schema() {
609        let loader = ConfigLoader::with_builtin_schema().unwrap();
610        let yaml = create_test_yaml();
611
612        let config = loader.load_from_yaml(&yaml).unwrap();
613        assert!(loader.validate(&config).is_ok());
614    }
615
616    #[test]
617    fn test_load_from_yaml_with_schema_validation() {
618        let loader = ConfigLoader::with_builtin_schema().unwrap();
619        let yaml = create_test_yaml();
620
621        let result = loader.load_from_yaml(&yaml);
622        assert!(result.is_ok());
623    }
624
625    #[test]
626    fn test_load_from_json_with_schema_validation() {
627        let loader = ConfigLoader::with_builtin_schema().unwrap();
628        let json = create_test_json();
629
630        let result = loader.load_from_json(&json);
631        assert!(result.is_ok());
632    }
633
634    #[test]
635    fn test_load_from_yaml_missing_required_field() {
636        let loader = ConfigLoader::with_builtin_schema().unwrap();
637        let yaml = r#"
638domain: web
639name: "Web Agent"
640# Missing description and capabilities
641"#;
642
643        let result = loader.load_from_yaml(yaml);
644        assert!(result.is_err());
645    }
646
647    #[test]
648    fn test_load_from_json_missing_required_field() {
649        let loader = ConfigLoader::with_builtin_schema().unwrap();
650        let json = r#"{
651  "domain": "web",
652  "name": "Web Agent"
653}"#;
654
655        let result = loader.load_from_json(json);
656        assert!(result.is_err());
657    }
658
659    #[test]
660    fn test_load_from_yaml_empty_capabilities() {
661        let loader = ConfigLoader::with_builtin_schema().unwrap();
662        let yaml = r#"
663domain: web
664name: "Web Agent"
665description: "Web development agent"
666capabilities: []
667"#;
668
669        let result = loader.load_from_yaml(yaml);
670        assert!(result.is_err());
671    }
672
673    #[test]
674    fn test_load_from_yaml_invalid_domain_format() {
675        let loader = ConfigLoader::with_builtin_schema().unwrap();
676        let yaml = r#"
677domain: "Web Domain!"
678name: "Web Agent"
679description: "Web development agent"
680capabilities:
681  - name: "Framework"
682    description: "Select frameworks"
683    technologies: ["React"]
684"#;
685
686        let result = loader.load_from_yaml(yaml);
687        assert!(result.is_err());
688    }
689
690    #[test]
691    fn test_load_from_yaml_with_best_practices() {
692        let loader = ConfigLoader::new();
693        let yaml = r#"
694domain: web
695name: "Web Agent"
696description: "Web development agent"
697capabilities:
698  - name: "Framework"
699    description: "Select frameworks"
700    technologies: ["React"]
701best_practices:
702  - title: "Component-Based Architecture"
703    description: "Use components"
704    technologies: ["React"]
705    implementation: "Break UI into components"
706"#;
707
708        let config = loader.load_from_yaml(yaml).unwrap();
709        assert_eq!(config.best_practices.len(), 1);
710        assert_eq!(config.best_practices[0].title, "Component-Based Architecture");
711    }
712
713    #[test]
714    fn test_load_from_yaml_with_tech_recommendations() {
715        let loader = ConfigLoader::new();
716        let yaml = r#"
717domain: web
718name: "Web Agent"
719description: "Web development agent"
720capabilities:
721  - name: "Framework"
722    description: "Select frameworks"
723    technologies: ["React"]
724technology_recommendations:
725  - technology: "React"
726    use_cases: ["SPAs"]
727    pros: ["Ecosystem"]
728    cons: ["Learning curve"]
729    alternatives: ["Vue"]
730"#;
731
732        let config = loader.load_from_yaml(yaml).unwrap();
733        assert_eq!(config.technology_recommendations.len(), 1);
734        assert_eq!(config.technology_recommendations[0].technology, "React");
735    }
736
737    #[test]
738    fn test_load_from_yaml_with_patterns() {
739        let loader = ConfigLoader::new();
740        let yaml = r#"
741domain: web
742name: "Web Agent"
743description: "Web development agent"
744capabilities:
745  - name: "Framework"
746    description: "Select frameworks"
747    technologies: ["React"]
748patterns:
749  - name: "Component Pattern"
750    description: "Component-based architecture"
751    technologies: ["React"]
752    use_cases: ["UI development"]
753"#;
754
755        let config = loader.load_from_yaml(yaml).unwrap();
756        assert_eq!(config.patterns.len(), 1);
757        assert_eq!(config.patterns[0].name, "Component Pattern");
758    }
759
760    #[test]
761    fn test_load_from_yaml_with_anti_patterns() {
762        let loader = ConfigLoader::new();
763        let yaml = r#"
764domain: web
765name: "Web Agent"
766description: "Web development agent"
767capabilities:
768  - name: "Framework"
769    description: "Select frameworks"
770    technologies: ["React"]
771anti_patterns:
772  - name: "God Component"
773    description: "Component that does too much"
774    why_avoid: "Violates SRP"
775    better_alternative: "Break into smaller components"
776"#;
777
778        let config = loader.load_from_yaml(yaml).unwrap();
779        assert_eq!(config.anti_patterns.len(), 1);
780        assert_eq!(config.anti_patterns[0].name, "God Component");
781    }
782
783    #[test]
784    fn test_validate_config_without_schema() {
785        let loader = ConfigLoader::new();
786        let config = AgentConfig {
787            domain: "web".to_string(),
788            name: "Web Agent".to_string(),
789            description: "Web development agent".to_string(),
790            capabilities: vec![],
791            best_practices: vec![],
792            technology_recommendations: vec![],
793            patterns: vec![],
794            anti_patterns: vec![],
795        };
796
797        // Should pass validation even without schema
798        assert!(loader.validate(&config).is_ok());
799    }
800
801    #[test]
802    fn test_load_from_yaml_with_all_fields() {
803        let loader = ConfigLoader::new();
804        let yaml = r#"
805domain: backend
806name: "Backend Agent"
807description: "Backend development agent"
808capabilities:
809  - name: "API Design"
810    description: "API design patterns"
811    technologies: ["REST", "GraphQL"]
812best_practices:
813  - title: "API Versioning"
814    description: "Maintain backward compatibility"
815    technologies: ["REST"]
816    implementation: "Use URL versioning"
817technology_recommendations:
818  - technology: "PostgreSQL"
819    use_cases: ["Relational data"]
820    pros: ["Reliable"]
821    cons: ["Vertical scaling"]
822    alternatives: ["MySQL"]
823patterns:
824  - name: "MVC Pattern"
825    description: "Model-View-Controller"
826    technologies: ["Django"]
827    use_cases: ["Web applications"]
828anti_patterns:
829  - name: "God Object"
830    description: "Class that does too much"
831    why_avoid: "Violates SRP"
832    better_alternative: "Break into smaller classes"
833"#;
834
835        let config = loader.load_from_yaml(yaml).unwrap();
836        assert_eq!(config.domain, "backend");
837        assert_eq!(config.capabilities.len(), 1);
838        assert_eq!(config.best_practices.len(), 1);
839        assert_eq!(config.technology_recommendations.len(), 1);
840        assert_eq!(config.patterns.len(), 1);
841        assert_eq!(config.anti_patterns.len(), 1);
842    }
843}