Add a wrapper types for success and error responses (#26)
This commit is contained in:
parent
f1f9e2e938
commit
25192b5dc1
|
@ -120,10 +120,9 @@ dependencies = [
|
|||
name = "example-build"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"percent-encoding",
|
||||
"progenitor",
|
||||
"progenitor-client",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
@ -134,9 +133,7 @@ dependencies = [
|
|||
name = "example-macro"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"percent-encoding",
|
||||
"progenitor",
|
||||
"reqwest",
|
||||
"schemars",
|
||||
|
@ -681,6 +678,7 @@ dependencies = [
|
|||
"getopts",
|
||||
"openapiv3",
|
||||
"percent-encoding",
|
||||
"progenitor-client",
|
||||
"progenitor-impl",
|
||||
"progenitor-macro",
|
||||
"reqwest",
|
||||
|
@ -694,7 +692,9 @@ dependencies = [
|
|||
name = "progenitor-client"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
|
@ -702,7 +702,6 @@ dependencies = [
|
|||
name = "progenitor-impl"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case",
|
||||
"expectorate",
|
||||
"getopts",
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
# Progenitor
|
||||
|
||||
Progenitor is a Rust crate for generating opinionated clients from API
|
||||
descriptions specified in the OpenAPI 3.0.x format. It makes use of Rust
|
||||
futures for async API calls and `Streams` for paginated interfaces.
|
||||
|
||||
It generates a type called `Client` with methods that correspond to the
|
||||
operations specified in the OpenAPI document.
|
||||
|
||||
## 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
|
||||
generate_api("path/to/openapi_document.json");
|
||||
```
|
||||
|
||||
You'll need to add add the following to `Cargo.toml`:
|
||||
|
||||
```diff
|
||||
[dependencies]
|
||||
+progenitor = { git = "https://github.com/oxidecomputer/progenitor" }
|
||||
+reqwest = { version = "0.11", features = ["json", "stream"] }
|
||||
+serde = { version = "1.0", features = ["derive"] }
|
||||
```
|
||||
|
||||
In addition, if the OpenAPI document contains string types with the `format`
|
||||
field set to `date` or `date-time`, include
|
||||
|
||||
```diff
|
||||
[dependencies]
|
||||
+chrono = { version = "0.4", features = ["serde"] }
|
||||
```
|
||||
|
||||
Similarly if there is a `format` field set to `uuid`:
|
||||
|
||||
```diff
|
||||
[dependencies]
|
||||
+uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||
```
|
||||
|
||||
Note that the macro will be re-evaluated when the OpenAPI json document
|
||||
changes (when it's mtime is updated).
|
||||
|
||||
### Builder
|
||||
|
||||
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.
|
||||
|
||||
The `build.rs` file should look something like this:
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
let src = "../sample_openapi/keeper.json";
|
||||
println!("cargo:rerun-if-changed={}", src);
|
||||
let file = File::open(src).unwrap();
|
||||
let spec = serde_json::from_reader(file).unwrap();
|
||||
let mut generator = progenitor::Generator::new();
|
||||
|
||||
let content = generator.generate_text(&spec).unwrap();
|
||||
|
||||
let mut out_file = Path::new(&env::var("OUT_DIR").unwrap()).to_path_buf();
|
||||
out_file.push("codegen.rs");
|
||||
|
||||
fs::write(out_file, content).unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
In a source file (often `main.rs`, `lib.rs`, or `mod.rs`) include the generated
|
||||
code:
|
||||
|
||||
```rust
|
||||
include!(concat!(env!("OUT_DIR"), "/codegen.rs"));
|
||||
```
|
||||
|
||||
You'll need to add add the following to `Cargo.toml`:
|
||||
|
||||
```diff
|
||||
[dependencies]
|
||||
+progenitor-client = { git = "https://github.com/oxidecomputer/progenitor" }
|
||||
+reqwest = { version = "0.11", features = ["json", "stream"] }
|
||||
+serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
[build-dependencies]
|
||||
+progenitor = { git = "https://github.com/oxidecomputer/progenitor" }
|
||||
+serde_json = "1.0"
|
||||
```
|
||||
|
||||
(`chrono` and `uuid` as above)
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
progenitor
|
||||
|
||||
Options:
|
||||
-i INPUT OpenAPI definition document (JSON)
|
||||
-o OUTPUT Generated Rust crate directory
|
||||
-n CRATE Target Rust crate name
|
||||
-v VERSION Target Rust crate version
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
`cargo run --bin progenitor -- -i sample_penapi/keeper.json -o keeper -n keeper -v 0.1.0`
|
||||
|
||||
This will produce a package in the specified directory. The output has no
|
||||
persistent dependency on Progenitor including the `progenitor-client` crate.
|
||||
Here's a excerpt from the emitted `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
futures = "0.3"
|
||||
percent-encoding = "2.1"
|
||||
reqwest = { version = "0.11", features = ["json", "stream"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||
```
|
||||
|
||||
Note that there is a dependency on `percent-encoding` which macro- and
|
||||
build.rs-generated clients is included from `progenitor-client`.
|
|
@ -0,0 +1,67 @@
|
|||
# Generated Methods
|
||||
|
||||
Progenitor generates methods according to operations within an OpenAPI
|
||||
document. Each method takes the following general form:
|
||||
|
||||
```rust
|
||||
impl Client {
|
||||
pub async fn operation_name<'a>(
|
||||
&'a self,
|
||||
// Path parameters (if any) come first and are always mandatory
|
||||
path_parameter_1: String,
|
||||
path_parameter_2: u32,
|
||||
// Query parameters (if any) come next and may be optional
|
||||
query_parameter_1: String,
|
||||
query_parameter_2: Option<u32>,
|
||||
// A body parameter (if specified) comes last
|
||||
body: &ThisOperationBody,
|
||||
) -> Result<
|
||||
ResponseValue<types::SuccessResponseType>,
|
||||
Error<types::ErrorResponseType>
|
||||
> {
|
||||
..
|
||||
```
|
||||
|
||||
For more info on the `ResponseValue<T>` and `Error<E>` types, see
|
||||
[progenitor_client](./progenitor_client).
|
||||
|
||||
Note that methods are `async` so must be `await`ed to get the response.
|
||||
|
||||
### Dropshot Paginated Operations
|
||||
|
||||
The Dropshot crate defines a mechanism for pagination. If that mechanism is
|
||||
used for a particular operation, Progenitor will generate an additional method
|
||||
that produces a `Stream`. Consumers can iterate over all items in the paginated
|
||||
collection without manually fetching individual pages.
|
||||
|
||||
Here's the signature for a typical generated method:
|
||||
|
||||
```rust
|
||||
pub fn operation_name_stream<'a>(
|
||||
&'a self,
|
||||
// Specific parameters...
|
||||
limit: Option<std::num::NonZeroU32>,
|
||||
) -> impl futures::Stream<
|
||||
Item = Result<types::SuccessResponseType, Error<types::ErrorResponseType>>
|
||||
> + Unpin + '_ {
|
||||
..
|
||||
```
|
||||
|
||||
A typical consumer of this method might look like this:
|
||||
|
||||
```rust
|
||||
let mut stream = client.operation_name_stream(None);
|
||||
loop {
|
||||
match stream.try_next().await {
|
||||
Ok(Some(item)) => println!("item {:?}", item),
|
||||
Ok(None) => {
|
||||
println!("done.");
|
||||
break;
|
||||
}
|
||||
Err(_) => {
|
||||
println!("error!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
|
@ -0,0 +1,87 @@
|
|||
# Progenitor Client
|
||||
|
||||
The `progenitor-client` crate contains types that are exported by generated
|
||||
clients as well as functions that are used internally by generated clients.
|
||||
Depending on how `progenitor` is being used, the crate will be included in
|
||||
different ways (see ["Using Progenitor"](../README.md#using_progenitor)).
|
||||
|
||||
- For macro consumers, it comes from the `progenitor` dependency.
|
||||
|
||||
- For builder consumers, it must be specified under `[dependencies]` (while `progenitor` is under `[build-dependencies]`).
|
||||
|
||||
- For statically generated consumers, the code is emitted into
|
||||
`src/progenitor_client.rs`.
|
||||
|
||||
The two types that `progenitor-client` exports are `Error<E>` and
|
||||
`ResponseValue<T>`. A typical generated method will use these types in its
|
||||
signature:
|
||||
|
||||
```rust
|
||||
impl Client {
|
||||
pub async fn operation_name<'a>(
|
||||
&'a self,
|
||||
// parameters ...
|
||||
) -> Result<
|
||||
ResponseValue<types::SuccessResponseType>,
|
||||
Error<types::ErrorResponseType>>
|
||||
{
|
||||
..
|
||||
```
|
||||
|
||||
## `ResponseValue<T>`
|
||||
|
||||
OpenAPI documents defines the types that an operation returns. Generated
|
||||
methods wrap these types in `ResponseValue<T>` for two reasons: there is
|
||||
additional information that may be included in a response such as the specific
|
||||
status code and headers, and that information cannot simply be included in the
|
||||
response type because that type may be used in other context (e.g. as a body
|
||||
parameter).
|
||||
|
||||
These are the relevant implementations for `ResponseValue<T>`:
|
||||
|
||||
```rust
|
||||
/// Success value returned by generated client methods.
|
||||
pub struct ResponseValue<T> { .. }
|
||||
|
||||
impl<T> ResponseValue<T> {
|
||||
pub fn status(&self) -> &reqwest::StatusCode { .. }
|
||||
pub fn headers(&self) -> &reqwest::header::HeaderMap { .. }
|
||||
pub fn into_inner(self) -> T { .. }
|
||||
}
|
||||
impl<T> std::ops::Deref for ResponseValue<T> {
|
||||
type Target = T;
|
||||
..
|
||||
}
|
||||
impl<T> std::ops::DerefMut for ResponseValue<T> { .. }
|
||||
|
||||
impl<T: std::fmt::Debug> std::fmt::Debug for ResponseValue<T> { .. }
|
||||
```
|
||||
|
||||
It can be used as the type `T` in most instances and extracted as a `T` using
|
||||
`into_inner()`.
|
||||
|
||||
## `Error<E>`
|
||||
|
||||
There are four sub-categories of error covered by the error type variants:
|
||||
- A communication error
|
||||
|
||||
- An expected error response, defined by the OpenAPI document with a 4xx or 5xx
|
||||
status code
|
||||
|
||||
- An expected status code (whose payload didn't deserialize as expected (this
|
||||
could be viewed as a sub-type of communication error), but it is separately
|
||||
identified as there's more information; note that this covers both success and
|
||||
error status codes
|
||||
|
||||
- An unexpected status code in the response
|
||||
|
||||
These errors are covered by the variants of the `Error<E>` type:
|
||||
|
||||
```rust
|
||||
pub enum Error<E> {
|
||||
CommunicationError(reqwest::Error),
|
||||
ErrorResponse(ResponseValue<E>),
|
||||
InvalidResponsePayload(reqwest::Error),
|
||||
UnexpectedResponse(reqwest::Response),
|
||||
}
|
||||
```
|
|
@ -5,9 +5,8 @@ authors = ["Adam H. Leventhal <ahl@oxidecomputer.com>"]
|
|||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
percent-encoding = "2.1"
|
||||
progenitor-client = { path = "../progenitor-client" }
|
||||
reqwest = { version = "0.11", features = ["json", "stream"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2021 Oxide Computer Company
|
||||
// Copyright 2022 Oxide Computer Company
|
||||
|
||||
use std::{
|
||||
env,
|
||||
|
@ -6,12 +6,12 @@ use std::{
|
|||
path::Path,
|
||||
};
|
||||
|
||||
use progenitor::Generator;
|
||||
|
||||
fn main() {
|
||||
let file = File::open("../sample_openapi/keeper.json").unwrap();
|
||||
let src = "../sample_openapi/keeper.json";
|
||||
println!("cargo:rerun-if-changed={}", src);
|
||||
let file = File::open(src).unwrap();
|
||||
let spec = serde_json::from_reader(file).unwrap();
|
||||
let mut generator = Generator::new();
|
||||
let mut generator = progenitor::Generator::new();
|
||||
|
||||
let content = generator.generate_text(&spec).unwrap();
|
||||
|
||||
|
|
|
@ -5,9 +5,7 @@ authors = ["Adam H. Leventhal <ahl@oxidecomputer.com>"]
|
|||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
percent-encoding = "2.1"
|
||||
progenitor = { path = "../progenitor" }
|
||||
reqwest = { version = "0.11", features = ["json", "stream"] }
|
||||
schemars = "0.8"
|
||||
|
|
|
@ -7,5 +7,7 @@ repository = "https://github.com/oxidecomputer/progenitor.git"
|
|||
description = "An OpenAPI client generator - client support"
|
||||
|
||||
[dependencies]
|
||||
reqwest = "0.11"
|
||||
percent-encoding = "2.1"
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
|
|
|
@ -2,35 +2,87 @@
|
|||
|
||||
//! Support code for generated clients.
|
||||
|
||||
use std::ops::Deref;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
/// Error produced by generated client methods.
|
||||
pub enum Error<E> {
|
||||
/// Indicates an error from the server, with the data, or with the
|
||||
/// connection.
|
||||
CommunicationError(reqwest::Error),
|
||||
|
||||
/// A documented error response.
|
||||
ErrorResponse(ResponseValue<E>),
|
||||
|
||||
/// A response not listed in the API description. This may represent a
|
||||
/// success or failure response; check `status()::is_success()`.
|
||||
UnexpectedResponse(reqwest::Response),
|
||||
}
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
/// Success value returned by generated client methods.
|
||||
pub struct ResponseValue<T> {
|
||||
inner: T,
|
||||
response: reqwest::Response,
|
||||
status: reqwest::StatusCode,
|
||||
headers: reqwest::header::HeaderMap,
|
||||
// TODO cookies?
|
||||
}
|
||||
|
||||
impl<T: DeserializeOwned> ResponseValue<T> {
|
||||
#[doc(hidden)]
|
||||
pub async fn from_response<E: std::fmt::Debug>(
|
||||
response: reqwest::Response,
|
||||
) -> Result<Self, Error<E>> {
|
||||
let status = response.status();
|
||||
let headers = response.headers().clone();
|
||||
let inner = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(Error::InvalidResponsePayload)?;
|
||||
|
||||
Ok(Self {
|
||||
inner,
|
||||
status,
|
||||
headers,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseValue<()> {
|
||||
#[doc(hidden)]
|
||||
pub fn empty(response: reqwest::Response) -> Self {
|
||||
let status = response.status();
|
||||
let headers = response.headers().clone();
|
||||
// TODO is there anything we want to do to confirm that there is no
|
||||
// content?
|
||||
Self {
|
||||
inner: (),
|
||||
status,
|
||||
headers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ResponseValue<T> {
|
||||
#[doc(hidden)]
|
||||
pub fn new(inner: T, response: reqwest::Response) -> Self {
|
||||
Self { inner, response }
|
||||
/// Consumes the ResponseValue, returning the wrapped value.
|
||||
pub fn into_inner(self) -> T {
|
||||
self.inner
|
||||
}
|
||||
|
||||
pub fn request(&self) -> &reqwest::Response {
|
||||
&self.response
|
||||
/// Get the status from this response.
|
||||
pub fn status(&self) -> &reqwest::StatusCode {
|
||||
&self.status
|
||||
}
|
||||
/// Get the headers from this response.
|
||||
pub fn headers(&self) -> &reqwest::header::HeaderMap {
|
||||
&self.headers
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn map<U: std::fmt::Debug, F, E>(
|
||||
self,
|
||||
f: F,
|
||||
) -> Result<ResponseValue<U>, E>
|
||||
where
|
||||
F: FnOnce(T) -> U,
|
||||
{
|
||||
let Self {
|
||||
inner,
|
||||
status,
|
||||
headers,
|
||||
} = self;
|
||||
|
||||
Ok(ResponseValue {
|
||||
inner: f(inner),
|
||||
status,
|
||||
headers,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,3 +93,87 @@ impl<T> Deref for ResponseValue<T> {
|
|||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DerefMut for ResponseValue<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: std::fmt::Debug> std::fmt::Debug for ResponseValue<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.inner.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Error produced by generated client methods.
|
||||
///
|
||||
/// The type parameter may be a struct if there's a single expected error type
|
||||
/// or an enum if there are multiple valid error types. It can be the unit type
|
||||
/// if there are no structured returns expected.
|
||||
#[derive(Debug)]
|
||||
pub enum Error<E: std::fmt::Debug> {
|
||||
/// A server error either with the data, or with the connection.
|
||||
CommunicationError(reqwest::Error),
|
||||
|
||||
/// A documented, expected error response.
|
||||
ErrorResponse(ResponseValue<E>),
|
||||
|
||||
/// An expected response code whose deserialization failed.
|
||||
// TODO we have stuff from the response; should we include it?
|
||||
InvalidResponsePayload(reqwest::Error),
|
||||
|
||||
/// A response not listed in the API description. This may represent a
|
||||
/// success or failure response; check `status().is_success()`.
|
||||
UnexpectedResponse(reqwest::Response),
|
||||
}
|
||||
|
||||
impl<E: std::fmt::Debug> From<reqwest::Error> for Error<E> {
|
||||
fn from(e: reqwest::Error) -> Self {
|
||||
Self::CommunicationError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: std::fmt::Debug> std::fmt::Display for Error<E> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Error::CommunicationError(e) => {
|
||||
write!(f, "Communication Error {}", e)
|
||||
}
|
||||
Error::ErrorResponse(rv) => {
|
||||
write!(f, "Error Response {:?}", rv)
|
||||
}
|
||||
Error::InvalidResponsePayload(e) => {
|
||||
write!(f, "Invalid Response Payload {}", e)
|
||||
}
|
||||
Error::UnexpectedResponse(r) => {
|
||||
write!(f, "Unexpected Response {:?}", r)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<E: std::fmt::Debug> std::error::Error for Error<E> {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Error::CommunicationError(e) => Some(e),
|
||||
Error::InvalidResponsePayload(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const PATH_SET: &percent_encoding::AsciiSet = &percent_encoding::CONTROLS
|
||||
.add(b' ')
|
||||
.add(b'"')
|
||||
.add(b'#')
|
||||
.add(b'<')
|
||||
.add(b'>')
|
||||
.add(b'?')
|
||||
.add(b'`')
|
||||
.add(b'{')
|
||||
.add(b'}');
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn encode_path(pc: &str) -> String {
|
||||
percent_encoding::utf8_percent_encode(pc, PATH_SET).to_string()
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ repository = "https://github.com/oxidecomputer/progenitor.git"
|
|||
description = "An OpenAPI client generator - core implementation"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
convert_case = "0.4"
|
||||
getopts = "0.2"
|
||||
indexmap = "1.7"
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
// Copyright 2022 Oxide Computer Company
|
||||
|
||||
use std::{cmp::Ordering, collections::HashMap};
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
collections::{BTreeSet, HashMap},
|
||||
};
|
||||
|
||||
use convert_case::{Case, Casing};
|
||||
use indexmap::IndexMap;
|
||||
|
@ -9,7 +12,6 @@ use openapiv3::{
|
|||
StatusCode,
|
||||
};
|
||||
use proc_macro2::TokenStream;
|
||||
|
||||
use quote::{format_ident, quote, ToTokens};
|
||||
use template::PathTemplate;
|
||||
use thiserror::Error;
|
||||
|
@ -78,11 +80,63 @@ enum OperationParameterType {
|
|||
}
|
||||
#[derive(Debug)]
|
||||
struct OperationResponse {
|
||||
status_code: StatusCode,
|
||||
status_code: OperationResponseStatus,
|
||||
typ: OperationResponseType,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
impl Eq for OperationResponse {}
|
||||
impl PartialEq for OperationResponse {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.status_code == other.status_code
|
||||
}
|
||||
}
|
||||
impl Ord for OperationResponse {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.status_code.cmp(&other.status_code)
|
||||
}
|
||||
}
|
||||
impl PartialOrd for OperationResponse {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
enum OperationResponseStatus {
|
||||
Code(u16),
|
||||
Range(u16),
|
||||
Default,
|
||||
}
|
||||
|
||||
impl OperationResponseStatus {
|
||||
fn to_value(&self) -> u16 {
|
||||
match self {
|
||||
OperationResponseStatus::Code(code) => {
|
||||
assert!(*code < 1000);
|
||||
*code
|
||||
}
|
||||
OperationResponseStatus::Range(range) => {
|
||||
assert!(*range < 10);
|
||||
*range * 100
|
||||
}
|
||||
OperationResponseStatus::Default => 1000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for OperationResponseStatus {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.to_value().cmp(&other.to_value())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for OperationResponseStatus {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
||||
enum OperationResponseType {
|
||||
Type(TypeId),
|
||||
None,
|
||||
|
@ -176,28 +230,9 @@ impl Generator {
|
|||
});
|
||||
|
||||
let file = quote! {
|
||||
use anyhow::Result;
|
||||
|
||||
mod progenitor_support {
|
||||
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
|
||||
|
||||
#[allow(dead_code)]
|
||||
const PATH_SET: &AsciiSet = &CONTROLS
|
||||
.add(b' ')
|
||||
.add(b'"')
|
||||
.add(b'#')
|
||||
.add(b'<')
|
||||
.add(b'>')
|
||||
.add(b'?')
|
||||
.add(b'`')
|
||||
.add(b'{')
|
||||
.add(b'}');
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn encode_path(pc: &str) -> String {
|
||||
utf8_percent_encode(pc, PATH_SET).to_string()
|
||||
}
|
||||
}
|
||||
// Re-export ResponseValue and Error since those are used by the
|
||||
// public interface of Client.
|
||||
pub use progenitor_client::{Error, ResponseValue};
|
||||
|
||||
pub mod types {
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -415,7 +450,7 @@ impl Generator {
|
|||
OperationParameterKind::Path,
|
||||
) => Ordering::Greater,
|
||||
|
||||
// Body params are last and should be unique
|
||||
// Body params are last and should be singular.
|
||||
(
|
||||
OperationParameterKind::Body,
|
||||
OperationParameterKind::Path,
|
||||
|
@ -438,11 +473,31 @@ impl Generator {
|
|||
|
||||
let mut responses = operation
|
||||
.responses
|
||||
.responses
|
||||
.default
|
||||
.iter()
|
||||
.map(|(status_code, response_or_ref)| {
|
||||
let response = response_or_ref.item(components)?;
|
||||
|
||||
.map(|response_or_ref| {
|
||||
Ok((
|
||||
OperationResponseStatus::Default,
|
||||
response_or_ref.item(components)?,
|
||||
))
|
||||
})
|
||||
.chain(operation.responses.responses.iter().map(
|
||||
|(status_code, response_or_ref)| {
|
||||
Ok((
|
||||
match status_code {
|
||||
StatusCode::Code(code) => {
|
||||
OperationResponseStatus::Code(*code)
|
||||
}
|
||||
StatusCode::Range(range) => {
|
||||
OperationResponseStatus::Range(*range)
|
||||
}
|
||||
},
|
||||
response_or_ref.item(components)?,
|
||||
))
|
||||
},
|
||||
))
|
||||
.map(|v: Result<(OperationResponseStatus, &Response)>| {
|
||||
let (status_code, response) = v?;
|
||||
let typ = if let Some(mt) =
|
||||
response.content.get("application/json")
|
||||
{
|
||||
|
@ -470,17 +525,17 @@ impl Generator {
|
|||
OperationResponseType::None
|
||||
};
|
||||
|
||||
// See if there's a status code that covers success cases.
|
||||
if matches!(
|
||||
status_code,
|
||||
StatusCode::Code(200..=299) | StatusCode::Range(2)
|
||||
OperationResponseStatus::Default
|
||||
| OperationResponseStatus::Code(200..=299)
|
||||
| OperationResponseStatus::Range(2)
|
||||
) {
|
||||
success = true;
|
||||
}
|
||||
|
||||
Ok(OperationResponse {
|
||||
status_code: status_code.clone(),
|
||||
typ,
|
||||
})
|
||||
Ok(OperationResponse { status_code, typ })
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
|
@ -490,7 +545,7 @@ impl Generator {
|
|||
// spec.
|
||||
if !success {
|
||||
responses.push(OperationResponse {
|
||||
status_code: StatusCode::Range(2),
|
||||
status_code: OperationResponseStatus::Range(2),
|
||||
typ: OperationResponseType::Raw,
|
||||
});
|
||||
}
|
||||
|
@ -597,32 +652,208 @@ impl Generator {
|
|||
|
||||
assert!(body_func.clone().count() <= 1);
|
||||
|
||||
let mut success_response_items =
|
||||
method.responses.iter().filter(|response| {
|
||||
let mut success_response_items = method
|
||||
.responses
|
||||
.iter()
|
||||
.filter(|response| {
|
||||
matches!(
|
||||
response.status_code,
|
||||
StatusCode::Code(200..=299) | StatusCode::Range(2)
|
||||
&response.status_code,
|
||||
OperationResponseStatus::Default
|
||||
| OperationResponseStatus::Code(200..=299)
|
||||
| OperationResponseStatus::Range(2)
|
||||
)
|
||||
});
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
success_response_items.sort();
|
||||
|
||||
assert_eq!(success_response_items.clone().count(), 1);
|
||||
// If we have a range and a default, we can pop off the default since it will never be hit.
|
||||
if let (
|
||||
Some(OperationResponse {
|
||||
status_code: OperationResponseStatus::Range(2),
|
||||
..
|
||||
}),
|
||||
Some(OperationResponse {
|
||||
status_code: OperationResponseStatus::Default,
|
||||
..
|
||||
}),
|
||||
) = last_two(&success_response_items)
|
||||
{
|
||||
success_response_items.pop();
|
||||
}
|
||||
|
||||
let (response_type, decode_response) = success_response_items
|
||||
let success_response_types = success_response_items
|
||||
.iter()
|
||||
.map(|response| response.typ.clone())
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
// TODO to deal with multiple success response types, we'll need to
|
||||
// create an enum.
|
||||
assert_eq!(success_response_types.len(), 1);
|
||||
|
||||
let response_type = success_response_types
|
||||
.iter()
|
||||
.next()
|
||||
.map(|response| match &response.typ {
|
||||
OperationResponseType::Type(type_id) => (
|
||||
self.type_space.get_type(type_id).unwrap().ident(),
|
||||
quote! { res.json().await? },
|
||||
),
|
||||
.map(|typ| match typ {
|
||||
OperationResponseType::Type(type_id) => {
|
||||
let type_name =
|
||||
self.type_space.get_type(type_id).unwrap().ident();
|
||||
quote! { ResponseValue<#type_name> }
|
||||
}
|
||||
OperationResponseType::None => {
|
||||
(quote! { reqwest::Response }, quote! { res })
|
||||
}
|
||||
OperationResponseType::Raw => {
|
||||
(quote! { reqwest::Response }, quote! { res })
|
||||
quote! { ResponseValue<()> }
|
||||
}
|
||||
// TODO Maybe this should be ResponseValue<Bytes>?
|
||||
OperationResponseType::Raw => quote! { reqwest::Response },
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let success_response_matches =
|
||||
success_response_items.iter().map(|response| {
|
||||
let pat = match &response.status_code {
|
||||
OperationResponseStatus::Code(code) => quote! { #code },
|
||||
OperationResponseStatus::Range(_)
|
||||
| OperationResponseStatus::Default => {
|
||||
quote! { 200 ..= 299 }
|
||||
}
|
||||
};
|
||||
|
||||
let decode = match &response.typ {
|
||||
OperationResponseType::Type(_) => {
|
||||
quote! {
|
||||
ResponseValue::from_response(response).await
|
||||
}
|
||||
}
|
||||
OperationResponseType::None => {
|
||||
quote! {
|
||||
Ok(ResponseValue::empty(response))
|
||||
}
|
||||
}
|
||||
OperationResponseType::Raw => quote! { Ok(response) },
|
||||
};
|
||||
|
||||
quote! { #pat => { #decode } }
|
||||
});
|
||||
|
||||
// Errors...
|
||||
let mut error_responses = method
|
||||
.responses
|
||||
.iter()
|
||||
.filter(|response| {
|
||||
matches!(
|
||||
&response.status_code,
|
||||
OperationResponseStatus::Code(400..=599)
|
||||
| OperationResponseStatus::Range(4)
|
||||
| OperationResponseStatus::Range(5)
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
error_responses.sort();
|
||||
|
||||
let error_response_types = error_responses
|
||||
.iter()
|
||||
.map(|response| response.typ.clone())
|
||||
.collect::<BTreeSet<_>>();
|
||||
// TODO create an enum if there are multiple error response types
|
||||
assert!(error_response_types.len() <= 1);
|
||||
|
||||
let error_type = error_response_types
|
||||
.iter()
|
||||
.next()
|
||||
.map(|typ| match typ {
|
||||
OperationResponseType::Type(type_id) => {
|
||||
let type_name =
|
||||
self.type_space.get_type(type_id).unwrap().ident();
|
||||
quote! { #type_name }
|
||||
}
|
||||
OperationResponseType::None => {
|
||||
quote! { () }
|
||||
}
|
||||
// TODO Maybe this should be ResponseValue<Bytes>?
|
||||
OperationResponseType::Raw => quote! { reqwest::Response },
|
||||
})
|
||||
.unwrap_or_else(|| quote! { () });
|
||||
|
||||
let error_response_matches = error_responses.iter().map(|response| {
|
||||
let pat = match &response.status_code {
|
||||
OperationResponseStatus::Code(code) => quote! { #code },
|
||||
OperationResponseStatus::Range(r) => {
|
||||
let min = r * 100;
|
||||
let max = min + 99;
|
||||
quote! { #min ..= #max }
|
||||
}
|
||||
OperationResponseStatus::Default => unreachable!(),
|
||||
};
|
||||
|
||||
let decode = match &response.typ {
|
||||
OperationResponseType::Type(_) => {
|
||||
quote! {
|
||||
Err(Error::ErrorResponse(
|
||||
ResponseValue::from_response(response)
|
||||
.await?
|
||||
))
|
||||
}
|
||||
}
|
||||
OperationResponseType::None => {
|
||||
quote! {
|
||||
Err(Error::ErrorResponse(
|
||||
ResponseValue::empty(response)
|
||||
))
|
||||
}
|
||||
}
|
||||
// TODO not sure how to handle this...
|
||||
OperationResponseType::Raw => todo!(),
|
||||
};
|
||||
|
||||
quote! { #pat => { #decode } }
|
||||
});
|
||||
|
||||
// Generate the catch-all case for other statuses. If the operation
|
||||
// specifies a default response, we've already handled the success
|
||||
// status codes above, so we only need to consider error codes.
|
||||
let default_response = method
|
||||
.responses
|
||||
.iter()
|
||||
.find(|response| {
|
||||
matches!(
|
||||
&response.status_code,
|
||||
OperationResponseStatus::Default
|
||||
)
|
||||
})
|
||||
.map_or_else(
|
||||
||
|
||||
// With no default response, unexpected status codes produce
|
||||
// and Error::UnexpectedResponse()
|
||||
quote!{
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
},
|
||||
// If we have a structured default response, we decode it and
|
||||
// return Error::ErrorResponse()
|
||||
|response| {
|
||||
let decode = match &response.typ {
|
||||
OperationResponseType::Type(_) => {
|
||||
quote! {
|
||||
Err(Error::ErrorResponse(
|
||||
ResponseValue::from_response(response)
|
||||
.await?
|
||||
))
|
||||
}
|
||||
}
|
||||
OperationResponseType::None => {
|
||||
quote! {
|
||||
Err(Error::ErrorResponse(
|
||||
ResponseValue::empty(response)
|
||||
))
|
||||
}
|
||||
}
|
||||
// TODO not sure how to handle this... maybe as a
|
||||
// ResponseValue<Bytes>
|
||||
OperationResponseType::Raw => todo!(),
|
||||
};
|
||||
|
||||
quote! { _ => { #decode } }
|
||||
},
|
||||
);
|
||||
|
||||
// TODO document parameters
|
||||
let doc_comment = format!(
|
||||
"{}{}: {} {}",
|
||||
|
@ -655,7 +886,10 @@ impl Generator {
|
|||
pub async fn #operation_id #bounds (
|
||||
&'a self,
|
||||
#(#params),*
|
||||
) -> Result<#response_type> {
|
||||
) -> Result<
|
||||
#response_type,
|
||||
Error<#error_type>,
|
||||
> {
|
||||
#url_path
|
||||
#query_build
|
||||
|
||||
|
@ -670,10 +904,42 @@ impl Generator {
|
|||
.await;
|
||||
#post_hook
|
||||
|
||||
// TODO we should do a match here for result?.status().as_u16()
|
||||
let res = result?.error_for_status()?;
|
||||
let response = result?;
|
||||
|
||||
Ok(#decode_response)
|
||||
match response.status().as_u16() {
|
||||
// These will be of the form...
|
||||
// 201 => ResponseValue::from_response(response).await,
|
||||
// 200..299 => ResponseValue::empty(response),
|
||||
// TODO this isn't implemented
|
||||
// ... or in the case of an operation with multiple
|
||||
// successful response types...
|
||||
// 200 => {
|
||||
// ResponseValue::from_response()
|
||||
// .await?
|
||||
// .map(OperationXResponse::ResponseTypeA)
|
||||
// }
|
||||
// 201 => {
|
||||
// ResponseValue::from_response()
|
||||
// .await?
|
||||
// .map(OperationXResponse::ResponseTypeB)
|
||||
// }
|
||||
#(#success_response_matches)*
|
||||
|
||||
// This is almost identical to the success types except
|
||||
// they are wrapped in Error::ErrorResponse...
|
||||
// 400 => {
|
||||
// Err(Error::ErrorResponse(
|
||||
// ResponseValue::from_response(response.await?)
|
||||
// ))
|
||||
// }
|
||||
#(#error_response_matches)*
|
||||
|
||||
// The default response is either an Error with a known
|
||||
// type if the operation defines a default (as above) or
|
||||
// an Error::UnexpectedResponse...
|
||||
// _ => Err(Error::UnexpectedResponse(response)),
|
||||
#default_response
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -750,7 +1016,10 @@ impl Generator {
|
|||
pub fn #stream_id #bounds (
|
||||
&'a self,
|
||||
#(#stream_params),*
|
||||
) -> impl futures::Stream<Item = Result<#item_type>> + Unpin + '_ {
|
||||
) -> impl futures::Stream<Item = Result<
|
||||
#item_type,
|
||||
Error<#error_type>,
|
||||
>> + Unpin + '_ {
|
||||
use futures::StreamExt;
|
||||
use futures::TryFutureExt;
|
||||
use futures::TryStreamExt;
|
||||
|
@ -761,6 +1030,7 @@ impl Generator {
|
|||
#(#first_params,)*
|
||||
)
|
||||
.map_ok(move |page| {
|
||||
let page = page.into_inner();
|
||||
// The first page is just an iter
|
||||
let first = futures::stream::iter(
|
||||
page.items.into_iter().map(Ok)
|
||||
|
@ -785,6 +1055,7 @@ impl Generator {
|
|||
#(#step_params,)*
|
||||
)
|
||||
.map_ok(|page| {
|
||||
let page = page.into_inner();
|
||||
Some((
|
||||
futures::stream::iter(
|
||||
page
|
||||
|
@ -864,7 +1135,8 @@ impl Generator {
|
|||
responses.iter().filter_map(|response| {
|
||||
match (&response.status_code, &response.typ) {
|
||||
(
|
||||
StatusCode::Code(200..=299) | StatusCode::Range(2),
|
||||
OperationResponseStatus::Code(200..=299)
|
||||
| OperationResponseStatus::Range(2),
|
||||
OperationResponseType::Type(type_id),
|
||||
) => Some(type_id),
|
||||
_ => None,
|
||||
|
@ -940,7 +1212,6 @@ impl Generator {
|
|||
|
||||
pub fn dependencies(&self) -> Vec<String> {
|
||||
let mut deps = vec![
|
||||
"anyhow = \"1.0\"",
|
||||
"percent-encoding = \"2.1\"",
|
||||
"serde = { version = \"1.0\", features = [\"derive\"] }",
|
||||
"reqwest = { version = \"0.11\", features = [\"json\", \"stream\"] }",
|
||||
|
@ -968,10 +1239,16 @@ impl Generator {
|
|||
}
|
||||
}
|
||||
|
||||
fn last_two<T>(items: &[T]) -> (Option<&T>, Option<&T>) {
|
||||
match items.len() {
|
||||
0 => (None, None),
|
||||
1 => (Some(&items[0]), None),
|
||||
n => (Some(&items[n - 2]), Some(&items[n - 1])),
|
||||
}
|
||||
}
|
||||
|
||||
/// Make the schema optional if it isn't already.
|
||||
pub fn make_optional(
|
||||
schema: schemars::schema::Schema,
|
||||
) -> schemars::schema::Schema {
|
||||
fn make_optional(schema: schemars::schema::Schema) -> schemars::schema::Schema {
|
||||
match &schema {
|
||||
// If the instance_type already includes Null then this is already
|
||||
// optional.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2021 Oxide Computer Company
|
||||
// Copyright 2022 Oxide Computer Company
|
||||
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{format_ident, quote};
|
||||
|
@ -32,7 +32,7 @@ impl PathTemplate {
|
|||
if let Component::Parameter(n) = &component {
|
||||
let param = format_ident!("{}", n);
|
||||
Some(quote! {
|
||||
progenitor_support::encode_path(&#param.to_string())
|
||||
progenitor_client::encode_path(&#param.to_string())
|
||||
})
|
||||
} else {
|
||||
None
|
||||
|
@ -160,10 +160,9 @@ impl ToString for PathTemplate {
|
|||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{parse, Component, PathTemplate};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
|
||||
#[test]
|
||||
fn basic() -> Result<()> {
|
||||
fn basic() {
|
||||
let trials = vec![
|
||||
(
|
||||
"/info",
|
||||
|
@ -193,15 +192,15 @@ mod test {
|
|||
];
|
||||
|
||||
for (path, want) in trials.iter() {
|
||||
let t = parse(path).with_context(|| anyhow!("path {}", path))?;
|
||||
assert_eq!(&t, want);
|
||||
match parse(path) {
|
||||
Ok(t) => assert_eq!(&t, want),
|
||||
Err(e) => panic!("path {} {}", path, e),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn names() -> Result<()> {
|
||||
fn names() {
|
||||
let trials = vec![
|
||||
("/info", vec![]),
|
||||
("/measure/{number}", vec!["number".to_string()]),
|
||||
|
@ -212,24 +211,23 @@ mod test {
|
|||
];
|
||||
|
||||
for (path, want) in trials.iter() {
|
||||
let t = parse(path).with_context(|| anyhow!("path {}", path))?;
|
||||
assert_eq!(&t.names(), want);
|
||||
match parse(path) {
|
||||
Ok(t) => assert_eq!(&t.names(), want),
|
||||
Err(e) => panic!("path {} {}", path, e),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compile() -> Result<()> {
|
||||
let t = parse("/measure/{number}")?;
|
||||
fn compile() {
|
||||
let t = parse("/measure/{number}").unwrap();
|
||||
let out = t.compile();
|
||||
let want = quote::quote! {
|
||||
let url = format!("{}/measure/{}",
|
||||
self.baseurl,
|
||||
progenitor_support::encode_path(&number.to_string()),
|
||||
progenitor_client::encode_path(&number.to_string()),
|
||||
);
|
||||
};
|
||||
assert_eq!(want.to_string(), out.to_string());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +1,4 @@
|
|||
use anyhow::Result;
|
||||
mod progenitor_support {
|
||||
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
|
||||
#[allow(dead_code)]
|
||||
const PATH_SET: &AsciiSet = &CONTROLS
|
||||
.add(b' ')
|
||||
.add(b'"')
|
||||
.add(b'#')
|
||||
.add(b'<')
|
||||
.add(b'>')
|
||||
.add(b'?')
|
||||
.add(b'`')
|
||||
.add(b'{')
|
||||
.add(b'}');
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn encode_path(pc: &str) -> String {
|
||||
utf8_percent_encode(pc, PATH_SET).to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub use progenitor_client::{Error, ResponseValue};
|
||||
pub mod types {
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
|
@ -182,55 +163,73 @@ impl Client {
|
|||
}
|
||||
|
||||
#[doc = "control_hold: POST /v1/control/hold"]
|
||||
pub async fn control_hold<'a>(&'a self) -> Result<()> {
|
||||
pub async fn control_hold<'a>(&'a self) -> Result<ResponseValue<()>, Error<()>> {
|
||||
let url = format!("{}/v1/control/hold", self.baseurl,);
|
||||
let request = self.client.post(url).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let res = result?.error_for_status()?;
|
||||
Ok(res.json().await?)
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
200u16 => ResponseValue::from_response(response).await,
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
|
||||
#[doc = "control_resume: POST /v1/control/resume"]
|
||||
pub async fn control_resume<'a>(&'a self) -> Result<reqwest::Response> {
|
||||
pub async fn control_resume<'a>(&'a self) -> Result<ResponseValue<()>, Error<()>> {
|
||||
let url = format!("{}/v1/control/resume", self.baseurl,);
|
||||
let request = self.client.post(url).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let res = result?.error_for_status()?;
|
||||
Ok(res)
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
200u16 => Ok(ResponseValue::empty(response)),
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
|
||||
#[doc = "task_get: GET /v1/task/{task}"]
|
||||
pub async fn task_get<'a>(&'a self, task: &'a str) -> Result<types::Task> {
|
||||
pub async fn task_get<'a>(
|
||||
&'a self,
|
||||
task: &'a str,
|
||||
) -> Result<ResponseValue<types::Task>, Error<()>> {
|
||||
let url = format!(
|
||||
"{}/v1/task/{}",
|
||||
self.baseurl,
|
||||
progenitor_support::encode_path(&task.to_string()),
|
||||
progenitor_client::encode_path(&task.to_string()),
|
||||
);
|
||||
let request = self.client.get(url).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let res = result?.error_for_status()?;
|
||||
Ok(res.json().await?)
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
200u16 => ResponseValue::from_response(response).await,
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
|
||||
#[doc = "tasks_get: GET /v1/tasks"]
|
||||
pub async fn tasks_get<'a>(&'a self) -> Result<Vec<types::Task>> {
|
||||
pub async fn tasks_get<'a>(&'a self) -> Result<ResponseValue<Vec<types::Task>>, Error<()>> {
|
||||
let url = format!("{}/v1/tasks", self.baseurl,);
|
||||
let request = self.client.get(url).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let res = result?.error_for_status()?;
|
||||
Ok(res.json().await?)
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
200u16 => ResponseValue::from_response(response).await,
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
|
||||
#[doc = "task_submit: POST /v1/tasks"]
|
||||
pub async fn task_submit<'a>(
|
||||
&'a self,
|
||||
body: &'a types::TaskSubmit,
|
||||
) -> Result<types::TaskSubmitResult> {
|
||||
) -> Result<ResponseValue<types::TaskSubmitResult>, Error<()>> {
|
||||
let url = format!("{}/v1/tasks", self.baseurl,);
|
||||
let request = self.client.post(url).json(body).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let res = result?.error_for_status()?;
|
||||
Ok(res.json().await?)
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
201u16 => ResponseValue::from_response(response).await,
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
|
||||
#[doc = "task_events_get: GET /v1/tasks/{task}/events"]
|
||||
|
@ -238,11 +237,11 @@ impl Client {
|
|||
&'a self,
|
||||
task: &'a str,
|
||||
minseq: Option<u32>,
|
||||
) -> Result<Vec<types::TaskEvent>> {
|
||||
) -> Result<ResponseValue<Vec<types::TaskEvent>>, Error<()>> {
|
||||
let url = format!(
|
||||
"{}/v1/tasks/{}/events",
|
||||
self.baseurl,
|
||||
progenitor_support::encode_path(&task.to_string()),
|
||||
progenitor_client::encode_path(&task.to_string()),
|
||||
);
|
||||
let mut query = Vec::new();
|
||||
if let Some(v) = &minseq {
|
||||
|
@ -251,21 +250,30 @@ impl Client {
|
|||
|
||||
let request = self.client.get(url).query(&query).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let res = result?.error_for_status()?;
|
||||
Ok(res.json().await?)
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
200u16 => ResponseValue::from_response(response).await,
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
|
||||
#[doc = "task_outputs_get: GET /v1/tasks/{task}/outputs"]
|
||||
pub async fn task_outputs_get<'a>(&'a self, task: &'a str) -> Result<Vec<types::TaskOutput>> {
|
||||
pub async fn task_outputs_get<'a>(
|
||||
&'a self,
|
||||
task: &'a str,
|
||||
) -> Result<ResponseValue<Vec<types::TaskOutput>>, Error<()>> {
|
||||
let url = format!(
|
||||
"{}/v1/tasks/{}/outputs",
|
||||
self.baseurl,
|
||||
progenitor_support::encode_path(&task.to_string()),
|
||||
progenitor_client::encode_path(&task.to_string()),
|
||||
);
|
||||
let request = self.client.get(url).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let res = result?.error_for_status()?;
|
||||
Ok(res.json().await?)
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
200u16 => ResponseValue::from_response(response).await,
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
|
||||
#[doc = "task_output_download: GET /v1/tasks/{task}/outputs/{output}"]
|
||||
|
@ -273,59 +281,76 @@ impl Client {
|
|||
&'a self,
|
||||
task: &'a str,
|
||||
output: &'a str,
|
||||
) -> Result<reqwest::Response> {
|
||||
) -> Result<reqwest::Response, Error<()>> {
|
||||
let url = format!(
|
||||
"{}/v1/tasks/{}/outputs/{}",
|
||||
self.baseurl,
|
||||
progenitor_support::encode_path(&task.to_string()),
|
||||
progenitor_support::encode_path(&output.to_string()),
|
||||
progenitor_client::encode_path(&task.to_string()),
|
||||
progenitor_client::encode_path(&output.to_string()),
|
||||
);
|
||||
let request = self.client.get(url).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let res = result?.error_for_status()?;
|
||||
Ok(res)
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
200..=299 => Ok(response),
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
|
||||
#[doc = "user_create: POST /v1/users"]
|
||||
pub async fn user_create<'a>(
|
||||
&'a self,
|
||||
body: &'a types::UserCreate,
|
||||
) -> Result<types::UserCreateResult> {
|
||||
) -> Result<ResponseValue<types::UserCreateResult>, Error<()>> {
|
||||
let url = format!("{}/v1/users", self.baseurl,);
|
||||
let request = self.client.post(url).json(body).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let res = result?.error_for_status()?;
|
||||
Ok(res.json().await?)
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
201u16 => ResponseValue::from_response(response).await,
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
|
||||
#[doc = "whoami: GET /v1/whoami"]
|
||||
pub async fn whoami<'a>(&'a self) -> Result<types::WhoamiResult> {
|
||||
pub async fn whoami<'a>(&'a self) -> Result<ResponseValue<types::WhoamiResult>, Error<()>> {
|
||||
let url = format!("{}/v1/whoami", self.baseurl,);
|
||||
let request = self.client.get(url).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let res = result?.error_for_status()?;
|
||||
Ok(res.json().await?)
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
200u16 => ResponseValue::from_response(response).await,
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
|
||||
#[doc = "worker_bootstrap: POST /v1/worker/bootstrap"]
|
||||
pub async fn worker_bootstrap<'a>(
|
||||
&'a self,
|
||||
body: &'a types::WorkerBootstrap,
|
||||
) -> Result<types::WorkerBootstrapResult> {
|
||||
) -> Result<ResponseValue<types::WorkerBootstrapResult>, Error<()>> {
|
||||
let url = format!("{}/v1/worker/bootstrap", self.baseurl,);
|
||||
let request = self.client.post(url).json(body).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let res = result?.error_for_status()?;
|
||||
Ok(res.json().await?)
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
201u16 => ResponseValue::from_response(response).await,
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
|
||||
#[doc = "worker_ping: GET /v1/worker/ping"]
|
||||
pub async fn worker_ping<'a>(&'a self) -> Result<types::WorkerPingResult> {
|
||||
pub async fn worker_ping<'a>(
|
||||
&'a self,
|
||||
) -> Result<ResponseValue<types::WorkerPingResult>, Error<()>> {
|
||||
let url = format!("{}/v1/worker/ping", self.baseurl,);
|
||||
let request = self.client.get(url).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let res = result?.error_for_status()?;
|
||||
Ok(res.json().await?)
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
200u16 => ResponseValue::from_response(response).await,
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
|
||||
#[doc = "worker_task_append: POST /v1/worker/task/{task}/append"]
|
||||
|
@ -333,16 +358,19 @@ impl Client {
|
|||
&'a self,
|
||||
task: &'a str,
|
||||
body: &'a types::WorkerAppendTask,
|
||||
) -> Result<reqwest::Response> {
|
||||
) -> Result<ResponseValue<()>, Error<()>> {
|
||||
let url = format!(
|
||||
"{}/v1/worker/task/{}/append",
|
||||
self.baseurl,
|
||||
progenitor_support::encode_path(&task.to_string()),
|
||||
progenitor_client::encode_path(&task.to_string()),
|
||||
);
|
||||
let request = self.client.post(url).json(body).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let res = result?.error_for_status()?;
|
||||
Ok(res)
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
201u16 => Ok(ResponseValue::empty(response)),
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
|
||||
#[doc = "worker_task_upload_chunk: POST /v1/worker/task/{task}/chunk"]
|
||||
|
@ -350,16 +378,19 @@ impl Client {
|
|||
&'a self,
|
||||
task: &'a str,
|
||||
body: B,
|
||||
) -> Result<types::UploadedChunk> {
|
||||
) -> Result<ResponseValue<types::UploadedChunk>, Error<()>> {
|
||||
let url = format!(
|
||||
"{}/v1/worker/task/{}/chunk",
|
||||
self.baseurl,
|
||||
progenitor_support::encode_path(&task.to_string()),
|
||||
progenitor_client::encode_path(&task.to_string()),
|
||||
);
|
||||
let request = self.client.post(url).body(body).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let res = result?.error_for_status()?;
|
||||
Ok(res.json().await?)
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
201u16 => ResponseValue::from_response(response).await,
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
|
||||
#[doc = "worker_task_complete: POST /v1/worker/task/{task}/complete"]
|
||||
|
@ -367,16 +398,19 @@ impl Client {
|
|||
&'a self,
|
||||
task: &'a str,
|
||||
body: &'a types::WorkerCompleteTask,
|
||||
) -> Result<reqwest::Response> {
|
||||
) -> Result<ResponseValue<()>, Error<()>> {
|
||||
let url = format!(
|
||||
"{}/v1/worker/task/{}/complete",
|
||||
self.baseurl,
|
||||
progenitor_support::encode_path(&task.to_string()),
|
||||
progenitor_client::encode_path(&task.to_string()),
|
||||
);
|
||||
let request = self.client.post(url).json(body).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let res = result?.error_for_status()?;
|
||||
Ok(res)
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
200u16 => Ok(ResponseValue::empty(response)),
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
|
||||
#[doc = "worker_task_add_output: POST /v1/worker/task/{task}/output"]
|
||||
|
@ -384,33 +418,44 @@ impl Client {
|
|||
&'a self,
|
||||
task: &'a str,
|
||||
body: &'a types::WorkerAddOutput,
|
||||
) -> Result<reqwest::Response> {
|
||||
) -> Result<ResponseValue<()>, Error<()>> {
|
||||
let url = format!(
|
||||
"{}/v1/worker/task/{}/output",
|
||||
self.baseurl,
|
||||
progenitor_support::encode_path(&task.to_string()),
|
||||
progenitor_client::encode_path(&task.to_string()),
|
||||
);
|
||||
let request = self.client.post(url).json(body).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let res = result?.error_for_status()?;
|
||||
Ok(res)
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
201u16 => Ok(ResponseValue::empty(response)),
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
|
||||
#[doc = "workers_list: GET /v1/workers"]
|
||||
pub async fn workers_list<'a>(&'a self) -> Result<types::WorkersResult> {
|
||||
pub async fn workers_list<'a>(
|
||||
&'a self,
|
||||
) -> Result<ResponseValue<types::WorkersResult>, Error<()>> {
|
||||
let url = format!("{}/v1/workers", self.baseurl,);
|
||||
let request = self.client.get(url).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let res = result?.error_for_status()?;
|
||||
Ok(res.json().await?)
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
200u16 => ResponseValue::from_response(response).await,
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
|
||||
#[doc = "workers_recycle: POST /v1/workers/recycle"]
|
||||
pub async fn workers_recycle<'a>(&'a self) -> Result<reqwest::Response> {
|
||||
pub async fn workers_recycle<'a>(&'a self) -> Result<ResponseValue<()>, Error<()>> {
|
||||
let url = format!("{}/v1/workers/recycle", self.baseurl,);
|
||||
let request = self.client.post(url).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let res = result?.error_for_status()?;
|
||||
Ok(res)
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
200u16 => Ok(ResponseValue::empty(response)),
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +1,4 @@
|
|||
use anyhow::Result;
|
||||
mod progenitor_support {
|
||||
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
|
||||
#[allow(dead_code)]
|
||||
const PATH_SET: &AsciiSet = &CONTROLS
|
||||
.add(b' ')
|
||||
.add(b'"')
|
||||
.add(b'#')
|
||||
.add(b'<')
|
||||
.add(b'>')
|
||||
.add(b'?')
|
||||
.add(b'`')
|
||||
.add(b'{')
|
||||
.add(b'}');
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn encode_path(pc: &str) -> String {
|
||||
utf8_percent_encode(pc, PATH_SET).to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub use progenitor_client::{Error, ResponseValue};
|
||||
pub mod types {
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
|
@ -123,65 +104,88 @@ impl Client {
|
|||
}
|
||||
|
||||
#[doc = "enrol: POST /enrol"]
|
||||
pub async fn enrol<'a>(&'a self, body: &'a types::EnrolBody) -> Result<reqwest::Response> {
|
||||
pub async fn enrol<'a>(
|
||||
&'a self,
|
||||
body: &'a types::EnrolBody,
|
||||
) -> Result<ResponseValue<()>, Error<()>> {
|
||||
let url = format!("{}/enrol", self.baseurl,);
|
||||
let request = self.client.post(url).json(body).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let res = result?.error_for_status()?;
|
||||
Ok(res)
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
201u16 => Ok(ResponseValue::empty(response)),
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
|
||||
#[doc = "global_jobs: GET /global/jobs"]
|
||||
pub async fn global_jobs<'a>(&'a self) -> Result<types::GlobalJobsResult> {
|
||||
pub async fn global_jobs<'a>(
|
||||
&'a self,
|
||||
) -> Result<ResponseValue<types::GlobalJobsResult>, Error<()>> {
|
||||
let url = format!("{}/global/jobs", self.baseurl,);
|
||||
let request = self.client.get(url).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let res = result?.error_for_status()?;
|
||||
Ok(res.json().await?)
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
201u16 => ResponseValue::from_response(response).await,
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
|
||||
#[doc = "ping: GET /ping"]
|
||||
pub async fn ping<'a>(&'a self) -> Result<types::PingResult> {
|
||||
pub async fn ping<'a>(&'a self) -> Result<ResponseValue<types::PingResult>, Error<()>> {
|
||||
let url = format!("{}/ping", self.baseurl,);
|
||||
let request = self.client.get(url).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let res = result?.error_for_status()?;
|
||||
Ok(res.json().await?)
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
201u16 => ResponseValue::from_response(response).await,
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
|
||||
#[doc = "report_finish: POST /report/finish"]
|
||||
pub async fn report_finish<'a>(
|
||||
&'a self,
|
||||
body: &'a types::ReportFinishBody,
|
||||
) -> Result<types::ReportResult> {
|
||||
) -> Result<ResponseValue<types::ReportResult>, Error<()>> {
|
||||
let url = format!("{}/report/finish", self.baseurl,);
|
||||
let request = self.client.post(url).json(body).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let res = result?.error_for_status()?;
|
||||
Ok(res.json().await?)
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
201u16 => ResponseValue::from_response(response).await,
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
|
||||
#[doc = "report_output: POST /report/output"]
|
||||
pub async fn report_output<'a>(
|
||||
&'a self,
|
||||
body: &'a types::ReportOutputBody,
|
||||
) -> Result<types::ReportResult> {
|
||||
) -> Result<ResponseValue<types::ReportResult>, Error<()>> {
|
||||
let url = format!("{}/report/output", self.baseurl,);
|
||||
let request = self.client.post(url).json(body).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let res = result?.error_for_status()?;
|
||||
Ok(res.json().await?)
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
201u16 => ResponseValue::from_response(response).await,
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
|
||||
#[doc = "report_start: POST /report/start"]
|
||||
pub async fn report_start<'a>(
|
||||
&'a self,
|
||||
body: &'a types::ReportStartBody,
|
||||
) -> Result<types::ReportResult> {
|
||||
) -> Result<ResponseValue<types::ReportResult>, Error<()>> {
|
||||
let url = format!("{}/report/start", self.baseurl,);
|
||||
let request = self.client.post(url).json(body).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let res = result?.error_for_status()?;
|
||||
Ok(res.json().await?)
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
201u16 => ResponseValue::from_response(response).await,
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -10,6 +10,45 @@ use serde::Deserialize;
|
|||
use serde_tokenstream::ParseWrapper;
|
||||
use syn::LitStr;
|
||||
|
||||
/// Generates a client from the given OpenAPI document
|
||||
///
|
||||
/// `generate_api!` can be invoked in two ways. The simple form, takes a path
|
||||
/// to the OpenAPI document:
|
||||
/// ```ignore
|
||||
/// generate_api!("path/to/spec.json");
|
||||
/// ```
|
||||
///
|
||||
/// The more complex form accepts the following key-value pairs in any order:
|
||||
/// ```ignore
|
||||
/// generate_api!(
|
||||
/// spec = "path/to/spec.json"
|
||||
/// [ inner_type = path::to:Type, ]
|
||||
/// [ pre_hook = closure::or::path::to::function, ]
|
||||
/// [ post_hook = closure::or::path::to::function, ]
|
||||
/// [ derives = [ path::to::DeriveMacro ], ]
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// The `spec` key is required; it is the OpenAPI document from which the
|
||||
/// client is derived.
|
||||
///
|
||||
/// The optional `inner_type` is for ancillary data, stored with the generated
|
||||
/// client that can be usd by the pre and post hooks.
|
||||
///
|
||||
/// The optional `pre_hook` is either a closure (that must be within
|
||||
/// parentheses: `(fn |inner, request| { .. })`) or a path to a function. The
|
||||
/// closure or function must take two parameters: the inner type and a
|
||||
/// `&reqwest::Request`. This allows clients to examine requests before they're
|
||||
/// sent to the server, for example to log them.
|
||||
///
|
||||
/// The optional `post_hook` is either a closure (that must be within
|
||||
/// parentheses: `(fn |inner, result| { .. })`) or a path to a function. The
|
||||
/// closure or function must take two parameters: the inner type and a
|
||||
/// `&Result<reqwest::Response, reqwest::Error>`. This allows clients to
|
||||
/// examine responses, for example to log them.
|
||||
///
|
||||
/// The optional `derives` array allows consumers to specify additional derive
|
||||
/// macros to apply to generated types.
|
||||
#[proc_macro]
|
||||
pub fn generate_api(item: TokenStream) -> TokenStream {
|
||||
match do_generate_api(item) {
|
||||
|
@ -112,6 +151,10 @@ fn do_generate_api(item: TokenStream) -> Result<TokenStream, syn::Error> {
|
|||
})?;
|
||||
|
||||
let output = quote! {
|
||||
// The progenitor_client is tautologically visible from macro
|
||||
// consumers.
|
||||
use progenitor::progenitor_client;
|
||||
|
||||
#code
|
||||
|
||||
// Force a rebuild when the given file is modified.
|
||||
|
|
|
@ -7,8 +7,9 @@ repository = "https://github.com/oxidecomputer/progenitor.git"
|
|||
description = "An OpenAPI client generator"
|
||||
|
||||
[dependencies]
|
||||
progenitor-macro = { path = "../progenitor-macro" }
|
||||
progenitor-client = { path = "../progenitor-client" }
|
||||
progenitor-impl = { path = "../progenitor-impl" }
|
||||
progenitor-macro = { path = "../progenitor-macro" }
|
||||
anyhow = "1.0"
|
||||
getopts = "0.2"
|
||||
openapiv3 = "1.0.0"
|
||||
|
|
|
@ -1,7 +1,16 @@
|
|||
// Copyright 2021 Oxide Computer Company
|
||||
// Copyright 2022 Oxide Computer Company
|
||||
|
||||
//! TODO This crate does stuff!
|
||||
//! Progenitor is a Rust crate for generating opinionated clients from API
|
||||
//! descriptions specified in the OpenAPI 3.0.x format. It makes use of Rust
|
||||
//! futures for async API calls and `Streams` for paginated interfaces.
|
||||
//!
|
||||
//! It generates a type called `Client` with methods that correspond to the
|
||||
//! operations specified in the OpenAPI document.
|
||||
//!
|
||||
//! For details see the [repo
|
||||
//! README](https://github.com/oxidecomputer/progenitor/README.md)
|
||||
|
||||
pub use progenitor_client;
|
||||
pub use progenitor_impl::Error;
|
||||
pub use progenitor_impl::Generator;
|
||||
pub use progenitor_macro::generate_api;
|
||||
|
|
|
@ -53,8 +53,8 @@ fn main() -> Result<()> {
|
|||
|
||||
let mut builder = Generator::new();
|
||||
|
||||
let fail = match builder.generate_text(&api) {
|
||||
Ok(out) => {
|
||||
match builder.generate_text(&api) {
|
||||
Ok(api_code) => {
|
||||
let type_space = builder.get_type_space();
|
||||
|
||||
println!("-----------------------------------------------------");
|
||||
|
@ -89,7 +89,9 @@ fn main() -> Result<()> {
|
|||
edition = \"2018\"\n\
|
||||
\n\
|
||||
[dependencies]\n\
|
||||
{}",
|
||||
{}\n\
|
||||
\n\
|
||||
[workspace]\n",
|
||||
name,
|
||||
version,
|
||||
builder.dependencies().join("\n"),
|
||||
|
@ -107,19 +109,25 @@ fn main() -> Result<()> {
|
|||
/*
|
||||
* Create the Rust source file containing the generated client:
|
||||
*/
|
||||
let mut librs = src;
|
||||
let lib_code = format!("mod progenitor_client;\n\n{}", api_code);
|
||||
let mut librs = src.clone();
|
||||
librs.push("lib.rs");
|
||||
save(librs, out.as_str())?;
|
||||
false
|
||||
save(librs, lib_code.as_str())?;
|
||||
|
||||
/*
|
||||
* Create the Rust source file containing the support code:
|
||||
*/
|
||||
let progenitor_client_code =
|
||||
include_str!("../../progenitor-client/src/lib.rs");
|
||||
let mut clientrs = src;
|
||||
clientrs.push("progenitor_client.rs");
|
||||
save(clientrs, progenitor_client_code)?;
|
||||
}
|
||||
|
||||
Err(e) => {
|
||||
println!("gen fail: {:?}", e);
|
||||
true
|
||||
bail!("generation experienced errors");
|
||||
}
|
||||
};
|
||||
|
||||
if fail {
|
||||
bail!("generation experienced errors");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -135,6 +143,7 @@ where
|
|||
Ok(serde_json::from_reader(f)?)
|
||||
}
|
||||
|
||||
// TODO some or all of this validation should be in progenitor-impl
|
||||
pub fn load_api<P>(p: P) -> Result<OpenAPI>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
|
@ -161,45 +170,6 @@ where
|
|||
bail!("tags not presently supported");
|
||||
}
|
||||
|
||||
if let Some(components) = api.components.as_ref() {
|
||||
if !components.security_schemes.is_empty() {
|
||||
bail!("component security schemes not supported");
|
||||
}
|
||||
|
||||
if !components.responses.is_empty() {
|
||||
bail!("component responses not supported");
|
||||
}
|
||||
|
||||
if !components.parameters.is_empty() {
|
||||
bail!("component parameters not supported");
|
||||
}
|
||||
|
||||
if !components.request_bodies.is_empty() {
|
||||
bail!("component request bodies not supported");
|
||||
}
|
||||
|
||||
if !components.headers.is_empty() {
|
||||
bail!("component headers not supported");
|
||||
}
|
||||
|
||||
if !components.links.is_empty() {
|
||||
bail!("component links not supported");
|
||||
}
|
||||
|
||||
if !components.callbacks.is_empty() {
|
||||
bail!("component callbacks not supported");
|
||||
}
|
||||
|
||||
/*
|
||||
* XXX Ignoring "examples" and "extensions" for now.
|
||||
* Explicitly allowing "schemas" through.
|
||||
*/
|
||||
}
|
||||
|
||||
/*
|
||||
* XXX Ignoring "external_docs" and "extensions" for now, as they seem not
|
||||
* to immediately affect our code generation.
|
||||
*/
|
||||
let mut opids = HashSet::new();
|
||||
for p in api.paths.paths.iter() {
|
||||
match p.1 {
|
||||
|
@ -217,10 +187,6 @@ where
|
|||
bail!("duplicate operation ID: {}", oid);
|
||||
}
|
||||
|
||||
if !o.tags.is_empty() {
|
||||
bail!("op {}: tags, unsupported", oid);
|
||||
}
|
||||
|
||||
if !o.servers.is_empty() {
|
||||
bail!("op {}: servers, unsupported", oid);
|
||||
}
|
||||
|
@ -228,10 +194,6 @@ where
|
|||
if o.security.is_some() {
|
||||
bail!("op {}: security, unsupported", oid);
|
||||
}
|
||||
|
||||
if o.responses.default.is_some() {
|
||||
bail!("op {}: has response default", oid);
|
||||
}
|
||||
} else {
|
||||
bail!("path {} is missing operation ID", p.0);
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue