一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

progenitor:实践指南

时间:2026-09-11 19:48:01 编辑:袖梨 来源:一聚教程网

团队讨论progenitor时,我会先把用途说清楚:OpenAPI 客户端生成器。从日常自动化的使用方式看,输入边界、依赖和失败处理如果不清楚就很难稳定复用是采用前必须回答的问题。先用一项范围明确的真实任务完成最小试跑更稳妥;过程中要观察配置时间、输出质量、异常信息和维护痕迹,失败也应能解释原因。如果团队属于愿意先做小范围验证并复查原始文档的团队,它有继续测试的理由;否则先看替代方案会更省时间。

祖先

Progenitor 是一个 Rust 箱,用于从 API 生成固执己见的客户 OpenAPI 3.0.x 规范中的描述。它利用 Rust async API 调用和分页接口的 Streams 的未来。

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

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

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

使用祖细胞

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.13", features = ["json", "query", "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 更新时)。

如果您在生成的类型上派生 schemars::JsonSchema (请参阅 derives = [... ] in the macro example above), add a dependency on schemars 并启用 规范中出现的每种格式化类型的必要功能。例如:

[dependencies]
schemars = { version = "0.8", features = ["chrono", "uuid1"] }

build.rs

Progenitor 包括一个适合在 build.rs 文件。虽然比宏稍微麻烦一些,但构建器的优点是使生成的代码可见。 生成 CLI 和 httpmock 助手的功能仅可使用 build.rs 实现 Generator 的功能分别为 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.13", features = ["json", "query", "stream"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

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

chronouuidbase64rand 如上所述)

注意,progenitor使用的是build.rs,但是生成的代码需要 progenitor-client.

静态板条箱

可以运行 Progenitor 来为生成的客户端发出独立的 crate。 这可以确保不会发生意外的更改(e.g。从更新到祖先)。它是 然而,使用 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-client 箱 如果祖先是在发布模式下构建的。当在调试模式下构建时, 默认情况下,progenitor-client 将内联到生成的 crate 中。的 命令行标志 --include-client true|false 可用于覆盖 默认行为。值 true 复制到客户端代码中;值为 false 在生成的文件中包含对 progenitor-client 的依赖项 Cargo.toml 文件。

以下是发出的 Cargo.toml 的摘录:

[dependencies]
bytes = "1.9"
chrono = { version = "0.4", default-features=false, features = ["serde"] }
futures-core = "0.3"
progenitor-client = "0.9.1"
reqwest = { version = "0.13", default-features=false, features = ["json", "query", "stream"] }
serde = { version = "1.0", features = ["derive"] }
serde_urlencoded = "0.7"

这是与 --include-client true 的依赖关系的另一个示例:

[dependencies]
bytes = "1.9"
chrono = { version = "0.4", default-features=false, features = ["serde"] }
futures-core = "0.3"
percent-encoding = "2.3"
reqwest = { version = "0.13", default-features=false, features = ["json", "query", "stream"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_urlencoded = "0.7"

一代风格

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() 他们。转换失败或缺少所需参数将导致 Error 来自 send() 调用的结果。

生成的 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 指定默认值。整洁的!

在 build.rs 中启用构建器样式

要启用构建器样式,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 binding = GenerationSettings::default();
    let settings = binding.with_interface(InterfaceStyle::Builder);
    let mut generator = progenitor::Generator::new(&settings);
    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();
}

更改默认客户端设置

目前,生成的代码不处理请求标头。要为所有请求添加默认标头,可以在构造 Client 时使用 default_headers 方法。

    let baseurl = std::env::var("API_URL").expect("$API_URL not set");
    
    let access_token = std::env::var("API_ACCESS_TOKEN").expect("$API_ACCESS_TOKEN not set");
    let authorization_header = format!("Bearer {}", access_token);

    let mut headers = reqwest::header::HeaderMap::new();
    headers.insert(
        reqwest::header::AUTHORIZATION,
        authorization_header.parse().unwrap(),
    );

    let client_with_custom_defaults = reqwest::ClientBuilder::new()
        .connect_timeout(Duration::from_secs(15))
        .timeout(Duration::from_secs(15))
        .default_headers(headers)
        .build()
        .unwrap();

    let client = Client::new_with_client(baseurl, client_with_custom_defaults);

热门栏目