use serde::{Deserialize, Serialize};
use version_migrate::{migrator, FromDomain, IntoDomain, MigratesTo, Versioned};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
struct TaskV1 {
id: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
struct TaskV2 {
id: String,
title: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
struct TaskV3 {
id: String,
title: String,
description: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
struct TaskEntity {
id: String,
title: String,
description: Option<String>,
}
impl Versioned for TaskV1 {
const VERSION: &'static str = "1.0.0";
}
impl Versioned for TaskV2 {
const VERSION: &'static str = "1.1.0";
}
impl Versioned for TaskV3 {
const VERSION: &'static str = "1.2.0";
}
impl MigratesTo<TaskV2> for TaskV1 {
fn migrate(self) -> TaskV2 {
TaskV2 {
id: self.id,
title: "Untitled".to_string(),
}
}
}
impl MigratesTo<TaskV3> for TaskV2 {
fn migrate(self) -> TaskV3 {
TaskV3 {
id: self.id,
title: self.title,
description: None,
}
}
}
impl IntoDomain<TaskEntity> for TaskV3 {
fn into_domain(self) -> TaskEntity {
TaskEntity {
id: self.id,
title: self.title,
description: self.description,
}
}
}
impl FromDomain<TaskEntity> for TaskV3 {
fn from_domain(entity: TaskEntity) -> Self {
TaskV3 {
id: entity.id,
title: entity.title,
description: entity.description,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_migrator_single_entity() {
let migrator = migrator!("task" => [TaskV1, TaskV2, TaskV3, TaskEntity]).unwrap();
let json = r#"{"version":"1.0.0","data":{"id":"123"}}"#;
let result: TaskEntity = migrator.load("task", json).unwrap();
assert_eq!(result.id, "123");
assert_eq!(result.title, "Untitled");
assert_eq!(result.description, None);
}
#[test]
fn test_migrator_multiple_entities() -> Result<(), Box<dyn std::error::Error>> {
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
struct UserV1 {
name: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
struct UserV2 {
name: String,
email: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
struct UserEntity {
name: String,
email: String,
}
impl Versioned for UserV1 {
const VERSION: &'static str = "1.0.0";
}
impl Versioned for UserV2 {
const VERSION: &'static str = "2.0.0";
}
impl MigratesTo<UserV2> for UserV1 {
fn migrate(self) -> UserV2 {
UserV2 {
name: self.name,
email: "unknown@example.com".to_string(),
}
}
}
impl IntoDomain<UserEntity> for UserV2 {
fn into_domain(self) -> UserEntity {
UserEntity {
name: self.name,
email: self.email,
}
}
}
let migrator = migrator!(
"task" => [TaskV1, TaskV2, TaskV3, TaskEntity],
"user" => [UserV1, UserV2, UserEntity]
)?;
let task_json = r#"{"version":"1.0.0","data":{"id":"123"}}"#;
let task: TaskEntity = migrator.load("task", task_json)?;
assert_eq!(task.id, "123");
let user_json = r#"{"version":"1.0.0","data":{"name":"Alice"}}"#;
let user: UserEntity = migrator.load("user", user_json)?;
assert_eq!(user.name, "Alice");
assert_eq!(user.email, "unknown@example.com");
Ok(())
}
#[test]
fn test_migrator_with_domain_entity() {
let migrator = migrator!("task" => [TaskV1, TaskV2, TaskV3, TaskEntity]).unwrap();
let json = r#"{"version":"1.0.0","data":{"id":"456"}}"#;
let entity: TaskEntity = migrator.load("task", json).unwrap();
assert_eq!(entity.id, "456");
assert_eq!(entity.title, "Untitled");
assert_eq!(entity.description, None);
}
#[test]
fn test_migrator_preserves_latest_version() {
let migrator = migrator!("task" => [TaskV1, TaskV2, TaskV3, TaskEntity]).unwrap();
assert_eq!(migrator.get_latest_version("task"), Some("1.2.0"));
}
#[test]
fn test_migrator_single_entity_with_custom_keys() {
let migrator = migrator!(
"task" => [TaskV1, TaskV2, TaskV3, TaskEntity],
version_key = "v",
data_key = "d"
)
.unwrap();
let json = r#"{"v":"1.0.0","d":{"id":"789"}}"#;
let result: TaskEntity = migrator.load("task", json).unwrap();
assert_eq!(result.id, "789");
assert_eq!(result.title, "Untitled");
assert_eq!(result.description, None);
}
#[test]
fn test_migrator_multiple_entities_with_custom_keys() -> Result<(), Box<dyn std::error::Error>>
{
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
struct UserV1 {
name: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
struct UserV2 {
name: String,
email: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
struct UserEntity {
name: String,
email: String,
}
impl Versioned for UserV1 {
const VERSION: &'static str = "1.0.0";
}
impl Versioned for UserV2 {
const VERSION: &'static str = "2.0.0";
}
impl MigratesTo<UserV2> for UserV1 {
fn migrate(self) -> UserV2 {
UserV2 {
name: self.name,
email: "unknown@example.com".to_string(),
}
}
}
impl IntoDomain<UserEntity> for UserV2 {
fn into_domain(self) -> UserEntity {
UserEntity {
name: self.name,
email: self.email,
}
}
}
let migrator = migrator!(
@keys version_key = "v", data_key = "d";
"task" => [TaskV1, TaskV2, TaskV3, TaskEntity],
"user" => [UserV1, UserV2, UserEntity]
)?;
let task_json = r#"{"v":"1.0.0","d":{"id":"123"}}"#;
let task: TaskEntity = migrator.load("task", task_json)?;
assert_eq!(task.id, "123");
let user_json = r#"{"v":"1.0.0","d":{"name":"Bob"}}"#;
let user: UserEntity = migrator.load("user", user_json)?;
assert_eq!(user.name, "Bob");
assert_eq!(user.email, "unknown@example.com");
Ok(())
}
#[test]
fn test_migrator_with_save_single_entity() {
let migrator =
migrator!("task" => [TaskV1, TaskV2, TaskV3, TaskEntity], save = true).unwrap();
let json = r#"{"version":"1.0.0","data":{"id":"save-test"}}"#;
let entity: TaskEntity = migrator.load("task", json).unwrap();
assert_eq!(entity.id, "save-test");
assert_eq!(entity.title, "Untitled");
let saved = migrator.save_domain("task", entity).unwrap();
assert!(saved.contains("\"version\":\"1.2.0\""));
assert!(saved.contains("\"id\":\"save-test\""));
}
#[test]
fn test_migrator_with_save_multiple_entities() -> Result<(), Box<dyn std::error::Error>> {
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
struct UserV1 {
name: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
struct UserV2 {
name: String,
email: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
struct UserEntity {
name: String,
email: String,
}
impl Versioned for UserV1 {
const VERSION: &'static str = "1.0.0";
}
impl Versioned for UserV2 {
const VERSION: &'static str = "2.0.0";
}
impl MigratesTo<UserV2> for UserV1 {
fn migrate(self) -> UserV2 {
UserV2 {
name: self.name,
email: "unknown@example.com".to_string(),
}
}
}
impl IntoDomain<UserEntity> for UserV2 {
fn into_domain(self) -> UserEntity {
UserEntity {
name: self.name,
email: self.email,
}
}
}
impl FromDomain<UserEntity> for UserV2 {
fn from_domain(entity: UserEntity) -> Self {
UserV2 {
name: entity.name,
email: entity.email,
}
}
}
let migrator = migrator!(
@save;
"task" => [TaskV1, TaskV2, TaskV3, TaskEntity],
"user" => [UserV1, UserV2, UserEntity]
)?;
let task_entity = TaskEntity {
id: "multi-save-task".to_string(),
title: "Test Task".to_string(),
description: Some("Description".to_string()),
};
let task_saved = migrator.save_domain("task", task_entity)?;
assert!(task_saved.contains("\"version\":\"1.2.0\""));
let user_entity = UserEntity {
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
};
let user_saved = migrator.save_domain("user", user_entity)?;
assert!(user_saved.contains("\"version\":\"2.0.0\""));
assert!(user_saved.contains("\"name\":\"Alice\""));
Ok(())
}
#[test]
fn test_migrator_with_save_and_custom_keys() {
let migrator = migrator!(
"task" => [TaskV1, TaskV2, TaskV3, TaskEntity],
version_key = "v",
data_key = "d",
save = true
)
.unwrap();
let json = r#"{"v":"1.0.0","d":{"id":"custom-key-test"}}"#;
let entity: TaskEntity = migrator.load("task", json).unwrap();
assert_eq!(entity.id, "custom-key-test");
let saved = migrator.save_domain("task", entity).unwrap();
assert!(saved.contains("\"v\":\"1.2.0\""));
assert!(saved.contains("\"d\":{"));
}
}