#openapi #generator #sdk #api-client #openapi-v3 #async-api #api-bindings

app cargo-progenitor

一个用于从 OpenAPI 生成 Rust 客户端 SDK 的 cargo 命令

4 个版本 (2 个破坏性更新)

0.7.0 2024 年 5 月 15 日
0.6.0 2024 年 2 月 28 日
0.4.1 2023 年 12 月 15 日
0.4.0 2023 年 9 月 25 日

#226 in Cargo 插件

MPL-2.0 许可证

265KB
5.5K SLoC

Progenitor

Progenitor 是一个 Rust 包,用于从 OpenAPI 3.0.x 规范中的 API 描述生成有偏好的客户端。它使用 Rust futures 进行 async API 调用和 Streams 进行分页接口。

它生成一个名为 Client 的类型,其中包含与 OpenAPI 文档中指定的操作相对应的方法。

Progenitor 还可以生成一个 CLI 与 OpenAPI 服务实例交互,以及 httpmock 辅助工具来创建 OpenAPI 服务的强类型模拟。

主要目标是 Dropshot 生成的 API 发出的 OpenAPI 文档,但它可以用于许多 OpenAPI 文档。由于 OpenAPI 覆盖了广泛的 API,Progenitor 可能会失败一些 OpenAPI 文档。如果您遇到问题,可以通过提交包含产生问题的 OpenAPI 文档的问题来帮助该项目。

使用 Progenitor

使用 progenitor 包有三种不同的方式。您选择的方式将取决于您的用例和偏好。

使用 Progenitor 最简单的方法是通过其 generate_api! 宏。

在源文件(通常是 main.rslib.rsmod.rs)中简单调用宏

generate_api!("path/to/openapi_document.json");

您需要在 Cargo.toml 中添加以下内容

[dependencies]
futures = "0.3"
progenitor = { git = "https://github.com/oxidecomputer/progenitor" }
reqwest = { version = "0.11", features = ["json", "stream"] }
serde = { version = "1.0", features = ["derive"] }

此外,如果 OpenAPI 文档包含字符串类型,且 format 字段设置为 datedate-time,请包括

[dependencies]
chrono = { version = "0.4", features = ["serde"] }

同样,如果存在 format 字段设置为 uuid

[dependencies]
uuid = { version = "1.0.0", features = ["serde", "v4"] }

并且有任何 WebSocket 通道端点

[dependencies]
base64 = "0.21"
rand = "0.8"

如果类型包含正则表达式验证

[dependencies]
regress = "0.4.1"

该宏还有一些额外的选项来控制生成的代码

generate_api!(
    spec = "path/to/openapi_document.json",      // The OpenAPI document
    interface = Builder,                         // Choose positional (default) or builder style
    tags = Separate,                             // Tags may be Merged or Separate (default)
    inner_type = my_client::InnerType,           // Client inner type available to pre and post hooks
    pre_hook = closure::or::path::to::function,  // Hook invoked before issuing the HTTP request
    post_hook = closure::or::path::to::function, // Hook invoked prior to receiving the HTTP response
    derives = [ schemars::JsonSchema ],          // Additional derive macros applied to generated types
);

请注意,当 spec OpenAPI 文档发生变化(即其 mtime 更新)时,该宏将被重新评估。

build.rs

Progenitor 包含一个适用于在 build.rs 文件中使用的接口。虽然比宏稍微繁琐一些,但构建器有使生成的代码可见的优点。生成 CLI 和 httpmock 辅助函数的功能仅可通过 build.rsGenerator 函数 clihttpmock 分别实现。

build.rs 文件应类似于以下内容

fn main() {
    let src = "../sample_openapi/keeper.json";
    println!("cargo:rerun-if-changed={}", src);
    let file = std::fs::File::open(src).unwrap();
    let spec = serde_json::from_reader(file).unwrap();
    let mut generator = progenitor::Generator::default();

    let tokens = generator.generate_tokens(&spec).unwrap();
    let ast = syn::parse2(tokens).unwrap();
    let content = prettyplease::unparse(&ast);

    let mut out_file = std::path::Path::new(&std::env::var("OUT_DIR").unwrap()).to_path_buf();
    out_file.push("codegen.rs");

    std::fs::write(out_file, content).unwrap();
}

在一个源文件中(通常是 main.rslib.rsmod.rs)包含生成的代码

include!(concat!(env!("OUT_DIR"), "/codegen.rs"));

您需要在 Cargo.toml 中添加以下内容

[dependencies]
futures = "0.3"
progenitor-client = { git = "https://github.com/oxidecomputer/progenitor" }
reqwest = { version = "0.11", features = ["json", "stream"] }
serde = { version = "1.0", features = ["derive"] }

[build-dependencies]
prettyplease = "0.1.25"
progenitor = { git = "https://github.com/oxidecomputer/progenitor" }
serde_json = "1.0"
syn = "1.0"

