From 210e0550c4dadb42198d2833662ea0af82dbdf71 Mon Sep 17 00:00:00 2001 From: Juha Kukkonen Date: Tue, 28 Mar 2023 02:34:55 +0300 Subject: [PATCH 1/3] Refactor alises support on `ToSchema` derive Prior to this commit the implementation was not able to resolve nested generics within aliases. That lead scenarios where types with extensive use of lifetimes was not possible. This commit takes another approach on aliases support for `ToSchema` derive macro that provides generic schema types. Instead of trying to parse `Generics` manually we parse `syn::Type` instead that contains generics as is allowing complex generic arguments with lifetimes to be used. Fundamental difference is that we create `TypeTree` for alias and the implementor type. Then we compare generic arguments to the field arguments and replace matching occurrences. ```rust #[derive(ToSchema)] #[aliases(Paginated1<'b> = Paginated<'b, String>, Paginated2<'b> = Paginated<'b, Cow<'b, bool>>)] struct Paginated<'r, T> { pub total: usize, pub data: Vec, pub next: Option<&'r str>, pub prev: Option<&'r str>, } ``` One caveat with this approach is that the lifetimes now need to be also defined on the left side of the equals (=) mark. --- utoipa-gen/src/component.rs | 42 --------- utoipa-gen/src/component/schema.rs | 121 ++++++++++++------------- utoipa-gen/src/path/response/derive.rs | 4 +- utoipa-gen/tests/schema_derive_test.rs | 4 +- 4 files changed, 63 insertions(+), 108 deletions(-) diff --git a/utoipa-gen/src/component.rs b/utoipa-gen/src/component.rs index 6f9c45bf..86991b7e 100644 --- a/utoipa-gen/src/component.rs +++ b/utoipa-gen/src/component.rs @@ -295,48 +295,6 @@ impl<'t> TypeTree<'t> { is } - fn find_mut_by_ident(&mut self, ident: &'_ Ident) -> Option<&mut Self> { - let is = self - .path - .as_mut() - .map(|path| path.segments.iter().any(|segment| &segment.ident == ident)) - .unwrap_or(false); - - if is { - Some(self) - } else { - self.children.as_mut().and_then(|children| { - children - .iter_mut() - .find_map(|child| Self::find_mut_by_ident(child, ident)) - }) - } - } - - /// Update current [`TypeTree`] from given `ident`. - /// - /// It will update everything else except `children` for the `TypeTree`. This means that the - /// `TypeTree` will not be changed and will be traveled as before update. - fn update(&mut self, ident: Ident) { - let new_path = Path::from(ident); - - let segments = &new_path.segments; - let last_segment = segments - .last() - .expect("TypeTree::update path should have at least one segment"); - - let generic_type = Self::get_generic_type(last_segment); - let value_type = if SchemaType(&new_path).is_primitive() { - ValueType::Primitive - } else { - ValueType::Object - }; - - self.value_type = value_type; - self.generic_type = generic_type; - self.path = Some(Cow::Owned(new_path)); - } - /// `Object` virtual type is used when generic object is required in OpenAPI spec. Typically used /// with `value_type` attribute to hinder the actual type. pub fn is_object(&self) -> bool { diff --git a/utoipa-gen/src/component/schema.rs b/utoipa-gen/src/component/schema.rs index b37aca25..f67325ab 100644 --- a/utoipa-gen/src/component/schema.rs +++ b/utoipa-gen/src/component/schema.rs @@ -4,9 +4,9 @@ use proc_macro2::{Ident, Span, TokenStream}; use proc_macro_error::abort; use quote::{format_ident, quote, ToTokens}; use syn::{ - parse::Parse, punctuated::Punctuated, token::Comma, Attribute, Data, Field, Fields, - FieldsNamed, FieldsUnnamed, GenericParam, Generics, Lifetime, LifetimeParam, Path, - PathArguments, Token, Variant, Visibility, + parse::Parse, parse_quote, punctuated::Punctuated, token::Comma, Attribute, Data, Field, + Fields, FieldsNamed, FieldsUnnamed, GenericParam, Generics, Lifetime, LifetimeParam, Path, + PathArguments, Token, Type, Variant, Visibility, }; use crate::{ @@ -78,24 +78,36 @@ impl<'a> Schema<'a> { impl ToTokens for Schema<'_> { fn to_tokens(&self, tokens: &mut TokenStream) { let ident = self.ident; - let variant = SchemaVariant::new(self.data, self.attributes, ident, self.generics, None); + let variant = SchemaVariant::new( + self.data, + self.attributes, + ident, + self.generics, + None::>, + ); let (_, ty_generics, where_clause) = self.generics.split_for_impl(); let life = &Lifetime::new(Schema::TO_SCHEMA_LIFETIME, Span::call_site()); + let schema_ty: Type = parse_quote!(#ident #ty_generics); + let schema_children = &*TypeTree::from_type(&schema_ty).children.unwrap_or_default(); + let aliases = self.aliases.as_ref().map(|aliases| { let alias_schemas = aliases .iter() .map(|alias| { let name = &*alias.name; + let alias_type_tree = TypeTree::from_type(&alias.ty); let variant = SchemaVariant::new( self.data, self.attributes, ident, self.generics, - Some(alias), + alias_type_tree + .children + .map(|children| children.into_iter().zip(schema_children)), ); quote! { (#name, #variant.into()) } }) @@ -114,12 +126,14 @@ impl ToTokens for Schema<'_> { .map(|alias| { let name = quote::format_ident!("{}", alias.name); let ty = &alias.ty; - let (_, alias_type_generics, _) = &alias.generics.split_for_impl(); + let name_generics = &alias.generics.as_ref().map(|generics| { + let (impl_generics, _, _) = generics.split_for_impl(); + impl_generics + }); let vis = self.vis; - let name_generics = &alias.get_name_lifetime_generics(); quote! { - #vis type #name #name_generics = #ty #alias_type_generics; + #vis type #name #name_generics = #ty; } }) .collect::() @@ -164,12 +178,12 @@ enum SchemaVariant<'a> { } impl<'a> SchemaVariant<'a> { - pub fn new( + pub fn new, &'a TypeTree<'a>)>>( data: &'a Data, attributes: &'a [Attribute], ident: &'a Ident, generics: &'a Generics, - alias: Option<&'a AliasSchema>, + aliases: Option, ) -> SchemaVariant<'a> { match data { Data::Struct(content) => match &content.fields { @@ -203,7 +217,7 @@ impl<'a> SchemaVariant<'a> { fields: named, generics: Some(generics), schema_as, - alias, + aliases: aliases.map(|aliases| aliases.into_iter().collect()), }) } Fields::Unit => Self::Unit(UnitStructVariant), @@ -260,7 +274,7 @@ pub struct NamedStructSchema<'a> { pub features: Option>, pub rename_all: Option, pub generics: Option<&'a Generics>, - pub alias: Option<&'a AliasSchema>, + pub aliases: Option, &'a TypeTree<'a>)>>, pub schema_as: Option, } @@ -279,6 +293,26 @@ impl NamedStructSchema<'_> { yield_: impl FnOnce(NamedStructFieldOptions<'_>) -> R, ) -> R { let type_tree = &mut TypeTree::from_type(&field.ty); + if let Some(aliases) = &self.aliases { + if let Some(ref mut field_types) = type_tree.children { + for (new_generic, old_generic_matcher) in aliases.iter() { + if let Some(field_old_generic) = field_types + .iter() + .enumerate() + .find_map(|(index, field_ty)| { + if field_ty == *old_generic_matcher { + Some(index) + } else { + None + } + }) + .and_then(|index| field_types.get_mut(index)) + { + *field_old_generic = new_generic.clone(); + } + } + }; + } let mut field_features = field .attrs @@ -315,25 +349,6 @@ impl NamedStructSchema<'_> { _ => None, }); - if let Some((generic_types, alias)) = self.generics.zip(self.alias) { - generic_types - .type_params() - .enumerate() - .for_each(|(index, generic)| { - if let Some(generic_type) = type_tree.find_mut_by_ident(&generic.ident) { - generic_type.update( - alias - .generics - .type_params() - .nth(index) - .unwrap() - .ident - .clone(), - ); - }; - }) - } - let deprecated = super::get_deprecated(&field.attrs); let value_type = field_features .as_mut() @@ -953,7 +968,7 @@ impl ComplexEnum<'_> { features: Some(named_struct_features), fields: &named_fields.named, generics: None, - alias: None, + aliases: None, schema_as: None, }, }) @@ -1041,7 +1056,7 @@ impl ComplexEnum<'_> { features: Some(named_struct_features), fields: &named_fields.named, generics: None, - alias: None, + aliases: None, schema_as: None, } .to_token_stream() @@ -1109,7 +1124,7 @@ impl ComplexEnum<'_> { features: Some(named_struct_features), fields: &named_fields.named, generics: None, - alias: None, + aliases: None, schema_as: None, }; let title = title_features.first().map(ToTokens::to_token_stream); @@ -1261,7 +1276,7 @@ impl ComplexEnum<'_> { features: Some(named_struct_features), fields: &named_fields.named, generics: None, - alias: None, + aliases: None, schema_as: None, }; let title = title_features.first().map(ToTokens::to_token_stream); @@ -1494,42 +1509,24 @@ fn is_flatten(rule: &Option) -> bool { #[cfg_attr(feature = "debug", derive(Debug))] pub struct AliasSchema { pub name: String, - pub ty: Ident, - pub generics: Generics, -} - -impl AliasSchema { - fn get_name_lifetime_generics(&self) -> Option { - let lifetimes = self - .generics - .lifetimes() - .filter(|lifetime| lifetime.lifetime.ident != "'static") - .map(|lifetime| GenericParam::Lifetime(lifetime.clone())) - .collect::>(); - - if !lifetimes.is_empty() { - Some(Generics { - params: lifetimes, - ..Default::default() - }) - } else { - None - } - } + pub generics: Option, + pub ty: Type, } impl Parse for AliasSchema { fn parse(input: syn::parse::ParseStream) -> syn::Result { let name = input.parse::()?; - if input.peek(Token![<]) { - input.parse::()?; - } + let generics = if input.peek(Token![<]) { + Some(input.parse::()?) + } else { + None + }; input.parse::()?; Ok(Self { name: name.to_string(), - ty: input.parse::()?, - generics: input.parse()?, + generics, + ty: input.parse::()?, }) } } diff --git a/utoipa-gen/src/path/response/derive.rs b/utoipa-gen/src/path/response/derive.rs index 7c139feb..43adb486 100644 --- a/utoipa-gen/src/path/response/derive.rs +++ b/utoipa-gen/src/path/response/derive.rs @@ -300,7 +300,7 @@ impl NamedStructResponse<'_> { let inline_schema = NamedStructSchema { attributes, fields, - alias: None, + aliases: None, features: None, generics: None, rename_all: None, @@ -364,7 +364,7 @@ impl<'p> ToResponseNamedStructResponse<'p> { let ty = Self::to_type(ident); let inline_schema = NamedStructSchema { - alias: None, + aliases: None, fields, features: None, generics: None, diff --git a/utoipa-gen/tests/schema_derive_test.rs b/utoipa-gen/tests/schema_derive_test.rs index 44c6244c..0e3a804d 100644 --- a/utoipa-gen/tests/schema_derive_test.rs +++ b/utoipa-gen/tests/schema_derive_test.rs @@ -4023,7 +4023,7 @@ fn derive_schema_with_generics_and_lifetimes() { struct TResult; let value = api_doc_aliases! { - #[aliases(Paginated1<'b> = Paginated<'b, String>, Paginated2 = Paginated<'b, Value>)] + #[aliases(Paginated1<'b> = Paginated<'b, String>, Paginated2<'b> = Paginated<'b, Cow<'b, bool>>)] struct Paginated<'r, TResult> { pub total: usize, pub data: Vec, @@ -4072,7 +4072,7 @@ fn derive_schema_with_generics_and_lifetimes() { "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Value", + "type": "boolean" } }, "next": { From fe32c07e6d95ade1656589719ff3cf15669ce885 Mon Sep 17 00:00:00 2001 From: Juha Kukkonen Date: Tue, 28 Mar 2023 03:32:13 +0300 Subject: [PATCH 2/3] fixup! Refactor alises support on `ToSchema` derive --- utoipa-gen/src/component.rs | 18 ++++++++++++++++++ utoipa-gen/src/component/schema.rs | 21 ++++----------------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/utoipa-gen/src/component.rs b/utoipa-gen/src/component.rs index 86991b7e..e9284bf0 100644 --- a/utoipa-gen/src/component.rs +++ b/utoipa-gen/src/component.rs @@ -295,6 +295,24 @@ impl<'t> TypeTree<'t> { is } + fn find_mut(&mut self, type_tree: &TypeTree) -> Option<&mut Self> { + let is = self + .path + .as_mut() + .map(|p| matches!(&type_tree.path, Some(path) if path.as_ref() == p.as_ref())) + .unwrap_or(false); + + if is { + Some(self) + } else { + self.children.as_mut().and_then(|children| { + children + .iter_mut() + .find_map(|child| Self::find_mut(child, type_tree)) + }) + } + } + /// `Object` virtual type is used when generic object is required in OpenAPI spec. Typically used /// with `value_type` attribute to hinder the actual type. pub fn is_object(&self) -> bool { diff --git a/utoipa-gen/src/component/schema.rs b/utoipa-gen/src/component/schema.rs index f67325ab..a31fe958 100644 --- a/utoipa-gen/src/component/schema.rs +++ b/utoipa-gen/src/component/schema.rs @@ -294,24 +294,11 @@ impl NamedStructSchema<'_> { ) -> R { let type_tree = &mut TypeTree::from_type(&field.ty); if let Some(aliases) = &self.aliases { - if let Some(ref mut field_types) = type_tree.children { - for (new_generic, old_generic_matcher) in aliases.iter() { - if let Some(field_old_generic) = field_types - .iter() - .enumerate() - .find_map(|(index, field_ty)| { - if field_ty == *old_generic_matcher { - Some(index) - } else { - None - } - }) - .and_then(|index| field_types.get_mut(index)) - { - *field_old_generic = new_generic.clone(); - } + for (new_generic, old_generic_matcher) in aliases.iter() { + if let Some(generic_match) = type_tree.find_mut(old_generic_matcher) { + *generic_match = new_generic.clone(); } - }; + } } let mut field_features = field From e81c89a894b2c7e927b44d5f5571c20ef1bba815 Mon Sep 17 00:00:00 2001 From: Juha Kukkonen Date: Tue, 28 Mar 2023 22:47:16 +0300 Subject: [PATCH 3/3] Remove neccecity of left side lifetimes Remove need to define lifetimes on left side of equals (=) sign. ```rust #[aliases(Paginated1 = Paginated<'b, String>, Paginated2 = Paginated<'b, Cow<'b, bool>>)] ``` --- utoipa-gen/Cargo.toml | 2 +- utoipa-gen/src/component/schema.rs | 59 +++++++++++++++++++------- utoipa-gen/tests/schema_derive_test.rs | 2 +- 3 files changed, 46 insertions(+), 17 deletions(-) diff --git a/utoipa-gen/Cargo.toml b/utoipa-gen/Cargo.toml index 21c6c3bc..a6d1dbcf 100644 --- a/utoipa-gen/Cargo.toml +++ b/utoipa-gen/Cargo.toml @@ -14,7 +14,7 @@ proc-macro = true [dependencies] proc-macro2 = "1.0" -syn = { version = "2.0", features = ["full"] } +syn = { version = "2.0", features = ["full", "extra-traits"] } quote = "1.0" proc-macro-error = "1.0" regex = { version = "1.7", optional = true } diff --git a/utoipa-gen/src/component/schema.rs b/utoipa-gen/src/component/schema.rs index a31fe958..ab0517c6 100644 --- a/utoipa-gen/src/component/schema.rs +++ b/utoipa-gen/src/component/schema.rs @@ -4,9 +4,9 @@ use proc_macro2::{Ident, Span, TokenStream}; use proc_macro_error::abort; use quote::{format_ident, quote, ToTokens}; use syn::{ - parse::Parse, parse_quote, punctuated::Punctuated, token::Comma, Attribute, Data, Field, - Fields, FieldsNamed, FieldsUnnamed, GenericParam, Generics, Lifetime, LifetimeParam, Path, - PathArguments, Token, Type, Variant, Visibility, + parse::Parse, parse_quote, punctuated::Punctuated, spanned::Spanned, token::Comma, Attribute, + Data, Field, Fields, FieldsNamed, FieldsUnnamed, GenericArgument, GenericParam, Generics, + Lifetime, LifetimeParam, Path, PathArguments, Token, Type, Variant, Visibility, }; use crate::{ @@ -126,14 +126,17 @@ impl ToTokens for Schema<'_> { .map(|alias| { let name = quote::format_ident!("{}", alias.name); let ty = &alias.ty; - let name_generics = &alias.generics.as_ref().map(|generics| { - let (impl_generics, _, _) = generics.split_for_impl(); - impl_generics - }); let vis = self.vis; + let name_generics = alias.get_lifetimes().fold( + Punctuated::<&GenericArgument, Comma>::new(), + |mut acc, lifetime| { + acc.push(lifetime); + acc + }, + ); quote! { - #vis type #name #name_generics = #ty; + #vis type #name < #name_generics > = #ty; } }) .collect::() @@ -1496,23 +1499,49 @@ fn is_flatten(rule: &Option) -> bool { #[cfg_attr(feature = "debug", derive(Debug))] pub struct AliasSchema { pub name: String, - pub generics: Option, pub ty: Type, } +impl AliasSchema { + fn get_lifetimes(&self) -> impl Iterator { + fn lifetimes_from_type(ty: &Type) -> impl Iterator { + match ty { + Type::Path(type_path) => type_path + .path + .segments + .iter() + .flat_map(|segment| match &segment.arguments { + PathArguments::AngleBracketed(angle_bracketed_args) => { + Some(angle_bracketed_args.args.iter()) + } + _ => None, + }) + .flatten() + .flat_map(|arg| match arg { + GenericArgument::Type(type_argument) => { + lifetimes_from_type(type_argument).collect::>() + } + _ => vec![arg], + }) + .filter(|generic_arg| matches!(generic_arg, syn::GenericArgument::Lifetime(lifetime) if lifetime.ident != "'static")), + _ => abort!( + &ty.span(), + "AliasSchema `get_lifetimes` only supports syn::TypePath types" + ), + } + } + + lifetimes_from_type(&self.ty) + } +} + impl Parse for AliasSchema { fn parse(input: syn::parse::ParseStream) -> syn::Result { let name = input.parse::()?; - let generics = if input.peek(Token![<]) { - Some(input.parse::()?) - } else { - None - }; input.parse::()?; Ok(Self { name: name.to_string(), - generics, ty: input.parse::()?, }) } diff --git a/utoipa-gen/tests/schema_derive_test.rs b/utoipa-gen/tests/schema_derive_test.rs index 0e3a804d..2bdd5f9a 100644 --- a/utoipa-gen/tests/schema_derive_test.rs +++ b/utoipa-gen/tests/schema_derive_test.rs @@ -4023,7 +4023,7 @@ fn derive_schema_with_generics_and_lifetimes() { struct TResult; let value = api_doc_aliases! { - #[aliases(Paginated1<'b> = Paginated<'b, String>, Paginated2<'b> = Paginated<'b, Cow<'b, bool>>)] + #[aliases(Paginated1 = Paginated<'b, String>, Paginated2 = Paginated<'b, Cow<'c, bool>>)] struct Paginated<'r, TResult> { pub total: usize, pub data: Vec,