fastskill_core/core/
validation.rs1use regex::Regex;
4use std::collections::HashSet;
5
6pub fn validate_semver(version: &str) -> Result<(), ValidationError> {
8 let semver_regex = Regex::new(r"^\d+\.\d+\.\d+$")
9 .map_err(|e| ValidationError::Internal(format!("Failed to compile semver regex: {}", e)))?;
10
11 if !semver_regex.is_match(version) {
12 return Err(ValidationError::InvalidSemver(version.to_string()));
13 }
14
15 Ok(())
16}
17
18pub fn validate_identifier(identifier: &str) -> Result<(), ValidationError> {
20 if identifier.is_empty() {
21 return Err(ValidationError::EmptyIdentifier);
22 }
23
24 let identifier_regex = Regex::new(r"^[a-zA-Z0-9_-]+$").map_err(|e| {
25 ValidationError::Internal(format!("Failed to compile identifier regex: {}", e))
26 })?;
27
28 if !identifier_regex.is_match(identifier) {
29 return Err(ValidationError::InvalidIdentifier(identifier.to_string()));
30 }
31
32 Ok(())
33}
34
35pub fn validate_project_structure(
37 has_metadata: bool,
38 has_dependencies: bool,
39) -> Result<(), ValidationError> {
40 if !has_metadata && !has_dependencies {
41 return Err(ValidationError::MissingSections);
42 }
43
44 Ok(())
45}
46
47pub fn validate_uniqueness(
50 name: &str,
51 version: &str,
52 existing_skills: &HashSet<(String, String)>,
53) -> Result<(), ValidationError> {
54 validate_identifier(name)?;
56 validate_semver(version)?;
57
58 let key = (name.to_string(), version.to_string());
59 if existing_skills.contains(&key) {
60 return Err(ValidationError::DuplicateSkill {
61 name: name.to_string(),
62 version: version.to_string(),
63 });
64 }
65
66 Ok(())
67}
68
69#[derive(Debug, thiserror::Error)]
71pub enum ValidationError {
72 #[error("Invalid semantic version format: {0}. Expected MAJOR.MINOR.PATCH")]
73 InvalidSemver(String),
74
75 #[error("Invalid identifier: {0}. Must contain only alphanumeric characters, hyphens, and underscores")]
76 InvalidIdentifier(String),
77
78 #[error("Empty identifier not allowed")]
79 EmptyIdentifier,
80
81 #[error("skill-project.toml must have at least one section ([metadata] or [dependencies])")]
82 MissingSections,
83
84 #[error("Duplicate skill: {name}@{version} already exists")]
85 DuplicateSkill { name: String, version: String },
86
87 #[error("Internal validation error: {0}")]
88 Internal(String),
89}
90
91#[cfg(test)]
92#[allow(clippy::unwrap_used, clippy::panic)]
93mod tests {
94 use super::*;
95
96 #[test]
98 fn test_validate_semver_valid() {
99 assert!(validate_semver("1.0.0").is_ok());
100 assert!(validate_semver("0.1.0").is_ok());
101 assert!(validate_semver("10.20.30").is_ok());
102 assert!(validate_semver("999.999.999").is_ok());
103 assert!(validate_semver("0.0.1").is_ok());
104 }
105
106 #[test]
107 fn test_validate_semver_invalid() {
108 assert!(validate_semver("1.0").is_err());
109 assert!(validate_semver("1").is_err());
110 assert!(validate_semver("v1.0.0").is_err());
111 assert!(validate_semver("1.0.0-beta").is_err());
112 assert!(validate_semver("1.0.0+metadata").is_err());
113 assert!(validate_semver("invalid").is_err());
114 assert!(validate_semver("1.0.0.0").is_err());
115 assert!(validate_semver("1.0").is_err());
116 }
117
118 #[test]
119 fn test_validate_semver_edge_cases() {
120 assert!(validate_semver("").is_err());
122
123 let long_version = "1".repeat(100) + ".0.0";
125 assert!(validate_semver(&long_version).is_ok());
127
128 assert!(validate_semver("1.0.0-alpha").is_err());
130 assert!(validate_semver("1.0.0_beta").is_err());
131 assert!(validate_semver("1.0.0.beta").is_err());
132
133 if let Err(ValidationError::InvalidSemver(_)) = validate_semver("1.0") {
135 } else {
137 panic!("Expected InvalidSemver error");
138 }
139 }
140
141 #[test]
143 fn test_validate_identifier_valid() {
144 assert!(validate_identifier("my-skill").is_ok());
145 assert!(validate_identifier("my_skill").is_ok());
146 assert!(validate_identifier("skill123").is_ok());
147 assert!(validate_identifier("skill-123_test").is_ok());
148 assert!(validate_identifier("a").is_ok());
149 assert!(validate_identifier("A").is_ok());
150 assert!(validate_identifier("123").is_ok());
151 assert!(validate_identifier(&"a".repeat(100)).is_ok());
152 }
153
154 #[test]
155 fn test_validate_identifier_invalid() {
156 assert!(validate_identifier("").is_err());
157 assert!(validate_identifier("my skill").is_err()); assert!(validate_identifier("my.skill").is_err()); assert!(validate_identifier("my@skill").is_err()); assert!(validate_identifier("my#skill").is_err()); assert!(validate_identifier("my!skill").is_err()); }
163
164 #[test]
165 fn test_validate_identifier_edge_cases() {
166 assert!(validate_identifier("").is_err());
168 if let Err(ValidationError::EmptyIdentifier) = validate_identifier("") {
169 } else {
171 panic!("Expected EmptyIdentifier error");
172 }
173
174 assert!(validate_identifier("a").is_ok());
176 assert!(validate_identifier("_").is_ok());
177 assert!(validate_identifier("-").is_ok());
178
179 let long_id = "a".repeat(1000);
181 assert!(validate_identifier(&long_id).is_ok());
182
183 assert!(validate_identifier("MySkill_123-Test").is_ok());
185 }
186
187 #[test]
189 fn test_validate_project_structure_valid() {
190 assert!(validate_project_structure(true, false).is_ok());
192
193 assert!(validate_project_structure(false, true).is_ok());
195
196 assert!(validate_project_structure(true, true).is_ok());
198 }
199
200 #[test]
201 fn test_validate_project_structure_invalid() {
202 assert!(validate_project_structure(false, false).is_err());
204
205 if let Err(ValidationError::MissingSections) = validate_project_structure(false, false) {
206 } else {
208 panic!("Expected MissingSections error");
209 }
210 }
211
212 #[test]
214 fn test_validate_uniqueness_unique() {
215 let mut existing_skills = HashSet::new();
216 existing_skills.insert(("other-skill".to_string(), "1.0.0".to_string()));
217
218 assert!(validate_uniqueness("my-skill", "1.0.0", &existing_skills).is_ok());
220
221 assert!(validate_uniqueness("other-skill", "2.0.0", &existing_skills).is_ok());
223
224 assert!(validate_uniqueness("new-skill", "1.0.0", &existing_skills).is_ok());
226 }
227
228 #[test]
229 fn test_validate_uniqueness_duplicate() {
230 let mut existing_skills = HashSet::new();
231 existing_skills.insert(("my-skill".to_string(), "1.0.0".to_string()));
232
233 assert!(validate_uniqueness("my-skill", "1.0.0", &existing_skills).is_err());
235
236 if let Err(ValidationError::DuplicateSkill { name, version }) =
237 validate_uniqueness("my-skill", "1.0.0", &existing_skills)
238 {
239 assert_eq!(name, "my-skill");
240 assert_eq!(version, "1.0.0");
241 } else {
242 panic!("Expected DuplicateSkill error");
243 }
244 }
245
246 #[test]
247 fn test_validate_uniqueness_invalid_inputs() {
248 let existing_skills = HashSet::new();
249
250 assert!(validate_uniqueness("", "1.0.0", &existing_skills).is_err());
252 assert!(validate_uniqueness("my skill", "1.0.0", &existing_skills).is_err());
253
254 assert!(validate_uniqueness("my-skill", "1.0", &existing_skills).is_err());
256 assert!(validate_uniqueness("my-skill", "invalid", &existing_skills).is_err());
257 }
258
259 #[test]
260 fn test_validate_uniqueness_edge_cases() {
261 let mut existing_skills = HashSet::new();
262 existing_skills.insert(("skill".to_string(), "0.0.1".to_string()));
263
264 let empty_set = HashSet::new();
266 assert!(validate_uniqueness("new-skill", "1.0.0", &empty_set).is_ok());
267
268 existing_skills.insert(("skill2".to_string(), "2.0.0".to_string()));
270 existing_skills.insert(("skill3".to_string(), "3.0.0".to_string()));
271 assert!(validate_uniqueness("skill4", "4.0.0", &existing_skills).is_ok());
272 assert!(validate_uniqueness("skill", "0.0.1", &existing_skills).is_err());
273 }
274}