systemprompt_config/
skill_validator.rs1use std::path::Path;
5
6use systemprompt_models::{DiskSkillConfig, SKILL_CONFIG_FILENAME};
7use systemprompt_traits::validation_report::{ValidationError, ValidationReport};
8use systemprompt_traits::{ConfigProvider, DomainConfig, DomainConfigError};
9
10#[derive(Debug, Default)]
11pub struct SkillConfigValidator {
12 skills_path: Option<String>,
13}
14
15impl SkillConfigValidator {
16 #[must_use]
17 pub fn new() -> Self {
18 Self::default()
19 }
20}
21
22impl DomainConfig for SkillConfigValidator {
23 fn domain_id(&self) -> &'static str {
24 "skills"
25 }
26
27 fn priority(&self) -> u32 {
28 25
29 }
30
31 fn load(&mut self, config: &dyn ConfigProvider) -> Result<(), DomainConfigError> {
32 let skills_path = config
33 .get("skills_path")
34 .ok_or_else(|| DomainConfigError::NotFound("skills_path not configured".into()))?;
35
36 self.skills_path = Some(skills_path);
37 Ok(())
38 }
39
40 fn validate(&self) -> Result<ValidationReport, DomainConfigError> {
41 let mut report = ValidationReport::new("skills");
42
43 let skills_path = self
44 .skills_path
45 .as_ref()
46 .ok_or_else(|| DomainConfigError::ValidationError("Skills path not set".into()))?;
47
48 let skills_dir = Path::new(skills_path);
49 if !skills_dir.exists() {
50 report.add_error(
51 ValidationError::new("skills_path", "Skills directory does not exist")
52 .with_path(skills_dir)
53 .with_suggestion("Create the skills directory or update skills_path in config"),
54 );
55 return Ok(report);
56 }
57
58 let entries = std::fs::read_dir(skills_dir).map_err(|e| {
59 DomainConfigError::LoadError(format!("Cannot read skills directory: {e}"))
60 })?;
61
62 for entry in entries {
63 let entry = entry.map_err(|e| {
64 DomainConfigError::LoadError(format!("Cannot read directory entry: {e}"))
65 })?;
66
67 if !entry.path().is_dir() {
68 continue;
69 }
70
71 let dir_name = entry.file_name().to_string_lossy().to_string();
72 let config_path = entry.path().join(SKILL_CONFIG_FILENAME);
73
74 if !config_path.exists() {
75 report.add_error(
76 ValidationError::new(
77 format!("skills.{dir_name}"),
78 format!("Missing {SKILL_CONFIG_FILENAME}"),
79 )
80 .with_path(&config_path)
81 .with_suggestion("Add a config.yaml with id, name, and description"),
82 );
83 continue;
84 }
85
86 let config_text = match std::fs::read_to_string(&config_path) {
87 Ok(text) => text,
88 Err(e) => {
89 report.add_error(
90 ValidationError::new(
91 format!("skills.{dir_name}"),
92 format!("Cannot read {SKILL_CONFIG_FILENAME}: {e}"),
93 )
94 .with_path(&config_path),
95 );
96 continue;
97 },
98 };
99
100 let config: DiskSkillConfig = match serde_yaml::from_str(&config_text) {
101 Ok(cfg) => cfg,
102 Err(e) => {
103 report.add_error(
104 ValidationError::new(
105 format!("skills.{dir_name}"),
106 format!("Invalid {SKILL_CONFIG_FILENAME}: {e}"),
107 )
108 .with_path(&config_path)
109 .with_suggestion(
110 "Ensure config.yaml has required fields: id, name, description",
111 ),
112 );
113 continue;
114 },
115 };
116
117 let content_file = config.content_file();
118 let content_path = entry.path().join(content_file);
119 if !content_path.exists() {
120 report.add_error(
121 ValidationError::new(
122 format!("skills.{dir_name}.file"),
123 format!("Content file '{content_file}' not found"),
124 )
125 .with_path(&content_path)
126 .with_suggestion(
127 "Create the content file or update the file field in config.yaml",
128 ),
129 );
130 }
131 }
132
133 Ok(report)
134 }
135}