Skip to main content

systemprompt_config/
skill_validator.rs

1//! `DomainConfig` implementation that validates the on-disk skill
2//! catalog referenced by `skills_path`.
3
4use 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}