1use crate::schema::*;
5use crate::types::Result;
6use kotoba_errors::KotobaError;
7use serde_json::Value;
8use jsonschema::{Draft, JSONSchema};
9use std::fs;
10use std::path::Path;
11
12#[derive(Debug)]
14pub struct SchemaValidator {
15 schema: JSONSchema,
16 schema_text: String,
17}
18
19impl SchemaValidator {
20 pub fn new() -> Result<Self> {
22 let schema_path = Path::new("schemas/process-network-schema.json");
23 let schema_text = fs::read_to_string(schema_path)
24 .map_err(|e| KotobaError::Io(std::io::Error::new(
25 std::io::ErrorKind::NotFound,
26 format!("Failed to read schema file: {}", e)
27 )))?;
28
29 let schema_value: serde_json::Value = serde_json::from_str(&schema_text)
30 .map_err(|e| KotobaError::Parse(format!("Invalid schema JSON: {}", e)))?;
31
32 let schema = JSONSchema::compile(&schema_value)
33 .map_err(|e| KotobaError::Validation(format!("Schema compilation failed: {:?}", e)))?;
34
35 Ok(Self {
36 schema,
37 schema_text,
38 })
39 }
40
41 pub fn validate<T: serde::Serialize>(&self, data: &T) -> Result<()> {
43 let json_value = serde_json::to_value(data)
44 .map_err(|e| KotobaError::Parse(format!("Data serialization failed: {}", e)))?;
45
46 let validation_result = self.schema.validate(&json_value);
47
48 if let Err(errors) = validation_result {
49 let error_messages: Vec<String> = errors
50 .map(|error| format!("{}", error))
51 .collect();
52
53 return Err(KotobaError::Validation(format!(
54 "Schema validation failed:\n{}",
55 error_messages.join("\n")
56 )));
57 }
58
59 Ok(())
60 }
61
62 pub fn validate_process_network(&self, network: &ProcessNetwork) -> Result<()> {
64 self.validate(network)
65 }
66
67 pub fn validate_graph_instance(&self, graph: &GraphInstance) -> Result<()> {
69 self.validate(graph)
70 }
71
72 pub fn validate_rule_dpo(&self, rule: &RuleDPO) -> Result<()> {
74 self.validate(rule)
75 }
76
77 pub fn validate_with_detailed_report<T: serde::Serialize>(&self, data: &T) -> ValidationReport {
79 let json_value = match serde_json::to_value(data) {
80 Ok(v) => v,
81 Err(e) => return ValidationReport {
82 is_valid: false,
83 errors: vec![format!("Data serialization failed: {}", e)],
84 warnings: vec![],
85 },
86 };
87
88 let validation_result = self.schema.validate(&json_value);
89
90 match validation_result {
91 Ok(_) => ValidationReport {
92 is_valid: true,
93 errors: vec![],
94 warnings: vec![],
95 },
96 Err(errors) => {
97 let error_messages: Vec<String> = errors
98 .map(|error| format!("{}", error))
99 .collect();
100
101 ValidationReport {
102 is_valid: false,
103 errors: error_messages,
104 warnings: vec![],
105 }
106 }
107 }
108 }
109
110 pub fn schema_text(&self) -> &str {
112 &self.schema_text
113 }
114
115 pub fn get_schema_version(&self) -> Result<String> {
117 let schema_value: serde_json::Value = serde_json::from_str(&self.schema_text)
118 .map_err(|e| KotobaError::Parse(format!("Schema parse error: {}", e)))?;
119
120 if let Some(version) = schema_value.get("version") {
121 if let Some(version_str) = version.as_str() {
122 Ok(version_str.to_string())
123 } else {
124 Ok("unknown".to_string())
125 }
126 } else {
127 Ok("0.1.0".to_string())
128 }
129 }
130}
131
132#[derive(Debug, Clone)]
134pub struct ValidationReport {
135 pub is_valid: bool,
136 pub errors: Vec<String>,
137 pub warnings: Vec<String>,
138}
139
140impl ValidationReport {
141 pub fn error_report(&self) -> String {
143 if self.errors.is_empty() {
144 "No errors found".to_string()
145 } else {
146 format!("Validation Errors:\n{}", self.errors.join("\n"))
147 }
148 }
149
150 pub fn warning_report(&self) -> String {
152 if self.warnings.is_empty() {
153 "No warnings found".to_string()
154 } else {
155 format!("Validation Warnings:\n{}", self.warnings.join("\n"))
156 }
157 }
158
159 pub fn full_report(&self) -> String {
161 let mut report = String::new();
162
163 if self.is_valid {
164 report.push_str("✅ Validation passed\n");
165 } else {
166 report.push_str("❌ Validation failed\n");
167 }
168
169 if !self.errors.is_empty() {
170 report.push_str(&format!("\nErrors:\n{}\n", self.errors.join("\n")));
171 }
172
173 if !self.warnings.is_empty() {
174 report.push_str(&format!("\nWarnings:\n{}\n", self.warnings.join("\n")));
175 }
176
177 report
178 }
179}
180
181pub mod utils {
183 use super::*;
184
185 pub fn validate_json_file<P: AsRef<Path>>(file_path: P, validator: &SchemaValidator) -> Result<ValidationReport> {
187 let json_text = fs::read_to_string(file_path)
188 .map_err(|e| KotobaError::Io(e))?;
189
190 let json_value: serde_json::Value = serde_json::from_str(&json_text)
191 .map_err(|e| KotobaError::Parse(format!("JSON parse error: {}", e)))?;
192
193 if let Ok(process_network) = serde_json::from_value::<ProcessNetwork>(json_value.clone()) {
195 let report = validator.validate_with_detailed_report(&process_network);
196 Ok(report)
197 } else {
198 let report = ValidationReport {
200 is_valid: false,
201 errors: vec!["Data does not match ProcessNetwork schema".to_string()],
202 warnings: vec!["Try validating individual components".to_string()],
203 };
204 Ok(report)
205 }
206 }
207
208 pub fn validate_json_directory<P: AsRef<Path>>(dir_path: P, validator: &SchemaValidator) -> Result<Vec<(String, ValidationReport)>> {
210 let dir_path = dir_path.as_ref();
211 let mut results = Vec::new();
212
213 if !dir_path.exists() || !dir_path.is_dir() {
214 return Err(KotobaError::Io(std::io::Error::new(
215 std::io::ErrorKind::NotFound,
216 "Directory not found"
217 )));
218 }
219
220 for entry in fs::read_dir(dir_path)
221 .map_err(|e| KotobaError::Io(e))?
222 {
223 let entry = entry.map_err(|e| KotobaError::Io(e))?;
224 let path = entry.path();
225
226 if path.extension().and_then(|s| s.to_str()) == Some("json") {
227 if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
228 match validate_json_file(&path, validator) {
229 Ok(report) => results.push((file_name.to_string(), report)),
230 Err(e) => results.push((file_name.to_string(), ValidationReport {
231 is_valid: false,
232 errors: vec![format!("File read error: {}", e)],
233 warnings: vec![],
234 })),
235 }
236 }
237 }
238 }
239
240 Ok(results)
241 }
242
243 pub fn check_schema_compatibility(validator: &SchemaValidator, data: &serde_json::Value) -> CompatibilityReport {
245 let validation_report = ValidationReport {
246 is_valid: validator.schema.validate(data).is_ok(),
247 errors: vec![],
248 warnings: vec![],
249 };
250
251 let schema_version = validator.get_schema_version().unwrap_or_else(|_| "unknown".to_string());
253
254 CompatibilityReport {
255 is_compatible: validation_report.is_valid,
256 schema_version,
257 validation_report,
258 }
259 }
260}
261
262#[derive(Debug)]
264pub struct CompatibilityReport {
265 pub is_compatible: bool,
266 pub schema_version: String,
267 pub validation_report: ValidationReport,
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273 use crate::schema::*;
274
275 #[test]
276 fn test_schema_validator_creation() {
277 let validator = SchemaValidator::new();
278 assert!(validator.is_ok());
279 }
280
281 #[test]
282 fn test_process_network_validation() {
283 let validator = SchemaValidator::new().unwrap();
284
285 let process_network = ProcessNetwork {
287 meta: Some(MetaInfo {
288 model: "GTS-DPO-OpenGraph-Merkle".to_string(),
289 version: "0.2.0".to_string(),
290 cid_algo: None,
291 }),
292 type_graph: GraphType {
293 core: GraphCore {
294 nodes: vec![],
295 edges: vec![],
296 boundary: None,
297 attrs: None,
298 },
299 kind: GraphKind::Graph,
300 cid: Cid::from_hex("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef").unwrap(),
301 typing: None,
302 },
303 graphs: vec![],
304 components: vec![],
305 rules: vec![],
306 strategies: vec![],
307 queries: vec![],
308 pg_view: None,
309 };
310
311 let result = validator.validate_process_network(&process_network);
312 assert!(result.is_ok());
313 }
314
315 #[test]
316 fn test_validation_report() {
317 let report = ValidationReport {
318 is_valid: false,
319 errors: vec!["Test error".to_string()],
320 warnings: vec!["Test warning".to_string()],
321 };
322
323 let full_report = report.full_report();
324 assert!(full_report.contains("❌ Validation failed"));
325 assert!(full_report.contains("Test error"));
326 assert!(full_report.contains("Test warning"));
327 }
328}