#最新版本 #迁移 #版本 #升级 #serde #同构

magic_migrate

自动加载并将反序列化的结构体迁移到最新版本

2个不稳定版本

0.2.0 2024年5月12日
0.1.0 2024年1月15日

#392 in 编码

37 每月下载量

MIT 协议

37KB
184

魔法迁移

自动加载并将反序列化的结构体迁移到最新版本。

🎵 如果你相信魔法,就跟我来

我们将一直跳舞到早上,直到只剩下你和我 🎵

这些文档旨在在 docs.rs 上阅读。

什么是

假设你创建了一个以某种方式序列化到磁盘的结构体;也许它使用toml。现在,假设你想要向该结构体添加一个新字段,但想要保留旧持久数据。你该怎么做?

你可以使用FromTryFrom来定义如何从一个结构体转换到另一个结构体,然后通过MigrateTryMigrate特质告诉Rust如何从一版本迁移到下一版本。现在,当你尝试将数据加载到当前结构体时,它将按反向顺序遍历结构体链,以找到第一个可以成功序列化的结构体。当发生这种情况时,它将自动将该结构体转换为最新版本。这是魔法!(实际上,这主要是通过特质边界进行的巧妙使用,但不管怎样)。

文档

有关更多信息,请参阅

带有 TryMigrate 的不可靠迁移示例与 try_migrate_deserializer_chain

一旦定义,通过在您希望反序列化的结构体上调用 try_from_str_migrations 关联函数来执行迁移。

要定义迁移,可以使用如下宏

use magic_migrate::{TryMigrate, try_migrate_deserializer_chain};
use serde::de::Deserializer;

// ...

try_migrate_deserializer_chain!(
    deserializer: toml::Deserializer::new,
    error: PersonMigrationError,
    chain: [PersonV1, PersonV2],
);
  • deserializer: 参数应该是一个函数,它接受一个 &str 并返回 impl Deserialize<'de>。要反序列化 TOML,可以使用 toml 包并指定 toml::Deserializer::new 函数。
  • error: 在反序列化失败时使用的错误枚举。每个 TryFrom 应返回一个定义了此提供错误 Into 的错误。
  • chain: 一个有序的从左到右的结构体列表,您希望在这些结构体之间迁移。每对结构体必须单独定义它自己的 TryFrom。在这种情况下,只需要一个 TryFrom<PersonV1> for PersonV2

完整示例

use magic_migrate::{TryMigrate, try_migrate_deserializer_chain};
use serde::de::Deserializer;

use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
use std::convert::Infallible;

#[derive(Deserialize, Serialize, Debug)]
#[serde(deny_unknown_fields)]
struct PersonV1 {
    name: String
}

#[derive(Deserialize, Serialize, Debug)]
#[serde(deny_unknown_fields)]
struct PersonV2 {
    name: String,
    updated_at: DateTime<Utc>
}
try_migrate_deserializer_chain!( // <=========== HERE
    deserializer: toml::Deserializer::new,
    error: PersonMigrationError,
    chain: [PersonV1, PersonV2],
);

impl TryFrom<PersonV1> for PersonV2 {
    type Error = NotRichard;

    fn try_from(value: PersonV1) -> Result<Self, NotRichard> {
        if &value.name == "Schneems" {
            Ok(PersonV2 {
                    name: value.name.clone(),
                    updated_at: Utc::now()
               })
        } else {
            Err(NotRichard { name: value.name.clone() })
        }
    }
}

#[derive(Debug, Eq, PartialEq)]
struct NotRichard {
  name: String
}

impl From<NotRichard> for PersonMigrationError {
    fn from(value: NotRichard) -> Self {
        PersonMigrationError::NotRichard(value)
    }
}

#[derive(Debug, Eq, PartialEq)]
enum PersonMigrationError {
    NotRichard(NotRichard),
}

// Create a V2 struct from V1 data
let person: PersonV2 = PersonV2::try_from_str_migrations("name = 'Schneems'").unwrap().unwrap();
assert_eq!(person.name, "Schneems".to_string());

提示:您可以通过创建类型别名来减少代码冗余,例如

pub(crate) type Person = PersonV2;

为什么

这个库是为了处理在 https://github.com/heroku/libcnb.rs 构建包中存储为 toml 的序列化元数据的情况。

在这个用例中,当运行云原生构建包 (CNB) 时,结构体会序列化到磁盘。通常,这些值代表应用程序缓存状态,对于缓存失效很重要。

构建包实现者无法控制构建包运行频率。这意味着无法保证最终用户将以连续的结构体版本运行它。一个用户可能使用最新版本的结构体序列化运行,而另一个可能使用多年前的版本。

这种场景在野外与 https://github.com/heroku/heroku-buildpack-ruby(一个“经典”构建包,即不是 CNB)中发生。

而不是强制程序员始终考虑所有可能的缓存状态,一种“迁移”方法允许程序员一次专注于一个缓存状态变化。这减少了程序员的认知负担,并(希望)减少了错误。

它不能做什么?(ABA 问题)

此库不能保证如果 PersonV1 结构体被序列化,则不能在没有迁移的情况下将其加载到 PersonV2。即它不保证运行了 FromTryFrom 代码。

例如,如果PersonV2结构体引入了一个Option<String>字段,而不是DateTime<Utc>,那么字符串"name = 'Richard'"可以被反序列化为PersonV1或PersonV2,而无需调用迁移。

在Serde的相关讨论中还有更多链接

您能做些什么来强化代码以抵御这种(ABA)问题?

  • 使用serde的deny_unknown_fields。此设置防止静默删除额外的结构体字段。此策略将处理V1有两个字段而V2只有一个字段的情况游乐场示例。但是,它将不会保护我们添加了可选字段的情况游乐场示例
  • 添加测试以确保一个结构体不能反序列化为链中的后续结构体。如果您的结构体具有许多可选字段,并且您想生成所有可能的排列,编写测试可能很困难。
  • 添加一个版本标记字段。此策略有效,但您必须在创建新结构体时注意并更新字段名称(可能存在程序员错误)。此外,它将实现细节泄露给可能查看您的序列化数据的人(这可能或可能不重要)。
  • 阅读这些文档并了解这种发生的原因。
  • 如果您有其他强化代码库的建议,请提出问题。

其他可能的“迁移”解决方案及其差异

与使用Serde的fromtry_from容器属性功能相比,magic migrate始终首先尝试转换为目标结构体,然后使用链中的最新可能的结构体进行迁移,允许结构体在整个链中迁移或存储和使用最新值。

  • 似乎Serde版本crate有重叠的目标。差异尚不清楚。如果您已尝试它,请更新这些文档。

依赖关系

~110–345KB