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 插件
87 每月下载量
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.rs
、lib.rs
或 mod.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
字段设置为 date
或 date-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.rs
和 Generator
函数的 cli
和 httpmock
。
下面的 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.rs
、lib.rs
或 mod.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"
(如上所示的 chrono
、uuid
、base64
和 rand
)
请注意,progenitor
在 build.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