use regex::Regex;
use std::collections::HashSet;
pub fn validate_semver(version: &str) -> Result<(), ValidationError> {
let semver_regex = Regex::new(r"^\d+\.\d+\.\d+$")
.map_err(|e| ValidationError::Internal(format!("Failed to compile semver regex: {}", e)))?;
if !semver_regex.is_match(version) {
return Err(ValidationError::InvalidSemver(version.to_string()));
}
Ok(())
}
pub fn validate_identifier(identifier: &str) -> Result<(), ValidationError> {
if identifier.is_empty() {
return Err(ValidationError::EmptyIdentifier);
}
let identifier_regex = Regex::new(r"^[a-zA-Z0-9_-]+$").map_err(|e| {
ValidationError::Internal(format!("Failed to compile identifier regex: {}", e))
})?;
if !identifier_regex.is_match(identifier) {
return Err(ValidationError::InvalidIdentifier(identifier.to_string()));
}
Ok(())
}
pub fn validate_project_structure(
has_metadata: bool,
has_dependencies: bool,
) -> Result<(), ValidationError> {
if !has_metadata && !has_dependencies {
return Err(ValidationError::MissingSections);
}
Ok(())
}
pub fn validate_uniqueness(
name: &str,
version: &str,
existing_skills: &HashSet<(String, String)>,
) -> Result<(), ValidationError> {
validate_identifier(name)?;
validate_semver(version)?;
let key = (name.to_string(), version.to_string());
if existing_skills.contains(&key) {
return Err(ValidationError::DuplicateSkill {
name: name.to_string(),
version: version.to_string(),
});
}
Ok(())
}
#[derive(Debug, thiserror::Error)]
pub enum ValidationError {
#[error("Invalid semantic version format: {0}. Expected MAJOR.MINOR.PATCH")]
InvalidSemver(String),
#[error("Invalid identifier: {0}. Must contain only alphanumeric characters, hyphens, and underscores")]
InvalidIdentifier(String),
#[error("Empty identifier not allowed")]
EmptyIdentifier,
#[error("skill-project.toml must have at least one section ([metadata] or [dependencies])")]
MissingSections,
#[error("Duplicate skill: {name}@{version} already exists")]
DuplicateSkill { name: String, version: String },
#[error("Internal validation error: {0}")]
Internal(String),
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn test_validate_semver_valid() {
assert!(validate_semver("1.0.0").is_ok());
assert!(validate_semver("0.1.0").is_ok());
assert!(validate_semver("10.20.30").is_ok());
assert!(validate_semver("999.999.999").is_ok());
assert!(validate_semver("0.0.1").is_ok());
}
#[test]
fn test_validate_semver_invalid() {
assert!(validate_semver("1.0").is_err());
assert!(validate_semver("1").is_err());
assert!(validate_semver("v1.0.0").is_err());
assert!(validate_semver("1.0.0-beta").is_err());
assert!(validate_semver("1.0.0+metadata").is_err());
assert!(validate_semver("invalid").is_err());
assert!(validate_semver("1.0.0.0").is_err());
assert!(validate_semver("1.0").is_err());
}
#[test]
fn test_validate_semver_edge_cases() {
assert!(validate_semver("").is_err());
let long_version = "1".repeat(100) + ".0.0";
assert!(validate_semver(&long_version).is_ok());
assert!(validate_semver("1.0.0-alpha").is_err());
assert!(validate_semver("1.0.0_beta").is_err());
assert!(validate_semver("1.0.0.beta").is_err());
if let Err(ValidationError::InvalidSemver(_)) = validate_semver("1.0") {
} else {
panic!("Expected InvalidSemver error");
}
}
#[test]
fn test_validate_identifier_valid() {
assert!(validate_identifier("my-skill").is_ok());
assert!(validate_identifier("my_skill").is_ok());
assert!(validate_identifier("skill123").is_ok());
assert!(validate_identifier("skill-123_test").is_ok());
assert!(validate_identifier("a").is_ok());
assert!(validate_identifier("A").is_ok());
assert!(validate_identifier("123").is_ok());
assert!(validate_identifier(&"a".repeat(100)).is_ok());
}
#[test]
fn test_validate_identifier_invalid() {
assert!(validate_identifier("").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()); assert!(validate_identifier("my!skill").is_err()); }
#[test]
fn test_validate_identifier_edge_cases() {
assert!(validate_identifier("").is_err());
if let Err(ValidationError::EmptyIdentifier) = validate_identifier("") {
} else {
panic!("Expected EmptyIdentifier error");
}
assert!(validate_identifier("a").is_ok());
assert!(validate_identifier("_").is_ok());
assert!(validate_identifier("-").is_ok());
let long_id = "a".repeat(1000);
assert!(validate_identifier(&long_id).is_ok());
assert!(validate_identifier("MySkill_123-Test").is_ok());
}
#[test]
fn test_validate_project_structure_valid() {
assert!(validate_project_structure(true, false).is_ok());
assert!(validate_project_structure(false, true).is_ok());
assert!(validate_project_structure(true, true).is_ok());
}
#[test]
fn test_validate_project_structure_invalid() {
assert!(validate_project_structure(false, false).is_err());
if let Err(ValidationError::MissingSections) = validate_project_structure(false, false) {
} else {
panic!("Expected MissingSections error");
}
}
#[test]
fn test_validate_uniqueness_unique() {
let mut existing_skills = HashSet::new();
existing_skills.insert(("other-skill".to_string(), "1.0.0".to_string()));
assert!(validate_uniqueness("my-skill", "1.0.0", &existing_skills).is_ok());
assert!(validate_uniqueness("other-skill", "2.0.0", &existing_skills).is_ok());
assert!(validate_uniqueness("new-skill", "1.0.0", &existing_skills).is_ok());
}
#[test]
fn test_validate_uniqueness_duplicate() {
let mut existing_skills = HashSet::new();
existing_skills.insert(("my-skill".to_string(), "1.0.0".to_string()));
assert!(validate_uniqueness("my-skill", "1.0.0", &existing_skills).is_err());
if let Err(ValidationError::DuplicateSkill { name, version }) =
validate_uniqueness("my-skill", "1.0.0", &existing_skills)
{
assert_eq!(name, "my-skill");
assert_eq!(version, "1.0.0");
} else {
panic!("Expected DuplicateSkill error");
}
}
#[test]
fn test_validate_uniqueness_invalid_inputs() {
let existing_skills = HashSet::new();
assert!(validate_uniqueness("", "1.0.0", &existing_skills).is_err());
assert!(validate_uniqueness("my skill", "1.0.0", &existing_skills).is_err());
assert!(validate_uniqueness("my-skill", "1.0", &existing_skills).is_err());
assert!(validate_uniqueness("my-skill", "invalid", &existing_skills).is_err());
}
#[test]
fn test_validate_uniqueness_edge_cases() {
let mut existing_skills = HashSet::new();
existing_skills.insert(("skill".to_string(), "0.0.1".to_string()));
let empty_set = HashSet::new();
assert!(validate_uniqueness("new-skill", "1.0.0", &empty_set).is_ok());
existing_skills.insert(("skill2".to_string(), "2.0.0".to_string()));
existing_skills.insert(("skill3".to_string(), "3.0.0".to_string()));
assert!(validate_uniqueness("skill4", "4.0.0", &existing_skills).is_ok());
assert!(validate_uniqueness("skill", "0.0.1", &existing_skills).is_err());
}
}