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