Skip to content

Commit

Permalink
feat: support multiple response types (oxidecomputer#344, oxidecomput…
Browse files Browse the repository at this point in the history
…er#395)

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 (oxidecomputer#344)
- Path to GitHub client (oxidecomputer#395)
  • Loading branch information
geoffreygarrett committed Jul 13, 2024
1 parent 9323b82 commit b6993ce
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 14 deletions.
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
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
}

0 comments on commit b6993ce

Please sign in to comment.