1use serde::{Deserialize, Serialize};
6use std::fmt;
7use std::path::PathBuf;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub enum ValidationLevel {
12 Error,
14 Warning,
16 Info,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ValidationIssue {
23 level: ValidationLevel,
24 pub(crate) field: Option<String>,
25 message: String,
26 suggestion: Option<String>,
27}
28
29impl fmt::Display for ValidationIssue {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 write!(f, "{}", self.message)?;
32 if let Some(field) = &self.field {
33 write!(f, " (field: {field})")?;
34 }
35 if let Some(suggestion) = &self.suggestion {
36 write!(f, " ā {suggestion}")?;
37 }
38 Ok(())
39 }
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct SkillValidationReport {
45 skill_name: String,
46 skill_path: PathBuf,
47 pub is_valid: bool,
48 errors: Vec<ValidationIssue>,
49 warnings: Vec<ValidationIssue>,
50 pub(crate) suggestions: Vec<ValidationIssue>,
51 pub stats: ValidationStats,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct ValidationStats {
56 total_issues: usize,
57 pub error_count: usize,
58 warning_count: usize,
59 suggestion_count: usize,
60 token_estimate: usize,
61}
62
63impl SkillValidationReport {
64 pub(crate) fn new(skill_name: String, skill_path: PathBuf) -> Self {
65 Self {
66 skill_name,
67 skill_path,
68 is_valid: true,
69 errors: Vec::new(),
70 warnings: Vec::new(),
71 suggestions: Vec::new(),
72 stats: ValidationStats {
73 total_issues: 0,
74 error_count: 0,
75 warning_count: 0,
76 suggestion_count: 0,
77 token_estimate: 0,
78 },
79 }
80 }
81
82 pub(crate) fn add_error(&mut self, field: Option<String>, message: String, suggestion: Option<String>) {
83 self.errors.push(ValidationIssue {
84 level: ValidationLevel::Error,
85 field,
86 message,
87 suggestion,
88 });
89 self.is_valid = false;
90 }
91
92 pub(crate) fn add_warning(&mut self, field: Option<String>, message: String, suggestion: Option<String>) {
93 self.warnings.push(ValidationIssue {
94 level: ValidationLevel::Warning,
95 field,
96 message,
97 suggestion,
98 });
99 }
100
101 pub(crate) fn add_suggestion(&mut self, field: Option<String>, message: String) {
102 self.suggestions.push(ValidationIssue {
103 level: ValidationLevel::Info,
104 field,
105 message,
106 suggestion: None,
107 });
108 }
109
110 pub fn finalize(&mut self) {
111 self.stats.error_count = self.errors.len();
112 self.stats.warning_count = self.warnings.len();
113 self.stats.suggestion_count = self.suggestions.len();
114 self.stats.total_issues = self.stats.error_count + self.stats.warning_count + self.stats.suggestion_count;
115 }
116
117 pub fn generate_summary(&self) -> String {
118 let mut summary = String::new();
119
120 summary.push_str(&format!("Skill: {}\n", self.skill_name));
121 summary.push_str(&format!("Path: {}\n", self.skill_path.display()));
122 summary.push_str(&format!("Status: {}\n", if self.is_valid { "v Valid" } else { "x Invalid" }));
123 summary.push_str("\nIssues found:\n");
124 summary.push_str(&format!(" Errors: {}\n", self.stats.error_count));
125 summary.push_str(&format!(" Warnings: {}\n", self.stats.warning_count));
126 summary.push_str(&format!(" Suggestions: {}\n", self.stats.suggestion_count));
127
128 if !self.errors.is_empty() {
129 summary.push_str("\nx Errors:\n");
130 for error in &self.errors {
131 summary.push_str(&format!(" - {}", error.message));
132 if let Some(field) = &error.field {
133 summary.push_str(&format!(" [{field}]"));
134 }
135 summary.push('\n');
136 if let Some(suggestion) = &error.suggestion {
137 summary.push_str(&format!(" š” Suggestion: {suggestion}\n"));
138 }
139 }
140 }
141
142 if !self.warnings.is_empty() {
143 summary.push_str("\n[!] Warnings:\n");
144 for warning in &self.warnings {
145 summary.push_str(&format!(" - {}", warning.message));
146 if let Some(field) = &warning.field {
147 summary.push_str(&format!(" [{field}]"));
148 }
149 summary.push('\n');
150 }
151 }
152
153 if !self.suggestions.is_empty() {
154 summary.push_str("\nš” Suggestions:\n");
155 for suggestion in &self.suggestions {
156 summary.push_str(&format!(" - {}", suggestion.message));
157 if let Some(field) = &suggestion.field {
158 summary.push_str(&format!(" [{field}]"));
159 }
160 summary.push('\n');
161 }
162 }
163
164 summary
165 }
166
167 pub fn to_json(&self) -> Result<String, serde_json::Error> {
168 serde_json::to_string_pretty(self)
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use std::path::PathBuf;
176
177 #[test]
178 fn test_validation_report() {
179 let mut report = SkillValidationReport::new("test-skill".to_string(), PathBuf::from("/tmp/test-skill"));
180
181 report.add_error(
182 Some("name".to_string()),
183 "Invalid characters in name".to_string(),
184 Some("Use only lowercase letters, numbers, and hyphens".to_string()),
185 );
186
187 report.add_warning(
188 None,
189 "Description is very short".to_string(),
190 Some("Consider adding more detail about when to use this skill".to_string()),
191 );
192
193 report.finalize();
194
195 assert!(!report.is_valid);
196 assert_eq!(report.stats.error_count, 1);
197 assert_eq!(report.stats.warning_count, 1);
198 assert!(report.generate_summary().contains("Invalid characters"));
199 }
200}