1use 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 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}