Add a wrapper types for success and error responses (#26)

This commit is contained in:
Adam Leventhal 2022-02-08 08:59:38 -08:00 committed by GitHub
parent f1f9e2e938
commit 25192b5dc1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 3262 additions and 1002 deletions

9
Cargo.lock generated
View File

@ -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",

142
README.md Normal file
View File

@ -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`.

67
docs/generated_methods.md Normal file
View File

@ -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;
}
}
}
```

87
docs/progenitor-client.md Normal file
View File

@ -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),
}
```

View File

@ -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"] }

View File

@ -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();

View File

@ -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"

View File

@ -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"

View File

@ -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()
}

View File

@ -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"

View File

@ -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.

View File

@ -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(())
}
}

View File

@ -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)),
}
}
}

View File

@ -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

View File

@ -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.

View File

@ -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"

View File

@ -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;

View File

@ -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