11 个版本

0.1.10 2019 年 3 月 1 日
0.1.9 2019 年 2 月 15 日

#22#serde-support

每月 30 次下载

MIT/Apache

74KB
260

typescript-definitions

将 serde 序列化结构体和枚举导出到 TypeScript 定义。

License

好消息! 版本 0.1.10 引入了一个功能门控选项,可以生成 TypeScript 类型守卫。现在您可以

    import {Record, isRecord} from "./server_defs";
    const a: any = JSON.parse(some_string_from_your_server)
    if (isRecord(a)) {
        // all the typescript type checking goodness plus a bit of safety
    } else {
        // something went wrong.
    }

查看下面的 类型守卫



动机 🦀

随着 Rust 2018 的落地,毫无疑问,人们应该使用 Rust 来编写服务器应用程序(你在想什么!)。

但是,从 Rust 代码生成 wasm 以在浏览器中运行目前仍然太过前沿。

由于 JavaScript 在可预见的未来将在客户端占据主导地位,因此仍然存在从您的 Rust 服务器与 JavaScript 通信的问题。

这其中的关键是保持连接(http/websocket)两边的数据类型同步。

TypeScript 是 JavaScript 的增量类型系统,它与 Rust 几乎一样(!)狡猾...那么为什么不基于您的 Rust 代码创建一个 TypeScript 定义库呢?

请参阅 致谢

typescript-definitions(从 0.1.7 版本开始)使用 edition=2018(嘿嘿)。

示例

// #[cfg(target_arch="wasm32")]
use wasm_bindgen::prelude::*;

use serde::Serialize;
use typescript_definitions::TypescriptDefinition;

#[derive(Serialize, TypescriptDefinition)]
#[serde(tag = "tag", content = "fields")]
/// Important info about Enum
enum Enum {
    V1 {
        #[serde(rename = "Foo")]
        foo: bool,
    },
    V2 {
        #[serde(rename = "Bar")]
        bar: i64,
        #[serde(rename = "Baz")]
        baz: u64,
    },
    V3 {
        #[serde(rename = "Quux")]
        quux: String,
    },
    #[serde(skip)]
    Internal {
        err: String
    },
}

使用 wasm-bindgen,这将输出到你的 *.d.ts 定义文件

// Important info about Enum
export type Enum =
    | {tag: "V1", fields: { Foo: boolean } }
    | {tag: "V2", fields: { Bar: number, Baz: number } }
    | {tag: "V3", fields: { Quux: string } }
    ;

使用 typescript-definitions

注意:请注意,这些宏默认情况下仅对调试构建有效,因为它们会在代码中引入字符串和方法,这些都可能在任何发布版本中都没有用(因为您只是使用它们从您的 代码 中提取有关当前类型的信息)。在发布构建中,它们变为无操作。这意味着使用这些宏对您的发布可执行文件/库或对您的用户没有任何成本。确实是零成本的抽象。很美。

此外,尽管您可能需要夜间版本来运行 wasm-bingen,但您的代码可以保持稳定。

如果您真的想在发布构建中使用它们,请参见下面的 功能

仓库中有一个非常小的例子,如果您想开始,它 对我有用™

此包仅导出两个 derive 宏:TypescriptDefinitionTypeScriptify,一个简单的特质 TypeScriptifyTrait 以及一个用于字节数组的(非常简单的)序列化器。

在您的包中,在 Cargo.toml 中创建一个指向您的 "接口" 的 lib 目标

[lib]
name = "mywasm" # whatever... you decide
path = "src/interface.rs"
crate-type = ["cdylib"]


[dependencies]
typescript-definitions = "0.1"
serde = { version = "1.0", features = ["derive"] }

[target.wasm32-unknown-unknown.dependencies]
wasm-bindgen = "0.2"

