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

app cargo-progenitor-hk

A cargo 命令,用于从 OpenAPI 生成 Rust 客户端 SDK

5 个版本

0.6.5 2024 年 3 月 22 日
0.6.4 2024 年 3 月 21 日
0.6.3 2024 年 3 月 21 日
0.6.2 2024 年 3 月 21 日
0.6.1 2024 年 3 月 21 日

#239 in Cargo 插件

Download history 277/week @ 2024-03-17 9/week @ 2024-03-24 46/week @ 2024-03-31 5/week @ 2024-04-07 1/week @ 2024-04-14

87 每月下载量

MPL-2.0 许可证

270KB
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

静态包

Progenitor 可以运行以生成生成客户端的独立包。这确保了没有意外更改(例如,来自 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 选项在发布静态包之前改进元数据。

如果 progenitor 从已发布的版本构建,默认情况下将使用发布的 progenitor-client 包。但是,当使用从仓库构建的 progenitor 时,默认情况下将自动将 progenitor-client 内联到静态包中。可以使用命令行标志 --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 结果。

生成的 struct 类型也具有构建器,因此可以就地构建 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指定了默认值的参数和结构体属性。真方便!

依赖项

~16–32MB
~543K SLoC