#macro-derive #reflection #enums #typescript #generate #run-time #type

nightly type_reflect

通过 Derive 宏实现可扩展的运行时反射

7 个版本

0.3.0 2023年12月9日
0.2.0 2023年11月27日
0.1.4 2023年11月26日

1384Rust 模式

Download history 75/week @ 2024-07-29

每月 75 次下载

Apache-2.0 许可

85KB
1.5K SLoC

Type Reflect

Crates.io Documentation

此包是更大工作区的一部分,有关更多详细信息,请参阅 monorepo README

此项目目前边缘相当粗糙。它目前是可用的,但例如 rust 文档仍在进行中。您可能还会遇到错误和限制,因为到目前为止它只在我自己使用的情况下进行了测试。如果您遇到问题,请报告它,以便我可以使这个工具变得更好

此库的实现旨在使 Rust 类型与 TypeScript 中的 Zod 模式之间的桥接更容易。

例如,在 Rust 后端服务与 TypeScript 前端交互的上下文中,这可能很有用。

存在其他解决方案来解决类似的问题,例如出色的 ts-rs 包,我从它那里大量借鉴,但它们没有解决我遇到的具体问题,即从 Rust 类型生成 Zod 模式。

例如,如果我有这个类型

struct Foo {
    name: String,
    id: u32,
    value: f64
}

此包提供了一种方法,可以自动生成如下 Zod 模式

export const FooSchema = z.object({
    name: z.string(),
    id: z.number(),
    value: z.number(),
});

export type Foo = z.infer<typeof FooSchema>;

为此目标,此包实现了一个过程宏,它为 Rust 类型提供了运行时类型反射。


目标

此包具有自己的观点。它的目的是使所有 Rust 中可表达类型的实用子集尽可能易于与其他语言共享,尤其是 TypeScript。

此包的目标是

  • 提供一个过程宏,可以将 Rust 类型通过一行代码转换为 TypeScript 可移植类型
  • 与 Serde 互操作,以便通过 JSON 在语言之间轻松共享类型
  • 让开发者控制在不同语言中导出类型定义的时间和地点

非目标

  • 此包不寻求支持每个 Rust 类型。目标是支持可以轻松在语言之间共享的类型。如果类型不受支持,则宏应快速失败并显示有意义的错误消息。不受支持的类型示例包括
    • 引用类型 (&)
    • Box, Ref, Arc, Mutex 等。
    • 基本上任何不能轻易序列化为 JSON 的东西
  • 此包目前不是针对性能优化的,而是针对生产力优化

重要细节

Serde 属性

Reflect》宏支持某些《serde》属性,以便更容易地保持所有表示与序列化表示一致。

具体包括

1. rename_all

此属性通常用于在键之间转换大小写约定,例如 snake_casecamelCase

例如,可以使用此属性将 Rust 的 snake_case 表示转换为 Zod 输出的 camelCase

#[derive(Reflect, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Foo {
    key_1: ...
    key_2: ...
}

这将导致 Zod 导出使用键名 key1key2

tag

对于具有关联数据的枚举类型,需要 tag 属性。例如,此声明

#[derive(Reflect, Serialize, Deserialize)]
enum MyEnum {
    VariantA { x: u32 }
    VariantB { text: String }
}

将引发错误。错误的原因是,默认情况下,serde 使用外部标记的枚举 JSON 表示形式。即上述代码将序列化为

{ "VariantA": { "x": 42 }}
{ "VariantB": { "text": "foo" }}

此类型的枚举表示形式由 type_reflect 禁止,因为它不便于与 TypeScript 联合类型桥接,而 TypeScript 联合类型是 TypeScript 中 ADT 的最佳类似物。

发射器必须实现 Default

由于发射器是通过 export_types! 实例化的,因此如果它们在宏中声明为目的地,则必须为发射器实现 Default 特性。

例如,此目的地

MyEmitter(...)

会被转换为这样

let mut MyEmitter {
    ..Default::default()
}

这是为了支持向具有默认值的发射器传递命名参数。

未将 Default 作为特性的要求,以便在宏之外创建发射器实例。


示例用法

一个工作示例可以在 type_reflect/examples/declare_and_export.rs 找到。

可以使用以下命令运行: cargo run -p type_reflect --example declare_and_export

输出文件将写入: type_reflect/example_output

简单结构定义

#[derive(Reflect)]
struct MyStruct {
    foo: i32,
    bar: String,
    baz: MyOtherType
}


export_types!(
    types: [
        MyStruct,
        MyOtherType,
    ]
    exports: [
        Zod("/export/dir/1"),
        Rust("/export/dir/2", "/export/dir/3"),
        MyCustomExport("/export/dir/4")
    ]
)

其中 export_types 转换为

let mut emitter = Zod {
    ..Default::default()
};
let mut file = emitter
    .init_destination_file(
        "/export/dir/1",
        "",
    )?;
