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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
8pub enum CompatibilityMode {
9    None,
10
11    #[default]
12    Backward,
13
14    Forward,
15
16    Full,
17
18    Strict,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub enum SchemaEvolution {
23    FieldAdded {
24        name: String,
25        field_type: String,
26        required: bool,
27        default_value: Option<serde_json::Value>,
28    },
29
30    FieldRemoved {
31        name: String,
32    },
33
34    FieldMadeOptional {
35        name: String,
36    },
37
38    FieldMadeRequired {
39        name: String,
40        default_value: serde_json::Value,
41    },
42
43    FieldTypeChanged {
44        name: String,
45        old_type: String,
46        new_type: String,
47    },
48
49    FieldRenamed {
50        old_name: String,
51        new_name: String,
52    },
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct CompatibilityCheck {
57    pub compatible: bool,
58
59    pub incompatibilities: Vec<String>,
60
61    pub evolutions: Vec<SchemaEvolution>,
62
63    pub warnings: Vec<String>,
64}
65
66impl CompatibilityCheck {
67    /// Check if two schemas are compatible
68    pub fn check(old: &MessageSchema, new: &MessageSchema, mode: CompatibilityMode) -> Self {
69        let mut incompatibilities = Vec::new();
70        let mut evolutions = Vec::new();
71        let mut warnings = Vec::new();
72
73        let old_fields: HashMap<String, &SchemaField> = old.fields.iter().map(|f| (f.name.clone(), f)).collect();
74        let new_fields: HashMap<String, &SchemaField> = new.fields.iter().map(|f| (f.name.clone(), f)).collect();
75
76        match mode {
77            CompatibilityMode::None => {
78                return Self {
79                    compatible: true,
80                    incompatibilities,
81                    evolutions,
82                    warnings,
83                };
84            }
85            CompatibilityMode::Strict => {
86                if old.fields.len() != new.fields.len() {
87                    incompatibilities.push("Field count changed".to_string());
88                }
89                for (name, old_field) in &old_fields {
90                    if let Some(new_field) = new_fields.get(name) {
91                        if old_field.field_type != new_field.field_type {
92                            incompatibilities.push(format!("Field '{}' type changed", name));
93                        }
94                        if old_field.required != new_field.required {
95                            incompatibilities.push(format!("Field '{}' required status changed", name));
96                        }
97                    } else {
98                        incompatibilities.push(format!("Field '{}' removed", name));
99                    }
100                }
101                for name in new_fields.keys() {
102                    if !old_fields.contains_key(name) {
103                        incompatibilities.push(format!("Field '{}' added", name));
104                    }
105                }
106            }
107            CompatibilityMode::Backward => {
108                for (name, old_field) in &old_fields {
109                    if let Some(new_field) = new_fields.get(name) {
110                        if old_field.field_type != new_field.field_type {
111                            incompatibilities.push(format!(
112                                "Field '{}' type changed from {} to {}",
113                                name, old_field.field_type, new_field.field_type
114                            ));
115                            evolutions.push(SchemaEvolution::FieldTypeChanged {
116                                name: name.clone(),
117                                old_type: old_field.field_type.clone(),
118                                new_type: new_field.field_type.clone(),
119                            });
120                        }
121
122                        if old_field.required && !new_field.required {
123                            evolutions.push(SchemaEvolution::FieldMadeOptional { name: name.clone() });
124                        } else if !old_field.required && new_field.required {
125                            incompatibilities.push(format!("Field '{}' made required (was optional)", name));
126                            evolutions.push(SchemaEvolution::FieldMadeRequired {
127                                name: name.clone(),
128                                default_value: serde_json::Value::Null,
129                            });
130                        }
131                    } else {
132                        if old_field.required {
133                            incompatibilities.push(format!("Required field '{}' removed", name));
134                        } else {
135                            warnings.push(format!("Optional field '{}' removed", name));
136                        }
137                        evolutions.push(SchemaEvolution::FieldRemoved { name: name.clone() });
138                    }
139                }
140
141                for (name, new_field) in &new_fields {
142                    if !old_fields.contains_key(name) {
143                        if new_field.required {
144                            incompatibilities.push(format!("Required field '{}' added", name));
145                        } else {
146                            warnings.push(format!("Optional field '{}' added", name));
147                        }
148                        evolutions.push(SchemaEvolution::FieldAdded {
149                            name: name.clone(),
150                            field_type: new_field.field_type.clone(),
151                            required: new_field.required,
152                            default_value: None,
153                        });
154                    }
155                }
156            }
157            CompatibilityMode::Forward => {
158                for (name, old_field) in &old_fields {
159                    if let Some(new_field) = new_fields.get(name) {
160                        if old_field.field_type != new_field.field_type {
161                            incompatibilities.push(format!("Field '{}' type changed", name));
162                            evolutions.push(SchemaEvolution::FieldTypeChanged {
163                                name: name.clone(),
164                                old_type: old_field.field_type.clone(),
165                                new_type: new_field.field_type.clone(),
166                            });
167                        }
168                    } else if old_field.required {
169                        incompatibilities.push(format!("Required field '{}' removed", name));
170                        evolutions.push(SchemaEvolution::FieldRemoved { name: name.clone() });
171                    }
172                }
173
174                for (name, new_field) in &new_fields {
175                    if !old_fields.contains_key(name) && new_field.required {
176                        incompatibilities.push(format!("Required field '{}' added", name));
177                        evolutions.push(SchemaEvolution::FieldAdded {
178                            name: name.clone(),
179                            field_type: new_field.field_type.clone(),
180                            required: new_field.required,
181                            default_value: None,
182                        });
183                    }
184                }
185            }
186            CompatibilityMode::Full => {
187                for (name, old_field) in &old_fields {
188                    if let Some(new_field) = new_fields.get(name) {
189                        if old_field.field_type != new_field.field_type {
190                            incompatibilities.push(format!("Field '{}' type changed", name));
191                        }
192                        if old_field.required != new_field.required {
193                            incompatibilities.push(format!("Field '{}' required status changed", name));
194                        }
195                    } else if old_field.required {
196                        incompatibilities.push(format!("Required field '{}' removed", name));
197                    }
198                }
199
200                for (name, new_field) in &new_fields {
201                    if !old_fields.contains_key(name) && new_field.required {
202                        incompatibilities.push(format!("Required field '{}' added", name));
203                    }
204                }
205            }
206        }
207
208        Self {
209            compatible: incompatibilities.is_empty(),
210            incompatibilities,
211            evolutions,
212            warnings,
213        }
214    }
215}