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