Handle byte streams for both success and error responses (#35)

This commit is contained in:
Adam Leventhal 2022-03-15 16:11:47 -07:00 committed by GitHub
parent ae7c178e21
commit ea80069ef3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 323 additions and 290 deletions

32
Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -232,7 +232,12 @@
},
"responses": {
"default": {
"description": ""
"description": "",
"content": {
"*/*": {
"schema": {}
}
}
}
}
}