Handle byte streams for both success and error responses (#35)
This commit is contained in:
parent
ae7c178e21
commit
ea80069ef3
|
@ -240,7 +240,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "dropshot"
|
||||
version = "0.6.1-dev"
|
||||
source = "git+https://github.com/oxidecomputer/dropshot#eadf3bb7169a5744eab633f159093d7f7017b9d6"
|
||||
source = "git+https://github.com/oxidecomputer/dropshot#18078c4909d6260dddb924915f830de12840871d"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
|
@ -278,7 +278,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "dropshot_endpoint"
|
||||
version = "0.6.1-dev"
|
||||
source = "git+https://github.com/oxidecomputer/dropshot#eadf3bb7169a5744eab633f159093d7f7017b9d6"
|
||||
source = "git+https://github.com/oxidecomputer/dropshot#18078c4909d6260dddb924915f830de12840871d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -574,7 +574,7 @@ checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03"
|
|||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"itoa 1.0.1",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -590,9 +590,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.5.1"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503"
|
||||
checksum = "9100414882e15fb7feccb4897e5f0ff0ff1ca7d1a86a23208ada4d7a18e6c6c4"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
|
@ -602,9 +602,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
|
|||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.16"
|
||||
version = "0.14.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7ec3e62bdc98a2f0393a5048e4c30ef659440ea6e0e572965103e72bd836f55"
|
||||
checksum = "043f0e083e9901b6cc658a77d1eb86f4fc650bbb977a4337dd63192826aa85dd"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
|
@ -615,7 +615,7 @@ dependencies = [
|
|||
"http-body",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa 0.4.8",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio",
|
||||
|
@ -674,12 +674,6 @@ version = "2.3.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.1"
|
||||
|
@ -1033,6 +1027,8 @@ dependencies = [
|
|||
name = "progenitor-client"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"percent-encoding",
|
||||
"reqwest",
|
||||
"serde",
|
||||
|
@ -1047,6 +1043,8 @@ dependencies = [
|
|||
"dropshot",
|
||||
"expectorate",
|
||||
"getopts",
|
||||
"http",
|
||||
"hyper",
|
||||
"indexmap",
|
||||
"openapiv3",
|
||||
"proc-macro2",
|
||||
|
@ -1365,7 +1363,7 @@ version = "1.0.79"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
|
||||
dependencies = [
|
||||
"itoa 1.0.1",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
@ -1388,7 +1386,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"itoa 1.0.1",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
@ -1607,7 +1605,7 @@ version = "0.3.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "004cbc98f30fa233c61a38bc77e96a9106e65c88f2d3bef182ae952027e5753d"
|
||||
dependencies = [
|
||||
"itoa 1.0.1",
|
||||
"itoa",
|
||||
"libc",
|
||||
"num_threads",
|
||||
"time-macros",
|
||||
|
|
|
@ -19,3 +19,5 @@ default-members = [
|
|||
|
||||
#[patch."https://github.com/oxidecomputer/typify"]
|
||||
#typify = { path = "../typify/typify" }
|
||||
#[patch."https://github.com/oxidecomputer/dropshot"]
|
||||
#dropshot = { path = "../dropshot/dropshot" }
|
||||
|
|
|
@ -7,6 +7,8 @@ repository = "https://github.com/oxidecomputer/progenitor.git"
|
|||
description = "An OpenAPI client generator - client support"
|
||||
|
||||
[dependencies]
|
||||
bytes = "1.1.0"
|
||||
futures-core = "0.3.21"
|
||||
percent-encoding = "2.1"
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
serde = "1.0"
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
// Copyright 2021 Oxide Computer Company
|
||||
// Copyright 2022 Oxide Computer Company
|
||||
|
||||
//! Support code for generated clients.
|
||||
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use bytes::Bytes;
|
||||
use futures_core::Stream;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
/// Represents a streaming, untyped byte stream for both success and error
|
||||
/// responses.
|
||||
pub type ByteStream = Box<dyn Stream<Item = reqwest::Result<Bytes>>>;
|
||||
|
||||
/// Success value returned by generated client methods.
|
||||
pub struct ResponseValue<T> {
|
||||
inner: T,
|
||||
|
@ -34,6 +40,19 @@ impl<T: DeserializeOwned> ResponseValue<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl ResponseValue<ByteStream> {
|
||||
#[doc(hidden)]
|
||||
pub fn stream(response: reqwest::Response) -> Self {
|
||||
let status = response.status();
|
||||
let headers = response.headers().clone();
|
||||
Self {
|
||||
inner: Box::new(response.bytes_stream()),
|
||||
status,
|
||||
headers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseValue<()> {
|
||||
#[doc(hidden)]
|
||||
pub fn empty(response: reqwest::Response) -> Self {
|
||||
|
@ -112,8 +131,7 @@ impl<T: std::fmt::Debug> std::fmt::Debug for ResponseValue<T> {
|
|||
/// 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 = ()> {
|
||||
pub enum Error<E = ()> {
|
||||
/// A server error either with the data, or with the connection.
|
||||
CommunicationError(reqwest::Error),
|
||||
|
||||
|
@ -129,7 +147,7 @@ pub enum Error<E: std::fmt::Debug = ()> {
|
|||
UnexpectedResponse(reqwest::Response),
|
||||
}
|
||||
|
||||
impl<E: std::fmt::Debug> Error<E> {
|
||||
impl<E> Error<E> {
|
||||
/// Returns the status code, if the error was generated from a response.
|
||||
pub fn status(&self) -> Option<reqwest::StatusCode> {
|
||||
match self {
|
||||
|
@ -162,20 +180,20 @@ impl<E: std::fmt::Debug> Error<E> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<E: std::fmt::Debug> From<reqwest::Error> for Error<E> {
|
||||
impl<E> 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> {
|
||||
impl<E> 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::ErrorResponse(_) => {
|
||||
write!(f, "Error Response")
|
||||
}
|
||||
Error::InvalidResponsePayload(e) => {
|
||||
write!(f, "Invalid Response Payload {}", e)
|
||||
|
@ -186,7 +204,12 @@ impl<E: std::fmt::Debug> std::fmt::Display for Error<E> {
|
|||
}
|
||||
}
|
||||
}
|
||||
impl<E: std::fmt::Debug> std::error::Error for Error<E> {
|
||||
impl<E> std::fmt::Debug for Error<E> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt(self, f)
|
||||
}
|
||||
}
|
||||
impl<E> std::error::Error for Error<E> {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Error::CommunicationError(e) => Some(e),
|
||||
|
|
|
@ -24,5 +24,7 @@ typify = { git = "https://github.com/oxidecomputer/typify" }
|
|||
unicode-xid = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
expectorate = "1.0"
|
||||
dropshot = { git = "https://github.com/oxidecomputer/dropshot" }
|
||||
expectorate = "1.0"
|
||||
http = "0.2.6"
|
||||
hyper = "0.14.17"
|
||||
|
|
|
@ -125,6 +125,28 @@ impl OperationResponseStatus {
|
|||
OperationResponseStatus::Default => 1000,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_success_or_default(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
OperationResponseStatus::Default
|
||||
| OperationResponseStatus::Code(200..=299)
|
||||
| OperationResponseStatus::Range(2)
|
||||
)
|
||||
}
|
||||
|
||||
fn is_error_or_default(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
OperationResponseStatus::Default
|
||||
| OperationResponseStatus::Code(400..=599)
|
||||
| OperationResponseStatus::Range(4..=5)
|
||||
)
|
||||
}
|
||||
|
||||
fn is_default(&self) -> bool {
|
||||
matches!(self, OperationResponseStatus::Default)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for OperationResponseStatus {
|
||||
|
@ -235,7 +257,7 @@ impl Generator {
|
|||
let file = quote! {
|
||||
// Re-export ResponseValue and Error since those are used by the
|
||||
// public interface of Client.
|
||||
pub use progenitor_client::{Error, ResponseValue};
|
||||
pub use progenitor_client::{ByteStream, Error, ResponseValue};
|
||||
|
||||
pub mod types {
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -516,6 +538,16 @@ impl Generator {
|
|||
))
|
||||
.map(|v: Result<(OperationResponseStatus, &Response)>| {
|
||||
let (status_code, response) = v?;
|
||||
|
||||
// We categorize responses as "typed" based on the
|
||||
// "application/json" content type, "raw" if there's any other
|
||||
// response content type (we don't investigate further), or
|
||||
// "none" if there is no content.
|
||||
// TODO if there are multiple response content types we could
|
||||
// treat those like different response types and create an
|
||||
// enum; the generated client method would check for the
|
||||
// content type of the response just as it currently examines
|
||||
// the status code.
|
||||
let typ = if let Some(mt) =
|
||||
response.content.get("application/json")
|
||||
{
|
||||
|
@ -559,7 +591,7 @@ impl Generator {
|
|||
|
||||
// If the API has declined to specify the characteristics of a
|
||||
// successful response, we cons up a generic one. Note that this is
|
||||
// technically permissible within OpenAPI, but advised against in the
|
||||
// technically permissible within OpenAPI, but advised against by the
|
||||
// spec.
|
||||
if !success {
|
||||
responses.push(OperationResponse {
|
||||
|
@ -681,60 +713,10 @@ impl Generator {
|
|||
|
||||
assert!(body_func.clone().count() <= 1);
|
||||
|
||||
let mut success_response_items = method
|
||||
.responses
|
||||
.iter()
|
||||
.filter(|response| {
|
||||
matches!(
|
||||
&response.status_code,
|
||||
OperationResponseStatus::Default
|
||||
| OperationResponseStatus::Code(200..=299)
|
||||
| OperationResponseStatus::Range(2)
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
success_response_items.sort();
|
||||
|
||||
// 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 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(|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! { ResponseValue<()> }
|
||||
}
|
||||
// TODO Maybe this should be ResponseValue<Bytes>?
|
||||
OperationResponseType::Raw => quote! { reqwest::Response },
|
||||
})
|
||||
.unwrap();
|
||||
let (success_response_items, response_type) = self.extract_responses(
|
||||
method,
|
||||
OperationResponseStatus::is_success_or_default,
|
||||
);
|
||||
|
||||
let success_response_matches =
|
||||
success_response_items.iter().map(|response| {
|
||||
|
@ -757,131 +739,77 @@ impl Generator {
|
|||
Ok(ResponseValue::empty(response))
|
||||
}
|
||||
}
|
||||
OperationResponseType::Raw => quote! { Ok(response) },
|
||||
OperationResponseType::Raw => {
|
||||
quote! {
|
||||
Ok(ResponseValue::stream(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_items, error_type) = self.extract_responses(
|
||||
method,
|
||||
OperationResponseStatus::is_error_or_default,
|
||||
);
|
||||
|
||||
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?
|
||||
))
|
||||
let error_response_matches =
|
||||
error_response_items.iter().map(|response| {
|
||||
let pat = match &response.status_code {
|
||||
OperationResponseStatus::Code(code) => {
|
||||
quote! { #code }
|
||||
}
|
||||
}
|
||||
OperationResponseType::None => {
|
||||
quote! {
|
||||
Err(Error::ErrorResponse(
|
||||
ResponseValue::empty(response)
|
||||
))
|
||||
OperationResponseStatus::Range(r) => {
|
||||
let min = r * 100;
|
||||
let max = min + 99;
|
||||
quote! { #min ..= #max }
|
||||
}
|
||||
}
|
||||
// TODO not sure how to handle this...
|
||||
OperationResponseType::Raw => todo!(),
|
||||
};
|
||||
|
||||
quote! { #pat => { #decode } }
|
||||
});
|
||||
OperationResponseStatus::Default => {
|
||||
quote! { _ }
|
||||
}
|
||||
};
|
||||
|
||||
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)
|
||||
))
|
||||
}
|
||||
}
|
||||
OperationResponseType::Raw => {
|
||||
quote! {
|
||||
Err(Error::ErrorResponse(
|
||||
ResponseValue::stream(response)
|
||||
))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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 } }
|
||||
},
|
||||
);
|
||||
// specifies a default response, we've already generated a default
|
||||
// match as part of error response code handling. (And we've handled
|
||||
// the default as a success response as well.) Otherwise the catch-all
|
||||
// produces an error corresponding to a response not specified in the
|
||||
// API description.
|
||||
let default_response = match method.responses.iter().last() {
|
||||
Some(response) if response.status_code.is_default() => quote! {},
|
||||
_ => quote! { _ => Err(Error::UnexpectedResponse(response)), },
|
||||
};
|
||||
|
||||
// TODO document parameters
|
||||
let doc_comment = format!(
|
||||
|
@ -916,7 +844,7 @@ impl Generator {
|
|||
&'a self,
|
||||
#(#params),*
|
||||
) -> Result<
|
||||
#response_type,
|
||||
ResponseValue<#response_type>,
|
||||
Error<#error_type>,
|
||||
> {
|
||||
#url_path
|
||||
|
@ -1117,6 +1045,64 @@ impl Generator {
|
|||
Ok(all)
|
||||
}
|
||||
|
||||
fn extract_responses<'a>(
|
||||
&self,
|
||||
method: &'a OperationMethod,
|
||||
filter: fn(&OperationResponseStatus) -> bool,
|
||||
) -> (Vec<&'a OperationResponse>, TokenStream) {
|
||||
let mut response_items = method
|
||||
.responses
|
||||
.iter()
|
||||
.filter(|response| filter(&response.status_code))
|
||||
.collect::<Vec<_>>();
|
||||
response_items.sort();
|
||||
|
||||
// If we have a success range and a default, we can pop off the default
|
||||
// since it will never be hit. Note that this is a no-op for error
|
||||
// responses.
|
||||
if let (
|
||||
Some(OperationResponse {
|
||||
status_code: OperationResponseStatus::Range(2),
|
||||
..
|
||||
}),
|
||||
Some(OperationResponse {
|
||||
status_code: OperationResponseStatus::Default,
|
||||
..
|
||||
}),
|
||||
) = last_two(&response_items)
|
||||
{
|
||||
response_items.pop();
|
||||
}
|
||||
|
||||
let response_types = response_items
|
||||
.iter()
|
||||
.map(|response| response.typ.clone())
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
// TODO to deal with multiple response types, we'll need to create an
|
||||
// enum type with variants for each of the response types.
|
||||
assert!(response_types.len() <= 1);
|
||||
let response_type = 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! { () }
|
||||
}
|
||||
OperationResponseType::Raw => {
|
||||
quote! { ByteStream }
|
||||
}
|
||||
})
|
||||
// TODO should this be a bytestream?
|
||||
.unwrap_or_else(|| quote! { () });
|
||||
(response_items, response_type)
|
||||
}
|
||||
|
||||
// Validates all the necessary conditions for Dropshot pagination. Returns
|
||||
// the paginated item type data if all conditions are met.
|
||||
fn dropshot_pagination_data(
|
||||
|
@ -1241,6 +1227,8 @@ impl Generator {
|
|||
|
||||
pub fn dependencies(&self) -> Vec<String> {
|
||||
let mut deps = vec![
|
||||
"bytes = \"1.1.0\"",
|
||||
"futures-core = \"0.3.21\"",
|
||||
"percent-encoding = \"2.1\"",
|
||||
"serde = { version = \"1.0\", features = [\"derive\"] }",
|
||||
"reqwest = { version = \"0.11\", features = [\"json\", \"stream\"] }",
|
||||
|
@ -1332,87 +1320,6 @@ trait ExtractJsonMediaType {
|
|||
fn content_json(&self) -> Result<openapiv3::MediaType>;
|
||||
}
|
||||
|
||||
impl ExtractJsonMediaType for openapiv3::Response {
|
||||
fn content_json(&self) -> Result<openapiv3::MediaType> {
|
||||
if self.content.len() != 1 {
|
||||
todo!("expected one content entry, found {}", self.content.len());
|
||||
}
|
||||
|
||||
if let Some(mt) = self.content.get("application/json") {
|
||||
Ok(mt.clone())
|
||||
} else {
|
||||
todo!(
|
||||
"could not find application/json, only found {}",
|
||||
self.content.keys().next().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn is_binary(&self, _components: &Option<Components>) -> Result<bool> {
|
||||
if self.content.is_empty() {
|
||||
/*
|
||||
* XXX If there are no content types, I guess it is not binary?
|
||||
*/
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if self.content.len() != 1 {
|
||||
todo!("expected one content entry, found {}", self.content.len());
|
||||
}
|
||||
|
||||
if let Some(mt) = self.content.get("application/octet-stream") {
|
||||
if !mt.encoding.is_empty() {
|
||||
todo!("XXX encoding");
|
||||
}
|
||||
|
||||
if let Some(s) = &mt.schema {
|
||||
use openapiv3::{
|
||||
SchemaKind, StringFormat, Type,
|
||||
VariantOrUnknownOrEmpty::Item,
|
||||
};
|
||||
|
||||
let s = s.item(&None)?;
|
||||
if s.schema_data.nullable {
|
||||
todo!("XXX nullable binary?");
|
||||
}
|
||||
if s.schema_data.default.is_some() {
|
||||
todo!("XXX default binary?");
|
||||
}
|
||||
if s.schema_data.discriminator.is_some() {
|
||||
todo!("XXX binary discriminator?");
|
||||
}
|
||||
match &s.schema_kind {
|
||||
SchemaKind::Type(Type::String(st)) => {
|
||||
if st.min_length.is_some() || st.max_length.is_some() {
|
||||
todo!("binary min/max length");
|
||||
}
|
||||
if !matches!(st.format, Item(StringFormat::Binary)) {
|
||||
todo!(
|
||||
"expected binary format string, got {:?}",
|
||||
st.format
|
||||
);
|
||||
}
|
||||
if st.pattern.is_some() {
|
||||
todo!("XXX pattern");
|
||||
}
|
||||
if !st.enumeration.is_empty() {
|
||||
todo!("XXX enumeration");
|
||||
}
|
||||
return Ok(true);
|
||||
}
|
||||
x => {
|
||||
todo!("XXX schemakind type {:?}", x);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
todo!("binary thing had no schema?");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtractJsonMediaType for openapiv3::RequestBody {
|
||||
fn content_json(&self) -> Result<openapiv3::MediaType> {
|
||||
if self.content.len() != 1 {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
pub use progenitor_client::{Error, ResponseValue};
|
||||
pub use progenitor_client::{ByteStream, Error, ResponseValue};
|
||||
pub mod types {
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
|
@ -281,7 +281,7 @@ impl Client {
|
|||
&'a self,
|
||||
task: &'a str,
|
||||
output: &'a str,
|
||||
) -> Result<reqwest::Response, Error<()>> {
|
||||
) -> Result<ResponseValue<ByteStream>, Error<()>> {
|
||||
let url = format!(
|
||||
"{}/v1/tasks/{}/outputs/{}",
|
||||
self.baseurl,
|
||||
|
@ -292,7 +292,7 @@ impl Client {
|
|||
let result = self.client.execute(request).await;
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
200..=299 => Ok(response),
|
||||
200..=299 => Ok(ResponseValue::stream(response)),
|
||||
_ => Err(Error::UnexpectedResponse(response)),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
pub use progenitor_client::{Error, ResponseValue};
|
||||
pub use progenitor_client::{ByteStream, Error, ResponseValue};
|
||||
pub mod types {
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
pub use progenitor_client::{Error, ResponseValue};
|
||||
pub use progenitor_client::{ByteStream, Error, ResponseValue};
|
||||
pub mod types {
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[doc = "A count of bytes, typically used either for memory or storage capacity\n\nThe maximum supported byte count is [`i64::MAX`]. This makes it somewhat inconvenient to define constructors: a u32 constructor can be infallible, but an i64 constructor can fail (if the value is negative) and a u64 constructor can fail (if the value is larger than i64::MAX). We provide all of these for consumers' convenience."]
|
||||
|
@ -1355,14 +1355,14 @@ impl Client {
|
|||
pub async fn spoof_login<'a>(
|
||||
&'a self,
|
||||
body: &'a types::LoginParams,
|
||||
) -> Result<ResponseValue<()>, Error<()>> {
|
||||
) -> Result<ResponseValue<ByteStream>, Error<ByteStream>> {
|
||||
let url = format!("{}/login", self.baseurl,);
|
||||
let request = self.client.post(url).json(body).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
200..=299 => Ok(ResponseValue::empty(response)),
|
||||
_ => Err(Error::ErrorResponse(ResponseValue::empty(response))),
|
||||
200..=299 => Ok(ResponseValue::stream(response)),
|
||||
_ => Err(Error::ErrorResponse(ResponseValue::stream(response))),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
pub use progenitor_client::{ByteStream, Error, ResponseValue};
|
||||
pub mod types {
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[doc = "Error information from a response."]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Error {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub error_code: Option<String>,
|
||||
pub message: String,
|
||||
pub request_id: String,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Client {
|
||||
baseurl: String,
|
||||
client: reqwest::Client,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(baseurl: &str) -> Self {
|
||||
let dur = std::time::Duration::from_secs(15);
|
||||
let client = reqwest::ClientBuilder::new()
|
||||
.connect_timeout(dur)
|
||||
.timeout(dur)
|
||||
.build()
|
||||
.unwrap();
|
||||
Self::new_with_client(baseurl, client)
|
||||
}
|
||||
|
||||
pub fn new_with_client(baseurl: &str, client: reqwest::Client) -> Self {
|
||||
Self {
|
||||
baseurl: baseurl.to_string(),
|
||||
client,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn baseurl(&self) -> &String {
|
||||
&self.baseurl
|
||||
}
|
||||
|
||||
pub fn client(&self) -> &reqwest::Client {
|
||||
&self.client
|
||||
}
|
||||
|
||||
#[doc = "freeform_response: GET "]
|
||||
pub async fn freeform_response<'a>(
|
||||
&'a self,
|
||||
) -> Result<ResponseValue<ByteStream>, Error<ByteStream>> {
|
||||
let url = format!("{}", self.baseurl,);
|
||||
let request = self.client.get(url).build()?;
|
||||
let result = self.client.execute(request).await;
|
||||
let response = result?;
|
||||
match response.status().as_u16() {
|
||||
200..=299 => Ok(ResponseValue::stream(response)),
|
||||
_ => Err(Error::ErrorResponse(ResponseValue::stream(response))),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
pub use progenitor_client::{Error, ResponseValue};
|
||||
pub use progenitor_client::{ByteStream, Error, ResponseValue};
|
||||
pub mod types {
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[doc = "Error information from a response."]
|
||||
|
|
|
@ -6,6 +6,8 @@ use dropshot::{
|
|||
endpoint, ApiDescription, HttpError, HttpResponseUpdatedNoContent, Path,
|
||||
Query, RequestContext,
|
||||
};
|
||||
use http::Response;
|
||||
use hyper::Body;
|
||||
use openapiv3::OpenAPI;
|
||||
use progenitor_impl::Generator;
|
||||
use schemars::JsonSchema;
|
||||
|
@ -38,11 +40,11 @@ struct CursedQuery {
|
|||
path = "/{ref}/{type}/{trait}",
|
||||
}]
|
||||
async fn renamed_parameters(
|
||||
_rqctx: Arc<RequestContext<Vec<String>>>,
|
||||
_rqctx: Arc<RequestContext<()>>,
|
||||
_path: Path<CursedPath>,
|
||||
_query: Query<CursedQuery>,
|
||||
) -> Result<HttpResponseUpdatedNoContent, HttpError> {
|
||||
unimplemented!();
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
/// Test parameters that conflict with Rust reserved words and therefore must
|
||||
|
@ -69,3 +71,36 @@ fn test_renamed_parameters() {
|
|||
&output,
|
||||
)
|
||||
}
|
||||
|
||||
#[endpoint {
|
||||
method = GET,
|
||||
path = "/",
|
||||
}]
|
||||
async fn freeform_response(
|
||||
_rqctx: Arc<RequestContext<()>>,
|
||||
) -> Result<Response<Body>, HttpError> {
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
/// Test freeform responses.
|
||||
#[test]
|
||||
fn test_freeform_response() {
|
||||
let mut api = ApiDescription::new();
|
||||
api.register(freeform_response).unwrap();
|
||||
|
||||
let mut out = Vec::new();
|
||||
|
||||
api.openapi("pagination-demo", "9000")
|
||||
.write(&mut out)
|
||||
.unwrap();
|
||||
|
||||
let out = from_utf8(&out).unwrap();
|
||||
let spec = serde_json::from_str::<OpenAPI>(out).unwrap();
|
||||
|
||||
let mut generator = Generator::new();
|
||||
let output = generator.generate_text(&spec).unwrap();
|
||||
expectorate::assert_contents(
|
||||
format!("tests/output/{}.out", "test_freeform_response"),
|
||||
&output,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -232,7 +232,12 @@
|
|||
},
|
||||
"responses": {
|
||||
"default": {
|
||||
"description": ""
|
||||
"description": "",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue