#uuid #type #sqlx #serde #human-readable #object-id #kinds

类型

由 UUID 支持的无成本类型标识符,其类型在序列化版本中可读

4 个版本 (1 个稳定版)

1.0.0 2024年2月26日
0.3.0 2024年2月16日
0.2.0 2024年2月8日
0.1.0 2024年2月5日

#219Rust 模式

MIT 许可证

35KB
584

MIT Latest Version Docs badge

为什么选择类型?

使用 类型,您

  • 在 Rust 中使用类型标识符,且无需 Uuid 的开销
  • 使类型在 JSON 和任何导出中可读且明显
  • (如果启用 sqlx 功能)仍然可以在数据库中使用 uuid
  • 无需编写代码,无需显式字符串化、解析、检查类型等。
  • 无需声明类型和标识符的样板代码
  • 您的 id 实现了 Copy、Display、FromStr、Serialize、Deserialize、Eq、Hash 等功能
  • 安全地处理同一类型的已标识对象和新的对象

另请参阅 类型动机和操作介绍

可选功能

  • serde: 为 IdIded 以及 id_enum! 枚举实现 SerializeDeserialize 实现
  • sqlx: 对于 Id(具有 uuid 列)和 Ided(具有 uuid 标识符的表)实现透明的读写
  • jsonschema: JSON 架构生成
  • openapi: 为 Id 实现开放 API ID 对象类型

在当前版本中,sqlx 功能仅对 postgresql 完整支持。

声明对象类型

您可以实现 Identifiable 特性,但最简单的解决方案是直接在您的结构体中添加属性

use kind::*;

#[derive(Identifiable)]
#[kind(class="Cust")]
pub struct Customer {
    // many fields
}

#[derive(Identifiable)]
#[kind(class="Cont")]
pub struct Contract {
    // many fields
}

Id

kind::Id 是强类型化的,以避免滥用 Rust API,尤其是在函数需要多个不同类型的 id 时。

kind::Id 还通过在内部使用的 id 前缀一个类前缀来防止任何基于字符串的 API(如 Rest 或 GraphQL)的滥用。

无成本:类型由类型系统处理,不会使编译的二进制文件变得混乱

assert_eq!(
    std::mem::size_of::<Id<Customer>>(),
    std::mem::size_of::<uuid::Uuid>(),
);

您可以从JSON或其他字符串中解析ID。

let id: Id<Customer> = "Cust_371c35ec-34d9-4315-ab31-7ea8889a419a"
    .parse().unwrap();

类型已检查,因此此客户ID不能误用为合同ID。

assert!(
    "Cust_371c35ec-34d9-4315-ab31-7ea8889a419a"
    .parse::<Id<Contract>>()
    .is_err()
);

注意:公共ID以不区分大小写的方式解析和检查。

assert_eq!(id, "cust_371c35ec-34d9-4315-ab31-7ea8889a419a".parse());
assert_eq!(id, "CUST_371C35EC-34D9-4315-AB31-7EA8889A419A".parse());

已标记

Ided是“已识别”的缩写。

有时,您必须处理没有ID的原始对象,因为这正是您从REST API创建时收到的,或者因为您在将行插入数据库时才提供ID。

这就是为什么我们的原始Customer类型没有ID。大多数API不处理仅仅是原始的Customer类型,而是处理一个Ided<Customer>,它保证有一个ID。

可以从ID和“实体”创建一个已标记的对象。

let new_customer = Customer { name: "John".to_string() };
let customer = Ided::new(id, new_customer);
assert_eq!(
    customer.id().to_string(),
    "Cust_371c35ec-34d9-4315-ab31-7ea8889a419a"
);
assert_eq!(customer.entity().name, "John");

已标记对象自动解析为实体类型,因此这也有效。

assert_eq!(customer.name, "John");

序列化

Ided对象以ID与其他字段相邻的方式序列化,没有不必要的嵌套。

#[derive(Identifiable, serde::Serialize, serde::Deserialize)]
#[kind(class="Cust")]
pub struct Customer {
    pub name: String,
}

let json = r#"{
    "id": "Cust_371c35ec-34d9-4315-ab31-7ea8889a419a",
    "name": "John"
}"#;

let customer: Ided<Customer> = serde_json::from_str(&json).unwrap();
assert_eq!(
    customer.id().to_string(),
    "Cust_371c35ec-34d9-4315-ab31-7ea8889a419a"
);
assert_eq!(customer.name, "John");

检查ID类型,以下反序列化失败,因为ID的前缀错误。

let json = r#"{
    "id": "Con_371c35ec-34d9-4315-ab31-7ea8889a419a",
    "name": "John"
}"#;
assert!(serde_json::from_str::<Ided<Customer>>(&json).is_err());

sqlx/PostgreSQL

在数据库中,ID只是一个uuid。数据库中ID的类型由查询和您的数据库结构隐式给出,读取/写入Rust到数据库时没有额外的检查,您在开始使用Kind时不需要更改数据库结构。

Id类型实现了EncodeDecode,因此它可以像任何其他主类型一样在sqlx查询中透明地使用(也支持Id的数组,这在PostgreSQL中很方便)。

至于serde,FromRowIded上的实现是从原始结构上的实现自动推导出来的。

因此,您通常会像这样声明结构体,以便从包含ID列和原始结构体列的sqlx::Row加载Ided

#[derive(Identifiable, sqlx::FromRow)]
#[kind(class="Cust")]
pub struct Customer {
    pub name: String,
}

JSON模式

如果您正在使用schemars crate为您对象生成JSON模式,您可以启用jsonschema功能,我们将为Id对象和任何Ided对象生成定义。

#[derive(JsonSchema, Identifiable)]
#[kind(class="Cust")]
pub struct Customer {
    pub name: String
}

fn main() {
    let schema = schema_for!(Ided<Customer>);
    println!("{}", serde_json::to_string_pretty(&schema).unwrap());
}

将打印出

{
  "$schema": "https://json-schema.fullstack.org.cn/draft-07/schema#",
  "title": "Customer_ided",
  "description": "Identified version of Customer",
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "format": "string"
    },
    "name": {
      "type": "string"
    }
  }
}

Open API

Open API支持(由openapi功能标志控制)目前非常基础。到目前为止,唯一支持的功能是定义可由其他模式引用的schema级别的Id对象。

Id包含到生成的模式中的示例

pub struct ApiDoc;
impl utoipa::OpenApi for ApiDoc {
    fn openapi() -> utoipa::openapi::OpenApi {
        let mut components = utoipa::openapi::ComponentsBuilder::new();
        let (kind_name, kind_schema) = kind::openapi_schema();
        components = components.schema(kind_name, kind_schema);
        //extra components and paths
        let mut openapi = utoipa::openapi::OpenApiBuilder::new()
            .components(Some(components.build()))
            .build();
        openapi
    }
}

依赖关系

~2-17MB
~245K SLoC