Repository: juhaku/utoipa
Pull request #14 (2022-01-26 15:40:22)

Add generic support
Add generic support to components. -> Only concrete generic types with known size are supported. No support for trait generic types.


Patch:
diff --git a/utoipa-gen/src/component.rs b/utoipa-gen/src/component.rs
index 9606368..43716fc 100644
--- a/utoipa-gen/src/component.rs
+++ b/utoipa-gen/src/component.rs
@@ -5,7 +5,7 @@ use proc_macro_error::{abort, abort_call_site, emit_error};
 use quote::{quote, ToTokens};
 use syn::{
     punctuated::Punctuated, token::Comma, Attribute, Data, Field, Fields, FieldsNamed,
-    FieldsUnnamed, GenericArgument, PathArguments, PathSegment, Type, TypePath, Variant,
+    FieldsUnnamed, GenericArgument, Generics, PathArguments, PathSegment, Type, TypePath, Variant,
 };
 
 use crate::{
@@ -21,13 +21,20 @@ mod attr;
 pub struct Component<'a> {
     ident: &'a Ident,
     variant: ComponentVariant<'a>,
+    generics: &'a Generics,
 }
 
 impl<'a> Component<'a> {
-    pub fn new(data: &'a Data, attributes: &'a [Attribute], ident: &'a Ident) -> Self {
+    pub fn new(
+        data: &'a Data,
+        attributes: &'a [Attribute],
+        ident: &'a Ident,
+        generics: &'a Generics,
+    ) -> Self {
         Self {
             ident,
             variant: ComponentVariant::new(data, attributes, ident),
+            generics,
         }
     }
 }
@@ -36,9 +43,10 @@ impl ToTokens for Component<'_> {
     fn to_tokens(&self, tokens: &mut TokenStream2) {
         let ident = self.ident;
         let variant = &self.variant;
+        let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();
 
         tokens.extend(quote! {
-            impl utoipa::Component for #ident {
+            impl #impl_generics utoipa::Component for #ident #ty_generics #where_clause {
                 fn component() -> utoipa::openapi::schema::Component {
                     use utoipa::openapi::ToArray;
                     #variant.into()
diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs
index ad8b3f1..0a9f98a 100644
--- a/utoipa-gen/src/lib.rs
+++ b/utoipa-gen/src/lib.rs
@@ -34,15 +34,16 @@ use crate::path::{Path, PathAttr, PathOperation};
 /// Component dervice
 pub fn derive_component(input: TokenStream) -> TokenStream {
     let DeriveInput {
-        attrs, ident, data, ..
+        attrs,
+        ident,
+        data,
+        generics,
+        ..
     } = syn::parse_macro_input!(input);
 
-    let component = Component::new(&data, &attrs, &ident);
+    let component = Component::new(&data, &attrs, &ident, &generics);
 
-    quote! {
-        #component
-    }
-    .into()
+    component.to_token_stream().into()
 }
 
 #[proc_macro_error]
diff --git a/utoipa-gen/src/openapi.rs b/utoipa-gen/src/openapi.rs
index b52b5e6..af92ebe 100644
--- a/utoipa-gen/src/openapi.rs
+++ b/utoipa-gen/src/openapi.rs
@@ -4,7 +4,7 @@ use syn::{
     bracketed,
     parse::{Parse, ParseStream},
     punctuated::Punctuated,
-    Attribute, Error, ExprPath, Token,
+    Attribute, Error, ExprPath, Generics, Token,
 };
 
 use proc_macro2::TokenStream;
@@ -20,7 +20,13 @@ const PATH_STRUCT_PREFIX: &str = "__path_";
 #[cfg_attr(feature = "debug", derive(Debug))]
 pub struct OpenApiAttr {
     handlers: Vec<ExprPath>,
-    components: Vec<Ident>,
+    components: Vec<Component>,
+}
+
+#[cfg_attr(feature = "debug", derive(Debug))]
+struct Component {
+    ty: Ident,
+    generics: Generics,
 }
 
 pub fn parse_openapi_attributes_from_attributes(attrs: &[Attribute]) -> Option<OpenApiAttr> {
@@ -87,14 +93,28 @@ fn parse_handlers(input: ParseStream) -> syn::Result<Vec<ExprPath>> {
     })
 }
 
-fn parse_components(input: ParseStream) -> syn::Result<Vec<Ident>> {
+fn parse_components(input: ParseStream) -> syn::Result<Vec<Component>> {
     parse_utils::parse_next(input, || {
         if input.peek(syn::token::Bracket) {
             let content;
             bracketed!(content in input);
-            let tokens = Punctuated::<Ident, Token![,]>::parse_terminated(&content)?;
 
-            Ok(tokens.into_iter().collect::<Vec<_>>())
+            let mut components = Vec::new();
+            loop {
+                components.push(Component {
+                    ty: content.parse()?,
+                    generics: content.parse()?,
+                });
+
+                if content.peek(Token![,]) {
+                    content.parse::<Token![,]>()?;
+                }
+                if content.is_empty() {
+                    break;
+                }
+            }
+
+            Ok(components)
         } else {
             Err(syn::Error::new(
                 input.span(),
@@ -115,16 +135,18 @@ impl ToTokens for OpenApi {
         let schema = attributes.components.iter().fold(
             quote! { utoipa::openapi::Schema::new() },
             |mut schema, component| {
-                let span = component.span();
-                let component_name = &*component.to_string();
+                let ident = &component.ty;
+                let span = ident.span();
+                let component_name = &*ident.to_string();
+                let (_, ty_generics, _) = component.generics.split_for_impl();
 
                 let assert_component = format_ident!("_AssertComponent{}", component_name);
                 tokens.extend(quote_spanned! {span=>
-                    struct #assert_component where #component: utoipa::Component;
+                    struct #assert_component where #ident #ty_generics: utoipa::Component;
                 });
 
                 schema.extend(quote! {
-                    .with_component(#component_name, #component::component())
+                    .with_component(#component_name, <#ident #ty_generics>::component())
                 });
 
                 schema
@@ -136,7 +158,7 @@ impl ToTokens for OpenApi {
         tokens.extend(quote! {
             impl utoipa::OpenApi for #ident {
                 fn openapi() -> utoipa::openapi::OpenApi {
-                    use utoipa::Path;
+                    use utoipa::{Component, Path};
                     utoipa::openapi::OpenApi::new(#info, #path_items)
                         .with_components(#schema)
                 }


Test patch:
diff --git a/tests/component_derive_test.rs b/tests/component_derive_test.rs
index 1739131..00a5970 100644
--- a/tests/component_derive_test.rs
+++ b/tests/component_derive_test.rs
@@ -26,9 +26,18 @@ macro_rules! api_doc {
         api_doc!(@doc $name)
     }};
 
-    ( @doc $name:ident ) => {{
+    ( $( #[$attr:meta] )* $key:ident $name:ident<$generic:ident> $body:tt ) => {{
+        #[allow(dead_code)]
+        #[derive(Component)]
+        $(#[$attr])*
+        $key $name<$generic> $body
+
+        api_doc!(@doc $name <$generic> )
+    }};
+
+    ( @doc $name:ident $( $generic:tt )* ) => {{
         #[derive(OpenApi)]
-        #[openapi(components = [$name])]
+        #[openapi(components = [$name$($generic)*])]
         struct ApiDoc;
 
         let json = serde_json::to_value(ApiDoc::openapi()).unwrap();
@@ -411,3 +420,21 @@ fn derive_enum_with_deprecated() {
         "deprecated" = r#"true"#, "Mode deprecated"
     };
 }
+
+#[test]
+fn derive_struct_with_generics() {
+    #[allow(unused)]
+    enum Type {
+        Foo,
+        Bar,
+    }
+    let status = api_doc! {
+        struct Status<Type> {
+            t: Type
+        }
+    };
+
+    assert_value! {status=>
+        "properties.t.$ref" = r###""#/components/schemas/Type""###, "Status t field"
+    };
+}
diff --git a/tests/utoipa_gen_test.rs b/tests/utoipa_gen_test.rs
index 767839e..458fc24 100644
--- a/tests/utoipa_gen_test.rs
+++ b/tests/utoipa_gen_test.rs
@@ -10,6 +10,17 @@ struct Pet {
     age: Option<i32>,
 }
 
+#[derive(Component)]
+struct Status<StatusType> {
+    status: StatusType,
+}
+
+#[derive(Component)]
+enum StatusType {
+    Ok,
+    NotOk,
+}
+
 mod pet_api {
     use super::*;
 
@@ -38,7 +49,7 @@ mod pet_api {
 }
 
 #[derive(OpenApi, Default)]
-#[openapi(handlers = [pet_api::get_pet_by_id], components = [Pet])]
+#[openapi(handlers = [pet_api::get_pet_by_id], components = [Pet, StatusType, Status<StatusType>])]
 struct ApiDoc;
 
 #[test]