use super::{MessageSchema, SchemaField};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum CompatibilityMode {
None,
#[default]
Backward,
Forward,
Full,
Strict,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SchemaEvolution {
FieldAdded {
name: String,
field_type: String,
required: bool,
default_value: Option<serde_json::Value>,
},
FieldRemoved { name: String },
FieldMadeOptional { name: String },
FieldMadeRequired {
name: String,
default_value: serde_json::Value,
},
FieldTypeChanged {
name: String,
old_type: String,
new_type: String,
},
FieldRenamed { old_name: String, new_name: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompatibilityCheck {
pub compatible: bool,
pub incompatibilities: Vec<String>,
pub evolutions: Vec<SchemaEvolution>,
pub warnings: Vec<String>,
}
impl CompatibilityCheck {
pub fn check(old: &MessageSchema, new: &MessageSchema, mode: CompatibilityMode) -> Self {
let mut incompatibilities = Vec::new();
let mut evolutions = Vec::new();
let mut warnings = Vec::new();
let old_fields: HashMap<String, &SchemaField> = old.fields.iter().map(|f| (f.name.clone(), f)).collect();
let new_fields: HashMap<String, &SchemaField> = new.fields.iter().map(|f| (f.name.clone(), f)).collect();
match mode {
CompatibilityMode::None => {
return Self {
compatible: true,
incompatibilities,
evolutions,
warnings,
};
}
CompatibilityMode::Strict => {
if old.fields.len() != new.fields.len() {
incompatibilities.push("Field count changed".to_string());
}
for (name, old_field) in &old_fields {
if let Some(new_field) = new_fields.get(name) {
if old_field.field_type != new_field.field_type {
incompatibilities.push(format!("Field '{}' type changed", name));
}
if old_field.required != new_field.required {
incompatibilities.push(format!("Field '{}' required status changed", name));
}
} else {
incompatibilities.push(format!("Field '{}' removed", name));
}
}
for name in new_fields.keys() {
if !old_fields.contains_key(name) {
incompatibilities.push(format!("Field '{}' added", name));
}
}
}
CompatibilityMode::Backward => {
for (name, old_field) in &old_fields {
if let Some(new_field) = new_fields.get(name) {
if old_field.field_type != new_field.field_type {
incompatibilities.push(format!(
"Field '{}' type changed from {} to {}",
name, old_field.field_type, new_field.field_type
));
evolutions.push(SchemaEvolution::FieldTypeChanged {
name: name.clone(),
old_type: old_field.field_type.clone(),
new_type: new_field.field_type.clone(),
});
}
if old_field.required && !new_field.required {
evolutions.push(SchemaEvolution::FieldMadeOptional { name: name.clone() });
} else if !old_field.required && new_field.required {
incompatibilities.push(format!("Field '{}' made required (was optional)", name));
evolutions.push(SchemaEvolution::FieldMadeRequired {
name: name.clone(),
default_value: serde_json::Value::Null,
});
}
} else {
if old_field.required {
incompatibilities.push(format!("Required field '{}' removed", name));
} else {
warnings.push(format!("Optional field '{}' removed", name));
}
evolutions.push(SchemaEvolution::FieldRemoved { name: name.clone() });
}
}
for (name, new_field) in &new_fields {
if !old_fields.contains_key(name) {
if new_field.required {
incompatibilities.push(format!("Required field '{}' added", name));
} else {
warnings.push(format!("Optional field '{}' added", name));
}
evolutions.push(SchemaEvolution::FieldAdded {
name: name.clone(),
field_type: new_field.field_type.clone(),
required: new_field.required,
default_value: None,
});
}
}
}
CompatibilityMode::Forward => {
for (name, old_field) in &old_fields {
if let Some(new_field) = new_fields.get(name) {
if old_field.field_type != new_field.field_type {
incompatibilities.push(format!("Field '{}' type changed", name));
evolutions.push(SchemaEvolution::FieldTypeChanged {
name: name.clone(),
old_type: old_field.field_type.clone(),
new_type: new_field.field_type.clone(),
});
}
} else if old_field.required {
incompatibilities.push(format!("Required field '{}' removed", name));
evolutions.push(SchemaEvolution::FieldRemoved { name: name.clone() });
}
}
for (name, new_field) in &new_fields {
if !old_fields.contains_key(name) && new_field.required {
incompatibilities.push(format!("Required field '{}' added", name));
evolutions.push(SchemaEvolution::FieldAdded {
name: name.clone(),
field_type: new_field.field_type.clone(),
required: new_field.required,
default_value: None,
});
}
}
}
CompatibilityMode::Full => {
for (name, old_field) in &old_fields {
if let Some(new_field) = new_fields.get(name) {
if old_field.field_type != new_field.field_type {
incompatibilities.push(format!("Field '{}' type changed", name));
}
if old_field.required != new_field.required {
incompatibilities.push(format!("Field '{}' required status changed", name));
}
} else if old_field.required {
incompatibilities.push(format!("Required field '{}' removed", name));
}
}
for (name, new_field) in &new_fields {
if !old_fields.contains_key(name) && new_field.required {
incompatibilities.push(format!("Required field '{}' added", name));
}
}
}
}
Self {
compatible: incompatibilities.is_empty(),
incompatibilities,
evolutions,
warnings,
}
}
}