(如上所述的 chronouuidbase64rand

请注意,progenitorbuild.rs 使用,但所需的生成代码需要 progenitor-client

静态 crate

Progenitor 可以运行以生成用于生成的客户端的独立 crate。这确保了没有意外的更改(例如,来自 progenitor 的更新)。然而,这是使用 Progenitor 的最手动方式。

用法

cargo progenitor

Options:
    -i INPUT            OpenAPI definition document (JSON or YAML)
    -o OUTPUT           Generated Rust crate directory
    -n CRATE            Target Rust crate name
    -v VERSION          Target Rust crate version

例如

cargo install cargo-progenitor
cargo progenitor -i sample_openapi/keeper.json -o keeper -n keeper -v 0.1.0

... 或在存储库内部

cargo run --bin cargo-progenitor -- progenitor -i sample_openapi/keeper.json -o keeper -n keeper -v 0.1.0

这将生成指定目录中的包。

选项 --license--registry-name 也可以用于在发布静态 crate 之前改进元数据。

如果 progenitor 是从已发布版本构建的,则默认情况下将使用已发布的 progenitor-client crate。然而,当使用从存储库构建的 progenitor 时,默认情况下将 progenitor-client 内联到静态 crate 中。可以使用命令行标志 --include-client 来覆盖默认行为。

要确保输出没有对 Progenitor 的持续依赖,请启用 --include-client

以下是从生成的 Cargo.toml 中的摘录

[dependencies]
bytes = "1.3.0"
chrono = { version = "0.4.23", default-features=false, features = ["serde"] }
futures-core = "0.3.25"
percent-encoding = "2.2.0"
reqwest = { version = "0.11.13", default-features=false, features = ["json", "stream"] }
serde = { version = "1.0.152", features = ["derive"] }
serde_urlencoded = "0.7.1"

生成的 Cargo.toml 中的依赖项版本与构建 progenitor 时使用的版本相同。

请注意,有一个对 percent-encoding 的依赖,宏和 build.rs 生成的客户端从 progenitor-client 中包含。

生成样式

Progenitor 可以生成两种不同的接口样式:位置和构建器(如下所述)。选择只是个人偏好的问题,很多都取决于 API 和个人品味。

位置(当前默认)

"位置"样式生成接受按顺序排列的参数的 Client 方法,例如

impl Client {
    pub async fn instance_create<'a>(
        &'a self,
        organization_name: &'a types::Name,
        project_name: &'a types::Name,
        body: &'a types::InstanceCreate,
    ) -> Result<ResponseValue<types::Instance>, Error<types::Error>> {
        // ...
    }
}

调用者通过指定位置来调用此接口

let result = client.instance_create(org, proj, body).await?;

请注意,每个参数的类型必须完全匹配--不会隐式转换。

构建器

"构建器"样式生成生成一个构建器结构体的 Client 方法。API 参数应用于该构建器,然后执行构建器(通过 send 方法)。代码更复杂,更复杂,以使消费者更简单、更易读

impl Client
    pub fn instance_create(&self) -> builder::InstanceCreate {
        builder::InstanceCreate::new(self)
    }
}

mod builder {
    pub struct InstanceCreate<'a> {
        client: &'a super::Client,
        organization_name: Result<types::Name, String>,
        project_name: Result<types::Name, String>,
        body: Result<types::InstanceCreate, String>,
    }

    impl<'a> InstanceCreate<'a> {
        pub fn new(client: &'a super::Client) -> Self {
            // ...
        }

        pub fn organization_name<V>(mut self, value: V) -> Self
        where
            V: TryInto<types::Name>,
        {
            // ...
        }

        pub fn project_name<V>(mut self, value: V) -> Self
        where
            V: TryInto<types::Name>,
        {
            // ...
        }

        pub fn body<V>(mut self, value: V) -> Self
        where
            V: TryInto<types::InstanceCreate>,
        {
            // ...
        }

        pub async fn send(self) ->
            Result<ResponseValue<types::Instance>, Error<types::Error>>
        {
            // ...
        }
    }
}

请注意,与位置生成不同,消费者可以提供兼容的(而不是不可变的)参数

let result = client
    .instance_create()
    .organization_name("org")
    .project_name("proj")
    .body(body)
    .send()
    .await?;

字符串参数将隐式调用 TryFrom::try_from()。转换失败或缺少必需的参数将导致 send() 调用返回 Error 结果。

生成的 结构体 类型也有构建器,这样就可以直接构造 body 参数

let result = client
    .instance_create()
    .organization_name("org")
    .project_name("proj")
    .body(types::InstanceCreate::builder()
        .name("...")
        .description("...")
        .hostname("...")
        .ncpus(types::InstanceCpuCount(4))
        .memory(types::ByteCount(1024 * 1024 * 1024)),
    )
    .send()
    .await?;

消费者不需要指定不必要或API指定默认值的参数和结构体属性。真方便!

依赖关系

~15–32MB
~546K SLoC