oxify_model/
schema.rs

1//! Schema versioning for model evolution
2//!
3//! This module provides versioning for the data models to enable
4//! forward and backward compatibility.
5
6use serde::{Deserialize, Serialize};
7
8#[cfg(feature = "openapi")]
9use utoipa::ToSchema;
10
11/// Current schema version
12pub const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion {
13    major: 1,
14    minor: 1,
15    patch: 0,
16};
17
18/// Schema version identifier
19#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
20#[cfg_attr(feature = "openapi", derive(ToSchema))]
21pub struct SchemaVersion {
22    /// Major version - breaking changes
23    pub major: u32,
24
25    /// Minor version - new features, backward compatible
26    pub minor: u32,
27
28    /// Patch version - bug fixes
29    pub patch: u32,
30}
31
32impl SchemaVersion {
33    /// Create a new schema version
34    pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
35        Self {
36            major,
37            minor,
38            patch,
39        }
40    }
41
42    /// Parse version string (e.g., "1.0.0")
43    pub fn parse(version: &str) -> Result<Self, String> {
44        let parts: Vec<&str> = version.split('.').collect();
45        if parts.len() != 3 {
46            return Err(format!(
47                "Invalid version format '{}': expected 'major.minor.patch'",
48                version
49            ));
50        }
51
52        let major = parts[0]
53            .parse::<u32>()
54            .map_err(|_| format!("Invalid major version: {}", parts[0]))?;
55        let minor = parts[1]
56            .parse::<u32>()
57            .map_err(|_| format!("Invalid minor version: {}", parts[1]))?;
58        let patch = parts[2]
59            .parse::<u32>()
60            .map_err(|_| format!("Invalid patch version: {}", parts[2]))?;
61
62        Ok(Self::new(major, minor, patch))
63    }
64
65    /// Check if this version is compatible with another
66    /// (same major version, this >= other)
67    pub fn is_compatible_with(&self, other: &SchemaVersion) -> bool {
68        self.major == other.major
69            && (self.minor > other.minor
70                || (self.minor == other.minor && self.patch >= other.patch))
71    }
72
73    /// Check if this version is newer than another
74    pub fn is_newer_than(&self, other: &SchemaVersion) -> bool {
75        self.major > other.major
76            || (self.major == other.major && self.minor > other.minor)
77            || (self.major == other.major && self.minor == other.minor && self.patch > other.patch)
78    }
79
80    /// Check if this version requires migration from another
81    pub fn requires_migration_from(&self, other: &SchemaVersion) -> bool {
82        self.major != other.major
83    }
84}
85
86impl Default for SchemaVersion {
87    fn default() -> Self {
88        CURRENT_SCHEMA_VERSION
89    }
90}
91
92impl std::fmt::Display for SchemaVersion {
93    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
95    }
96}
97
98impl PartialOrd for SchemaVersion {
99    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
100        Some(self.cmp(other))
101    }
102}
103
104impl Ord for SchemaVersion {
105    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
106        match self.major.cmp(&other.major) {
107            std::cmp::Ordering::Equal => match self.minor.cmp(&other.minor) {
108                std::cmp::Ordering::Equal => self.patch.cmp(&other.patch),
109                other => other,
110            },
111            other => other,
112        }
113    }
114}
115
116/// Versioned container for serialized data
117#[derive(Debug, Clone, Serialize, Deserialize)]
118#[cfg_attr(feature = "openapi", derive(ToSchema))]
119pub struct Versioned<T> {
120    /// Schema version of the data
121    #[serde(default)]
122    pub schema_version: SchemaVersion,
123
124    /// The actual data
125    pub data: T,
126
127    /// Migration notes (if data was migrated)
128    #[serde(default)]
129    pub migration_notes: Vec<String>,
130}
131
132impl<T> Versioned<T> {
133    /// Create a new versioned container with current schema version
134    pub fn new(data: T) -> Self {
135        Self {
136            schema_version: CURRENT_SCHEMA_VERSION,
137            data,
138            migration_notes: Vec::new(),
139        }
140    }
141
142    /// Create with specific version
143    pub fn with_version(data: T, version: SchemaVersion) -> Self {
144        Self {
145            schema_version: version,
146            data,
147            migration_notes: Vec::new(),
148        }
149    }
150
151    /// Add migration note
152    pub fn add_migration_note(&mut self, note: String) {
153        self.migration_notes.push(note);
154    }
155
156    /// Check if data needs migration
157    pub fn needs_migration(&self) -> bool {
158        CURRENT_SCHEMA_VERSION.requires_migration_from(&self.schema_version)
159    }
160}
161
162/// Schema migration trait
163pub trait SchemaMigration<T> {
164    /// Source version
165    fn source_version(&self) -> SchemaVersion;
166
167    /// Target version
168    fn target_version(&self) -> SchemaVersion;
169
170    /// Perform the migration
171    fn migrate(&self, data: &mut T) -> Result<Vec<String>, String>;
172}
173
174/// Schema migration registry
175pub struct MigrationRegistry<T> {
176    migrations: Vec<Box<dyn SchemaMigration<T>>>,
177}
178
179impl<T> MigrationRegistry<T> {
180    /// Create a new empty registry
181    pub fn new() -> Self {
182        Self {
183            migrations: Vec::new(),
184        }
185    }
186
187    /// Register a migration
188    pub fn register(&mut self, migration: Box<dyn SchemaMigration<T>>) {
189        self.migrations.push(migration);
190    }
191
192    /// Find migration path from source to target version
193    pub fn find_migration_path(
194        &self,
195        from: &SchemaVersion,
196        to: &SchemaVersion,
197    ) -> Option<Vec<&dyn SchemaMigration<T>>> {
198        if from >= to {
199            return None;
200        }
201
202        let mut path = Vec::new();
203        let mut current = *from;
204
205        while current < *to {
206            let next_migration = self
207                .migrations
208                .iter()
209                .find(|m| m.source_version() == current && m.target_version() > current);
210
211            match next_migration {
212                Some(migration) => {
213                    current = migration.target_version();
214                    path.push(migration.as_ref());
215                }
216                None => return None,
217            }
218        }
219
220        Some(path)
221    }
222
223    /// Apply all migrations to bring data up to current version
224    pub fn migrate_to_current(&self, versioned: &mut Versioned<T>) -> Result<(), String> {
225        if !versioned.needs_migration() {
226            return Ok(());
227        }
228
229        let path = self
230            .find_migration_path(&versioned.schema_version, &CURRENT_SCHEMA_VERSION)
231            .ok_or_else(|| {
232                format!(
233                    "No migration path from {} to {}",
234                    versioned.schema_version, CURRENT_SCHEMA_VERSION
235                )
236            })?;
237
238        for migration in path {
239            let notes = migration.migrate(&mut versioned.data)?;
240            for note in notes {
241                versioned.add_migration_note(note);
242            }
243            versioned.schema_version = migration.target_version();
244        }
245
246        Ok(())
247    }
248}
249
250impl<T> Default for MigrationRegistry<T> {
251    fn default() -> Self {
252        Self::new()
253    }
254}
255
256/// Model metadata with schema version
257#[derive(Debug, Clone, Serialize, Deserialize)]
258#[cfg_attr(feature = "openapi", derive(ToSchema))]
259pub struct ModelMetadata {
260    /// Schema version
261    #[serde(default)]
262    pub schema_version: SchemaVersion,
263
264    /// Model type identifier (e.g., "workflow", "schedule", "template")
265    pub model_type: String,
266
267    /// Checksum for data integrity (optional)
268    #[serde(default)]
269    pub checksum: Option<String>,
270
271    /// Serialization format used
272    #[serde(default = "default_format")]
273    pub format: String,
274}
275
276fn default_format() -> String {
277    "json".to_string()
278}
279
280impl ModelMetadata {
281    /// Create new metadata for a model type
282    pub fn new(model_type: &str) -> Self {
283        Self {
284            schema_version: CURRENT_SCHEMA_VERSION,
285            model_type: model_type.to_string(),
286            checksum: None,
287            format: "json".to_string(),
288        }
289    }
290}
291
292/// Container for preserving unknown fields during deserialization
293/// This enables forward compatibility - newer versions can be read by older code
294#[derive(Debug, Clone, Default, Serialize, Deserialize)]
295#[cfg_attr(feature = "openapi", derive(ToSchema))]
296pub struct PreservedFields {
297    /// Unknown fields stored as JSON values
298    #[serde(flatten)]
299    pub fields: std::collections::HashMap<String, serde_json::Value>,
300}
301
302impl PreservedFields {
303    /// Create a new empty preserved fields container
304    pub fn new() -> Self {
305        Self {
306            fields: std::collections::HashMap::new(),
307        }
308    }
309
310    /// Add a preserved field
311    pub fn add_field(&mut self, name: String, value: serde_json::Value) {
312        self.fields.insert(name, value);
313    }
314
315    /// Get a preserved field
316    pub fn get_field(&self, name: &str) -> Option<&serde_json::Value> {
317        self.fields.get(name)
318    }
319
320    /// Check if there are any preserved fields
321    pub fn is_empty(&self) -> bool {
322        self.fields.is_empty()
323    }
324
325    /// Get the number of preserved fields
326    pub fn len(&self) -> usize {
327        self.fields.len()
328    }
329}
330
331/// Enhanced versioned container with forward compatibility
332#[derive(Debug, Clone, Serialize, Deserialize)]
333#[cfg_attr(feature = "openapi", derive(ToSchema))]
334pub struct VersionedWithCompat<T> {
335    /// Schema version of the data
336    #[serde(default)]
337    pub schema_version: SchemaVersion,
338
339    /// The actual data
340    pub data: T,
341
342    /// Migration notes (if data was migrated)
343    #[serde(default)]
344    pub migration_notes: Vec<String>,
345
346    /// Preserved unknown fields for forward compatibility
347    #[serde(default, flatten)]
348    pub preserved: PreservedFields,
349}
350
351impl<T> VersionedWithCompat<T> {
352    /// Create a new versioned container with current schema version
353    pub fn new(data: T) -> Self {
354        Self {
355            schema_version: CURRENT_SCHEMA_VERSION,
356            data,
357            migration_notes: Vec::new(),
358            preserved: PreservedFields::new(),
359        }
360    }
361
362    /// Create with specific version
363    pub fn with_version(data: T, version: SchemaVersion) -> Self {
364        Self {
365            schema_version: version,
366            data,
367            migration_notes: Vec::new(),
368            preserved: PreservedFields::new(),
369        }
370    }
371
372    /// Add migration note
373    pub fn add_migration_note(&mut self, note: String) {
374        self.migration_notes.push(note);
375    }
376
377    /// Check if data needs migration
378    pub fn needs_migration(&self) -> bool {
379        CURRENT_SCHEMA_VERSION.requires_migration_from(&self.schema_version)
380    }
381
382    /// Check if there are preserved fields from a newer version
383    pub fn has_preserved_fields(&self) -> bool {
384        !self.preserved.is_empty()
385    }
386}
387
388/// Deprecated field mapping for backward compatibility
389#[derive(Debug, Clone)]
390pub struct DeprecatedField {
391    /// Old field name
392    pub old_name: String,
393
394    /// New field name (replacement)
395    pub new_name: String,
396
397    /// Version when the field was deprecated
398    pub deprecated_in: SchemaVersion,
399
400    /// Version when the field will be removed (optional)
401    pub removed_in: Option<SchemaVersion>,
402}
403
404impl DeprecatedField {
405    /// Create a new deprecated field mapping
406    pub fn new(old_name: String, new_name: String, deprecated_in: SchemaVersion) -> Self {
407        Self {
408            old_name,
409            new_name,
410            deprecated_in,
411            removed_in: None,
412        }
413    }
414
415    /// Set the version when the field will be removed
416    pub fn with_removal(mut self, removed_in: SchemaVersion) -> Self {
417        self.removed_in = Some(removed_in);
418        self
419    }
420
421    /// Check if the field is still supported in a version
422    pub fn is_supported_in(&self, version: &SchemaVersion) -> bool {
423        match &self.removed_in {
424            Some(removed) => version < removed,
425            None => true,
426        }
427    }
428}
429
430/// Field migration trait for automatic field transformations
431pub trait FieldMigration {
432    /// Get the deprecated field mappings
433    fn deprecated_fields(&self) -> Vec<DeprecatedField>;
434
435    /// Apply field migrations to JSON value
436    fn migrate_fields(&self, value: &mut serde_json::Value) -> Result<Vec<String>, String> {
437        let mut notes = Vec::new();
438
439        if let serde_json::Value::Object(map) = value {
440            for field in self.deprecated_fields() {
441                if let Some(old_value) = map.remove(&field.old_name) {
442                    map.insert(field.new_name.clone(), old_value);
443                    notes.push(format!(
444                        "Migrated field '{}' to '{}'",
445                        field.old_name, field.new_name
446                    ));
447                }
448            }
449        }
450
451        Ok(notes)
452    }
453}
454
455/// Backward compatibility helper
456pub struct BackwardCompatibility {
457    /// Registered deprecated field mappings
458    pub fields: Vec<DeprecatedField>,
459}
460
461impl BackwardCompatibility {
462    /// Create a new backward compatibility helper
463    pub fn new() -> Self {
464        Self { fields: Vec::new() }
465    }
466
467    /// Register a deprecated field
468    pub fn register_deprecated_field(&mut self, field: DeprecatedField) {
469        self.fields.push(field);
470    }
471
472    /// Apply all field migrations to a JSON value
473    pub fn migrate_json(&self, value: &mut serde_json::Value) -> Result<Vec<String>, String> {
474        let mut notes = Vec::new();
475
476        if let serde_json::Value::Object(map) = value {
477            for field in &self.fields {
478                if let Some(old_value) = map.remove(&field.old_name) {
479                    map.insert(field.new_name.clone(), old_value);
480                    notes.push(format!(
481                        "Migrated deprecated field '{}' to '{}' (deprecated in {})",
482                        field.old_name, field.new_name, field.deprecated_in
483                    ));
484                }
485            }
486        }
487
488        Ok(notes)
489    }
490}
491
492impl Default for BackwardCompatibility {
493    fn default() -> Self {
494        Self::new()
495    }
496}
497
498#[cfg(test)]
499mod tests {
500    use super::*;
501
502    #[test]
503    fn test_schema_version_parsing() {
504        let version = SchemaVersion::parse("1.2.3").unwrap();
505        assert_eq!(version.major, 1);
506        assert_eq!(version.minor, 2);
507        assert_eq!(version.patch, 3);
508    }
509
510    #[test]
511    fn test_version_comparison() {
512        let v1 = SchemaVersion::new(1, 0, 0);
513        let v2 = SchemaVersion::new(1, 1, 0);
514        let v3 = SchemaVersion::new(2, 0, 0);
515
516        assert!(v2.is_newer_than(&v1));
517        assert!(v3.is_newer_than(&v2));
518        assert!(!v1.is_newer_than(&v2));
519    }
520
521    #[test]
522    fn test_compatibility() {
523        let v1 = SchemaVersion::new(1, 0, 0);
524        let v2 = SchemaVersion::new(1, 1, 0);
525        let v3 = SchemaVersion::new(2, 0, 0);
526
527        // Same major version, newer minor/patch is compatible
528        assert!(v2.is_compatible_with(&v1));
529
530        // Different major version is not compatible
531        assert!(!v3.is_compatible_with(&v1));
532    }
533
534    #[test]
535    fn test_version_display() {
536        let version = SchemaVersion::new(1, 2, 3);
537        assert_eq!(version.to_string(), "1.2.3");
538    }
539
540    #[test]
541    fn test_versioned_container() {
542        let data = "test data".to_string();
543        let versioned = Versioned::new(data.clone());
544
545        assert_eq!(versioned.schema_version, CURRENT_SCHEMA_VERSION);
546        assert_eq!(versioned.data, data);
547        assert!(versioned.migration_notes.is_empty());
548    }
549
550    #[test]
551    fn test_migration_requirement() {
552        let old_version = SchemaVersion::new(0, 1, 0);
553        let same_major = SchemaVersion::new(1, 0, 0);
554
555        assert!(CURRENT_SCHEMA_VERSION.requires_migration_from(&old_version));
556        assert!(!CURRENT_SCHEMA_VERSION.requires_migration_from(&same_major));
557    }
558
559    #[test]
560    fn test_preserved_fields_creation() {
561        let mut preserved = PreservedFields::new();
562        assert!(preserved.is_empty());
563        assert_eq!(preserved.len(), 0);
564
565        preserved.add_field("unknown_field".to_string(), serde_json::json!("value"));
566        assert!(!preserved.is_empty());
567        assert_eq!(preserved.len(), 1);
568
569        let value = preserved.get_field("unknown_field");
570        assert!(value.is_some());
571        assert_eq!(value.unwrap(), &serde_json::json!("value"));
572    }
573
574    #[test]
575    fn test_versioned_with_compat() {
576        let data = "test data".to_string();
577        let versioned = VersionedWithCompat::new(data.clone());
578
579        assert_eq!(versioned.schema_version, CURRENT_SCHEMA_VERSION);
580        assert_eq!(versioned.data, data);
581        assert!(versioned.migration_notes.is_empty());
582        assert!(!versioned.has_preserved_fields());
583    }
584
585    #[test]
586    fn test_deprecated_field() {
587        let field = DeprecatedField::new(
588            "old_field".to_string(),
589            "new_field".to_string(),
590            SchemaVersion::new(1, 0, 0),
591        );
592
593        assert_eq!(field.old_name, "old_field");
594        assert_eq!(field.new_name, "new_field");
595        assert!(field.is_supported_in(&SchemaVersion::new(1, 0, 0)));
596
597        let field_with_removal = field.with_removal(SchemaVersion::new(2, 0, 0));
598        assert!(!field_with_removal.is_supported_in(&SchemaVersion::new(2, 0, 0)));
599        assert!(field_with_removal.is_supported_in(&SchemaVersion::new(1, 5, 0)));
600    }
601
602    #[test]
603    fn test_backward_compatibility_migration() {
604        let mut compat = BackwardCompatibility::new();
605
606        let field = DeprecatedField::new(
607            "oldName".to_string(),
608            "new_name".to_string(),
609            SchemaVersion::new(1, 0, 0),
610        );
611        compat.register_deprecated_field(field);
612
613        let mut json = serde_json::json!({
614            "oldName": "test_value",
615            "other_field": 123
616        });
617
618        let notes = compat.migrate_json(&mut json).unwrap();
619        assert_eq!(notes.len(), 1);
620        assert!(notes[0].contains("oldName"));
621        assert!(notes[0].contains("new_name"));
622
623        // Check that the old field was renamed
624        assert!(json.get("oldName").is_none());
625        assert_eq!(json.get("new_name").unwrap(), "test_value");
626        assert_eq!(json.get("other_field").unwrap(), 123);
627    }
628
629    #[test]
630    fn test_backward_compatibility_multiple_fields() {
631        let mut compat = BackwardCompatibility::new();
632
633        compat.register_deprecated_field(DeprecatedField::new(
634            "field1".to_string(),
635            "new_field1".to_string(),
636            SchemaVersion::new(1, 0, 0),
637        ));
638
639        compat.register_deprecated_field(DeprecatedField::new(
640            "field2".to_string(),
641            "new_field2".to_string(),
642            SchemaVersion::new(1, 0, 0),
643        ));
644
645        let mut json = serde_json::json!({
646            "field1": "value1",
647            "field2": "value2",
648            "unchanged": "value3"
649        });
650
651        let notes = compat.migrate_json(&mut json).unwrap();
652        assert_eq!(notes.len(), 2);
653
654        assert!(json.get("field1").is_none());
655        assert!(json.get("field2").is_none());
656        assert_eq!(json.get("new_field1").unwrap(), "value1");
657        assert_eq!(json.get("new_field2").unwrap(), "value2");
658        assert_eq!(json.get("unchanged").unwrap(), "value3");
659    }
660
661    #[test]
662    fn test_preserved_fields_serialization() {
663        let mut preserved = PreservedFields::new();
664        preserved.add_field("future_field".to_string(), serde_json::json!(42));
665        preserved.add_field("another_field".to_string(), serde_json::json!("test"));
666
667        let json = serde_json::to_string(&preserved).unwrap();
668        let deserialized: PreservedFields = serde_json::from_str(&json).unwrap();
669
670        assert_eq!(deserialized.len(), 2);
671        assert_eq!(deserialized.get_field("future_field").unwrap(), 42);
672        assert_eq!(deserialized.get_field("another_field").unwrap(), "test");
673    }
674}