1use serde::{Deserialize, Serialize};
7
8#[cfg(feature = "openapi")]
9use utoipa::ToSchema;
10
11pub const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion {
13 major: 1,
14 minor: 1,
15 patch: 0,
16};
17
18#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
20#[cfg_attr(feature = "openapi", derive(ToSchema))]
21pub struct SchemaVersion {
22 pub major: u32,
24
25 pub minor: u32,
27
28 pub patch: u32,
30}
31
32impl SchemaVersion {
33 pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
35 Self {
36 major,
37 minor,
38 patch,
39 }
40 }
41
42 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 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
118#[cfg_attr(feature = "openapi", derive(ToSchema))]
119pub struct Versioned<T> {
120 #[serde(default)]
122 pub schema_version: SchemaVersion,
123
124 pub data: T,
126
127 #[serde(default)]
129 pub migration_notes: Vec<String>,
130}
131
132impl<T> Versioned<T> {
133 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 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 pub fn add_migration_note(&mut self, note: String) {
153 self.migration_notes.push(note);
154 }
155
156 pub fn needs_migration(&self) -> bool {
158 CURRENT_SCHEMA_VERSION.requires_migration_from(&self.schema_version)
159 }
160}
161
162pub trait SchemaMigration<T> {
164 fn source_version(&self) -> SchemaVersion;
166
167 fn target_version(&self) -> SchemaVersion;
169
170 fn migrate(&self, data: &mut T) -> Result<Vec<String>, String>;
172}
173
174pub struct MigrationRegistry<T> {
176 migrations: Vec<Box<dyn SchemaMigration<T>>>,
177}
178
179impl<T> MigrationRegistry<T> {
180 pub fn new() -> Self {
182 Self {
183 migrations: Vec::new(),
184 }
185 }
186
187 pub fn register(&mut self, migration: Box<dyn SchemaMigration<T>>) {
189 self.migrations.push(migration);
190 }
191
192 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
258#[cfg_attr(feature = "openapi", derive(ToSchema))]
259pub struct ModelMetadata {
260 #[serde(default)]
262 pub schema_version: SchemaVersion,
263
264 pub model_type: String,
266
267 #[serde(default)]
269 pub checksum: Option<String>,
270
271 #[serde(default = "default_format")]
273 pub format: String,
274}
275
276fn default_format() -> String {
277 "json".to_string()
278}
279
280impl ModelMetadata {
281 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
295#[cfg_attr(feature = "openapi", derive(ToSchema))]
296pub struct PreservedFields {
297 #[serde(flatten)]
299 pub fields: std::collections::HashMap<String, serde_json::Value>,
300}
301
302impl PreservedFields {
303 pub fn new() -> Self {
305 Self {
306 fields: std::collections::HashMap::new(),
307 }
308 }
309
310 pub fn add_field(&mut self, name: String, value: serde_json::Value) {
312 self.fields.insert(name, value);
313 }
314
315 pub fn get_field(&self, name: &str) -> Option<&serde_json::Value> {
317 self.fields.get(name)
318 }
319
320 pub fn is_empty(&self) -> bool {
322 self.fields.is_empty()
323 }
324
325 pub fn len(&self) -> usize {
327 self.fields.len()
328 }
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize)]
333#[cfg_attr(feature = "openapi", derive(ToSchema))]
334pub struct VersionedWithCompat<T> {
335 #[serde(default)]
337 pub schema_version: SchemaVersion,
338
339 pub data: T,
341
342 #[serde(default)]
344 pub migration_notes: Vec<String>,
345
346 #[serde(default, flatten)]
348 pub preserved: PreservedFields,
349}
350
351impl<T> VersionedWithCompat<T> {
352 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 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 pub fn add_migration_note(&mut self, note: String) {
374 self.migration_notes.push(note);
375 }
376
377 pub fn needs_migration(&self) -> bool {
379 CURRENT_SCHEMA_VERSION.requires_migration_from(&self.schema_version)
380 }
381
382 pub fn has_preserved_fields(&self) -> bool {
384 !self.preserved.is_empty()
385 }
386}
387
388#[derive(Debug, Clone)]
390pub struct DeprecatedField {
391 pub old_name: String,
393
394 pub new_name: String,
396
397 pub deprecated_in: SchemaVersion,
399
400 pub removed_in: Option<SchemaVersion>,
402}
403
404impl DeprecatedField {
405 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 pub fn with_removal(mut self, removed_in: SchemaVersion) -> Self {
417 self.removed_in = Some(removed_in);
418 self
419 }
420
421 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
430pub trait FieldMigration {
432 fn deprecated_fields(&self) -> Vec<DeprecatedField>;
434
435 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
455pub struct BackwardCompatibility {
457 pub fields: Vec<DeprecatedField>,
459}
460
461impl BackwardCompatibility {
462 pub fn new() -> Self {
464 Self { fields: Vec::new() }
465 }
466
467 pub fn register_deprecated_field(&mut self, field: DeprecatedField) {
469 self.fields.push(field);
470 }
471
472 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 assert!(v2.is_compatible_with(&v1));
529
530 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 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}