kotoba_core/
schema_validator.rs

1//! JSON Schemaバリデーション機能
2//! プロジェクトの公式JSON Schemaによるデータ検証
3
4use 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/// JSON Schemaバリデーター
13#[derive(Debug)]
14pub struct SchemaValidator {
15    schema: JSONSchema,
16    schema_text: String,
17}
18
19impl SchemaValidator {
20    /// 公式JSON Schemaからバリデーターを作成
21    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    /// データをJSON Schemaで検証
42    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    /// ProcessNetworkデータを検証
63    pub fn validate_process_network(&self, network: &ProcessNetwork) -> Result<()> {
64        self.validate(network)
65    }
66
67    /// GraphInstanceデータを検証
68    pub fn validate_graph_instance(&self, graph: &GraphInstance) -> Result<()> {
69        self.validate(graph)
70    }
71
72    /// RuleDPOデータを検証
73    pub fn validate_rule_dpo(&self, rule: &RuleDPO) -> Result<()> {
74        self.validate(rule)
75    }
76
77    /// バリデーションエラーを詳細にレポート
78    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    /// スキーマテキストを取得
111    pub fn schema_text(&self) -> &str {
112        &self.schema_text
113    }
114
115    /// スキーマのバージョンを取得
116    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/// バリデーション結果レポート
133#[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    /// エラーレポートを文字列として取得
142    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    /// 警告レポートを文字列として取得
151    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    /// 完全なレポートを文字列として取得
160    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
181/// スキーマ検証ユーティリティ関数
182pub mod utils {
183    use super::*;
184
185    /// ファイルからJSONを読み込んで検証
186    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        // ProcessNetworkとして検証を試みる
194        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            // 個別の構造体として検証を試みる
199            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    /// ディレクトリ内のすべてのJSONファイルを検証
209    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    /// スキーマの互換性をチェック
244    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        // スキーマバージョンチェック
252        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/// スキーマ互換性レポート
263#[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        // 有効なProcessNetworkデータ
286        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}