Skip to main content

mecha10_core/schema/
compatibility.rs

1//! Schema evolution and compatibility checking.
2
3use super::{MessageSchema, SchemaField};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// Schema compatibility mode
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
9pub enum CompatibilityMode {
10    /// No compatibility checking
11    None,
12
13    /// New schema can read data written by old schema (backward compatible)
14    #[default]
15    Backward,
16
17    /// Old schema can read data written by new schema (forward compatible)
18    Forward,
19
20    /// Both backward and forward compatible
21    Full,
22
23    /// No schema changes allowed
24    Strict,
25}
26
27/// Schema evolution operation
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub enum SchemaEvolution {
30    /// Field added (name, type, optional/required, default_value)
31    FieldAdded {
32        name: String,
33        field_type: String,
34        required: bool,
35        default_value: Option<serde_json::Value>,
36    },
37
38    /// Field removed
39    FieldRemoved { name: String },
40
41    /// Field made optional (was required)
42    FieldMadeOptional { name: String },
43
44    /// Field made required (was optional) - requires default value
45    FieldMadeRequired {
46        name: String,
47        default_value: serde_json::Value,
48    },
49
50    /// Field type changed
51    FieldTypeChanged {
52        name: String,
53        old_type: String,
54        new_type: String,
55    },
56
57    /// Field renamed
58    FieldRenamed { old_name: String, new_name: String },
59}
60
61/// Schema compatibility check result
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct CompatibilityCheck {
64    /// Whether schemas are compatible
65    pub compatible: bool,
66
67    /// List of incompatibilities found
68    pub incompatibilities: Vec<String>,
69
70    /// List of evolutions detected
71    pub evolutions: Vec<SchemaEvolution>,
72
73    /// Warnings (non-breaking but notable changes)
74    pub warnings: Vec<String>,
75}
76
77impl CompatibilityCheck {
78    /// Check if two schemas are compatible
79    pub fn check(old: &MessageSchema, new: &MessageSchema, mode: CompatibilityMode) -> Self {
80        let mut incompatibilities = Vec::new();
81        let mut evolutions = Vec::new();
82        let mut warnings = Vec::new();
83
84        // Build field maps for easier comparison
85        let old_fields: HashMap<String, &SchemaField> = old.fields.iter().map(|f| (f.name.clone(), f)).collect();
86        let new_fields: HashMap<String, &SchemaField> = new.fields.iter().map(|f| (f.name.clone(), f)).collect();
87
88        match mode {
89            CompatibilityMode::None => {
90                // No checking required
91                return Self {
92                    compatible: true,
93                    incompatibilities,
94                    evolutions,
95                    warnings,
96                };
97            }
98            CompatibilityMode::Strict => {
99                // No changes allowed
100                if old.fields.len() != new.fields.len() {
101                    incompatibilities.push("Field count changed".to_string());
102                }
103                for (name, old_field) in &old_fields {
104                    if let Some(new_field) = new_fields.get(name) {
105                        if old_field.field_type != new_field.field_type {
106                            incompatibilities.push(format!("Field '{}' type changed", name));
107                        }
108                        if old_field.required != new_field.required {
109                            incompatibilities.push(format!("Field '{}' required status changed", name));
110                        }
111                    } else {
112                        incompatibilities.push(format!("Field '{}' removed", name));
113                    }
114                }
115                for name in new_fields.keys() {
116                    if !old_fields.contains_key(name) {
117                        incompatibilities.push(format!("Field '{}' added", name));
118                    }
119                }
120            }
121            CompatibilityMode::Backward => {
122                // New schema can read old data
123                // - Can add optional fields
124                // - Can make required fields optional
125                // - Cannot remove required fields
126                // - Cannot add required fields
127                // - Cannot change field types
128
129                for (name, old_field) in &old_fields {
130                    if let Some(new_field) = new_fields.get(name) {
131                        // Field exists in both
132                        if old_field.field_type != new_field.field_type {
133                            incompatibilities.push(format!(
134                                "Field '{}' type changed from {} to {}",
135                                name, old_field.field_type, new_field.field_type
136                            ));
137                            evolutions.push(SchemaEvolution::FieldTypeChanged {
138                                name: name.clone(),
139                                old_type: old_field.field_type.clone(),
140                                new_type: new_field.field_type.clone(),
141                            });
142                        }
143
144                        if old_field.required && !new_field.required {
145                            // Field made optional - OK for backward compatibility
146                            evolutions.push(SchemaEvolution::FieldMadeOptional { name: name.clone() });
147                        } else if !old_field.required && new_field.required {
148                            // Field made required - breaks backward compatibility
149                            incompatibilities.push(format!("Field '{}' made required (was optional)", name));
150                            evolutions.push(SchemaEvolution::FieldMadeRequired {
151                                name: name.clone(),
152                                default_value: serde_json::Value::Null,
153                            });
154                        }
155                    } else {
156                        // Field removed
157                        if old_field.required {
158                            incompatibilities.push(format!("Required field '{}' removed", name));
159                        } else {
160                            warnings.push(format!("Optional field '{}' removed", name));
161                        }
162                        evolutions.push(SchemaEvolution::FieldRemoved { name: name.clone() });
163                    }
164                }
165
166                // Check for added fields
167                for (name, new_field) in &new_fields {
168                    if !old_fields.contains_key(name) {
169                        if new_field.required {
170                            incompatibilities.push(format!("Required field '{}' added", name));
171                        } else {
172                            warnings.push(format!("Optional field '{}' added", name));
173                        }
174                        evolutions.push(SchemaEvolution::FieldAdded {
175                            name: name.clone(),
176                            field_type: new_field.field_type.clone(),
177                            required: new_field.required,
178                            default_value: None,
179                        });
180                    }
181                }
182            }
183            CompatibilityMode::Forward => {
184                // Old schema can read new data
185                // - Can remove optional fields
186                // - Can make optional fields required
187                // - Cannot add required fields
188                // - Cannot remove required fields
189                // - Cannot change field types
190
191                for (name, old_field) in &old_fields {
192                    if let Some(new_field) = new_fields.get(name) {
193                        if old_field.field_type != new_field.field_type {
194                            incompatibilities.push(format!("Field '{}' type changed", name));
195                            evolutions.push(SchemaEvolution::FieldTypeChanged {
196                                name: name.clone(),
197                                old_type: old_field.field_type.clone(),
198                                new_type: new_field.field_type.clone(),
199                            });
200                        }
201                    } else if old_field.required {
202                        incompatibilities.push(format!("Required field '{}' removed", name));
203                        evolutions.push(SchemaEvolution::FieldRemoved { name: name.clone() });
204                    }
205                }
206
207                for (name, new_field) in &new_fields {
208                    if !old_fields.contains_key(name) && new_field.required {
209                        incompatibilities.push(format!("Required field '{}' added", name));
210                        evolutions.push(SchemaEvolution::FieldAdded {
211                            name: name.clone(),
212                            field_type: new_field.field_type.clone(),
213                            required: new_field.required,
214                            default_value: None,
215                        });
216                    }
217                }
218            }
219            CompatibilityMode::Full => {
220                // Both backward and forward compatible
221                // - Can only add/remove optional fields
222                // - Cannot change field types
223                // - Cannot change required status
224
225                for (name, old_field) in &old_fields {
226                    if let Some(new_field) = new_fields.get(name) {
227                        if old_field.field_type != new_field.field_type {
228                            incompatibilities.push(format!("Field '{}' type changed", name));
229                        }
230                        if old_field.required != new_field.required {
231                            incompatibilities.push(format!("Field '{}' required status changed", name));
232                        }
233                    } else if old_field.required {
234                        incompatibilities.push(format!("Required field '{}' removed", name));
235                    }
236                }
237
238                for (name, new_field) in &new_fields {
239                    if !old_fields.contains_key(name) && new_field.required {
240                        incompatibilities.push(format!("Required field '{}' added", name));
241                    }
242                }
243            }
244        }
245
246        Self {
247            compatible: incompatibilities.is_empty(),
248            incompatibilities,
249            evolutions,
250            warnings,
251        }
252    }
253}