Add support for multiple response types in the generated client, including handling JSON deserializable types, empty bodies, streams of bytes, and upgraded connections. This change addresses the following issues: - Multiple response types not supported (#344) - Path to GitHub client (#395)
This commit is contained in:
parent
9323b82b88
commit
b6993ce02e
|
@ -78,6 +78,18 @@ impl<T: DeserializeOwned> ResponseValue<T> {
|
||||||
headers,
|
headers,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Transforms the inner data of this `ResponseValue` using a provided function, returning a new `ResponseValue` with the transformed data.
|
||||||
|
pub fn map_inner<U, F>(self, op: F) -> ResponseValue<U>
|
||||||
|
where
|
||||||
|
F: FnOnce(T) -> U,
|
||||||
|
{
|
||||||
|
ResponseValue {
|
||||||
|
inner: op(self.inner), // Apply the operation to the inner data
|
||||||
|
status: self.status, // Preserve the status
|
||||||
|
headers: self.headers, // Preserve the headers
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
|
|
@ -16,6 +16,7 @@ use crate::{
|
||||||
util::{sanitize, Case},
|
util::{sanitize, Case},
|
||||||
validate_openapi, Generator, Result,
|
validate_openapi, Generator, Result,
|
||||||
};
|
};
|
||||||
|
use crate::util::generate_multi_type_identifier;
|
||||||
|
|
||||||
struct MockOp {
|
struct MockOp {
|
||||||
when: TokenStream,
|
when: TokenStream,
|
||||||
|
@ -313,6 +314,18 @@ impl Generator {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
crate::method::OperationResponseKind::Multi(types) => {
|
||||||
|
let arg_type = generate_multi_type_identifier(types, &self.type_space);
|
||||||
|
(
|
||||||
|
quote! {
|
||||||
|
value: #arg_type,
|
||||||
|
},
|
||||||
|
quote! {
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.json_body_obj(value)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
crate::method::OperationResponseKind::None => {
|
crate::method::OperationResponseKind::None => {
|
||||||
Default::default()
|
Default::default()
|
||||||
}
|
}
|
||||||
|
|
|
@ -357,6 +357,7 @@ impl Generator {
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
let types = self.type_space.to_stream();
|
let types = self.type_space.to_stream();
|
||||||
|
let multi_types = self.generate_multi_types_stream(&raw_methods, &self.type_space);
|
||||||
|
|
||||||
// Generate an implementation of a `Self::as_inner` method, if an inner
|
// Generate an implementation of a `Self::as_inner` method, if an inner
|
||||||
// type is defined.
|
// type is defined.
|
||||||
|
@ -427,6 +428,7 @@ impl Generator {
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
#types
|
#types
|
||||||
|
#multi_types
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|
|
@ -11,11 +11,11 @@ use proc_macro2::TokenStream;
|
||||||
use quote::{format_ident, quote, ToTokens};
|
use quote::{format_ident, quote, ToTokens};
|
||||||
use typify::{TypeId, TypeSpace};
|
use typify::{TypeId, TypeSpace};
|
||||||
|
|
||||||
use crate::{
|
use crate::{Error, Generator, method, Result, TagStyle, template::PathTemplate, util::{
|
||||||
template::PathTemplate,
|
Case, generate_multi_type_for_types_stream, generate_multi_type_identifier, items, parameter_map,
|
||||||
util::{items, parameter_map, sanitize, unique_ident_from, Case},
|
sanitize,
|
||||||
Error, Generator, Result, TagStyle,
|
unique_ident_from,
|
||||||
};
|
}};
|
||||||
use crate::{to_schema::ToSchema, util::ReferenceOrExt};
|
use crate::{to_schema::ToSchema, util::ReferenceOrExt};
|
||||||
|
|
||||||
/// The intermediate representation of an operation that will become a method.
|
/// The intermediate representation of an operation that will become a method.
|
||||||
|
@ -262,6 +262,7 @@ pub(crate) enum OperationResponseKind {
|
||||||
None,
|
None,
|
||||||
Raw,
|
Raw,
|
||||||
Upgrade,
|
Upgrade,
|
||||||
|
Multi(Vec<Box<OperationResponseKind>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OperationResponseKind {
|
impl OperationResponseKind {
|
||||||
|
@ -280,6 +281,10 @@ impl OperationResponseKind {
|
||||||
OperationResponseKind::Upgrade => {
|
OperationResponseKind::Upgrade => {
|
||||||
quote! { reqwest::Upgraded }
|
quote! { reqwest::Upgraded }
|
||||||
}
|
}
|
||||||
|
OperationResponseKind::Multi(ref types) => {
|
||||||
|
let type_name = generate_multi_type_identifier(types, type_space);
|
||||||
|
quote! { types::#type_name }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1032,9 +1037,22 @@ impl Generator {
|
||||||
ResponseValue::upgrade(#response_ident).await
|
ResponseValue::upgrade(#response_ident).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
OperationResponseKind::Multi(_) => {
|
||||||
|
panic!("Shouldn't occur for the original response")
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
quote! { #pat => { #decode } }
|
match &response_type {
|
||||||
|
OperationResponseKind::Multi(types) => {
|
||||||
|
let multi_type_name = generate_multi_type_identifier(
|
||||||
|
&types,
|
||||||
|
&self.type_space,
|
||||||
|
);
|
||||||
|
let type_name = &response.typ.clone().into_tokens(&self.type_space);
|
||||||
|
quote! { #pat => { #decode.map(|v: ResponseValue<#type_name>| v.map_inner(types::#multi_type_name::from)) } }
|
||||||
|
}
|
||||||
|
_ => { quote! { #pat => { #decode } } }
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Errors...
|
// Errors...
|
||||||
|
@ -1095,9 +1113,22 @@ impl Generator {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
OperationResponseKind::Multi(_) => {
|
||||||
|
panic!("Shouldn't occur for the original response")
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
quote! { #pat => { #decode } }
|
match &response_type {
|
||||||
|
OperationResponseKind::Multi(types) => {
|
||||||
|
let multi_type_name = generate_multi_type_identifier(
|
||||||
|
&types,
|
||||||
|
&self.type_space,
|
||||||
|
);
|
||||||
|
let type_name = &response.typ.clone().into_tokens(&self.type_space);
|
||||||
|
quote! { #pat => { #decode.map(|v: ResponseValue<#type_name>| v.map_inner(types::#multi_type_name::from)) } }
|
||||||
|
}
|
||||||
|
_ => { quote! { #pat => { #decode } } }
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let accept_header = matches!(
|
let accept_header = matches!(
|
||||||
|
@ -1218,6 +1249,29 @@ impl Generator {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn generate_multi_types_stream(
|
||||||
|
&self,
|
||||||
|
input_methods: &[method::OperationMethod],
|
||||||
|
type_space: &TypeSpace) -> TokenStream {
|
||||||
|
let mut streams = Vec::new();
|
||||||
|
|
||||||
|
for method in input_methods {
|
||||||
|
let (success_response_items, response_type) = self.extract_responses(
|
||||||
|
method,
|
||||||
|
method::OperationResponseStatus::is_success_or_default,
|
||||||
|
);
|
||||||
|
|
||||||
|
if let OperationResponseKind::Multi(types) = response_type {
|
||||||
|
let multi_stream = generate_multi_type_for_types_stream(&types, type_space);
|
||||||
|
streams.push(multi_stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
#(#streams)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Extract responses that match criteria specified by the `filter`. The
|
/// Extract responses that match criteria specified by the `filter`. The
|
||||||
/// result is a `Vec<OperationResponse>` that enumerates the cases matching
|
/// result is a `Vec<OperationResponse>` that enumerates the cases matching
|
||||||
/// the filter, and a `TokenStream` that represents the generated type for
|
/// the filter, and a `TokenStream` that represents the generated type for
|
||||||
|
@ -1261,12 +1315,18 @@ impl Generator {
|
||||||
|
|
||||||
// TODO to deal with multiple response types, we'll need to create an
|
// TODO to deal with multiple response types, we'll need to create an
|
||||||
// enum type with variants for each of the response types.
|
// enum type with variants for each of the response types.
|
||||||
assert!(response_types.len() <= 1);
|
// assert!(response_types.len() <= 1);
|
||||||
let response_type = response_types
|
let response_type = if response_types.len() > 1 {
|
||||||
.into_iter()
|
OperationResponseKind::Multi(response_types.into_iter().map(Box::new).collect())
|
||||||
.next()
|
} else {
|
||||||
// TODO should this be OperationResponseType::Raw?
|
response_types
|
||||||
.unwrap_or(OperationResponseKind::None);
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
// TODO should this be OperationResponseType::Raw?
|
||||||
|
.unwrap_or(OperationResponseKind::None)
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
(response_items, response_type)
|
(response_items, response_type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,9 +6,13 @@ use indexmap::IndexMap;
|
||||||
use openapiv3::{
|
use openapiv3::{
|
||||||
Components, Parameter, ReferenceOr, RequestBody, Response, Schema,
|
Components, Parameter, ReferenceOr, RequestBody, Response, Schema,
|
||||||
};
|
};
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
use typify::TypeSpace;
|
||||||
use unicode_ident::{is_xid_continue, is_xid_start};
|
use unicode_ident::{is_xid_continue, is_xid_start};
|
||||||
|
|
||||||
use crate::Result;
|
use crate::method::OperationResponseKind;
|
||||||
|
use crate::{Result};
|
||||||
|
|
||||||
pub(crate) trait ReferenceOrExt<T: ComponentLookup> {
|
pub(crate) trait ReferenceOrExt<T: ComponentLookup> {
|
||||||
fn item<'a>(&'a self, components: &'a Option<Components>) -> Result<&'a T>;
|
fn item<'a>(&'a self, components: &'a Option<Components>) -> Result<&'a T>;
|
||||||
|
@ -142,3 +146,116 @@ pub(crate) fn unique_ident_from(
|
||||||
name.insert_str(0, "_");
|
name.insert_str(0, "_");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generates a unique identifier by concatenating the type identifiers for a collection of `OperationResponseKind`.
|
||||||
|
/// This function is used to dynamically create enum variant names or type combinations based on the types contained within a `Multi` response kind.
|
||||||
|
pub(crate) fn generate_multi_type_identifier(types: &Vec<Box<OperationResponseKind>>, type_space: &TypeSpace) -> TokenStream {
|
||||||
|
let identifiers: Vec<TokenStream> = types.iter()
|
||||||
|
.map(|type_kind| {
|
||||||
|
match type_kind.as_ref() {
|
||||||
|
OperationResponseKind::None => {
|
||||||
|
// Directly return a TokenStream representing 'None' if the type is None.
|
||||||
|
// This case handles the scenario where the generated tokens would have been ().
|
||||||
|
quote! { None }
|
||||||
|
}
|
||||||
|
OperationResponseKind::Upgrade => {
|
||||||
|
// Directly return a TokenStream representing 'Upgrade' if the type is Upgrade.
|
||||||
|
// This case handles the scenario where the generated tokens would have been reqwest::Upgraded.
|
||||||
|
quote! { Upgraded }
|
||||||
|
}
|
||||||
|
OperationResponseKind::Type(type_id) => {
|
||||||
|
// Directly use the Ident returned from TypeSpace, ensuring no invalid string manipulation
|
||||||
|
let type_name = format_ident!("{}", type_space.get_type(type_id).unwrap().ident().to_string().replace("types :: ", ""));
|
||||||
|
quote! { #type_name }
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Otherwise, generate tokens normally using the `into_tokens` method.
|
||||||
|
type_kind.clone().into_tokens(type_space)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Convert each TokenStream to string, concatenate them with "Or", and prepend with "types::"
|
||||||
|
let concatenated_type = identifiers.iter()
|
||||||
|
.map(|ts| ts.to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("Or");
|
||||||
|
|
||||||
|
// Parse the concatenated string back to a TokenStream to ensure that it can be used in code generation.
|
||||||
|
// This step assumes that the concatenated string is a valid Rust identifier or code.
|
||||||
|
let tokens = concatenated_type.parse::<TokenStream>().unwrap_or_else(|_| quote! { InvalidIdentifier });
|
||||||
|
quote! { #tokens } // Return the new identifier as a TokenStream
|
||||||
|
}
|
||||||
|
pub(crate) fn generate_multi_type_for_types_stream(types: &Vec<Box<OperationResponseKind>>, type_space: &TypeSpace) -> TokenStream {
|
||||||
|
let enum_name = generate_multi_type_identifier(types, type_space);
|
||||||
|
|
||||||
|
// Generate enum variants and their `From` implementations
|
||||||
|
let variants: Vec<TokenStream> = types.iter().map(|type_kind| {
|
||||||
|
match type_kind.as_ref() {
|
||||||
|
OperationResponseKind::None => {
|
||||||
|
quote! { None }
|
||||||
|
}
|
||||||
|
OperationResponseKind::Upgrade => {
|
||||||
|
quote! { Upgraded(reqwest::Upgraded) }
|
||||||
|
}
|
||||||
|
OperationResponseKind::Type(type_id) => {
|
||||||
|
let type_ident = type_space.get_type(type_id).unwrap().ident().to_string().replace("types :: ", "").parse::<TokenStream>().unwrap();
|
||||||
|
quote! { #type_ident(#type_ident) }
|
||||||
|
}
|
||||||
|
_ => quote! { Unknown },
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
let from_impls: Vec<TokenStream> = types.iter().map(|type_kind| {
|
||||||
|
match type_kind.as_ref() {
|
||||||
|
OperationResponseKind::None => {
|
||||||
|
quote! {
|
||||||
|
impl From<()> for #enum_name {
|
||||||
|
fn from(_: ()) -> Self {
|
||||||
|
#enum_name::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OperationResponseKind::Upgrade => {
|
||||||
|
quote! {
|
||||||
|
impl From<reqwest::Upgraded> for #enum_name {
|
||||||
|
fn from(value: reqwest::Upgraded) -> Self {
|
||||||
|
#enum_name::Upgraded(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OperationResponseKind::Type(type_id) => {
|
||||||
|
let type_ident = type_space.get_type(type_id).unwrap().ident().to_string().replace("types :: ", "").parse::<TokenStream>().unwrap();
|
||||||
|
quote! {
|
||||||
|
impl From<#type_ident> for #enum_name {
|
||||||
|
fn from(value: #type_ident) -> Self {
|
||||||
|
#enum_name::#type_ident(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
todo!() // Possibility of nested Multi types given openapi spec?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
let tokens = quote! {
|
||||||
|
// Define the enum
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum #enum_name {
|
||||||
|
#(#variants),*
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the From implementations
|
||||||
|
#(#from_impls)*
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("Tokens: {}", tokens);
|
||||||
|
|
||||||
|
tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue