1use 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#[derive(Debug, Clone)]
29pub struct ConfigLoader {
30 schema: Option<Arc<JSONSchema>>,
31}
32
33impl ConfigLoader {
34 pub fn new() -> Self {
40 Self { schema: None }
41 }
42
43 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 pub fn load_from_file(&self, path: &Path) -> DomainResult<AgentConfig> {
84 let content = fs::read_to_string(path).map_err(|e| {
86 DomainError::config_error(format!("Failed to read file: {}", e))
87 })?;
88
89 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 pub fn load_from_yaml(&self, yaml: &str) -> DomainResult<AgentConfig> {
114 let value: Value = serde_yaml::from_str(yaml).map_err(|e| {
116 DomainError::config_error(format!("Failed to parse YAML: {}", e))
117 })?;
118
119 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 serde_json::from_value(value).map_err(|e| {
132 DomainError::config_error(format!("Failed to deserialize configuration: {}", e))
133 })
134 }
135
136 pub fn load_from_json(&self, json: &str) -> DomainResult<AgentConfig> {
146 let value: Value = serde_json::from_str(json).map_err(|e| {
148 DomainError::config_error(format!("Failed to parse JSON: {}", e))
149 })?;
150
151 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 serde_json::from_value(value).map_err(|e| {
164 DomainError::config_error(format!("Failed to deserialize configuration: {}", e))
165 })
166 }
167
168 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 pub fn with_builtin_schema() -> DomainResult<Self> {
201 let schema_json = Self::load_builtin_schema_json()?;
203 Self::with_schema(&schema_json)
204 }
205
206 fn load_builtin_schema_json() -> DomainResult<String> {
208 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 Ok(Self::embedded_schema().to_string())
223 }
224
225 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 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}