diff --git a/src/lib.rs b/src/lib.rs index 8f30ee1b..98f3a53e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -312,6 +312,10 @@ pub trait OpenApi { /// ``` pub trait Component { fn component() -> openapi::schema::Component; + + fn aliases() -> Vec<(&'static str, openapi::schema::Component)> { + Vec::new() + } } /// Trait for implementing OpenAPI PathItem object with path. diff --git a/src/openapi/schema.rs b/src/openapi/schema.rs index 96087990..c91d5d11 100644 --- a/src/openapi/schema.rs +++ b/src/openapi/schema.rs @@ -111,6 +111,39 @@ impl ComponentsBuilder { self } + /// Add [`Component`]s from iterator. + /// + /// # Examples + /// ```rust + /// # use utoipa::openapi::schema::{ComponentsBuilder, ObjectBuilder, + /// # PropertyBuilder, ComponentType}; + /// ComponentsBuilder::new().components_from_iter([( + /// "Pet", + /// ObjectBuilder::new() + /// .property( + /// "name", + /// PropertyBuilder::new().component_type(ComponentType::String), + /// ) + /// .required("name"), + /// )]); + /// ``` + pub fn components_from_iter< + I: IntoIterator, + C: Into, + S: Into, + >( + mut self, + components: I, + ) -> Self { + self.schemas.extend( + components + .into_iter() + .map(|(name, component)| (name.into(), component.into())), + ); + + self + } + /// Add [`SecurityScheme`] to [`Components`]. /// /// Accepts two arguments where first is the name of the [`SecurityScheme`]. This is later when diff --git a/tests/component_derive_test.rs b/tests/component_derive_test.rs index 4be45264..da3c84f7 100644 --- a/tests/component_derive_test.rs +++ b/tests/component_derive_test.rs @@ -886,3 +886,27 @@ fn derive_component_with_generic_types_having_path_expression() { "properties.args.type" = r#""array""#, "Args type" } } + +#[test] +fn derive_component_with_aliases() { + struct A; + + #[derive(Debug, OpenApi)] + #[openapi(components(MyAlias))] + struct ApiDoc; + + #[derive(Component)] + #[aliases(MyAlias = Bar)] + struct Bar { + #[allow(dead_code)] + bar: R, + } + + let doc = ApiDoc::openapi(); + let doc_value = &serde_json::to_value(doc).unwrap(); + + let value = common::get_json_path(doc_value, "components.schemas"); + assert_value! {value=> + "MyAlias.properties.bar.$ref" = r###""#/components/schemas/A""###, "MyAlias aliased property" + } +} diff --git a/tests/utoipa_gen_test.rs b/tests/utoipa_gen_test.rs index 47dc3503..fa855fe9 100644 --- a/tests/utoipa_gen_test.rs +++ b/tests/utoipa_gen_test.rs @@ -79,7 +79,7 @@ mod pet_api { #[derive(Default, OpenApi)] #[openapi( handlers(pet_api::get_pet_by_id), - components(Pet), + components(Pet, GenericC, GenericD), modifiers(&Foo), security( (), @@ -99,6 +99,23 @@ macro_rules! build_foo { }; } +#[derive(Deserialize, Serialize, Component)] +struct A { + a: String, +} + +#[derive(Deserialize, Serialize, Component)] +struct B { + b: i64, +} + +#[derive(Deserialize, Serialize, Component)] +#[aliases(GenericC = C, GenericD = C)] +struct C { + field_1: R, + field_2: T, +} + #[test] #[ignore = "this is just a test bed to run macros"] fn derive_openapi() { diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs index 7bd78345..42d6a65c 100644 --- a/utoipa-gen/src/lib.rs +++ b/utoipa-gen/src/lib.rs @@ -43,7 +43,7 @@ use crate::path::{Path, PathAttr}; use ext::ArgumentResolver; #[proc_macro_error] -#[proc_macro_derive(Component, attributes(component))] +#[proc_macro_derive(Component, attributes(component, aliases))] /// Component derive macro /// /// This is `#[derive]` implementation for [`Component`][c] trait. The macro accepts one `component` @@ -58,18 +58,18 @@ use ext::ArgumentResolver; /// OpenAPI. OpenAPI has only a boolean flag to determine deprecation. While it is totally okay to declare deprecated with reason /// `#[deprecated = "There is better way to do this"]` the reason would not render in OpenAPI spec. /// -/// # Struct Optional Configuration Options +/// # Struct Optional Configuration Options for `#[component(...)]` /// * `example = ...` Can be either _`json!(...)`_ or literal string that can be parsed to json. _`json!`_ /// should be something that _`serde_json::json!`_ can parse as a _`serde_json::Value`_. [^json] /// * `xml(...)` Can be used to define [`Xml`][xml] object properties applicable to Structs. /// /// [^json]: **json** feature need to be enabled for _`json!(...)`_ type to work. /// -/// # Enum Optional Configuration Options +/// # Enum Optional Configuration Options for `#[component(...)]` /// * `example = ...` Can be literal value, method reference or _`json!(...)`_. [^json2] /// * `default = ...` Can be literal value, method reference or _`json!(...)`_. [^json2] /// -/// # Unnamed Field Struct Optional Configuration Options +/// # Unnamed Field Struct Optional Configuration Options for `#[component(...)]` /// * `example = ...` Can be literal value, method reference or _`json!(...)`_. [^json2] /// * `default = ...` Can be literal value, method reference or _`json!(...)`_. [^json2] /// * `format = ...` [`ComponentFormat`][format] to use for the property. By default the format is derived from @@ -79,7 +79,7 @@ use ext::ArgumentResolver; /// any third-party types are used which are not components nor primitive types. With **value_type** we can enforce /// type used to certain type. Value type may only be [`primitive`][primitive] type or [`String`]. Generic types are not allowed. /// -/// # Named Fields Optional Configuration Options +/// # Named Fields Optional Configuration Options for `#[component(...)]` /// * `example = ...` Can be literal value, method reference or _`json!(...)`_. [^json2] /// * `default = ...` Can be literal value, method reference or _`json!(...)`_. [^json2] /// * `format = ...` [`ComponentFormat`][format] to use for the property. By default the format is derived from @@ -139,6 +139,35 @@ use ext::ArgumentResolver; /// } /// ``` /// +/// # Generic components with aliases +/// +/// Components can also be generic which allows reusing types. This enables certain behaviour patters +/// where super type delcares common code for type aliases. +/// +/// In this example we have common `Status` type which accepts one generic type. It is then defined +/// with `#[aliases(...)]` that it is going to be used with [`std::string::String`] and [`i32`] values. +/// The generic argument could also be another [`Component`][c] as well. +/// ```rust +/// # use utoipa::{Component, OpenApi}; +/// #[derive(Component)] +/// #[aliases(StatusMessage = Status, StatusNumber = Status)] +/// struct Status { +/// value: T +/// } +/// +/// #[derive(OpenApi)] +/// #[openapi( +/// components(StatusMessage, StatusNumber) +/// )] +/// struct ApiDoc; +/// ``` +/// +/// The `#[aliases(...)]` is just syntatic sugar and will create Rust [type aliases](https://doc.rust-lang.org/reference/items/type-aliases.html) +/// behind the scenes which then can be later referenced anywhere in code. +/// +/// **Note!** You should never register generic type itself in `components(...)` so according above example `Status<...>` should not be registered +/// because it will not render the type correctly and will cause an error in generated OpenAPI spec. +/// /// # Examples /// /// Example struct with struct level example. @@ -269,10 +298,10 @@ pub fn derive_component(input: TokenStream) -> TokenStream { ident, data, generics, - .. + vis, } = syn::parse_macro_input!(input); - let component = Component::new(&data, &attrs, &ident, &generics); + let component = Component::new(&data, &attrs, &ident, &generics, &vis); component.to_token_stream().into() } diff --git a/utoipa-gen/src/openapi.rs b/utoipa-gen/src/openapi.rs index c961e8e3..04ecb562 100644 --- a/utoipa-gen/src/openapi.rs +++ b/utoipa-gen/src/openapi.rs @@ -294,6 +294,7 @@ fn impl_components( }; schema.extend(quote! { .component(#component_name, <#ident #ty_generics>::component()) + .components_from_iter(<#ident #ty_generics>::aliases()) }); schema diff --git a/utoipa-gen/src/schema.rs b/utoipa-gen/src/schema.rs index 5a134c97..7d38d12e 100644 --- a/utoipa-gen/src/schema.rs +++ b/utoipa-gen/src/schema.rs @@ -1,5 +1,3 @@ -use std::rc::Rc; - use proc_macro2::Ident; use proc_macro_error::{abort, abort_call_site}; use syn::{ @@ -33,7 +31,7 @@ struct ComponentPart<'a> { pub ident: &'a Ident, pub value_type: ValueType, pub generic_type: Option, - pub child: Option>>, + pub child: Option>>, } impl<'a> ComponentPart<'a> { @@ -105,7 +103,7 @@ impl<'a> ComponentPart<'a> { let mut generic_component_type = ComponentPart::convert(&segment.ident, segment); - generic_component_type.child = Some(Rc::new(ComponentPart::from_type( + generic_component_type.child = Some(Box::new(ComponentPart::from_type( match &segment.arguments { PathArguments::AngleBracketed(angle_bracketed_args) => { ComponentPart::get_generic_arg_type(0, angle_bracketed_args) @@ -161,6 +159,36 @@ impl<'a> ComponentPart<'a> { _ => None, } } + + fn find_mut_by_ident(&mut self, ident: &'a Ident) -> Option<&mut Self> { + match self.generic_type { + Some(GenericType::Map) => None, + Some(GenericType::Vec) + | Some(GenericType::Option) + | Some(GenericType::Cow) + | Some(GenericType::Box) + | Some(GenericType::RefCell) => { + Self::find_mut_by_ident(self.child.as_mut().unwrap().as_mut(), ident) + } + None => { + if ident == self.ident { + Some(self) + } else { + None + } + } + } + } + + fn update_ident(&mut self, ident: &'a Ident) { + self.ident = ident + } +} + +impl<'a> AsMut> for ComponentPart<'a> { + fn as_mut(&mut self) -> &mut ComponentPart<'a> { + self + } } #[cfg_attr(feature = "debug", derive(Debug))] diff --git a/utoipa-gen/src/schema/component.rs b/utoipa-gen/src/schema/component.rs index 8211cbc5..b08541f9 100644 --- a/utoipa-gen/src/schema/component.rs +++ b/utoipa-gen/src/schema/component.rs @@ -1,11 +1,11 @@ use std::mem; use proc_macro2::{Ident, TokenStream as TokenStream2}; -use proc_macro_error::abort; +use proc_macro_error::{abort, ResultExt}; use quote::{quote, ToTokens}; use syn::{ - punctuated::Punctuated, token::Comma, Attribute, Data, Field, Fields, FieldsNamed, - FieldsUnnamed, Generics, Variant, + parse::Parse, punctuated::Punctuated, token::Comma, Attribute, Data, Field, Fields, + FieldsNamed, FieldsUnnamed, Generics, Token, Variant, Visibility, }; use crate::{ @@ -29,8 +29,11 @@ mod xml; pub struct Component<'a> { ident: &'a Ident, - variant: ComponentVariant<'a>, + attributes: &'a [Attribute], generics: &'a Generics, + aliases: Option>, + data: &'a Data, + vis: &'a Visibility, } impl<'a> Component<'a> { @@ -39,11 +42,21 @@ impl<'a> Component<'a> { attributes: &'a [Attribute], ident: &'a Ident, generics: &'a Generics, + vis: &'a Visibility, ) -> Self { + let aliases = if generics.type_params().count() > 0 { + parse_aliases(attributes) + } else { + None + }; + Self { + data, ident, - variant: ComponentVariant::new(data, attributes, ident), + attributes, generics, + aliases, + vis, } } } @@ -51,15 +64,63 @@ impl<'a> Component<'a> { impl ToTokens for Component<'_> { fn to_tokens(&self, tokens: &mut TokenStream2) { let ident = self.ident; - let variant = &self.variant; + let variant = ComponentVariant::new(self.data, self.attributes, ident, self.generics, None); let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl(); + let aliases = self.aliases.as_ref().map(|aliases| { + let alias_components = aliases + .iter() + .map(|alias| { + let name = &*alias.name; + + let variant = ComponentVariant::new( + self.data, + self.attributes, + ident, + self.generics, + Some(alias), + ); + quote! { (#name, #variant.into()) } + }) + .collect::>(); + + quote! { + fn aliases() -> Vec<(&'static str, utoipa::openapi::schema::Component)> { + #alias_components.to_vec() + } + } + }); + + let type_aliases = self.aliases.as_ref().map(|aliases| { + aliases + .iter() + .map(|alias| { + let name = quote::format_ident!("{}", alias.name); + let ty = &alias.ty; + let (_, alias_type_generics, _) = &alias.generics.split_for_impl(); + let vis = self.vis; + + quote! { + #vis type #name = #ty #alias_type_generics; + } + }) + .fold(quote! {}, |mut tokens, alias| { + tokens.extend(alias); + + tokens + }) + }); + tokens.extend(quote! { impl #impl_generics utoipa::Component for #ident #ty_generics #where_clause { fn component() -> utoipa::openapi::schema::Component { #variant.into() } + + #aliases } + + #type_aliases }) } } @@ -75,6 +136,8 @@ impl<'a> ComponentVariant<'a> { data: &'a Data, attributes: &'a [Attribute], ident: &'a Ident, + generics: &'a Generics, + alias: Option<&'a AliasComponent>, ) -> ComponentVariant<'a> { match data { Data::Struct(content) => match &content.fields { @@ -90,6 +153,8 @@ impl<'a> ComponentVariant<'a> { Self::Named(NamedStructComponent { attributes, fields: named, + generics: Some(generics), + alias, }) } Fields::Unit => abort!( @@ -123,6 +188,8 @@ impl ToTokens for ComponentVariant<'_> { struct NamedStructComponent<'a> { fields: &'a Punctuated, attributes: &'a [Attribute], + generics: Option<&'a Generics>, + alias: Option<&'a AliasComponent>, } impl ToTokens for NamedStructComponent<'_> { @@ -147,7 +214,23 @@ impl ToTokens for NamedStructComponent<'_> { let name = &rename_field(&mut container_rules, &mut field_rule, field_name) .unwrap_or_else(|| String::from(field_name)); - let component_part = &ComponentPart::from_type(&field.ty); + let component_part = &mut ComponentPart::from_type(&field.ty); + + 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) = + component_part.find_mut_by_ident(&generic.ident) + { + generic_type.update_ident( + &alias.generics.type_params().nth(index).unwrap().ident, + ); + }; + }) + } + let deprecated = super::get_deprecated(&field.attrs); let attrs = ComponentAttr::::from_attributes_validated( &field.attrs, @@ -396,6 +479,8 @@ impl ToTokens for ComplexEnum<'_> { let named_enum = NamedStructComponent { attributes: &variant.attrs, fields: &named_fields.named, + generics: None, + alias: None, }; let name = &*variant.ident.to_string(); @@ -506,7 +591,7 @@ where } Some(GenericType::Vec) => { let component_property = ComponentProperty::new( - self.component_part.child.as_ref().unwrap(), + self.component_part.child.as_ref().unwrap().as_ref(), self.comments, self.attrs, self.deprecated, @@ -536,7 +621,7 @@ where | Some(GenericType::Box) | Some(GenericType::RefCell) => { let component_property = ComponentProperty::new( - self.component_part.child.as_ref().unwrap(), + self.component_part.child.as_ref().unwrap().as_ref(), self.comments, self.attrs, self.deprecated, @@ -647,3 +732,34 @@ fn rename<'a>( .and_then(rename) .or_else(|| container_rule.as_mut().and_then(rename)) } + +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct AliasComponent { + pub name: String, + pub ty: Ident, + pub generics: Generics, +} + +impl Parse for AliasComponent { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let name = input.parse::()?; + input.parse::()?; + + Ok(Self { + name: name.to_string(), + ty: input.parse::()?, + generics: input.parse()?, + }) + } +} + +fn parse_aliases(attributes: &[Attribute]) -> Option> { + attributes + .iter() + .find(|attribute| attribute.path.is_ident("aliases")) + .map(|aliases| { + aliases + .parse_args_with(Punctuated::::parse_terminated) + .unwrap_or_abort() + }) +} diff --git a/utoipa-gen/src/schema/into_params.rs b/utoipa-gen/src/schema/into_params.rs index b769a08d..118d9fe7 100644 --- a/utoipa-gen/src/schema/into_params.rs +++ b/utoipa-gen/src/schema/into_params.rs @@ -124,7 +124,7 @@ impl ToTokens for ParamType<'_> { let ty = self.0; match &ty.generic_type { Some(GenericType::Vec) => { - let param_type = ParamType(ty.child.as_ref().unwrap()); + let param_type = ParamType(ty.child.as_ref().unwrap().as_ref()); tokens.extend(quote! { #param_type.to_array_builder() }); } @@ -154,7 +154,7 @@ impl ToTokens for ParamType<'_> { | Some(GenericType::Cow) | Some(GenericType::Box) | Some(GenericType::RefCell) => { - let param_type = ParamType(ty.child.as_ref().unwrap()); + let param_type = ParamType(ty.child.as_ref().unwrap().as_ref()); tokens.extend(param_type.into_token_stream()) }