mecha10-core 0.6.2

Mecha10 core runtime and foundational types
Documentation
//! Schema evolution and compatibility checking.

use super::{MessageSchema, SchemaField};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Schema compatibility mode
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum CompatibilityMode {
    /// No compatibility checking
    None,

    /// New schema can read data written by old schema (backward compatible)
    #[default]
    Backward,

    /// Old schema can read data written by new schema (forward compatible)
    Forward,

    /// Both backward and forward compatible
    Full,

    /// No schema changes allowed
    Strict,
}

/// Schema evolution operation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SchemaEvolution {
    /// Field added (name, type, optional/required, default_value)
    FieldAdded {
        name: String,
        field_type: String,
        required: bool,
        default_value: Option<serde_json::Value>,
    },

    /// Field removed
    FieldRemoved { name: String },

    /// Field made optional (was required)
    FieldMadeOptional { name: String },

    /// Field made required (was optional) - requires default value
    FieldMadeRequired {
        name: String,
        default_value: serde_json::Value,
    },

    /// Field type changed
    FieldTypeChanged {
        name: String,
        old_type: String,
        new_type: String,
    },

    /// Field renamed
    FieldRenamed { old_name: String, new_name: String },
}

/// Schema compatibility check result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompatibilityCheck {
    /// Whether schemas are compatible
    pub compatible: bool,

    /// List of incompatibilities found
    pub incompatibilities: Vec<String>,

    /// List of evolutions detected
    pub evolutions: Vec<SchemaEvolution>,

    /// Warnings (non-breaking but notable changes)
    pub warnings: Vec<String>,
}

impl CompatibilityCheck {
    /// Check if two schemas are compatible
    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();

        // Build field maps for easier comparison
        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 => {
                // No checking required
                return Self {
                    compatible: true,
                    incompatibilities,
                    evolutions,
                    warnings,
                };
            }
            CompatibilityMode::Strict => {
                // No changes allowed
                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 => {
                // New schema can read old data
                // - Can add optional fields
                // - Can make required fields optional
                // - Cannot remove required fields
                // - Cannot add required fields
                // - Cannot change field types

                for (name, old_field) in &old_fields {
                    if let Some(new_field) = new_fields.get(name) {
                        // Field exists in both
                        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 {
                            // Field made optional - OK for backward compatibility
                            evolutions.push(SchemaEvolution::FieldMadeOptional { name: name.clone() });
                        } else if !old_field.required && new_field.required {
                            // Field made required - breaks backward compatibility
                            incompatibilities.push(format!("Field '{}' made required (was optional)", name));
                            evolutions.push(SchemaEvolution::FieldMadeRequired {
                                name: name.clone(),
                                default_value: serde_json::Value::Null,
                            });
                        }
                    } else {
                        // Field removed
                        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() });
                    }
                }

                // Check for added fields
                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 => {
                // Old schema can read new data
                // - Can remove optional fields
                // - Can make optional fields required
                // - Cannot add required fields
                // - Cannot remove required fields
                // - Cannot change field types

                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 => {
                // Both backward and forward compatible
                // - Can only add/remove optional fields
                // - Cannot change field types
                // - Cannot change required status

                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,
        }
    }
}