7 个版本
0.3.0 | 2023年12月9日 |
---|---|
0.2.0 | 2023年11月27日 |
0.1.4 | 2023年11月26日 |
1384 在 Rust 模式
每月 75 次下载
85KB
1.5K SLoC
Type Reflect
此包是更大工作区的一部分,有关更多详细信息,请参阅 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_case
和 camelCase
。
例如,可以使用此属性将 Rust 的 snake_case
表示转换为 Zod 输出的 camelCase
。
#[derive(Reflect, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Foo {
key_1: ...
key_2: ...
}
这将导致 Zod
导出使用键名 key1
和 key2
。
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 发射器输出类型 Foo
、Bar
和 Baz
,然后使用 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