rustant_core/skills/
mod.rs1pub mod parser;
8pub mod types;
9pub mod validator;
10
11pub use parser::{ParseError, parse_skill_md};
12pub use types::{SkillConfig, SkillDefinition, SkillRequirement, SkillRiskLevel, SkillToolDef};
13pub use validator::{ValidationError, ValidationResult, validate_skill};
14
15use std::collections::HashMap;
16use std::path::{Path, PathBuf};
17
18#[derive(Debug, Default)]
20pub struct SkillRegistry {
21 skills: HashMap<String, SkillDefinition>,
22}
23
24impl SkillRegistry {
25 pub fn new() -> Self {
26 Self::default()
27 }
28
29 pub fn register(&mut self, skill: SkillDefinition) {
31 self.skills.insert(skill.name.clone(), skill);
32 }
33
34 pub fn get(&self, name: &str) -> Option<&SkillDefinition> {
36 self.skills.get(name)
37 }
38
39 pub fn list_names(&self) -> Vec<&str> {
41 self.skills.keys().map(|k| k.as_str()).collect()
42 }
43
44 pub fn len(&self) -> usize {
46 self.skills.len()
47 }
48
49 pub fn is_empty(&self) -> bool {
51 self.skills.is_empty()
52 }
53}
54
55pub struct SkillLoader {
57 skills_dir: PathBuf,
58}
59
60impl SkillLoader {
61 pub fn new(skills_dir: impl Into<PathBuf>) -> Self {
62 Self {
63 skills_dir: skills_dir.into(),
64 }
65 }
66
67 pub fn scan(&self) -> Vec<Result<SkillDefinition, (PathBuf, ParseError)>> {
69 let mut results = Vec::new();
70
71 let entries = match std::fs::read_dir(&self.skills_dir) {
72 Ok(entries) => entries,
73 Err(_) => return results,
74 };
75
76 for entry in entries.flatten() {
77 let path = entry.path();
78 if path.extension().map(|e| e == "md").unwrap_or(false) {
79 match self.load_file(&path) {
80 Ok(mut skill) => {
81 skill.source_path = Some(path.to_string_lossy().into_owned());
82 results.push(Ok(skill));
83 }
84 Err(e) => {
85 results.push(Err((path, e)));
86 }
87 }
88 }
89 }
90
91 results
92 }
93
94 pub fn load_file(&self, path: &Path) -> Result<SkillDefinition, ParseError> {
96 let content = std::fs::read_to_string(path)
97 .map_err(|e| ParseError::InvalidYaml(format!("Failed to read file: {}", e)))?;
98 parse_skill_md(&content)
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn test_skill_registry_register_and_get() {
108 let mut registry = SkillRegistry::new();
109 let skill = SkillDefinition {
110 name: "test".into(),
111 version: "1.0.0".into(),
112 description: "Test skill".into(),
113 author: None,
114 requires: vec![],
115 tools: vec![],
116 config: SkillConfig::default(),
117 risk_level: SkillRiskLevel::Low,
118 source_path: None,
119 };
120
121 registry.register(skill);
122 assert_eq!(registry.len(), 1);
123 assert!(!registry.is_empty());
124
125 let found = registry.get("test").unwrap();
126 assert_eq!(found.version, "1.0.0");
127 }
128
129 #[test]
130 fn test_skill_registry_list_names() {
131 let mut registry = SkillRegistry::new();
132 for name in &["alpha", "beta", "gamma"] {
133 registry.register(SkillDefinition {
134 name: name.to_string(),
135 version: "1.0.0".into(),
136 description: "".into(),
137 author: None,
138 requires: vec![],
139 tools: vec![],
140 config: Default::default(),
141 risk_level: SkillRiskLevel::Low,
142 source_path: None,
143 });
144 }
145 let names = registry.list_names();
146 assert_eq!(names.len(), 3);
147 }
148
149 #[test]
150 fn test_skill_loader_scan_empty_dir() {
151 let dir = tempfile::TempDir::new().unwrap();
152 let loader = SkillLoader::new(dir.path());
153 let results = loader.scan();
154 assert!(results.is_empty());
155 }
156
157 #[test]
158 fn test_skill_loader_scan_with_file() {
159 let dir = tempfile::TempDir::new().unwrap();
160 let skill_path = dir.path().join("test.skill.md");
161 std::fs::write(
162 &skill_path,
163 "---\nname: test-skill\nversion: 1.0.0\ndescription: A test\n---\n",
164 )
165 .unwrap();
166
167 let loader = SkillLoader::new(dir.path());
168 let results = loader.scan();
169 assert_eq!(results.len(), 1);
170 let skill = results[0].as_ref().unwrap();
171 assert_eq!(skill.name, "test-skill");
172 }
173
174 #[test]
175 fn test_skill_loader_scan_nonexistent_dir() {
176 let loader = SkillLoader::new("/nonexistent/path");
177 let results = loader.scan();
178 assert!(results.is_empty());
179 }
180}