claude_priority/validators/
json.rs1use 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 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 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 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 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 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 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}