use serde::{Deserialize, Serialize};
use version_migrate::{IntoDomain, MigratesTo, Migrator, Versioned, VersionedWrapper};
#[derive(Serialize, Deserialize, Versioned, Debug, Clone)]
#[versioned(version = "1.0.0")]
struct SettingV1 {
name: String,
}
#[derive(Serialize, Deserialize, Versioned, Debug, Clone)]
#[versioned(version = "2.0.0")]
struct SettingV2 {
name: String,
description: String,
}
impl MigratesTo<SettingV2> for SettingV1 {
fn migrate(self) -> SettingV2 {
SettingV2 {
name: self.name,
description: "No description".to_string(),
}
}
}
#[derive(Serialize, Deserialize, Versioned, Debug, Clone)]
#[versioned(version = "1.0.0")]
struct ItemV1 {
id: String,
}
#[derive(Serialize, Deserialize, Versioned, Debug, Clone)]
#[versioned(version = "2.0.0")]
struct ItemV2 {
id: String,
label: String,
}
impl MigratesTo<ItemV2> for ItemV1 {
fn migrate(self) -> ItemV2 {
ItemV2 {
id: self.id.clone(),
label: format!("Item {}", self.id),
}
}
}
#[derive(Serialize, Deserialize, Versioned, Debug)]
#[versioned(version = "1.0.0")]
struct ConfigV1 {
setting: SettingV1,
items: Vec<ItemV1>,
}
#[derive(Serialize, Deserialize, Versioned, Debug)]
#[versioned(version = "2.0.0")]
struct ConfigV2 {
setting: SettingV2,
items: Vec<ItemV2>,
}
impl MigratesTo<ConfigV2> for ConfigV1 {
fn migrate(self) -> ConfigV2 {
ConfigV2 {
setting: self.setting.migrate(),
items: self.items.into_iter().map(|item| item.migrate()).collect(),
}
}
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct ConfigDomain {
setting_name: String,
setting_description: String,
items: Vec<ItemDomain>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct ItemDomain {
id: String,
label: String,
}
impl IntoDomain<ConfigDomain> for ConfigV2 {
fn into_domain(self) -> ConfigDomain {
ConfigDomain {
setting_name: self.setting.name,
setting_description: self.setting.description,
items: self
.items
.into_iter()
.map(|item| ItemDomain {
id: item.id,
label: item.label,
})
.collect(),
}
}
}
#[test]
fn test_nested_migration_v1_to_domain() {
let config_v1 = ConfigV1 {
setting: SettingV1 {
name: "MyApp".to_string(),
},
items: vec![
ItemV1 {
id: "item-1".to_string(),
},
ItemV1 {
id: "item-2".to_string(),
},
ItemV1 {
id: "item-3".to_string(),
},
],
};
let wrapper = VersionedWrapper::from_versioned(config_v1);
let json = serde_json::to_string(&wrapper).expect("Failed to serialize");
let config_path = Migrator::define("config")
.from::<ConfigV1>()
.step::<ConfigV2>()
.into::<ConfigDomain>();
let mut migrator = Migrator::new();
migrator.register(config_path).unwrap();
let domain: ConfigDomain = migrator.load("config", &json).expect("Migration failed");
assert_eq!(domain.setting_name, "MyApp");
assert_eq!(domain.setting_description, "No description");
assert_eq!(domain.items.len(), 3);
assert_eq!(domain.items[0].id, "item-1");
assert_eq!(domain.items[0].label, "Item item-1");
assert_eq!(domain.items[1].id, "item-2");
assert_eq!(domain.items[1].label, "Item item-2");
assert_eq!(domain.items[2].id, "item-3");
assert_eq!(domain.items[2].label, "Item item-3");
}
#[test]
fn test_nested_no_migration_needed() {
let config_v2 = ConfigV2 {
setting: SettingV2 {
name: "LatestApp".to_string(),
description: "A great app".to_string(),
},
items: vec![ItemV2 {
id: "item-x".to_string(),
label: "Custom Label".to_string(),
}],
};
let wrapper = VersionedWrapper::from_versioned(config_v2);
let json = serde_json::to_string(&wrapper).expect("Failed to serialize");
let config_path = Migrator::define("config")
.from::<ConfigV2>()
.into::<ConfigDomain>();
let mut migrator = Migrator::new();
migrator.register(config_path).unwrap();
let domain: ConfigDomain = migrator.load("config", &json).expect("Load failed");
assert_eq!(domain.setting_name, "LatestApp");
assert_eq!(domain.setting_description, "A great app");
assert_eq!(domain.items.len(), 1);
assert_eq!(domain.items[0].id, "item-x");
assert_eq!(domain.items[0].label, "Custom Label");
}
#[test]
fn test_nested_save_and_load_roundtrip() {
let migrator = Migrator::new();
let config_v1 = ConfigV1 {
setting: SettingV1 {
name: "Roundtrip".to_string(),
},
items: vec![
ItemV1 {
id: "rt-1".to_string(),
},
ItemV1 {
id: "rt-2".to_string(),
},
],
};
let json = migrator.save(config_v1).expect("Save failed");
assert!(json.contains("\"version\":\"1.0.0\""));
assert!(json.contains("Roundtrip"));
assert!(json.contains("rt-1"));
assert!(json.contains("rt-2"));
let config_path = Migrator::define("config")
.from::<ConfigV1>()
.step::<ConfigV2>()
.into::<ConfigDomain>();
let mut migrator = Migrator::new();
migrator.register(config_path).unwrap();
let domain: ConfigDomain = migrator.load("config", &json).expect("Load failed");
assert_eq!(domain.setting_name, "Roundtrip");
assert_eq!(domain.items.len(), 2);
assert_eq!(domain.items[0].id, "rt-1");
assert_eq!(domain.items[1].id, "rt-2");
}
#[test]
fn test_nested_empty_items() {
let config_v1 = ConfigV1 {
setting: SettingV1 {
name: "Empty".to_string(),
},
items: vec![],
};
let wrapper = VersionedWrapper::from_versioned(config_v1);
let json = serde_json::to_string(&wrapper).expect("Failed to serialize");
let config_path = Migrator::define("config")
.from::<ConfigV1>()
.step::<ConfigV2>()
.into::<ConfigDomain>();
let mut migrator = Migrator::new();
migrator.register(config_path).unwrap();
let domain: ConfigDomain = migrator.load("config", &json).expect("Migration failed");
assert_eq!(domain.setting_name, "Empty");
assert_eq!(domain.items.len(), 0);
}
#[test]
fn test_nested_large_items_array() {
let items: Vec<ItemV1> = (0..100)
.map(|i| ItemV1 {
id: format!("item-{}", i),
})
.collect();
let config_v1 = ConfigV1 {
setting: SettingV1 {
name: "Large".to_string(),
},
items,
};
let wrapper = VersionedWrapper::from_versioned(config_v1);
let json = serde_json::to_string(&wrapper).expect("Failed to serialize");
let config_path = Migrator::define("config")
.from::<ConfigV1>()
.step::<ConfigV2>()
.into::<ConfigDomain>();
let mut migrator = Migrator::new();
migrator.register(config_path).unwrap();
let domain: ConfigDomain = migrator.load("config", &json).expect("Migration failed");
assert_eq!(domain.items.len(), 100);
assert_eq!(domain.items[0].id, "item-0");
assert_eq!(domain.items[99].id, "item-99");
assert_eq!(domain.items[50].label, "Item item-50");
}