rusty_schema_diff/analyzer/
json_schema.rs

1//! JSON Schema specific analyzer implementation
2//!
3//! This module provides functionality for analyzing JSON Schema changes and
4//! generating compatibility reports and migration paths.
5
6use crate::analyzer::{SchemaAnalyzer, SchemaChange, ChangeType};
7use crate::{Schema, CompatibilityReport, MigrationPlan, ValidationResult};
8use crate::error::Result;
9use serde_json::Value;
10use std::collections::HashMap;
11
12/// Analyzes JSON Schema changes and generates compatibility reports.
13pub struct JsonSchemaAnalyzer;
14
15impl SchemaAnalyzer for JsonSchemaAnalyzer {
16    /// Analyzes compatibility between two JSON Schema versions.
17    ///
18    /// # Arguments
19    ///
20    /// * `old` - The original JSON Schema version.
21    /// * `new` - The new JSON Schema version to compare against.
22    ///
23    /// # Returns
24    ///
25    /// A `CompatibilityReport` detailing the differences and compatibility status.
26    fn analyze_compatibility(&self, old: &Schema, new: &Schema) -> Result<CompatibilityReport> {
27        let old_schema: Value = serde_json::from_str(&old.content)?;
28        let new_schema: Value = serde_json::from_str(&new.content)?;
29
30        let mut changes = Vec::new();
31        self.compare_schemas(&old_schema, &new_schema, "", &mut changes);
32
33        let compatibility_score = self.calculate_compatibility_score(&changes);
34        let is_compatible = compatibility_score >= 80;
35
36        Ok(CompatibilityReport {
37            changes,
38            compatibility_score,
39            is_compatible,
40            issues: vec![],  // TODO: Implement issue detection
41            metadata: Default::default(),
42        })
43    }
44
45    /// Generates a migration path between JSON Schema versions.
46    ///
47    /// # Arguments
48    ///
49    /// * `old` - The source JSON Schema version.
50    /// * `new` - The target JSON Schema version.
51    ///
52    /// # Returns
53    ///
54    /// A `MigrationPlan` detailing the required changes.
55    fn generate_migration_path(&self, old: &Schema, new: &Schema) -> Result<MigrationPlan> {
56        let mut changes = Vec::new();
57        let old_schema: Value = serde_json::from_str(&old.content)?;
58        let new_schema: Value = serde_json::from_str(&new.content)?;
59
60        self.compare_schemas(&old_schema, &new_schema, "", &mut changes);
61
62        Ok(MigrationPlan::new(
63            old.version.to_string(),
64            new.version.to_string(),
65            changes,
66        ))
67    }
68
69    fn validate_changes(&self, _changes: &[SchemaChange]) -> Result<ValidationResult> {
70        Ok(ValidationResult {
71            is_valid: true,
72            errors: Vec::new(),
73            context: HashMap::new(),
74        })
75    }
76}
77
78impl JsonSchemaAnalyzer {
79    /// Compares two JSON schemas and collects changes
80    fn compare_schemas(&self, old: &Value, new: &Value, path: &str, changes: &mut Vec<SchemaChange>) {
81        match (old, new) {
82            (Value::Object(old_obj), Value::Object(new_obj)) => {
83                self.compare_objects(old_obj, new_obj, path, changes);
84            }
85            (Value::Array(old_arr), Value::Array(new_arr)) => {
86                self.compare_arrays(old_arr, new_arr, path, changes);
87            }
88            _ if old != new => {
89                let mut metadata = HashMap::new();
90                metadata.insert("old_value".to_string(), old.to_string());
91                metadata.insert("new_value".to_string(), new.to_string());
92                
93                changes.push(SchemaChange::new(
94                    ChangeType::Modification,
95                    path.to_string(),
96                    format!("Value changed from {:?} to {:?}", old, new),
97                    metadata,
98                ));
99            }
100            _ => {}
101        }
102    }
103
104    fn calculate_compatibility_score(&self, changes: &[SchemaChange]) -> u8 {
105        let base_score: u8 = 100;
106        let mut deductions: u8 = 0;
107        
108        for change in changes {
109            match change.change_type {
110                ChangeType::Addition => deductions = deductions.saturating_add(5),
111                ChangeType::Removal => deductions = deductions.saturating_add(20),
112                ChangeType::Modification => deductions = deductions.saturating_add(10),
113                ChangeType::Rename => deductions = deductions.saturating_add(8),
114            }
115        }
116        
117        base_score.saturating_sub(deductions)
118    }
119
120    #[allow(dead_code)]
121    fn detect_schema_changes(&self, path: &str, old_schema: &Value, new_schema: &Value, changes: &mut Vec<SchemaChange>) {
122        match (old_schema, new_schema) {
123            (Value::Object(old_obj), Value::Object(new_obj)) => {
124                // Compare properties
125                for (key, old_value) in old_obj {
126                    if let Some(new_value) = new_obj.get(key) {
127                        if old_value != new_value {
128                            let mut metadata = HashMap::new();
129                            metadata.insert("property".to_string(), key.clone());
130                            
131                            changes.push(SchemaChange::new(
132                                ChangeType::Modification,
133                                format!("{}/{}", path, key),
134                                format!("Property '{}' was modified", key),
135                                metadata,
136                            ));
137                        }
138                    } else {
139                        let mut metadata = HashMap::new();
140                        metadata.insert("property".to_string(), key.clone());
141                        
142                        changes.push(SchemaChange::new(
143                            ChangeType::Removal,
144                            format!("{}/{}", path, key),
145                            format!("Property '{}' was removed", key),
146                            metadata,
147                        ));
148                    }
149                }
150
151                // Check for new properties
152                for key in new_obj.keys() {
153                    if !old_obj.contains_key(key) {
154                        let mut metadata = HashMap::new();
155                        metadata.insert("property".to_string(), key.clone());
156                        
157                        changes.push(SchemaChange::new(
158                            ChangeType::Addition,
159                            format!("{}/{}", path, key),
160                            format!("New property '{}' was added", key),
161                            metadata,
162                        ));
163                    }
164                }
165            }
166            (old_val, new_val) if old_val != new_val => {
167                let mut metadata = HashMap::new();
168                metadata.insert("old_value".to_string(), old_val.to_string());
169                metadata.insert("new_value".to_string(), new_val.to_string());
170                
171                changes.push(SchemaChange::new(
172                    ChangeType::Modification,
173                    path.to_string(),
174                    format!("Value changed from {:?} to {:?}", old_val, new_val),
175                    metadata,
176                ));
177            }
178            _ => {}
179        }
180    }
181
182    fn compare_objects(&self, old_obj: &serde_json::Map<String, Value>, new_obj: &serde_json::Map<String, Value>, path: &str, changes: &mut Vec<SchemaChange>) {
183        // Compare properties
184        for (key, old_value) in old_obj {
185            if let Some(new_value) = new_obj.get(key) {
186                self.compare_schemas(old_value, new_value, &format!("{}/{}", path, key), changes);
187            } else {
188                let mut metadata = HashMap::new();
189                metadata.insert("property".to_string(), key.clone());
190                
191                changes.push(SchemaChange::new(
192                    ChangeType::Removal,
193                    format!("{}/{}", path, key),
194                    format!("Property '{}' was removed", key),
195                    metadata,
196                ));
197            }
198        }
199
200        // Check for new properties
201        for key in new_obj.keys() {
202            if !old_obj.contains_key(key) {
203                let mut metadata = HashMap::new();
204                metadata.insert("property".to_string(), key.clone());
205                
206                changes.push(SchemaChange::new(
207                    ChangeType::Addition,
208                    format!("{}/{}", path, key),
209                    format!("New property '{}' was added", key),
210                    metadata,
211                ));
212            }
213        }
214    }
215
216    fn compare_arrays(&self, old_arr: &[Value], new_arr: &[Value], path: &str, changes: &mut Vec<SchemaChange>) {
217        if old_arr.len() != new_arr.len() {
218            let mut metadata = HashMap::new();
219            metadata.insert("old_length".to_string(), old_arr.len().to_string());
220            metadata.insert("new_length".to_string(), new_arr.len().to_string());
221            
222            changes.push(SchemaChange::new(
223                ChangeType::Modification,
224                path.to_string(),
225                format!("Array length changed from {} to {}", old_arr.len(), new_arr.len()),
226                metadata,
227            ));
228        }
229
230        for (i, (old_value, new_value)) in old_arr.iter().zip(new_arr.iter()).enumerate() {
231            self.compare_schemas(old_value, new_value, &format!("{}/{}", path, i), changes);
232        }
233    }
234}