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]]
|
[[package]]
|
||||||
name = "dropshot"
|
name = "dropshot"
|
||||||
version = "0.6.1-dev"
|
version = "0.6.1-dev"
|
||||||
source = "git+https://github.com/oxidecomputer/dropshot#eadf3bb7169a5744eab633f159093d7f7017b9d6"
|
source = "git+https://github.com/oxidecomputer/dropshot#18078c4909d6260dddb924915f830de12840871d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-stream",
|
"async-stream",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
@ -278,7 +278,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dropshot_endpoint"
|
name = "dropshot_endpoint"
|
||||||
version = "0.6.1-dev"
|
version = "0.6.1-dev"
|
||||||
source = "git+https://github.com/oxidecomputer/dropshot#eadf3bb7169a5744eab633f159093d7f7017b9d6"
|
source = "git+https://github.com/oxidecomputer/dropshot#18078c4909d6260dddb924915f830de12840871d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -574,7 +574,7 @@ checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
"itoa 1.0.1",
|
"itoa",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -590,9 +590,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httparse"
|
name = "httparse"
|
||||||
version = "1.5.1"
|
version = "1.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503"
|
checksum = "9100414882e15fb7feccb4897e5f0ff0ff1ca7d1a86a23208ada4d7a18e6c6c4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httpdate"
|
name = "httpdate"
|
||||||
|
@ -602,9 +602,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "0.14.16"
|
version = "0.14.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b7ec3e62bdc98a2f0393a5048e4c30ef659440ea6e0e572965103e72bd836f55"
|
checksum = "043f0e083e9901b6cc658a77d1eb86f4fc650bbb977a4337dd63192826aa85dd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
|
@ -615,7 +615,7 @@ dependencies = [
|
||||||
"http-body",
|
"http-body",
|
||||||
"httparse",
|
"httparse",
|
||||||
"httpdate",
|
"httpdate",
|
||||||
"itoa 0.4.8",
|
"itoa",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
@ -674,12 +674,6 @@ version = "2.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9"
|
checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itoa"
|
|
||||||
version = "0.4.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
@ -1033,6 +1027,8 @@ dependencies = [
|
||||||
name = "progenitor-client"
|
name = "progenitor-client"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -1047,6 +1043,8 @@ dependencies = [
|
||||||
"dropshot",
|
"dropshot",
|
||||||
"expectorate",
|
"expectorate",
|
||||||
"getopts",
|
"getopts",
|
||||||
|
"http",
|
||||||
|
"hyper",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"openapiv3",
|
"openapiv3",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
@ -1365,7 +1363,7 @@ version = "1.0.79"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
|
checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa 1.0.1",
|
"itoa",
|
||||||
"ryu",
|
"ryu",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
@ -1388,7 +1386,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"itoa 1.0.1",
|
"itoa",
|
||||||
"ryu",
|
"ryu",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
@ -1607,7 +1605,7 @@ version = "0.3.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "004cbc98f30fa233c61a38bc77e96a9106e65c88f2d3bef182ae952027e5753d"
|
checksum = "004cbc98f30fa233c61a38bc77e96a9106e65c88f2d3bef182ae952027e5753d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa 1.0.1",
|
"itoa",
|
||||||
"libc",
|
"libc",
|
||||||
"num_threads",
|
"num_threads",
|
||||||
"time-macros",
|
"time-macros",
|
||||||
|
|
|
@ -19,3 +19,5 @@ default-members = [
|
||||||
|
|
||||||
#[patch."https://github.com/oxidecomputer/typify"]
|
#[patch."https://github.com/oxidecomputer/typify"]
|
||||||
#typify = { path = "../typify/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"
|
description = "An OpenAPI client generator - client support"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
bytes = "1.1.0"
|
||||||
|
futures-core = "0.3.21"
|
||||||
percent-encoding = "2.1"
|
percent-encoding = "2.1"
|
||||||
reqwest = { version = "0.11", features = ["json"] }
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
// Copyright 2021 Oxide Computer Company
|
// Copyright 2022 Oxide Computer Company
|
||||||
|
|
||||||
//! Support code for generated clients.
|
//! Support code for generated clients.
|
||||||
|
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
|
use futures_core::Stream;
|
||||||
use serde::de::DeserializeOwned;
|
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.
|
/// Success value returned by generated client methods.
|
||||||
pub struct ResponseValue<T> {
|
pub struct ResponseValue<T> {
|
||||||
inner: 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<()> {
|
impl ResponseValue<()> {
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub fn empty(response: reqwest::Response) -> Self {
|
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
|
/// 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
|
/// or an enum if there are multiple valid error types. It can be the unit type
|
||||||
/// if there are no structured returns expected.
|
/// if there are no structured returns expected.
|
||||||
#[derive(Debug)]
|
pub enum Error<E = ()> {
|
||||||
pub enum Error<E: std::fmt::Debug = ()> {
|
|
||||||
/// A server error either with the data, or with the connection.
|
/// A server error either with the data, or with the connection.
|
||||||
CommunicationError(reqwest::Error),
|
CommunicationError(reqwest::Error),
|
||||||
|
|
||||||
|
@ -129,7 +147,7 @@ pub enum Error<E: std::fmt::Debug = ()> {
|
||||||
UnexpectedResponse(reqwest::Response),
|
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.
|
/// Returns the status code, if the error was generated from a response.
|
||||||
pub fn status(&self) -> Option<reqwest::StatusCode> {
|
pub fn status(&self) -> Option<reqwest::StatusCode> {
|
||||||
match self {
|
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 {
|
fn from(e: reqwest::Error) -> Self {
|
||||||
Self::CommunicationError(e)
|
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 {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Error::CommunicationError(e) => {
|
Error::CommunicationError(e) => {
|
||||||
write!(f, "Communication Error {}", e)
|
write!(f, "Communication Error {}", e)
|
||||||
}
|
}
|
||||||
Error::ErrorResponse(rv) => {
|
Error::ErrorResponse(_) => {
|
||||||
write!(f, "Error Response {:?}", rv)
|
write!(f, "Error Response")
|
||||||
}
|
}
|
||||||
Error::InvalidResponsePayload(e) => {
|
Error::InvalidResponsePayload(e) => {
|
||||||
write!(f, "Invalid Response Payload {}", 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)> {
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
match self {
|
match self {
|
||||||
Error::CommunicationError(e) => Some(e),
|
Error::CommunicationError(e) => Some(e),
|
||||||
|
|
|
@ -24,5 +24,7 @@ typify = { git = "https://github.com/oxidecomputer/typify" }
|
||||||
unicode-xid = "0.2"
|
unicode-xid = "0.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
expectorate = "1.0"
|
|
||||||
dropshot = { git = "https://github.com/oxidecomputer/dropshot" }
|
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,
|
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 {
|
impl Ord for OperationResponseStatus {
|
||||||
|
@ -235,7 +257,7 @@ impl Generator {
|
||||||
let file = quote! {
|
let file = quote! {
|
||||||
// Re-export ResponseValue and Error since those are used by the
|
// Re-export ResponseValue and Error since those are used by the
|
||||||
// public interface of Client.
|
// public interface of Client.
|
||||||
pub use progenitor_client::{Error, ResponseValue};
|
pub use progenitor_client::{ByteStream, Error, ResponseValue};
|
||||||
|
|
||||||
pub mod types {
|
pub mod types {
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -516,6 +538,16 @@ impl Generator {
|
||||||
))
|
))
|
||||||
.map(|v: Result<(OperationResponseStatus, &Response)>| {
|
.map(|v: Result<(OperationResponseStatus, &Response)>| {
|
||||||
let (status_code, response) = v?;
|
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) =
|
let typ = if let Some(mt) =
|
||||||
response.content.get("application/json")
|
response.content.get("application/json")
|
||||||
{
|
{
|
||||||
|
@ -559,7 +591,7 @@ impl Generator {
|
||||||
|
|
||||||
// If the API has declined to specify the characteristics of a
|
// If the API has declined to specify the characteristics of a
|
||||||
// successful response, we cons up a generic one. Note that this is
|
// 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.
|
// spec.
|
||||||
if !success {
|
if !success {
|
||||||
responses.push(OperationResponse {
|
responses.push(OperationResponse {
|
||||||
|
@ -681,60 +713,10 @@ impl Generator {
|
||||||
|
|
||||||
assert!(body_func.clone().count() <= 1);
|
assert!(body_func.clone().count() <= 1);
|
||||||
|
|
||||||
let mut success_response_items = method
|
let (success_response_items, response_type) = self.extract_responses(
|
||||||
.responses
|
method,
|
||||||
.iter()
|
OperationResponseStatus::is_success_or_default,
|
||||||
.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_matches =
|
let success_response_matches =
|
||||||
success_response_items.iter().map(|response| {
|
success_response_items.iter().map(|response| {
|
||||||
|
@ -757,131 +739,77 @@ impl Generator {
|
||||||
Ok(ResponseValue::empty(response))
|
Ok(ResponseValue::empty(response))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OperationResponseType::Raw => quote! { Ok(response) },
|
OperationResponseType::Raw => {
|
||||||
|
quote! {
|
||||||
|
Ok(ResponseValue::stream(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
quote! { #pat => { #decode } }
|
quote! { #pat => { #decode } }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Errors...
|
// Errors...
|
||||||
let mut error_responses = method
|
let (error_response_items, error_type) = self.extract_responses(
|
||||||
.responses
|
method,
|
||||||
.iter()
|
OperationResponseStatus::is_error_or_default,
|
||||||
.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
|
let error_response_matches =
|
||||||
.iter()
|
error_response_items.iter().map(|response| {
|
||||||
.map(|response| response.typ.clone())
|
let pat = match &response.status_code {
|
||||||
.collect::<BTreeSet<_>>();
|
OperationResponseStatus::Code(code) => {
|
||||||
// TODO create an enum if there are multiple error response types
|
quote! { #code }
|
||||||
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?
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
OperationResponseStatus::Range(r) => {
|
||||||
OperationResponseType::None => {
|
let min = r * 100;
|
||||||
quote! {
|
let max = min + 99;
|
||||||
Err(Error::ErrorResponse(
|
quote! { #min ..= #max }
|
||||||
ResponseValue::empty(response)
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// 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
|
// Generate the catch-all case for other statuses. If the operation
|
||||||
// specifies a default response, we've already handled the success
|
// specifies a default response, we've already generated a default
|
||||||
// status codes above, so we only need to consider error codes.
|
// match as part of error response code handling. (And we've handled
|
||||||
let default_response = method
|
// the default as a success response as well.) Otherwise the catch-all
|
||||||
.responses
|
// produces an error corresponding to a response not specified in the
|
||||||
.iter()
|
// API description.
|
||||||
.find(|response| {
|
let default_response = match method.responses.iter().last() {
|
||||||
matches!(
|
Some(response) if response.status_code.is_default() => quote! {},
|
||||||
&response.status_code,
|
_ => quote! { _ => Err(Error::UnexpectedResponse(response)), },
|
||||||
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
|
// TODO document parameters
|
||||||
let doc_comment = format!(
|
let doc_comment = format!(
|
||||||
|
@ -916,7 +844,7 @@ impl Generator {
|
||||||
&'a self,
|
&'a self,
|
||||||
#(#params),*
|
#(#params),*
|
||||||
) -> Result<
|
) -> Result<
|
||||||
#response_type,
|
ResponseValue<#response_type>,
|
||||||
Error<#error_type>,
|
Error<#error_type>,
|
||||||
> {
|
> {
|
||||||
#url_path
|
#url_path
|
||||||
|
@ -1117,6 +1045,64 @@ impl Generator {
|
||||||
Ok(all)
|
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
|
// Validates all the necessary conditions for Dropshot pagination. Returns
|
||||||
// the paginated item type data if all conditions are met.
|
// the paginated item type data if all conditions are met.
|
||||||
fn dropshot_pagination_data(
|
fn dropshot_pagination_data(
|
||||||
|
@ -1241,6 +1227,8 @@ impl Generator {
|
||||||
|
|
||||||
pub fn dependencies(&self) -> Vec<String> {
|
pub fn dependencies(&self) -> Vec<String> {
|
||||||
let mut deps = vec![
|
let mut deps = vec![
|
||||||
|
"bytes = \"1.1.0\"",
|
||||||
|
"futures-core = \"0.3.21\"",
|
||||||
"percent-encoding = \"2.1\"",
|
"percent-encoding = \"2.1\"",
|
||||||
"serde = { version = \"1.0\", features = [\"derive\"] }",
|
"serde = { version = \"1.0\", features = [\"derive\"] }",
|
||||||
"reqwest = { version = \"0.11\", features = [\"json\", \"stream\"] }",
|
"reqwest = { version = \"0.11\", features = [\"json\", \"stream\"] }",
|
||||||
|
@ -1332,87 +1320,6 @@ trait ExtractJsonMediaType {
|
||||||
fn content_json(&self) -> Result<openapiv3::MediaType>;
|
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 {
|
impl ExtractJsonMediaType for openapiv3::RequestBody {
|
||||||
fn content_json(&self) -> Result<openapiv3::MediaType> {
|
fn content_json(&self) -> Result<openapiv3::MediaType> {
|
||||||
if self.content.len() != 1 {
|
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 {
|
pub mod types {
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
@ -281,7 +281,7 @@ impl Client {
|
||||||
&'a self,
|
&'a self,
|
||||||
task: &'a str,
|
task: &'a str,
|
||||||
output: &'a str,
|
output: &'a str,
|
||||||
) -> Result<reqwest::Response, Error<()>> {
|
) -> Result<ResponseValue<ByteStream>, Error<()>> {
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"{}/v1/tasks/{}/outputs/{}",
|
"{}/v1/tasks/{}/outputs/{}",
|
||||||
self.baseurl,
|
self.baseurl,
|
||||||
|
@ -292,7 +292,7 @@ impl Client {
|
||||||
let result = self.client.execute(request).await;
|
let result = self.client.execute(request).await;
|
||||||
let response = result?;
|
let response = result?;
|
||||||
match response.status().as_u16() {
|
match response.status().as_u16() {
|
||||||
200..=299 => Ok(response),
|
200..=299 => Ok(ResponseValue::stream(response)),
|
||||||
_ => Err(Error::UnexpectedResponse(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 {
|
pub mod types {
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[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 {
|
pub mod types {
|
||||||
use serde::{Deserialize, Serialize};
|
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."]
|
#[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>(
|
pub async fn spoof_login<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
body: &'a types::LoginParams,
|
body: &'a types::LoginParams,
|
||||||
) -> Result<ResponseValue<()>, Error<()>> {
|
) -> Result<ResponseValue<ByteStream>, Error<ByteStream>> {
|
||||||
let url = format!("{}/login", self.baseurl,);
|
let url = format!("{}/login", self.baseurl,);
|
||||||
let request = self.client.post(url).json(body).build()?;
|
let request = self.client.post(url).json(body).build()?;
|
||||||
let result = self.client.execute(request).await;
|
let result = self.client.execute(request).await;
|
||||||
let response = result?;
|
let response = result?;
|
||||||
match response.status().as_u16() {
|
match response.status().as_u16() {
|
||||||
200..=299 => Ok(ResponseValue::empty(response)),
|
200..=299 => Ok(ResponseValue::stream(response)),
|
||||||
_ => Err(Error::ErrorResponse(ResponseValue::empty(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 {
|
pub mod types {
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
#[doc = "Error information from a response."]
|
#[doc = "Error information from a response."]
|
||||||
|
|
|
@ -6,6 +6,8 @@ use dropshot::{
|
||||||
endpoint, ApiDescription, HttpError, HttpResponseUpdatedNoContent, Path,
|
endpoint, ApiDescription, HttpError, HttpResponseUpdatedNoContent, Path,
|
||||||
Query, RequestContext,
|
Query, RequestContext,
|
||||||
};
|
};
|
||||||
|
use http::Response;
|
||||||
|
use hyper::Body;
|
||||||
use openapiv3::OpenAPI;
|
use openapiv3::OpenAPI;
|
||||||
use progenitor_impl::Generator;
|
use progenitor_impl::Generator;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
|
@ -38,11 +40,11 @@ struct CursedQuery {
|
||||||
path = "/{ref}/{type}/{trait}",
|
path = "/{ref}/{type}/{trait}",
|
||||||
}]
|
}]
|
||||||
async fn renamed_parameters(
|
async fn renamed_parameters(
|
||||||
_rqctx: Arc<RequestContext<Vec<String>>>,
|
_rqctx: Arc<RequestContext<()>>,
|
||||||
_path: Path<CursedPath>,
|
_path: Path<CursedPath>,
|
||||||
_query: Query<CursedQuery>,
|
_query: Query<CursedQuery>,
|
||||||
) -> Result<HttpResponseUpdatedNoContent, HttpError> {
|
) -> Result<HttpResponseUpdatedNoContent, HttpError> {
|
||||||
unimplemented!();
|
unreachable!();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test parameters that conflict with Rust reserved words and therefore must
|
/// Test parameters that conflict with Rust reserved words and therefore must
|
||||||
|
@ -69,3 +71,36 @@ fn test_renamed_parameters() {
|
||||||
&output,
|
&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": {
|
"responses": {
|
||||||
"default": {
|
"default": {
|
||||||
"description": ""
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"*/*": {
|
||||||
|
"schema": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue