8个版本 (破坏性更新)

0.7.0 2024年5月15日
0.6.0 2024年2月28日
0.5.0 2023年12月16日
0.4.1 2023年12月15日
0.1.1 2022年5月13日

#828 in 过程宏

Download history 3721/week @ 2024-04-15 3371/week @ 2024-04-22 3872/week @ 2024-04-29 5051/week @ 2024-05-06 6514/week @ 2024-05-13 3884/week @ 2024-05-20 5131/week @ 2024-05-27 2668/week @ 2024-06-03 4382/week @ 2024-06-10 4788/week @ 2024-06-17 4424/week @ 2024-06-24 3326/week @ 2024-07-01 3702/week @ 2024-07-08 2952/week @ 2024-07-15 5047/week @ 2024-07-22 4126/week @ 2024-07-29

16,054 monthly downloads
12 个crate中使用 (5 个直接使用)

MPL-2.0 许可证

215KB
4.5K SLoC

Progenitor

Progenitor 是一个Rust crate,用于从OpenAPI 3.0.x规范中的API描述生成具有观点的客户端。它使用Rust futures进行异步API调用,并使用Streams进行分页接口。

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

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

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

使用Progenitor

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

使用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。可以使用命令行标志--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()。失败的转换或缺少必需的参数将导致从 Errorsend() 调用返回错误。

生成的 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指定默认值的参数和结构体属性。真棒!

依赖项

~12MB
~208K SLoC