From ea80069ef3c87cd1eaaf9e67bfeed6d091f30e76 Mon Sep 17 00:00:00 2001 From: Adam Leventhal Date: Tue, 15 Mar 2022 16:11:47 -0700 Subject: [PATCH] Handle byte streams for both success and error responses (#35) --- Cargo.lock | 32 +- Cargo.toml | 2 + progenitor-client/Cargo.toml | 2 + progenitor-client/src/lib.rs | 41 +- progenitor-impl/Cargo.toml | 4 +- progenitor-impl/src/lib.rs | 409 +++++++----------- progenitor-impl/tests/output/buildomat.out | 6 +- progenitor-impl/tests/output/keeper.out | 2 +- progenitor-impl/tests/output/nexus.out | 8 +- .../tests/output/test_freeform_response.out | 59 +++ .../tests/output/test_renamed_parameters.out | 2 +- progenitor-impl/tests/test_specific.rs | 39 +- sample_openapi/nexus.json | 7 +- 13 files changed, 323 insertions(+), 290 deletions(-) create mode 100644 progenitor-impl/tests/output/test_freeform_response.out diff --git a/Cargo.lock b/Cargo.lock index c425b7c..1d927ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index f1d6fa7..533a18e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/progenitor-client/Cargo.toml b/progenitor-client/Cargo.toml index c44f2f5..d4f66e7 100644 --- a/progenitor-client/Cargo.toml +++ b/progenitor-client/Cargo.toml @@ -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" diff --git a/progenitor-client/src/lib.rs b/progenitor-client/src/lib.rs index d4b649b..35e5543 100644 --- a/progenitor-client/src/lib.rs +++ b/progenitor-client/src/lib.rs @@ -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>>; + /// Success value returned by generated client methods. pub struct ResponseValue { inner: T, @@ -34,6 +40,19 @@ impl ResponseValue { } } +impl ResponseValue { + #[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 std::fmt::Debug for ResponseValue { /// 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 { +pub enum Error { /// A server error either with the data, or with the connection. CommunicationError(reqwest::Error), @@ -129,7 +147,7 @@ pub enum Error { UnexpectedResponse(reqwest::Response), } -impl Error { +impl Error { /// Returns the status code, if the error was generated from a response. pub fn status(&self) -> Option { match self { @@ -162,20 +180,20 @@ impl Error { } } -impl From for Error { +impl From for Error { fn from(e: reqwest::Error) -> Self { Self::CommunicationError(e) } } -impl std::fmt::Display for Error { +impl std::fmt::Display for Error { 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 std::fmt::Display for Error { } } } -impl std::error::Error for Error { +impl std::fmt::Debug for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self, f) + } +} +impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Error::CommunicationError(e) => Some(e), diff --git a/progenitor-impl/Cargo.toml b/progenitor-impl/Cargo.toml index abff488..cc0835c 100644 --- a/progenitor-impl/Cargo.toml +++ b/progenitor-impl/Cargo.toml @@ -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" diff --git a/progenitor-impl/src/lib.rs b/progenitor-impl/src/lib.rs index 2357d05..5226b9e 100644 --- a/progenitor-impl/src/lib.rs +++ b/progenitor-impl/src/lib.rs @@ -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::>(); - 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::>(); - - // 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? - 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::>(); - 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::>(); - // 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? - 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 - 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::>(); + 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::>(); + + // 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 { 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; } -impl ExtractJsonMediaType for openapiv3::Response { - fn content_json(&self) -> Result { - 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) -> Result { - 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 { if self.content.len() != 1 { diff --git a/progenitor-impl/tests/output/buildomat.out b/progenitor-impl/tests/output/buildomat.out index 406b320..2142ca8 100644 --- a/progenitor-impl/tests/output/buildomat.out +++ b/progenitor-impl/tests/output/buildomat.out @@ -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> { + ) -> Result, 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)), } } diff --git a/progenitor-impl/tests/output/keeper.out b/progenitor-impl/tests/output/keeper.out index 1fb062b..9349f11 100644 --- a/progenitor-impl/tests/output/keeper.out +++ b/progenitor-impl/tests/output/keeper.out @@ -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)] diff --git a/progenitor-impl/tests/output/nexus.out b/progenitor-impl/tests/output/nexus.out index 3a0e1fd..97423c8 100644 --- a/progenitor-impl/tests/output/nexus.out +++ b/progenitor-impl/tests/output/nexus.out @@ -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, Error<()>> { + ) -> Result, Error> { 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))), } } diff --git a/progenitor-impl/tests/output/test_freeform_response.out b/progenitor-impl/tests/output/test_freeform_response.out new file mode 100644 index 0000000..a44766a --- /dev/null +++ b/progenitor-impl/tests/output/test_freeform_response.out @@ -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, + 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, Error> { + 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))), + } + } +} diff --git a/progenitor-impl/tests/output/test_renamed_parameters.out b/progenitor-impl/tests/output/test_renamed_parameters.out index 1cf00f4..86babcc 100644 --- a/progenitor-impl/tests/output/test_renamed_parameters.out +++ b/progenitor-impl/tests/output/test_renamed_parameters.out @@ -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."] diff --git a/progenitor-impl/tests/test_specific.rs b/progenitor-impl/tests/test_specific.rs index a5548ad..8a6ef67 100644 --- a/progenitor-impl/tests/test_specific.rs +++ b/progenitor-impl/tests/test_specific.rs @@ -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>>, + _rqctx: Arc>, _path: Path, _query: Query, ) -> Result { - 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>, +) -> Result, 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::(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, + ) +} diff --git a/sample_openapi/nexus.json b/sample_openapi/nexus.json index 85acae5..abc980f 100644 --- a/sample_openapi/nexus.json +++ b/sample_openapi/nexus.json @@ -232,7 +232,12 @@ }, "responses": { "default": { - "description": "" + "description": "", + "content": { + "*/*": { + "schema": {} + } + } } } }