Repository: juhaku/utoipa
Pull request #16 (2022-01-29 22:44:41)

Feature enhanced enums
* Add new OpenAPI type for Schema (oneOf)
* Add support for complex enums in a form of oneOf type. Currently supports only externally tagged serde format which is the default
* Map unit field type in complex enum as a string type with one enum value with the name of variant. This way it makes it a "constant string value"
* Add tests for the case


Patch:
diff --git a/src/openapi/mod.rs b/src/openapi/mod.rs
index c42f336..24e6413 100644
--- a/src/openapi/mod.rs
+++ b/src/openapi/mod.rs
@@ -14,7 +14,8 @@ pub use self::{
     path::{PathItem, PathItemType, Paths},
     response::{Response, Responses},
     schema::{
-        Array, Component, ComponentFormat, ComponentType, Object, Property, Ref, Schema, ToArray,
+        Array, Component, ComponentFormat, ComponentType, Object, OneOf, Property, Ref, Schema,
+        ToArray,
     },
     security::Security,
     server::Server,
diff --git a/src/openapi/schema.rs b/src/openapi/schema.rs
index c4280ed..3ac714d 100644
--- a/src/openapi/schema.rs
+++ b/src/openapi/schema.rs
@@ -46,6 +46,43 @@ pub struct Component {
 
     #[serde(flatten, skip_serializing_if = "Option::is_none")]
     array_component: Option<Array>,
+
+    #[serde(skip_serializing_if = "Vec::is_empty")]
+    one_of: Vec<Component>,
+}
+
+#[derive(Default)]
+pub struct OneOf {
+    items: Vec<Component>,
+}
+
+impl OneOf {
+    pub fn new() -> Self {
+        Self {
+            ..Default::default()
+        }
+    }
+
+    pub fn with_capacity(capacity: usize) -> Self {
+        Self {
+            items: Vec::with_capacity(capacity),
+        }
+    }
+
+    pub fn append<I: Into<Component>>(mut self, component: I) -> Self {
+        self.items.push(component.into());
+
+        self
+    }
+}
+
+impl From<OneOf> for Component {
+    fn from(one_of: OneOf) -> Self {
+        Self {
+            one_of: one_of.items,
+            ..Default::default()
+        }
+    }
 }
 
 #[derive(Default, Serialize, Deserialize, Clone)]
diff --git a/utoipa-gen/Cargo.toml b/utoipa-gen/Cargo.toml
index 58b5c95..5d3bde9 100644
--- a/utoipa-gen/Cargo.toml
+++ b/utoipa-gen/Cargo.toml
@@ -18,5 +18,5 @@ regex = { version = "1.5.4", optional = true }
 lazy_static = { version = "1.4.0", optional = true }
 
 [features]
-debug = []
+debug = ["syn/extra-traits"]
 actix_extras = ["regex", "lazy_static"]
diff --git a/utoipa-gen/src/component.rs b/utoipa-gen/src/component.rs
index ca19924..8f88ecd 100644
--- a/utoipa-gen/src/component.rs
+++ b/utoipa-gen/src/component.rs
@@ -1,7 +1,7 @@
 use std::rc::Rc;
 
 use proc_macro2::{Ident, TokenStream as TokenStream2};
-use proc_macro_error::{abort, abort_call_site, emit_error};
+use proc_macro_error::{abort, abort_call_site};
 use quote::{quote, ToTokens};
 use syn::{
     punctuated::Punctuated, token::Comma, AngleBracketedGenericArguments, Attribute, Data, Field,
@@ -46,6 +46,7 @@ impl ToTokens for Component<'_> {
         let variant = &self.variant;
         let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();
 
+        // TODO implement sub components for complex enum variants
         tokens.extend(quote! {
             impl #impl_generics utoipa::Component for #ident #ty_generics #where_clause {
                 fn component() -> utoipa::openapi::schema::Component {
@@ -236,11 +237,38 @@ struct EnumComponent<'a> {
 
 impl ToTokens for EnumComponent<'_> {
     fn to_tokens(&self, tokens: &mut TokenStream2) {
-        self.variants
+        if self
+            .variants
             .iter()
-            .filter(|variant| !matches!(variant.fields, Fields::Unit))
-            .for_each(|variant| emit_error!(variant.ident.span(), "Currently unsupported enum variant, expected Unit variant without additional fields"));
+            .all(|variant| matches!(variant.fields, Fields::Unit))
+        {
+            tokens.extend(
+                SimpleEnum {
+                    attributes: self.attributes,
+                    variants: self.variants,
+                }
+                .to_token_stream(),
+            )
+        } else {
+            tokens.extend(
+                ComplexEnum {
+                    attributes: self.attributes,
+                    variants: self.variants,
+                }
+                .to_token_stream(),
+            )
+        };
+    }
+}
+
+#[cfg_attr(feature = "debug", derive(Debug))]
+struct SimpleEnum<'a> {
+    variants: &'a Punctuated<Variant, Comma>,
+    attributes: &'a [Attribute],
+}
 
+impl ToTokens for SimpleEnum<'_> {
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
         let enum_values = self
             .variants
             .iter()
@@ -250,7 +278,7 @@ impl ToTokens for EnumComponent<'_> {
 
         tokens.extend(quote! {
             utoipa::openapi::Property::new(utoipa::openapi::ComponentType::String)
-                .with_enum_values(#enum_values)
+            .with_enum_values(#enum_values)
         });
 
         let attrs = attr::parse_component_attr::<ComponentAttr<Enum>>(self.attributes);
@@ -273,6 +301,78 @@ impl ToTokens for EnumComponent<'_> {
     }
 }
 
+struct ComplexEnum<'a> {
+    variants: &'a Punctuated<Variant, Comma>,
+    attributes: &'a [Attribute],
+}
+
+impl ToTokens for ComplexEnum<'_> {
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
+        if self
+            .attributes
+            .iter()
+            .any(|attribute| attribute.path.get_ident().unwrap() == "component")
+        {
+            abort!(
+                self.attributes.first().unwrap(),
+                "component macro attribute not expected on complex enum";
+
+                help = "Try adding the #[component(...)] on variant of the enum";
+            );
+        }
+
+        let capasity = self.variants.len();
+        tokens.extend(quote! {
+            utoipa::openapi::OneOf::with_capacity(#capasity)
+        });
+
+        // serde, externally tagged format supported by now
+        self.variants
+            .iter()
+            .map(|variant| match &variant.fields {
+                Fields::Named(named_fields) => {
+                    let named_enum = NamedStructComponent {
+                        attributes: &variant.attrs,
+                        fields: &named_fields.named,
+                    };
+                    let name = &*variant.ident.to_string();
+
+                    quote! {
+                        utoipa::openapi::schema::Object::new()
+                            .with_property(#name, #named_enum)
+                    }
+                }
+                Fields::Unnamed(unnamed_fields) => {
+                    let unnamed_enum = UnnamedStructComponent {
+                        attributes: &variant.attrs,
+                        fields: &unnamed_fields.unnamed,
+                    };
+                    let name = &*variant.ident.to_string();
+
+                    quote! {
+                        utoipa::openapi::schema::Object::new()
+                            .with_property(#name, #unnamed_enum)
+                    }
+                }
+                Fields::Unit => {
+                    let mut enum_values = Punctuated::<Variant, Comma>::new();
+                    enum_values.push(variant.clone());
+
+                    SimpleEnum {
+                        attributes: &variant.attrs,
+                        variants: &enum_values,
+                    }
+                    .to_token_stream()
+                }
+            })
+            .for_each(|inline_variant| {
+                tokens.extend(quote! {
+                    .append(#inline_variant)
+                })
+            })
+    }
+}
+
 fn get_deprecated(attributes: &[Attribute]) -> Option<Deprecated> {
     attributes.iter().find_map(|attribute| {
         if *attribute.path.get_ident().unwrap() == "deprecated" {


Test patch:
diff --git a/tests/component_derive_test.rs b/tests/component_derive_test.rs
index d20ea14..563a76c 100644
--- a/tests/component_derive_test.rs
+++ b/tests/component_derive_test.rs
@@ -399,8 +399,11 @@ fn derive_unnamed_struct_example_json_array_success() {
     };
 
     assert_value! {pet_age=>
+        "type" = r#""array""#, "PetAge type"
         "items.example" = r#""0""#, "PetAge example"
         "items.default" = r#"0"#, "PetAge default"
+        "items.type" = r#""integer""#, "PetAge default"
+        "items.format" = r#""int64""#, "PetAge default"
     }
 }
 
@@ -491,3 +494,30 @@ fn derive_with_box_and_refcell() {
         "required.[1]" = r###""ref_cell_foo""###, "Greeting required 1"
     };
 }
+
+#[test]
+fn derive_complex_enum_with_named_and_unnamed_fields() {
+    struct Foo;
+    let complex_enum = api_doc! {
+        enum Bar {
+            UnitValue,
+            NamedFields {
+                id: &'static str,
+                names: Option<Vec<String>>
+            },
+            UnnamedFields(Foo),
+        }
+    };
+
+    common::assert_json_array_len(complex_enum.get("oneOf").unwrap(), 3);
+    assert_value! {complex_enum=>
+        "oneOf.[0].type" = r###""string""###, "Complex enum unit value type"
+        "oneOf.[0].enum" = r###"["UnitValue"]"###, "Complex enum unit value enum"
+        "oneOf.[1].type" = r###""object""###, "Complex enum named fields type"
+        "oneOf.[1].properties.NamedFields.type" = r###""object""###, "Complex enum named fields object type"
+        "oneOf.[1].properties.NamedFields.properties.id.type" = r###""string""###, "Complex enum named fields id type"
+        "oneOf.[1].properties.NamedFields.properties.names.type" = r###""array""###, "Complex enum named fields names type"
+        "oneOf.[2].type" = r###""object""###, "Complex enum unnamed fields type"
+        "oneOf.[2].properties.UnnamedFields.$ref" = r###""#/components/schemas/Foo""###, "Complex enum unnamed fields type"
+    }
+}
diff --git a/tests/utoipa_gen_test.rs b/tests/utoipa_gen_test.rs
index 83c08a7..0f4615d 100644
--- a/tests/utoipa_gen_test.rs
+++ b/tests/utoipa_gen_test.rs
@@ -1,4 +1,5 @@
 #![cfg(feature = "actix_extras")]
+#![allow(dead_code)]
 
 use std::borrow::Cow;
 
@@ -24,6 +25,14 @@ enum StatusType {
     NotOk,
 }
 
+#[derive(Component)]
+enum Random {
+    Response { id: String },
+    PetResponse(Pet),
+    Ids(Vec<String>),
+    UnitValue,
+}
+
 #[derive(Serialize, Deserialize, Component)]
 struct Simple {
     greeting: &'static str,
@@ -58,7 +67,7 @@ mod pet_api {
 }
 
 #[derive(OpenApi, Default)]
-#[openapi(handlers = [pet_api::get_pet_by_id], components = [Pet, StatusType, Status<StatusType>, Simple])]
+#[openapi(handlers = [pet_api::get_pet_by_id], components = [Pet, StatusType, Status<StatusType>, Simple, Random])]
 struct ApiDoc;
 
 #[test]