Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support multiple response types #857

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
target/

.idea/
.env
progenitor-impl/tests/output/Cargo.lock
12 changes: 12 additions & 0 deletions progenitor-client/src/progenitor_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,18 @@ impl<T: DeserializeOwned> ResponseValue<T> {
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"))]
Expand Down
12 changes: 8 additions & 4 deletions progenitor-impl/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ impl Generator {
}
}
crate::method::OperationResponseKind::Raw
| crate::method::OperationResponseKind::Multi(_)
| crate::method::OperationResponseKind::Upgrade => {
quote! {
{
Expand All @@ -279,6 +280,7 @@ impl Generator {
}
}
crate::method::OperationResponseKind::Raw
| crate::method::OperationResponseKind::Multi(_)
| crate::method::OperationResponseKind::Upgrade => {
quote! {
{
Expand Down Expand Up @@ -306,6 +308,7 @@ impl Generator {
}
crate::method::OperationResponseKind::None => quote! { () },
crate::method::OperationResponseKind::Raw => todo!(),
crate::method::OperationResponseKind::Multi(_) => todo!(),
crate::method::OperationResponseKind::Upgrade => todo!(),
};
let error_output = match error_kind {
Expand All @@ -319,6 +322,7 @@ impl Generator {
}
}
crate::method::OperationResponseKind::Raw
| crate::method::OperationResponseKind::Multi(_)
| crate::method::OperationResponseKind::Upgrade => {
quote! {
{
Expand Down Expand Up @@ -512,10 +516,10 @@ impl Generator {
CliBodyArg::Required => Some(true),
CliBodyArg::Optional => Some(false),
})
.map(|required| {
let help = "Path to a file that contains the full json body.";
.map(|required| {
let help = "Path to a file that contains the full json body.";

quote! {
quote! {
.arg(
clap::Arg::new("json-body")
.long("json-body")
Expand All @@ -533,7 +537,7 @@ impl Generator {
.help("XXX")
)
}
});
});

let parser = quote! {
#(
Expand Down
13 changes: 13 additions & 0 deletions progenitor-impl/src/httpmock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use crate::{
util::{sanitize, Case},
validate_openapi, Generator, Result,
};
use crate::util::generate_multi_type_identifier;

struct MockOp {
when: TokenStream,
Expand Down Expand Up @@ -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 => {
Default::default()
}
Expand Down
2 changes: 2 additions & 0 deletions progenitor-impl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@ impl Generator {
}?;

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
// type is defined.
Expand Down Expand Up @@ -427,6 +428,7 @@ impl Generator {
use std::convert::TryFrom;

#types
#multi_types
}

#[derive(Clone, Debug)]
Expand Down
86 changes: 73 additions & 13 deletions progenitor-impl/src/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ use proc_macro2::TokenStream;
use quote::{format_ident, quote, ToTokens};
use typify::{TypeId, TypeSpace};

use crate::{
template::PathTemplate,
util::{items, parameter_map, sanitize, unique_ident_from, Case},
Error, Generator, Result, TagStyle,
};
use crate::{Error, Generator, method, Result, TagStyle, template::PathTemplate, util::{
Case, generate_multi_type_for_types_stream, generate_multi_type_identifier, items, parameter_map,
sanitize,
unique_ident_from,
}};
use crate::{to_schema::ToSchema, util::ReferenceOrExt};

/// The intermediate representation of an operation that will become a method.
Expand Down Expand Up @@ -262,6 +262,7 @@ pub(crate) enum OperationResponseKind {
None,
Raw,
Upgrade,
Multi(Vec<Box<OperationResponseKind>>),
}

impl OperationResponseKind {
Expand All @@ -280,6 +281,10 @@ impl OperationResponseKind {
OperationResponseKind::Upgrade => {
quote! { reqwest::Upgraded }
}
OperationResponseKind::Multi(ref types) => {
let type_name = generate_multi_type_identifier(types, type_space);
quote! { types::#type_name }
}
}
}
}
Expand Down Expand Up @@ -1032,9 +1037,22 @@ impl Generator {
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...
Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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
/// result is a `Vec<OperationResponse>` that enumerates the cases matching
/// the filter, and a `TokenStream` that represents the generated type for
Expand Down Expand Up @@ -1261,12 +1315,18 @@ impl Generator {

// 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
.into_iter()
.next()
// TODO should this be OperationResponseType::Raw?
.unwrap_or(OperationResponseKind::None);
// assert!(response_types.len() <= 1);
let response_type = if response_types.len() > 1 {
OperationResponseKind::Multi(response_types.into_iter().map(Box::new).collect())
} else {
response_types
.into_iter()
.next()
// TODO should this be OperationResponseType::Raw?
.unwrap_or(OperationResponseKind::None)
};


(response_items, response_type)
}

Expand Down
119 changes: 118 additions & 1 deletion progenitor-impl/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ use indexmap::IndexMap;
use openapiv3::{
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 crate::Result;
use crate::method::OperationResponseKind;
use crate::{Result};

pub(crate) trait ReferenceOrExt<T: ComponentLookup> {
fn item<'a>(&'a self, components: &'a Option<Components>) -> Result<&'a T>;
Expand Down Expand Up @@ -142,3 +146,116 @@ pub(crate) fn unique_ident_from(
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
}