claude_priority/validators/
json.rs

1use crate::models::{CheckType, MarketplaceJson, ValidationResult};
2use regex::Regex;
3use std::fs;
4use std::path::Path;
5
6const SEMVER_PATTERN: &str = r"^[0-9]+\.[0-9]+\.[0-9]+$";
7
8pub fn validate_json(plugin_path: &Path) -> ValidationResult {
9    let mut result = ValidationResult::new(CheckType::Json);
10    let marketplace_json = plugin_path.join("marketplace.json");
11
12    if !marketplace_json.exists() {
13        result.add_error("marketplace.json not found".to_string());
14        return result;
15    }
16
17    // Read and parse JSON
18    let content = match fs::read_to_string(&marketplace_json) {
19        Ok(c) => c,
20        Err(e) => {
21            result.add_error(format!("Failed to read marketplace.json: {}", e));
22            return result;
23        }
24    };
25
26    let marketplace: MarketplaceJson = match serde_json::from_str(&content) {
27        Ok(m) => m,
28        Err(e) => {
29            result.add_error(format!("marketplace.json is not valid JSON: {}", e));
30            return result;
31        }
32    };
33
34    // Validate required fields
35    if marketplace.name.is_empty() {
36        result.add_error("marketplace.json missing or has empty 'name' field".to_string());
37    }
38
39    if marketplace.version.is_empty() {
40        result.add_error("marketplace.json missing or has empty 'version' field".to_string());
41    } else {
42        // Validate semantic versioning
43        let semver_pattern = Regex::new(SEMVER_PATTERN).unwrap();
44        if !semver_pattern.is_match(&marketplace.version) {
45            result.add_error(format!(
46                "Version must follow semantic versioning (x.y.z): {}",
47                marketplace.version
48            ));
49        }
50    }
51
52    if marketplace.description.is_empty() {
53        result.add_error("marketplace.json missing or has empty 'description' field".to_string());
54    }
55
56    // Validate author field (should be object, not string)
57    if marketplace.author.is_string() {
58        result.add_warning("'author' field should be an object, not a string".to_string());
59    } else if !marketplace.author.is_object() {
60        result.add_error("'author' field must be an object".to_string());
61    }
62
63    // Validate skills array if present
64    if let Some(skills) = &marketplace.skills {
65        if !skills.is_empty() {
66            for (idx, skill) in skills.iter().enumerate() {
67                if !skill.is_object() {
68                    result.add_error(format!(
69                        "skills[{}] must be an object",
70                        idx
71                    ));
72                }
73            }
74        }
75    }
76
77    // Check for plugin.json if it exists
78    let plugin_json = plugin_path.join(".claude-plugin").join("plugin.json");
79    if plugin_json.exists() {
80        if let Ok(content) = fs::read_to_string(&plugin_json) {
81            if serde_json::from_str::<serde_json::Value>(&content).is_err() {
82                result.add_error("plugin.json is not valid JSON".to_string());
83            }
84        }
85    }
86
87    result
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use std::io::Write;
94    use tempfile::TempDir;
95
96    #[test]
97    fn test_validate_json_missing_file() {
98        let temp_dir = TempDir::new().unwrap();
99        let result = validate_json(temp_dir.path());
100        assert!(!result.passed);
101        assert!(result.errors.iter().any(|e| e.contains("marketplace.json not found")));
102    }
103
104    #[test]
105    fn test_validate_json_valid() {
106        let temp_dir = TempDir::new().unwrap();
107        let marketplace_json = temp_dir.path().join("marketplace.json");
108
109        let content = r#"{
110            "name": "test-plugin",
111            "version": "1.0.0",
112            "description": "Test plugin",
113            "author": {
114                "name": "Test Author"
115            }
116        }"#;
117
118        let mut file = fs::File::create(&marketplace_json).unwrap();
119        file.write_all(content.as_bytes()).unwrap();
120
121        let result = validate_json(temp_dir.path());
122        assert!(result.passed, "Expected validation to pass, errors: {:?}", result.errors);
123    }
124
125    #[test]
126    fn test_validate_json_invalid_semver() {
127        let temp_dir = TempDir::new().unwrap();
128        let marketplace_json = temp_dir.path().join("marketplace.json");
129
130        let content = r#"{
131            "name": "test-plugin",
132            "version": "1.0",
133            "description": "Test plugin",
134            "author": {
135                "name": "Test Author"
136            }
137        }"#;
138
139        let mut file = fs::File::create(&marketplace_json).unwrap();
140        file.write_all(content.as_bytes()).unwrap();
141
142        let result = validate_json(temp_dir.path());
143        assert!(!result.passed);
144        assert!(result.errors.iter().any(|e| e.contains("semantic versioning")));
145    }
146}