2022-02-08 16:59:38 +00:00
|
|
|
# Progenitor
|
|
|
|
|
|
|
|
Progenitor is a Rust crate for generating opinionated clients from API
|
2022-09-11 00:15:14 +00:00
|
|
|
descriptions in the OpenAPI 3.0.x specification. It makes use of Rust
|
|
|
|
futures for `async` API calls and `Streams` for paginated interfaces.
|
2022-02-08 16:59:38 +00:00
|
|
|
|
|
|
|
It generates a type called `Client` with methods that correspond to the
|
|
|
|
operations specified in the OpenAPI document.
|
|
|
|
|
2024-01-06 17:06:22 +00:00
|
|
|
Progenitor can also generate a CLI to interact with an OpenAPI service
|
|
|
|
instance, and [`httpmock`](https://crates.io/crates/httpmock) helpers to
|
|
|
|
create a strongly typed mock of the OpenAPI service.
|
|
|
|
|
2022-09-11 00:15:14 +00:00
|
|
|
The primary target is OpenAPI documents emitted by
|
|
|
|
[Dropshot](https://github.com/oxidecomputer/dropshot)-generated APIs, but it
|
|
|
|
can be used for many OpenAPI documents. As OpenAPI covers a wide range of APIs,
|
|
|
|
Progenitor may fail for some OpenAPI documents. If you encounter a problem, you
|
|
|
|
can help the project by filing an issue that includes the OpenAPI document that
|
|
|
|
produced the problem.
|
|
|
|
|
2022-02-08 16:59:38 +00:00
|
|
|
## Using Progenitor
|
|
|
|
|
|
|
|
There are three different ways of using the `progenitor` crate. The one you
|
|
|
|
choose will depend on your use case and preferences.
|
|
|
|
|
|
|
|
### Macro
|
|
|
|
|
|
|
|
The simplest way to use Progenitor is via its `generate_api!` macro.
|
|
|
|
|
|
|
|
In a source file (often `main.rs`, `lib.rs`, or `mod.rs`) simply invoke the
|
|
|
|
macro:
|
|
|
|
|
|
|
|
```rust
|
2022-06-02 01:22:51 +00:00
|
|
|
generate_api!("path/to/openapi_document.json");
|
2022-02-08 16:59:38 +00:00
|
|
|
```
|
|
|
|
|
2023-01-05 00:46:54 +00:00
|
|
|
You'll need to add the following to `Cargo.toml`:
|
2022-02-08 16:59:38 +00:00
|
|
|
|
2024-03-19 17:32:04 +00:00
|
|
|
```toml
|
2022-02-08 16:59:38 +00:00
|
|
|
[dependencies]
|
2024-03-19 17:32:04 +00:00
|
|
|
futures = "0.3"
|
|
|
|
progenitor = { git = "https://github.com/oxidecomputer/progenitor" }
|
|
|
|
reqwest = { version = "0.11", features = ["json", "stream"] }
|
|
|
|
serde = { version = "1.0", features = ["derive"] }
|
2022-02-08 16:59:38 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
In addition, if the OpenAPI document contains string types with the `format`
|
|
|
|
field set to `date` or `date-time`, include
|
|
|
|
|
2024-03-19 17:32:04 +00:00
|
|
|
```toml
|
2022-02-08 16:59:38 +00:00
|
|
|
[dependencies]
|
2024-03-19 17:32:04 +00:00
|
|
|
chrono = { version = "0.4", features = ["serde"] }
|
2022-02-08 16:59:38 +00:00
|
|
|
```
|
|
|
|
|
2023-01-14 16:58:57 +00:00
|
|
|
Similarly, if there is a `format` field set to `uuid`:
|
2022-02-08 16:59:38 +00:00
|
|
|
|
2024-03-19 17:32:04 +00:00
|
|
|
```toml
|
2022-02-08 16:59:38 +00:00
|
|
|
[dependencies]
|
2024-03-19 17:32:04 +00:00
|
|
|
uuid = { version = "1.0.0", features = ["serde", "v4"] }
|
2022-05-20 21:52:37 +00:00
|
|
|
```
|
|
|
|
|
2022-09-28 20:40:07 +00:00
|
|
|
And if there are any websocket channel endpoints:
|
2024-01-06 17:06:22 +00:00
|
|
|
|
2024-03-19 17:32:04 +00:00
|
|
|
```toml
|
2022-09-28 20:40:07 +00:00
|
|
|
[dependencies]
|
2024-03-19 17:32:04 +00:00
|
|
|
base64 = "0.21"
|
|
|
|
rand = "0.8"
|
2022-09-28 20:40:07 +00:00
|
|
|
```
|
|
|
|
|
2023-01-18 22:40:02 +00:00
|
|
|
If types include regular expression validation:
|
2024-01-06 17:06:22 +00:00
|
|
|
|
2024-03-19 17:32:04 +00:00
|
|
|
```toml
|
2023-01-18 22:40:02 +00:00
|
|
|
[dependencies]
|
2024-03-19 17:32:04 +00:00
|
|
|
regress = "0.4.1"
|
2023-01-18 22:40:02 +00:00
|
|
|
```
|
|
|
|
|
2022-05-20 21:52:37 +00:00
|
|
|
The macro has some additional fancy options to control the generated code:
|
|
|
|
|
|
|
|
```rust
|
|
|
|
generate_api!(
|
2022-07-03 02:09:38 +00:00
|
|
|
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
|
2022-05-20 21:52:37 +00:00
|
|
|
);
|
2022-02-08 16:59:38 +00:00
|
|
|
```
|
|
|
|
|
2023-01-14 16:58:57 +00:00
|
|
|
Note that the macro will be re-evaluated when the `spec` OpenAPI document
|
2022-02-08 17:02:00 +00:00
|
|
|
changes (when its mtime is updated).
|
2022-02-08 16:59:38 +00:00
|
|
|
|
2022-09-11 00:15:14 +00:00
|
|
|
### `build.rs`
|
2022-02-08 16:59:38 +00:00
|
|
|
|
|
|
|
Progenitor includes an interface appropriate for use in a
|
|
|
|
[`build.rs`](https://doc.rust-lang.org/cargo/reference/build-scripts.html)
|
|
|
|
file. While slightly more onerous than the macro, a builder has the advantage of making the generated code visible.
|
2024-01-06 17:06:22 +00:00
|
|
|
The capability of generating a CLI and `httpmock` helpers is only available using `build.rs`
|
|
|
|
and the `Generator` functions `cli` and `httpmock` respectively.
|
2022-02-08 16:59:38 +00:00
|
|
|
|
|
|
|
The `build.rs` file should look something like this:
|
|
|
|
|
|
|
|
```rust
|
|
|
|
fn main() {
|
|
|
|
let src = "../sample_openapi/keeper.json";
|
|
|
|
println!("cargo:rerun-if-changed={}", src);
|
2023-02-21 00:10:22 +00:00
|
|
|
let file = std::fs::File::open(src).unwrap();
|
2022-02-08 16:59:38 +00:00
|
|
|
let spec = serde_json::from_reader(file).unwrap();
|
2022-07-03 02:09:38 +00:00
|
|
|
let mut generator = progenitor::Generator::default();
|
2022-02-08 16:59:38 +00:00
|
|
|
|
2023-04-26 14:55:49 +00:00
|
|
|
let tokens = generator.generate_tokens(&spec).unwrap();
|
|
|
|
let ast = syn::parse2(tokens).unwrap();
|
|
|
|
let content = prettyplease::unparse(&ast);
|
2022-02-08 16:59:38 +00:00
|
|
|
|
2023-02-21 00:10:22 +00:00
|
|
|
let mut out_file = std::path::Path::new(&std::env::var("OUT_DIR").unwrap()).to_path_buf();
|
2022-02-08 16:59:38 +00:00
|
|
|
out_file.push("codegen.rs");
|
|
|
|
|
2023-02-21 00:10:22 +00:00
|
|
|
std::fs::write(out_file, content).unwrap();
|
2022-02-08 16:59:38 +00:00
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
In a source file (often `main.rs`, `lib.rs`, or `mod.rs`) include the generated
|
|
|
|
code:
|
|
|
|
|
|
|
|
```rust
|
|
|
|
include!(concat!(env!("OUT_DIR"), "/codegen.rs"));
|
|
|
|
```
|
|
|
|
|
2023-01-14 16:58:57 +00:00
|
|
|
You'll need to add the following to `Cargo.toml`:
|
2022-02-08 16:59:38 +00:00
|
|
|
|
2024-03-19 17:32:04 +00:00
|
|
|
```toml
|
2022-02-08 16:59:38 +00:00
|
|
|
[dependencies]
|
2024-03-19 17:32:04 +00:00
|
|
|
futures = "0.3"
|
|
|
|
progenitor-client = { git = "https://github.com/oxidecomputer/progenitor" }
|
|
|
|
reqwest = { version = "0.11", features = ["json", "stream"] }
|
|
|
|
serde = { version = "1.0", features = ["derive"] }
|
2022-02-08 16:59:38 +00:00
|
|
|
|
|
|
|
[build-dependencies]
|
2024-03-19 17:32:04 +00:00
|
|
|
prettyplease = "0.1.25"
|
|
|
|
progenitor = { git = "https://github.com/oxidecomputer/progenitor" }
|
|
|
|
serde_json = "1.0"
|
|
|
|
syn = "1.0"
|
2022-02-08 16:59:38 +00:00
|
|
|
```
|
|
|
|
|
2022-09-28 20:40:07 +00:00
|
|
|
(`chrono`, `uuid`, `base64`, and `rand` as above)
|
2022-02-08 16:59:38 +00:00
|
|
|
|
|
|
|
Note that `progenitor` is used by `build.rs`, but the generated code required
|
|
|
|
`progenitor-client`.
|
|
|
|
|
|
|
|
### Static Crate
|
|
|
|
|
|
|
|
Progenitor can be run to emit a stand-alone crate for the generated client.
|
|
|
|
This ensures no unexpected changes (e.g. from updates to progenitor). It is
|
|
|
|
however, the most manual way to use Progenitor.
|
|
|
|
|
|
|
|
Usage:
|
|
|
|
|
|
|
|
```
|
2023-05-17 16:46:17 +00:00
|
|
|
cargo progenitor
|
2022-02-08 16:59:38 +00:00
|
|
|
|
|
|
|
Options:
|
2023-01-14 16:58:57 +00:00
|
|
|
-i INPUT OpenAPI definition document (JSON or YAML)
|
2022-02-08 16:59:38 +00:00
|
|
|
-o OUTPUT Generated Rust crate directory
|
|
|
|
-n CRATE Target Rust crate name
|
|
|
|
-v VERSION Target Rust crate version
|
|
|
|
```
|
|
|
|
|
|
|
|
For example:
|
|
|
|
|
2023-05-17 16:46:17 +00:00
|
|
|
```
|
|
|
|
cargo install cargo-progenitor
|
|
|
|
cargo progenitor -i sample_openapi/keeper.json -o keeper -n keeper -v 0.1.0
|
|
|
|
```
|
|
|
|
|
|
|
|
... or within the repo:
|
|
|
|
```
|
|
|
|
cargo run --bin cargo-progenitor -- progenitor -i sample_openapi/keeper.json -o keeper -n keeper -v 0.1.0
|
|
|
|
```
|
2022-02-08 16:59:38 +00:00
|
|
|
|
2023-01-17 22:24:44 +00:00
|
|
|
This will produce a package in the specified directory.
|
|
|
|
|
2023-01-18 08:06:35 +00:00
|
|
|
Options `--license` and `--registry-name` may also be used to improve metadata
|
|
|
|
before publishing the static crate.
|
|
|
|
|
2023-01-17 22:24:44 +00:00
|
|
|
The output will use the published `progenitor-client` crate by default
|
|
|
|
if progenitor was built from a released version. However, when using progenitor
|
|
|
|
built from the repository, the `progenitor-client` will be inlined into the
|
|
|
|
static crate by default. The command line flag `--include-client` can be used
|
|
|
|
to override the default behaviour.
|
|
|
|
|
|
|
|
To ensure the output has no persistent dependency on Progenitor, enable `--include-client`.
|
|
|
|
|
2023-01-14 16:58:57 +00:00
|
|
|
Here is an excerpt from the emitted `Cargo.toml`:
|
2022-02-08 16:59:38 +00:00
|
|
|
|
|
|
|
```toml
|
|
|
|
[dependencies]
|
2023-01-17 22:24:44 +00:00
|
|
|
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"
|
2022-02-08 16:59:38 +00:00
|
|
|
```
|
|
|
|
|
2023-01-17 22:24:44 +00:00
|
|
|
The dependency versions in the generated `Cargo.toml` are the same as the
|
|
|
|
versions that were used when progenitor was built.
|
|
|
|
|
2022-02-08 16:59:38 +00:00
|
|
|
Note that there is a dependency on `percent-encoding` which macro- and
|
2022-02-08 17:02:00 +00:00
|
|
|
build.rs-generated clients is included from `progenitor-client`.
|
2022-09-11 00:15:14 +00:00
|
|
|
|
|
|
|
## Generation Styles
|
|
|
|
|
|
|
|
Progenitor can generate two distinct interface styles: positional and builder
|
|
|
|
(described below). The choice is simply a matter of preference that many vary
|
|
|
|
by API and taste.
|
|
|
|
|
|
|
|
## Positional (current default)
|
|
|
|
|
|
|
|
The "positional" style generates `Client` methods that accept parameters in
|
|
|
|
order, for example:
|
|
|
|
|
|
|
|
```rust
|
|
|
|
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>> {
|
|
|
|
// ...
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
A caller invokes this interface by specifying parameters by position:
|
|
|
|
|
|
|
|
```rust
|
|
|
|
let result = client.instance_create(org, proj, body).await?;
|
|
|
|
```
|
|
|
|
|
|
|
|
Note that the type of each parameter must match precisely--no conversion is
|
|
|
|
done implicitly.
|
|
|
|
|
|
|
|
## Builder
|
|
|
|
|
|
|
|
The "builder" style generates `Client` methods that produce a builder struct.
|
|
|
|
API parameters are applied to that builder, and then the builder is executed
|
|
|
|
(via a `send` method). The code is more extensive and more complex to enable
|
|
|
|
simpler and more legible consumers:
|
|
|
|
|
|
|
|
```rust
|
|
|
|
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>>
|
|
|
|
{
|
|
|
|
// ...
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
Note that, unlike positional generation, consumers can supply compatible
|
|
|
|
(rather than invariant) parameters:
|
|
|
|
|
|
|
|
```rust
|
|
|
|
let result = client
|
|
|
|
.instance_create()
|
|
|
|
.organization_name("org")
|
|
|
|
.project_name("proj")
|
|
|
|
.body(body)
|
|
|
|
.send()
|
|
|
|
.await?;
|
|
|
|
```
|
|
|
|
|
2023-07-03 23:09:17 +00:00
|
|
|
The string parameters will implicitly have `TryFrom::try_from()` invoked on
|
2022-09-11 00:15:14 +00:00
|
|
|
them. Failed conversions or missing required parameters will result in an
|
|
|
|
`Error` result from the `send()` call.
|
|
|
|
|
|
|
|
Generated `struct` types also have builders so that the `body` parameter can be
|
|
|
|
constructed inline:
|
|
|
|
|
|
|
|
```rust
|
|
|
|
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?;
|
|
|
|
```
|
|
|
|
|
|
|
|
Consumers do not need to specify parameters and struct properties that are not
|
2022-09-28 20:40:07 +00:00
|
|
|
required or for which the API specifies defaults. Neat!
|