file.write_all(emitter.emit::<MyStruct>().as_bytes())?;
file.write_all(emitter.emit::<MyOtherType>().as_bytes())?;
emitter.finalize("/export/dir/1")?;
...

这里所有目录都是相对于执行二进制的当前工作目录的相对目录。

自定义前缀

还可以支持为输出文件支持自定义前缀。

这可能很有用,例如,如果我们希望导出的类型依赖于在目标项目中直接定义的类型。

例如,假设我们像这样定义并导出了一个类型

#[define(Reflect, Serialize, Deserialize)]
struct Foo {
    bar: Bar
}
...
export_types!(
    types: [
        Foo,
    ]
    exports: [
        TypeScript("./export/foo.ts"),
    ]
)

默认情况下,这将生成以下 .export/foo.ts 文件

export type Foo {
  bar: Bar
}

当然,这不是有效的 TypeScript,因为这里的类型 bar 是未定义的。

因此,我们可以添加一个前缀来从不同位置导入 Bar

export_types!(
    types: [
        Foo,
    ]
    exports: [
        TypeScript(
            "./export/foo.ts",
            prefix: "import { Bar } from './bar.ts'",
        ),
    ]
)

这将转换为这样

let mut emitter = TypeScript {
    ..Default::default()
};
let mut file = emitter
    .init_destination_file(
        "/export/dir/1",
        "import { Bar } from './bar.ts'",
    )?;

并将生成以下 TypeScript

import { Bar } from './bar.ts'

export type Foo = {
  bar: Bar;
};

默认情况下,前缀将添加到输出文件 之前emitter.dependencies(),但可以在 TypeEmitter 实现中自定义。

自定义 TypeEmitter 参数

通过 export_types 宏,还可以将初始化参数传递给类型发射器。

例如,TypeScript 发射器支持一个 tab_size 参数来定义输出缩进的大小。

如果在 export_types 函数中如此指定参数

    ...
    exports: [
        TypeScript(
            "/export/dir/1",
            tab_size: 4,
        ),
    ]
    ...

这将被转换成如下形式

let mut emitter = TypeScript {
    tab_size: 4,
    ..Default::default()
};

prefix 参数不会被转发到发射器初始化,因为它被传递给了 init_destination_file 的调用。

多发射器目的地

export_types 中也可以使用多个发射器来定义目的地。这可能很有用,例如,如果你想使用多个类型发射器将输出写入同一文件。例如

export_types! {
    types: [
        Foo,
        Bar,
        Baz,
    ],
    destinations: [
        (
            "./ouptu_file.ts",
            emitters: [
                TypeScript(),
                TSValidation(),
            ]
        ),
    ]
}

这首先会使用 TypeScript 发射器输出类型 FooBarBaz,然后使用 TSValidation 发射器。

枚举转换

枚举的转换取决于枚举的类型。

简单枚举,定义为没有关联数据的枚举,将直接转换为带有字符串值的 TypeScript 枚举。

例如这个枚举


enum Foo {
    Bar,
    Baz
}

将输出以下内容


export enum SimpleEnumsExample {
    Foo = "Foo",
    Bar = "Bar,
}

export const SimpleEnumsExampleSchema = z.enum([
    SimpleEnumsExample.Foo,
    SimpleEnumsExample.Bar,
])

带有关联数据的枚举默认转换为联合类型,其中 _case 字段用于区分各个情况。

例如这个枚举

#[derive(Debug, Reflect, Serialize, Deserialize)]
#[serde(tag = "_case", content = "data")]
enum Status {
    Initial,
    #[serde(rename_all = "camelCase")]
    InProgress {
        progress: f32,
        should_convert: bool,
    },
    Complete {
        urls: Vec<String>,
    },
}

将输出如下


export enum StatusCase {
    Initial = "Initial",
    InProgress = "InProgress",
    Complete = "Complete",
}

export const StatusCaseInitialSchema = z.object({
    _case: z.literal(StatusCase.Initial),
});
export type StatusCaseInitial = z.infer<typeof StatusCaseInitialSchema>

export const StatusCaseInProgressSchema = z.object({
    _case: z.literal(StatusCase.InProgress),
    data: z.object({
        progress: z.number(),
    shouldConvert: z.bool(),
    })});
export type StatusCaseInProgress = z.infer<typeof StatusCaseInProgressSchema>

export const StatusCaseCompleteSchema = z.object({
    _case: z.literal(StatusCase.Complete),
    data: z.object({
        urls: z.array(z.string()),
    })});
export type StatusCaseComplete = z.infer<typeof StatusCaseCompleteSchema>

export const StatusSchema = z.union([
    StatusCaseInitialSchema,
    StatusCaseInProgressSchema,
    StatusCaseCompleteSchema,
]);
export type Status = z.infer<typeof StatusSchema>

依赖

~11MB
~252K SLoC