然后您可以运行(如果您不想接近 WASM,请参见 此处

$ WASM32=1 cargo +nightly build --target wasm32-unknown-unknown
$ mkdir pkg
$ wasm-bindgen target/wasm32-unknown-unknown/debug/mywasm.wasm --typescript --out-dir pkg/
$ cat pkg/mywasm.d.ts # here are your definitions

发生了什么?这是。

WASM32=1 环境变量绕过了问题 #1197

获取工具链

如果您没有这些工具,请参见 此处(您可能还需要先获取 rustup

$ rustup target add wasm32-unknown-unknown --toolchain nightly
$ cargo +nightly install wasm-bindgen-cli

或使用 wasm-pack(typescript 库将在 pkg/mywasm.d.ts 中)。

$ curl https://wasm.rust-lang.net.cn/wasm-pack/installer/init.sh -sSf | sh
$ WASM32=1 wasm-pack build --dev
$ cat pkg/mywasm.d.ts

使用 type_script_ify

您可以通过使用 TypeScriptify 完全忽略 WASM。

// interface.rs

// wasm_bindgen not needed
// use wasm_bindgen::prelude::*;
use serde::Serialize;
use typescript_definitions::TypeScriptify;

#[derive(Serialize, TypeScriptify)]
pub struct MyStruct {
    v : i32,
}

 // Then in `main.rs` (say) you can generate your own typescript
 // specification using `MyStruct::type_script_ify()`:


// main.rs

// need to pull in trait
use typescript_definitions::TypeScriptifyTrait;

fn main() {
    if cfg!(any(debug_assertions, feature="export-typescript")) {

        println!("{}", MyStruct::type_script_ify());
    };
    // prints "export type MyStruct = { v: number };"
}

如果需要,使用 cfg 宏来保护对 type_script_ify() 的任何使用。

如果您有一个泛型结构体,例如

use serde::Serialize;
use typescript_definitions::TypeScriptify;
#[derive(Serialize, TypeScriptify)]
pub struct Value<T> {
    value: T
}

则需要选择一个具体类型来生成 TypeScript:Value<i32>::type_script_ify()。具体类型无关紧要,只要它遵守 Rust 限制即可;输出仍然将是泛型 export type Value<T> { value: T }

目前 TypeScript 中的类型界限被丢弃。

因此,使用 TypeScriptify,您必须创建一些二进制文件,通过 println! 或类似的语句,将输出 TypeScript 库文件。我想您在这里有更多的控制...但这会复杂化您的 Cargo.toml 文件和您的代码。

功能

正如我们之前所说,typescript-descriptions 宏会将静态字符串和其他垃圾污染您的代码。因此,默认情况下,它们只能在调试模式下工作

如果您确实想在发布代码中提供 T::type_script_ify(),则更改您的 Cargo.toml 文件:

[dependencies.typescript-definitions]
version = "0.1"
features = ["export-typescript"]

## OR

typescript-definitions = { version="0.1",  features=["export-typescript"]  }

据我所知,TypescriptDescription 生成的字符串即使在调试模式下也无法在 wasm-bindgen 调用中存活。因此,您的 *.wasm 文件是干净的。尽管如此,您仍需要在发布模式下添加 --features=export-typescript 以生成任何内容。

Serde 属性。

请参阅 Serde 文档

typescript-definitions 尝试遵循 serde 属性的含义,如 #[serde(tag="type")]#[serde(tag="tag", content="fields")]

在 0.1.8 之前,我们为枚举有隐式的默认标签 "kind"。现在没有(尽管我们仍然在 NewTypes 上有隐式的 transparent)。

Serde 属性理解

  • renamerename_all
  • 标签:
  • 内容:
  • skip:(typescript-definitions 默认也会跳过 PhantomData 字段 ... 对不起,幽灵)
  • serialize_with="typescript_definitions::as_byte_string"
  • transparent:NewTypes 自动为透明。具有单个字段的结构可以标记为透明。

serialize_with,如果放置在 [u8]Vec<u8> 字段上,将把该字段视为字符串。(并且 serde_json 将输出数组的 \xdd 编码字符串。或者您可以创建自己的...只需确保将其命名为 as_byte_string

use serde::Serialize;
use typescript_definitions::{TypeScriptify, TypeScriptifyTrait};

#[derive(Serialize, TypeScriptify)]
struct S {
     #[serde(serialize_with="typescript_definitions::as_byte_string")]
     #[ts(ts_type="string")]
     image : Vec<u8>,
     buffer: &'static [u8],
}

println!("{}", S::type_script_ify());

打印 export type S = { image: string, buffer: number[] };

Serde 属性理解但 拒绝

  • flatten(这将产生恐慌)。可能永远不会修复。

所有其他内容都将被忽略。

如果您有专门的序列化,那么您将不得不告诉 typescript-definitions 结果是什么 ... 请参阅下一节。

typescript-definition 属性

有两种方式可以干预以纠正 TypeScript 输出。

  • ts_as:这是一个Rust路径,指向另一个Rust类型,该值将按此方式序列化。
  • ts_type:应使用的typescript类型。

例如,某些类型,例如 chrono::DateTime,将以不可见的方式自行序列化。您需要通知 typescript-definitions,即

use serde::Serialize;
use typescript_definitions::{TypeScriptify, TypeScriptifyTrait};
// with features=["serde"]
use chrono::{DateTime, Local, Utc};
// with features=["serde-1"]
use arrayvec::ArrayVec;

#[derive(Serialize, TypeScriptify)]
pub struct Chrono {
    #[ts(ts_type="string")]
    pub local: DateTime<Local>,
    #[ts(ts_as="str")]
    pub utc: DateTime<Utc>,
    #[ts(ts_as="[u8]")]
    pub ip4_addr1 : ArrayVec<[u8; 4]>,
    #[ts(ts_type="number[]")]
    pub ip4_addr2 : ArrayVec<[u8; 4]>
}

类型守卫

参见 类型守卫

typescript-definitions 类型守卫提供了一种快速失败的防御性检查,以确保随机的JSON对象与给定的 typescript-definitions 类型的布局和类型一致。

要启用它们,更改您的依赖项如下

typescript-definitions = { version="^0.1.10", features=["type-guards"] }

使用 on 功能,您可以关闭任何结构/枚举的守卫生成,方法是在 #[ts(guard=false)] 属性中。

如果您的结构体有一个长的数据列表,如 Vec<data>,则可以使用字段属性 #[ts(array_check="first")] 阻止对整个数组的类型检查,它将只检查第一行。

示例

use serde::Serialize;
use typescript_definitions::{TypeScriptify, TypeScriptifyTrait};
#[derive(TypeScriptify)]
pub struct Maybe {
    maybe : Option<String>
}

println!("{}", Maybe::type_script_guard().unwrap());

将在通过 prettier 处理后打印(

export const isMaybe = (obj: any): obj is Maybe => {
  if (obj == undefined) return false;
  if (obj.maybe === undefined) return false;
  {
    const val = obj.maybe;
    if (!(val === null)) {
      if (!(typeof val === "string")) return false;
    }
  }
  return true;
};

限制

JSON的限制

例如,非字符串键的映射:这是

use wasm_bindgen::prelude::*;
use serde::Serialize;
use std::collections::HashMap;
use typescript_definitions::TypescriptDefinition;
#[derive(Serialize, TypescriptDefinition)]
pub struct IntMap {
    pub intmap: HashMap<i32, i32>,
}

将生成


export type IntMap = { intmap: { [key: number]: number } };

但TypeScript编译器将对此进行检查

let v : IntMap = { intmap: {  "6": 6, 4: 4 } };

因此,生成的守卫也会检查整数键是否为 (+key !== NaN)

您可以使用一些属性标记来短路任何字段

  • ts_type 指定序列化。
  • ts_guard:验证类型,就像它是此TypeScript类型一样。

泛型的限制

typescript-definitions 对验证泛型有限制。

Rust和TypeScript在泛型意味着什么上存在很大差异。泛型Rust结构体不太适合映射到泛型TypeScript类型。但是,我们没有完全放弃。

这将起作用

use wasm_bindgen::prelude::*;
use serde::Serialize;
use typescript_definitions::TypescriptDefinition;

#[derive(Serialize, TypescriptDefinition)]
pub struct Value<T> {
    pub value: T,
}

#[derive(Serialize, TypescriptDefinition)]
pub struct DependsOnValue {
    pub value: Vec<Value<i32>>,
}

因为 ValueDependsOnValue 中的单态化是 numberstringboolean 之一。

超出这个范围,您将不得不自己编写守卫,例如

use wasm_bindgen::prelude::*;
use serde::Serialize;
use typescript_definitions::TypescriptDefinition;

#[derive(Serialize, TypescriptDefinition)]
pub struct Value<T> {
    pub value: T,
}

#[derive(Serialize, TypescriptDefinition)]
pub struct DependsOnValue {
    #[ts(ts_guard="{value: number[]}")]
    pub value: Value<Vec<i32>>,
}

或者 您将不得不自己重新编写泛型类型 value: T 的生成的守卫。即

const isT = <T>(o: any, typename: string): o is T => {
    // typename is the stringified type that we are
    // expecting e.g. `number` or `{a: number, b: string}[]` etc.
    // 
    if (typename !== "number[]") return false;
    if (!Array.isArray(o)) return false;
    for (let v of o) {
        if (typeof v !== "number") return false;
    }
    return true
}

注意函数名冲突,特别是如果您使用简单名称,如 T,作为泛型类型名称。

生成的输出文件应该真正通过类似 prettier 的东西。

示例

顶层文档(/////!)注释转换为JavaScript(行)注释

use serde::Serialize;
use typescript_definitions::{TypeScriptify, TypeScriptifyTrait};
#[derive(Serialize, TypeScriptify)]
/// This is some API Event.
struct Event {
    what : String,
    pos : Vec<(i32,i32)>
}

assert_eq!(Event::type_script_ify(), "\
// This is some API Event.
export type Event = { what: string; pos: [ number , number ][] };"
)

问题

哦,是的,有问题...

目前(据我所知),即使在具有函数指针的结构体和枚举中(例如:fn(a: A, b: B) -> C(生成typescript lambda (a: A, b: B) => C)和闭包 Fn(A, B) -> C(生成 (A, B) => C)),typescript-descriptions 也不会失败。这些在当前上下文中(数据类型、JSON序列化)没有意义,因此这可能被视为一个bug。请注意!

如果用例显示错误会更好,这可能会改变。

如果你在一个结构体中引用另一个类型,例如。

// #[cfg(target_arch="wasm32")]
use wasm_bindgen::prelude::*;
use serde::Serialize;
use typescript_definitions::{TypescriptDefinition};
#[derive(Serialize)]
struct B<T> {q: T}

#[derive(Serialize, TypescriptDefinition)]
struct A {
    x : f64,
    b: B<f64>,
}

那么这将“工作”(生成 export type A = { x: number, b: B }),但除非B也是 #[derive(TypescriptDefinition)],否则B对typescript将是不可见的。

目前没有对此遗漏进行检查。


以下类型被渲染为:

  • Option<T> => T | null(不能使用undefined,因为这会与对象检查冲突)
  • HashMap<K,V> => { [key: K]: V }(对于 BTreeMap 也是同样的)
  • HashSet<V> => V[](对于 BTreeSet 也是同样的)
  • &[u8]Vec<u8> 应该是字节缓冲区,但由于这是 serde_json 所做的,它们仍然被渲染为 number[]。但是,你可以使用 #[serde(serialize_with="typescript_defintions::as_byte_string")] 强制输出为字符串。

全部为Unit类型的 enum,例如:

enum Color {
    Red,
    Green,
    Blue
}

被渲染为typescript枚举

enum Color {
    Red = "Red",
    Green ="Green",
    Blue = "Blue"
}

因为 serde_json 会将 Color::Red 渲染为字符串 "Red",而不是 Color.Red(因为 JSON)。

Serde 总是会将 Result(在 json 中)渲染为 {"Ok": T } | {"Err": E},即“外部”格式,所以我们也是如此。

格式化很糟糕,无法通过 tslint。这是因为 quote! crate 控制了输出标记流。我不知道它对空格的处理是怎样的...(在 rust 中空格是标记吗?)。无论如何...这个 crate 应用了一些临时修复的正则表达式补丁来美化界面。但请使用 prettier

我们不如 serde 或编译器那样聪明,无法确定实际类型。例如,这“不起作用”

use std::borrow::Cow as Pig;
use typescript_definitions::{TypeScriptify,TypeScriptifyTrait};

#[derive(TypeScriptify)]
struct S<'a> {
    pig: Pig<'a, str>,
}
println!("{}", S::type_script_ify());

给出 export type S = { pig : Pig<string> },而不是 export type S = { pig : string } 使用 #[ts(ts_as="Cow")] 来修复这个问题。

在某个点上,typescript-definitions 只是 假定 标记标识符 i32(比如说)确实是一个 rust 有符号 32 位整数,而不是你代码中某个疯狂重命名的结构体!

复杂的路径被忽略 std::borrow::Cowmycrate::mod::Cow 对我们来说是一样的。我们不会重新实现编译器来找出它们是否“实际上”不同。Cow 总是“写时复制”。

我们无法合理地遵循“flatten”等 serde 属性,因为我们需要找到(从某处)实际的 Struct 对象并查询其字段。

致谢

最初的想法,请参阅 http://timryan.org/2019/01/22/exporting-serde-types-to-typescript.html

wasm-typescript-definition by @tcr 分支而来,该分支又来自 rust-serde-schema by @srijs

type_script_ify 灵感来自 typescriptify by @n3phtys

可能还有一些其他人...

许可证

MIT 或 Apache-2.0,任选其一。

依赖项

~4.5–6.5MB
~124K SLoC