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日 |
#277 in 过程宏
每月 3,455 次下载
在 13 个crate中使用 (直接使用2个)
245KB
5K SLoC
Progenitor
Progenitor 是一个用于从OpenAPI 3.0.x规范中的API描述生成有观点客户端的Rust crate。它使用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.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
);
请注意,当OpenAPI文档中的spec
更改时(当它的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
。
静态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()
。转换失败或缺少必需的参数将导致 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指定默认值的参数。真棒!
依赖项
~13MB
~229K SLoC