From d1659b710d57f594b161db519022f6cd77c317b6 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Fri, 27 May 2022 16:24:44 +1000 Subject: [PATCH 01/20] Implement serde `tag` property for simple enums - Implement serde `tag` property for simple enums - Add assert-json-diff for improved json comparison ergonimics in tests --- Cargo.toml | 1 + tests/component_derive_test.rs | 234 ++++++++++++++++++++++++++++- utoipa-gen/src/schema/component.rs | 49 +++++- 3 files changed, 276 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 59281163..626563ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ serde_yaml = { version = "0.8", optional = true } utoipa-gen = { version = "1.0.2", path = "./utoipa-gen" } [dev-dependencies] +assert-json-diff = "2" actix-web = { version = "4" } paste = "1" chrono = { version = "0.4", features = ["serde"] } diff --git a/tests/component_derive_test.rs b/tests/component_derive_test.rs index da3c84f7..af4a8a77 100644 --- a/tests/component_derive_test.rs +++ b/tests/component_derive_test.rs @@ -1,11 +1,12 @@ #![cfg(feature = "serde_json")] use std::{borrow::Cow, cell::RefCell, collections::HashMap, marker::PhantomData, vec}; +use assert_json_diff::assert_json_eq; #[cfg(any(feature = "chrono", feature = "chrono_with_format"))] use chrono::{Date, DateTime, Duration, Utc}; use serde::Serialize; -use serde_json::Value; +use serde_json::{json, Value}; use utoipa::{Component, OpenApi}; use crate::common::get_json_path; @@ -533,6 +534,237 @@ fn derive_complex_enum_with_named_and_unnamed_fields() { } } +#[test] +fn derive_complex_enum_without_serde_tag() { + #[derive(Serialize)] + struct Foo(String); + + let value: Value = api_doc! { + #[derive(Serialize)] + #[serde(rename_all = "snake_case")] + enum Bar { + UnitValue, + NamedFields { + id: &'static str, + names: Option> + }, + UnnamedFields(Foo), + } + }; + + assert_json_eq!( + value, + json!({ + "oneOf": [ + { + "enum": [ + "unit_value" + ], + "type": "string" + }, + { + "properties": { + "named_fields": { + "properties": { + "id": { + "type": "string" + }, + "names": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + "type": "object" + }, + { + "properties": { + "unnamed_fields": { + "$ref": "#/components/schemas/Foo" + } + }, + "type": "object" + } + ] + }) + ); +} + +#[test] +fn derive_simple_enum_without_serde_tag() { + let value: Value = api_doc! { + #[derive(Serialize)] + enum Bar { + A, + B, + C, + } + }; + + assert_json_eq!( + value, + json!({ + "enum": [ + "A", + "B", + "C", + ], + "type": "string", + }) + ); +} + +#[test] +fn derive_simple_enum_with_serde_tag() { + let value: Value = api_doc! { + #[derive(Serialize)] + #[serde(tag = "tag")] + enum Bar { + A, + B, + C, + } + }; + + assert_json_eq!( + value, + json!({ + "oneOf": [ + { + "type": "object", + "properties": { + "tag": { + "type": "string", + "enum": [ + "A", + ], + }, + }, + "required": [ + "tag", + ], + }, + { + "type": "object", + "properties": { + "tag": { + "type": "string", + "enum": [ + "B", + ], + }, + }, + "required": [ + "tag", + ], + }, + { + "type": "object", + "properties": { + "tag": { + "type": "string", + "enum": [ + "C", + ], + }, + }, + "required": [ + "tag", + ], + }, + ], + }) + ); +} + +#[test] +#[ignore = "todo"] +fn derive_complex_enum_with_serde_tag() { + #[derive(Serialize)] + struct Foo(String); + + let value: Value = api_doc! { + #[derive(Serialize)] + #[serde(tag = "tag", rename_all = "snake_case")] + enum Bar { + UnitValue, + NamedFields { + id: &'static str, + names: Option> + }, + UnnamedFields(Foo), + } + }; + + assert_json_eq!( + value, + json!({ + "oneOf": [ + { + "type": "object", + "properties": { + "tag": { + "enum": [ + "unit_value" + ], + "type": "string" + } + }, + "required": [ + "tag", + ], + }, + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "names": { + "type": "array", + "items": { + "type": "string" + }, + }, + "tag": { + "type": "string", + "enum": [ + "named_fields" + ] + } + }, + "required": [ + "id", + "tag", + ], + }, + { + "type": "object", + "properties": { + "tag": { + "type": "string" + }, + "unnamed_fields": { + "$ref": "#/components/schemas/Foo" + } + }, + "required": [ + "id", + "unnamed_fields", + ] + } + ] + }) + ); +} + #[test] fn derive_struct_with_read_only_and_write_only() { let user = api_doc! { diff --git a/utoipa-gen/src/schema/component.rs b/utoipa-gen/src/schema/component.rs index b08541f9..c5b2e86d 100644 --- a/utoipa-gen/src/schema/component.rs +++ b/utoipa-gen/src/schema/component.rs @@ -413,11 +413,46 @@ impl ToTokens for SimpleEnum<'_> { .collect::>(); let len = enum_values.len(); - tokens.extend(quote! { - utoipa::openapi::PropertyBuilder::new() - .component_type(utoipa::openapi::ComponentType::String) - .enum_values::<[&str; #len], &str>(Some(#enum_values)) - }); + match container_rules { + // Handle the serde enum tag = "" property + Some(Serde::Container(container)) if container.tag.is_some() => { + let tag = container.tag.expect("Expected tag to be present"); + tokens.extend(quote! { + Into::::into(utoipa::openapi::OneOf::with_capacity(#len)) + }); + enum_values + .iter() + .map(|enum_value: &String| { + quote! { + utoipa::openapi::schema::ObjectBuilder::new() + .property( + #tag, + utoipa::openapi::schema::PropertyBuilder::new() + .component_type(utoipa::openapi::ComponentType::String) + .enum_values::<[&str; 1], &str>(Some([#enum_value])) + .build() + ) + .required(#tag) + .build() + } + }) + .for_each(|object: TokenStream2| { + tokens.extend(quote! { + .item(#object) + }) + }); + tokens.extend(quote! { + .build() + }); + } + _ => { + tokens.extend(quote! { + utoipa::openapi::PropertyBuilder::new() + .component_type(utoipa::openapi::ComponentType::String) + .enum_values::<[&str; #len], &str>(Some(#enum_values)) + }); + } + } let attrs = attr::parse_component_attr::>(self.attributes); if let Some(attributes) = attrs { @@ -456,9 +491,9 @@ impl ToTokens for ComplexEnum<'_> { ); } - let capasity = self.variants.len(); + let capacity = self.variants.len(); tokens.extend(quote! { - Into::::into(utoipa::openapi::OneOf::with_capacity(#capasity)) + Into::::into(utoipa::openapi::OneOf::with_capacity(#capacity)) }); let mut container_rule = serde::parse_container(self.attributes); From ba14399b1b82bbc2e580f33f4efe4a50196c3dde Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Fri, 27 May 2022 17:40:59 +1000 Subject: [PATCH 02/20] Make simple enum serde tag handling more functional --- utoipa-gen/src/schema/component.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/utoipa-gen/src/schema/component.rs b/utoipa-gen/src/schema/component.rs index c5b2e86d..09fce002 100644 --- a/utoipa-gen/src/schema/component.rs +++ b/utoipa-gen/src/schema/component.rs @@ -413,14 +413,11 @@ impl ToTokens for SimpleEnum<'_> { .collect::>(); let len = enum_values.len(); - match container_rules { + tokens.extend(match container_rules { // Handle the serde enum tag = "" property Some(Serde::Container(container)) if container.tag.is_some() => { let tag = container.tag.expect("Expected tag to be present"); - tokens.extend(quote! { - Into::::into(utoipa::openapi::OneOf::with_capacity(#len)) - }); - enum_values + let items: TokenStream2 = enum_values .iter() .map(|enum_value: &String| { quote! { @@ -436,23 +433,26 @@ impl ToTokens for SimpleEnum<'_> { .build() } }) - .for_each(|object: TokenStream2| { - tokens.extend(quote! { + .map(|object: TokenStream2| { + quote! { .item(#object) - }) - }); - tokens.extend(quote! { - .build() - }); + } + }) + .collect(); + quote! { + Into::::into(utoipa::openapi::OneOf::with_capacity(#len)) + #items + .build() + } } _ => { - tokens.extend(quote! { + quote! { utoipa::openapi::PropertyBuilder::new() .component_type(utoipa::openapi::ComponentType::String) .enum_values::<[&str; #len], &str>(Some(#enum_values)) - }); + } } - } + }); let attrs = attr::parse_component_attr::>(self.attributes); if let Some(attributes) = attrs { From 00bc262db4cfe404ec7664bbd4e601d874f7abbf Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Fri, 27 May 2022 17:50:42 +1000 Subject: [PATCH 03/20] Reformat and reorganise serde tag tests --- tests/component_derive_test.rs | 155 ++++++++++++++++----------------- 1 file changed, 77 insertions(+), 78 deletions(-) diff --git a/tests/component_derive_test.rs b/tests/component_derive_test.rs index af4a8a77..d634180e 100644 --- a/tests/component_derive_test.rs +++ b/tests/component_derive_test.rs @@ -534,69 +534,6 @@ fn derive_complex_enum_with_named_and_unnamed_fields() { } } -#[test] -fn derive_complex_enum_without_serde_tag() { - #[derive(Serialize)] - struct Foo(String); - - let value: Value = api_doc! { - #[derive(Serialize)] - #[serde(rename_all = "snake_case")] - enum Bar { - UnitValue, - NamedFields { - id: &'static str, - names: Option> - }, - UnnamedFields(Foo), - } - }; - - assert_json_eq!( - value, - json!({ - "oneOf": [ - { - "enum": [ - "unit_value" - ], - "type": "string" - }, - { - "properties": { - "named_fields": { - "properties": { - "id": { - "type": "string" - }, - "names": { - "items": { - "type": "string" - }, - "type": "array" - } - }, - "required": [ - "id" - ], - "type": "object" - } - }, - "type": "object" - }, - { - "properties": { - "unnamed_fields": { - "$ref": "#/components/schemas/Foo" - } - }, - "type": "object" - } - ] - }) - ); -} - #[test] fn derive_simple_enum_without_serde_tag() { let value: Value = api_doc! { @@ -685,7 +622,69 @@ fn derive_simple_enum_with_serde_tag() { } #[test] -#[ignore = "todo"] +fn derive_complex_enum_without_serde_tag() { + #[derive(Serialize)] + struct Foo(String); + + let value: Value = api_doc! { + #[derive(Serialize)] + #[serde(rename_all = "snake_case")] + enum Bar { + UnitValue, + NamedFields { + id: &'static str, + names: Option> + }, + UnnamedFields(Foo), + } + }; + + assert_json_eq!( + value, + json!({ + "oneOf": [ + { + "type": "string", + "enum": [ + "unit_value", + ], + }, + { + "type": "object", + "properties": { + "named_fields": { + "type": "object", + "properties": { + "id": { + "type": "string", + }, + "names": { + "type": "array", + "items": { + "type": "string", + }, + }, + }, + "required": [ + "id", + ], + }, + }, + }, + { + "type": "object", + "properties": { + "unnamed_fields": { + "$ref": "#/components/schemas/Foo", + }, + }, + }, + ], + }) + ); +} + +#[test] fn derive_complex_enum_with_serde_tag() { #[derive(Serialize)] struct Foo(String); @@ -711,11 +710,11 @@ fn derive_complex_enum_with_serde_tag() { "type": "object", "properties": { "tag": { + "type": "string", "enum": [ - "unit_value" + "unit_value", ], - "type": "string" - } + }, }, "required": [ "tag", @@ -725,20 +724,20 @@ fn derive_complex_enum_with_serde_tag() { "type": "object", "properties": { "id": { - "type": "string" + "type": "string", }, "names": { "type": "array", "items": { - "type": "string" + "type": "string", }, }, "tag": { "type": "string", "enum": [ - "named_fields" - ] - } + "named_fields", + ], + }, }, "required": [ "id", @@ -749,18 +748,18 @@ fn derive_complex_enum_with_serde_tag() { "type": "object", "properties": { "tag": { - "type": "string" + "type": "string", }, "unnamed_fields": { - "$ref": "#/components/schemas/Foo" - } + "$ref": "#/components/schemas/Foo", + }, }, "required": [ "id", "unnamed_fields", - ] - } - ] + ], + }, + ], }) ); } From bb4a6b19fa2cf5649e221786e80fae127263ff36 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Fri, 27 May 2022 21:58:11 +1000 Subject: [PATCH 04/20] Documentation for `rename()`. --- utoipa-gen/src/schema/component.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/utoipa-gen/src/schema/component.rs b/utoipa-gen/src/schema/component.rs index 09fce002..21762075 100644 --- a/utoipa-gen/src/schema/component.rs +++ b/utoipa-gen/src/schema/component.rs @@ -751,6 +751,9 @@ fn rename_variant<'a>( }) } +/// Resolves the appropriate [`RenameRule`] to apply during a `rename_op` given a `container_rule` +/// (`struct` or `enum` level) and `field_rule` (`struct` field or `enum` variant level). Returns +/// `Some` of the result of the `rename_op` if a rename is required by the supplied rules. #[inline] fn rename<'a>( container_rule: &'a mut Option, From c74c3b6514da53664cd2bd80f81257b4284461c2 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Fri, 27 May 2022 22:01:43 +1000 Subject: [PATCH 05/20] Documentation for `rename_field()` and `rename_variant()` --- utoipa-gen/src/schema/component.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/utoipa-gen/src/schema/component.rs b/utoipa-gen/src/schema/component.rs index 21762075..7ae59a1f 100644 --- a/utoipa-gen/src/schema/component.rs +++ b/utoipa-gen/src/schema/component.rs @@ -731,6 +731,10 @@ fn is_not_skipped(rule: &Option) -> bool { .unwrap_or(true) } +/// Resolves the appropriate [`RenameRule`] to apply to the specified `struct` `field` name given a +/// `container_rule` (`struct` or `enum` level) and `field_rule` (`struct` field or `enum` variant +/// level). Returns `Some` of the result of the `rename_op` if a rename is required by the supplied +/// rules. #[inline] fn rename_field<'a>( container_rule: &'a mut Option, @@ -740,14 +744,18 @@ fn rename_field<'a>( rename(container_rule, field_rule, &|rule| rule.rename(field)) } +/// Resolves the appropriate [`RenameRule`] to apply to the specified `enum` `variant` name given a +/// `container_rule` (`struct` or `enum` level) and `field_rule` (`struct` field or `enum` variant +/// level). Returns `Some` of the result of the `rename_op` if a rename is required by the supplied +/// rules. #[inline] fn rename_variant<'a>( container_rule: &'a mut Option, field_rule: &'a mut Option, - field: &str, + variant: &str, ) -> Option { rename(container_rule, field_rule, &|rule| { - rule.rename_variant(field) + rule.rename_variant(variant) }) } From 475936246dd50be8480e5342ae7048301e965595 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Sat, 28 May 2022 20:18:03 +1000 Subject: [PATCH 06/20] Rename some component derive tests, add new tests for variant renames --- tests/component_derive_test.rs | 91 ++++++++++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 10 deletions(-) diff --git a/tests/component_derive_test.rs b/tests/component_derive_test.rs index d634180e..831284b2 100644 --- a/tests/component_derive_test.rs +++ b/tests/component_derive_test.rs @@ -535,7 +535,7 @@ fn derive_complex_enum_with_named_and_unnamed_fields() { } #[test] -fn derive_simple_enum_without_serde_tag() { +fn derive_simple_enum() { let value: Value = api_doc! { #[derive(Serialize)] enum Bar { @@ -559,7 +559,7 @@ fn derive_simple_enum_without_serde_tag() { } #[test] -fn derive_simple_enum_with_serde_tag() { +fn derive_simple_enum_serde_tag() { let value: Value = api_doc! { #[derive(Serialize)] #[serde(tag = "tag")] @@ -622,7 +622,7 @@ fn derive_simple_enum_with_serde_tag() { } #[test] -fn derive_complex_enum_without_serde_tag() { +fn derive_complex_enum() { #[derive(Serialize)] struct Foo(String); @@ -685,13 +685,81 @@ fn derive_complex_enum_without_serde_tag() { } #[test] -fn derive_complex_enum_with_serde_tag() { +fn derive_complex_enum_serde_rename_variant() { #[derive(Serialize)] struct Foo(String); let value: Value = api_doc! { #[derive(Serialize)] - #[serde(tag = "tag", rename_all = "snake_case")] + enum Bar { + #[serde(rename = "renamed_unit_value")] + UnitValue, + #[serde(rename = "renamed_named_fields")] + NamedFields { + #[serde(rename = "renamed_id")] + id: &'static str, + #[serde(rename = "renamed_names")] + names: Option> + }, + #[serde(rename = "renamed_unnamed_fields")] + UnnamedFields(Foo), + } + }; + + assert_json_eq!( + value, + json!({ + "oneOf": [ + { + "type": "string", + "enum": [ + "renamed_unit_value", + ], + }, + { + "type": "object", + "properties": { + "renamed_named_fields": { + "type": "object", + "properties": { + "renamed_id": { + "type": "string", + }, + "renamed_names": { + "type": "array", + "items": { + "type": "string", + }, + }, + }, + "required": [ + "renamed_id", + ], + }, + }, + }, + { + "type": "object", + "properties": { + "renamed_unnamed_fields": { + "$ref": "#/components/schemas/Foo", + }, + }, + }, + ], + }) + ); +} + +#[ignore = "todo"] +#[test] +fn derive_complex_enum_serde_tag() { + #[derive(Serialize)] + struct Foo(String); + + let value: Value = api_doc! { + #[derive(Serialize)] + #[serde(tag = "tag")] enum Bar { UnitValue, NamedFields { @@ -712,7 +780,7 @@ fn derive_complex_enum_with_serde_tag() { "tag": { "type": "string", "enum": [ - "unit_value", + "UnitValue", ], }, }, @@ -735,7 +803,7 @@ fn derive_complex_enum_with_serde_tag() { "tag": { "type": "string", "enum": [ - "named_fields", + "NamedFields", ], }, }, @@ -749,14 +817,17 @@ fn derive_complex_enum_with_serde_tag() { "properties": { "tag": { "type": "string", + "enum": [ + "UnnamedFields", + ], }, - "unnamed_fields": { + "UnnamedFields": { "$ref": "#/components/schemas/Foo", }, }, "required": [ - "id", - "unnamed_fields", + "tag", + "UnnamedFields", ], }, ], From 3af0e8c5429a39c6c897508e53c5492c147d819b Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Sat, 28 May 2022 21:14:12 +1000 Subject: [PATCH 07/20] Simplify ComplexEnum Component derive --- utoipa-gen/src/schema/component.rs | 74 ++++++++++++++---------------- 1 file changed, 34 insertions(+), 40 deletions(-) diff --git a/utoipa-gen/src/schema/component.rs b/utoipa-gen/src/schema/component.rs index 7ae59a1f..119add0a 100644 --- a/utoipa-gen/src/schema/component.rs +++ b/utoipa-gen/src/schema/component.rs @@ -427,10 +427,8 @@ impl ToTokens for SimpleEnum<'_> { utoipa::openapi::schema::PropertyBuilder::new() .component_type(utoipa::openapi::ComponentType::String) .enum_values::<[&str; 1], &str>(Some([#enum_value])) - .build() ) .required(#tag) - .build() } }) .map(|object: TokenStream2| { @@ -442,7 +440,6 @@ impl ToTokens for SimpleEnum<'_> { quote! { Into::::into(utoipa::openapi::OneOf::with_capacity(#len)) #items - .build() } } _ => { @@ -501,7 +498,7 @@ impl ToTokens for ComplexEnum<'_> { // serde, externally tagged format supported by now self.variants .iter() - .filter_map(|variant| { + .filter_map(|variant: &Variant| { let variant_rules = serde::parse_value(&variant.attrs); if is_not_skipped(&variant_rules) { Some((variant, variant_rules)) @@ -509,47 +506,44 @@ impl ToTokens for ComplexEnum<'_> { None } }) - .map(|(variant, mut variant_rule)| match &variant.fields { - Fields::Named(named_fields) => { - let named_enum = NamedStructComponent { - attributes: &variant.attrs, - fields: &named_fields.named, - generics: None, - alias: None, - }; - let name = &*variant.ident.to_string(); - - let renamed = rename_variant(&mut container_rule, &mut variant_rule, name) - .unwrap_or_else(|| String::from(name)); + .map(|(variant, mut variant_rule)| { + let variant_name = &*variant.ident.to_string(); + let renamed_variant = + rename_variant(&mut container_rule, &mut variant_rule, variant_name) + .unwrap_or_else(|| String::from(variant_name)); + + match &variant.fields { + Fields::Named(named_fields) => { + let named_enum = NamedStructComponent { + attributes: &variant.attrs, + fields: &named_fields.named, + generics: None, + alias: None, + }; - quote! { - utoipa::openapi::schema::ObjectBuilder::new() - .property(#renamed, #named_enum) + quote! { + utoipa::openapi::schema::ObjectBuilder::new() + .property(#renamed_variant, #named_enum) + } } - } - Fields::Unnamed(unnamed_fields) => { - let unnamed_enum = UnnamedStructComponent { - attributes: &variant.attrs, - fields: &unnamed_fields.unnamed, - }; - let name = &*variant.ident.to_string(); - let renamed = rename_variant(&mut container_rule, &mut variant_rule, name) - .unwrap_or_else(|| String::from(name)); + Fields::Unnamed(unnamed_fields) => { + let unnamed_enum = UnnamedStructComponent { + attributes: &variant.attrs, + fields: &unnamed_fields.unnamed, + }; - quote! { - utoipa::openapi::schema::ObjectBuilder::new() - .property(#renamed, #unnamed_enum) + quote! { + utoipa::openapi::schema::ObjectBuilder::new() + .property(#renamed_variant, #unnamed_enum) + } } - } - Fields::Unit => { - let mut enum_values = Punctuated::::new(); - enum_values.push(variant.clone()); - - SimpleEnum { - attributes: self.attributes, - variants: &enum_values, + Fields::Unit => { + quote! { + utoipa::openapi::PropertyBuilder::new() + .component_type(utoipa::openapi::ComponentType::String) + .enum_values::<[&str; 1], &str>(Some([#renamed_variant])) + } } - .to_token_stream() } }) .for_each(|inline_variant| { From ccfd02c4c49bcaa393fbd2c87a159bf08b91ce13 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Sat, 28 May 2022 21:25:47 +1000 Subject: [PATCH 08/20] Refactor SimpleEnum ToTokens --- utoipa-gen/src/schema/component.rs | 80 +++++++++++++++++------------- 1 file changed, 46 insertions(+), 34 deletions(-) diff --git a/utoipa-gen/src/schema/component.rs b/utoipa-gen/src/schema/component.rs index 119add0a..74c58ab2 100644 --- a/utoipa-gen/src/schema/component.rs +++ b/utoipa-gen/src/schema/component.rs @@ -391,6 +391,49 @@ struct SimpleEnum<'a> { attributes: &'a [Attribute], } +impl SimpleEnum<'_> { + fn tagged_variants_tokens( + enum_values: Array, + serde_container: serde::SerdeContainer, + ) -> TokenStream2 { + let len = enum_values.len(); + let tag = serde_container.tag.expect("Expected tag to be present"); + let items: TokenStream2 = enum_values + .iter() + .map(|enum_value: &String| { + quote! { + utoipa::openapi::schema::ObjectBuilder::new() + .property( + #tag, + utoipa::openapi::schema::PropertyBuilder::new() + .component_type(utoipa::openapi::ComponentType::String) + .enum_values::<[&str; 1], &str>(Some([#enum_value])) + ) + .required(#tag) + } + }) + .map(|object: TokenStream2| { + quote! { + .item(#object) + } + }) + .collect(); + quote! { + Into::::into(utoipa::openapi::OneOf::with_capacity(#len)) + #items + } + } + + fn variants_tokens(enum_values: Array) -> TokenStream2 { + let len = enum_values.len(); + quote! { + utoipa::openapi::PropertyBuilder::new() + .component_type(utoipa::openapi::ComponentType::String) + .enum_values::<[&str; #len], &str>(Some(#enum_values)) + } + } +} + impl ToTokens for SimpleEnum<'_> { fn to_tokens(&self, tokens: &mut TokenStream2) { let mut container_rules = serde::parse_container(self.attributes); @@ -411,44 +454,13 @@ impl ToTokens for SimpleEnum<'_> { } }) .collect::>(); - let len = enum_values.len(); tokens.extend(match container_rules { // Handle the serde enum tag = "" property - Some(Serde::Container(container)) if container.tag.is_some() => { - let tag = container.tag.expect("Expected tag to be present"); - let items: TokenStream2 = enum_values - .iter() - .map(|enum_value: &String| { - quote! { - utoipa::openapi::schema::ObjectBuilder::new() - .property( - #tag, - utoipa::openapi::schema::PropertyBuilder::new() - .component_type(utoipa::openapi::ComponentType::String) - .enum_values::<[&str; 1], &str>(Some([#enum_value])) - ) - .required(#tag) - } - }) - .map(|object: TokenStream2| { - quote! { - .item(#object) - } - }) - .collect(); - quote! { - Into::::into(utoipa::openapi::OneOf::with_capacity(#len)) - #items - } - } - _ => { - quote! { - utoipa::openapi::PropertyBuilder::new() - .component_type(utoipa::openapi::ComponentType::String) - .enum_values::<[&str; #len], &str>(Some(#enum_values)) - } + Some(Serde::Container(serde_container)) if serde_container.tag.is_some() => { + Self::tagged_variants_tokens(enum_values, serde_container) } + _ => Self::variants_tokens(enum_values), }); let attrs = attr::parse_component_attr::>(self.attributes); From 57ab646c59dc62eb6648c9cfad44015105971617 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Sat, 28 May 2022 21:27:42 +1000 Subject: [PATCH 09/20] Documentation for SimpleEnum --- utoipa-gen/src/schema/component.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/utoipa-gen/src/schema/component.rs b/utoipa-gen/src/schema/component.rs index 74c58ab2..849a7158 100644 --- a/utoipa-gen/src/schema/component.rs +++ b/utoipa-gen/src/schema/component.rs @@ -392,6 +392,8 @@ struct SimpleEnum<'a> { } impl SimpleEnum<'_> { + /// Produce tokens that represent each variant for the situation where the serde enum tag = + /// "" attribute applies. fn tagged_variants_tokens( enum_values: Array, serde_container: serde::SerdeContainer, @@ -424,6 +426,7 @@ impl SimpleEnum<'_> { } } + /// Produce tokens that represent each variant. fn variants_tokens(enum_values: Array) -> TokenStream2 { let len = enum_values.len(); quote! { @@ -456,7 +459,6 @@ impl ToTokens for SimpleEnum<'_> { .collect::>(); tokens.extend(match container_rules { - // Handle the serde enum tag = "" property Some(Serde::Container(serde_container)) if serde_container.tag.is_some() => { Self::tagged_variants_tokens(enum_values, serde_container) } From fe96b97cab94da64e9bb763c319d0236e2a85987 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Sat, 28 May 2022 21:34:01 +1000 Subject: [PATCH 10/20] Rename variable in ComplexEnum implementation --- utoipa-gen/src/schema/component.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/utoipa-gen/src/schema/component.rs b/utoipa-gen/src/schema/component.rs index 849a7158..2bfadf00 100644 --- a/utoipa-gen/src/schema/component.rs +++ b/utoipa-gen/src/schema/component.rs @@ -513,17 +513,17 @@ impl ToTokens for ComplexEnum<'_> { self.variants .iter() .filter_map(|variant: &Variant| { - let variant_rules = serde::parse_value(&variant.attrs); - if is_not_skipped(&variant_rules) { - Some((variant, variant_rules)) + let variant_serde_rules = serde::parse_value(&variant.attrs); + if is_not_skipped(&variant_serde_rules) { + Some((variant, variant_serde_rules)) } else { None } }) - .map(|(variant, mut variant_rule)| { + .map(|(variant, mut variant_serde_rules)| { let variant_name = &*variant.ident.to_string(); let renamed_variant = - rename_variant(&mut container_rule, &mut variant_rule, variant_name) + rename_variant(&mut container_rule, &mut variant_serde_rules, variant_name) .unwrap_or_else(|| String::from(variant_name)); match &variant.fields { From c29bd9adbe7a83822d159c3ddb21ebe87ac6132a Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Sat, 28 May 2022 21:43:34 +1000 Subject: [PATCH 11/20] Rename variable in ComplexEnum implementation --- utoipa-gen/src/schema/component.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/utoipa-gen/src/schema/component.rs b/utoipa-gen/src/schema/component.rs index 2bfadf00..4149742b 100644 --- a/utoipa-gen/src/schema/component.rs +++ b/utoipa-gen/src/schema/component.rs @@ -522,7 +522,7 @@ impl ToTokens for ComplexEnum<'_> { }) .map(|(variant, mut variant_serde_rules)| { let variant_name = &*variant.ident.to_string(); - let renamed_variant = + let variant_name = rename_variant(&mut container_rule, &mut variant_serde_rules, variant_name) .unwrap_or_else(|| String::from(variant_name)); @@ -537,7 +537,7 @@ impl ToTokens for ComplexEnum<'_> { quote! { utoipa::openapi::schema::ObjectBuilder::new() - .property(#renamed_variant, #named_enum) + .property(#variant_name, #named_enum) } } Fields::Unnamed(unnamed_fields) => { @@ -548,14 +548,14 @@ impl ToTokens for ComplexEnum<'_> { quote! { utoipa::openapi::schema::ObjectBuilder::new() - .property(#renamed_variant, #unnamed_enum) + .property(#variant_name, #unnamed_enum) } } Fields::Unit => { quote! { utoipa::openapi::PropertyBuilder::new() .component_type(utoipa::openapi::ComponentType::String) - .enum_values::<[&str; 1], &str>(Some([#renamed_variant])) + .enum_values::<[&str; 1], &str>(Some([#variant_name])) } } } From ff1999e61d25d6e9c52ceec68ec58ef23da04739 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Sat, 28 May 2022 22:40:47 +1000 Subject: [PATCH 12/20] Support internally tagged enum representation --- tests/component_derive_test.rs | 82 +++++++++++---- utoipa-gen/src/schema/component.rs | 158 ++++++++++++++++++++--------- 2 files changed, 172 insertions(+), 68 deletions(-) diff --git a/tests/component_derive_test.rs b/tests/component_derive_test.rs index 831284b2..bb2de287 100644 --- a/tests/component_derive_test.rs +++ b/tests/component_derive_test.rs @@ -626,6 +626,68 @@ fn derive_complex_enum() { #[derive(Serialize)] struct Foo(String); + let value: Value = api_doc! { + #[derive(Serialize)] + enum Bar { + UnitValue, + NamedFields { + id: &'static str, + names: Option> + }, + UnnamedFields(Foo), + } + }; + + assert_json_eq!( + value, + json!({ + "oneOf": [ + { + "type": "string", + "enum": [ + "UnitValue", + ], + }, + { + "type": "object", + "properties": { + "NamedFields": { + "type": "object", + "properties": { + "id": { + "type": "string", + }, + "names": { + "type": "array", + "items": { + "type": "string", + }, + }, + }, + "required": [ + "id", + ], + }, + }, + }, + { + "type": "object", + "properties": { + "UnnamedFields": { + "$ref": "#/components/schemas/Foo", + }, + }, + }, + ], + }) + ); +} + +#[test] +fn derive_complex_enum_serde_rename_all() { + #[derive(Serialize)] + struct Foo(String); + let value: Value = api_doc! { #[derive(Serialize)] #[serde(rename_all = "snake_case")] @@ -751,7 +813,6 @@ fn derive_complex_enum_serde_rename_variant() { ); } -#[ignore = "todo"] #[test] fn derive_complex_enum_serde_tag() { #[derive(Serialize)] @@ -766,7 +827,6 @@ fn derive_complex_enum_serde_tag() { id: &'static str, names: Option> }, - UnnamedFields(Foo), } }; @@ -812,24 +872,6 @@ fn derive_complex_enum_serde_tag() { "tag", ], }, - { - "type": "object", - "properties": { - "tag": { - "type": "string", - "enum": [ - "UnnamedFields", - ], - }, - "UnnamedFields": { - "$ref": "#/components/schemas/Foo", - }, - }, - "required": [ - "tag", - "UnnamedFields", - ], - }, ], }) ); diff --git a/utoipa-gen/src/schema/component.rs b/utoipa-gen/src/schema/component.rs index 4149742b..2579560e 100644 --- a/utoipa-gen/src/schema/component.rs +++ b/utoipa-gen/src/schema/component.rs @@ -394,12 +394,8 @@ struct SimpleEnum<'a> { impl SimpleEnum<'_> { /// Produce tokens that represent each variant for the situation where the serde enum tag = /// "" attribute applies. - fn tagged_variants_tokens( - enum_values: Array, - serde_container: serde::SerdeContainer, - ) -> TokenStream2 { + fn tagged_variants_tokens(tag: String, enum_values: Array) -> TokenStream2 { let len = enum_values.len(); - let tag = serde_container.tag.expect("Expected tag to be present"); let items: TokenStream2 = enum_values .iter() .map(|enum_value: &String| { @@ -460,7 +456,8 @@ impl ToTokens for SimpleEnum<'_> { tokens.extend(match container_rules { Some(Serde::Container(serde_container)) if serde_container.tag.is_some() => { - Self::tagged_variants_tokens(enum_values, serde_container) + let tag = serde_container.tag.expect("Expected tag to be present"); + Self::tagged_variants_tokens(tag, enum_values) } _ => Self::variants_tokens(enum_values), }); @@ -487,6 +484,85 @@ struct ComplexEnum<'a> { attributes: &'a [Attribute], } +impl ComplexEnum<'_> { + fn unit_variant_tokens(variant_name: String) -> TokenStream2 { + quote! { + utoipa::openapi::PropertyBuilder::new() + .component_type(utoipa::openapi::ComponentType::String) + .enum_values::<[&str; 1], &str>(Some([#variant_name])) + } + } + /// Produce tokens that represent a variant of a [`ComplexEnum`]. + fn variant_tokens(variant_name: String, variant: &Variant) -> TokenStream2 { + match &variant.fields { + Fields::Named(named_fields) => { + let named_enum = NamedStructComponent { + attributes: &variant.attrs, + fields: &named_fields.named, + generics: None, + alias: None, + }; + + quote! { + utoipa::openapi::schema::ObjectBuilder::new() + .property(#variant_name, #named_enum) + } + } + Fields::Unnamed(unnamed_fields) => { + let unnamed_enum = UnnamedStructComponent { + attributes: &variant.attrs, + fields: &unnamed_fields.unnamed, + }; + + quote! { + utoipa::openapi::schema::ObjectBuilder::new() + .property(#variant_name, #unnamed_enum) + } + } + Fields::Unit => Self::unit_variant_tokens(variant_name), + } + } + + /// Produce tokens that represent a variant of a [`ComplexEnum`] where serde enum attribute + /// `tag = ` applies. + fn tagged_variant_tokens(tag: &str, variant_name: String, variant: &Variant) -> TokenStream2 { + match &variant.fields { + Fields::Named(named_fields) => { + let named_enum = NamedStructComponent { + attributes: &variant.attrs, + fields: &named_fields.named, + generics: None, + alias: None, + }; + + let variant_name_tokens = Self::unit_variant_tokens(variant_name); + + quote! { + #named_enum + .property(#tag, #variant_name_tokens) + .required(#tag) + } + } + Fields::Unnamed(_) => { + abort!( + variant, + "Unnamed (tuple) enum variants are unsupported for internally tagged enums using the `tag = ` serde attribute"; + + help = "Try using a different serde enum representation"; + ); + } + Fields::Unit => { + let variant_tokens = Self::unit_variant_tokens(variant_name); + quote! { + utoipa::openapi::schema::ObjectBuilder::new() + .property(#tag, #variant_tokens) + .required(#tag) + } + } + } + } +} + impl ToTokens for ComplexEnum<'_> { fn to_tokens(&self, tokens: &mut TokenStream2) { if self @@ -503,14 +579,20 @@ impl ToTokens for ComplexEnum<'_> { } let capacity = self.variants.len(); - tokens.extend(quote! { - Into::::into(utoipa::openapi::OneOf::with_capacity(#capacity)) - }); + tokens.extend(quote! {}); - let mut container_rule = serde::parse_container(self.attributes); + // TODO: refactor this to return Option + let mut container_rules = serde::parse_container(self.attributes); + let tag: Option = + if let Some(Serde::Container(serde_container)) = &mut container_rules { + serde_container.tag.take() + } else { + None + }; // serde, externally tagged format supported by now - self.variants + let items: TokenStream2 = self + .variants .iter() .filter_map(|variant: &Variant| { let variant_serde_rules = serde::parse_value(&variant.attrs); @@ -523,48 +605,28 @@ impl ToTokens for ComplexEnum<'_> { .map(|(variant, mut variant_serde_rules)| { let variant_name = &*variant.ident.to_string(); let variant_name = - rename_variant(&mut container_rule, &mut variant_serde_rules, variant_name) + rename_variant(&mut container_rules, &mut variant_serde_rules, variant_name) .unwrap_or_else(|| String::from(variant_name)); - match &variant.fields { - Fields::Named(named_fields) => { - let named_enum = NamedStructComponent { - attributes: &variant.attrs, - fields: &named_fields.named, - generics: None, - alias: None, - }; - - quote! { - utoipa::openapi::schema::ObjectBuilder::new() - .property(#variant_name, #named_enum) - } - } - Fields::Unnamed(unnamed_fields) => { - let unnamed_enum = UnnamedStructComponent { - attributes: &variant.attrs, - fields: &unnamed_fields.unnamed, - }; - - quote! { - utoipa::openapi::schema::ObjectBuilder::new() - .property(#variant_name, #unnamed_enum) - } - } - Fields::Unit => { - quote! { - utoipa::openapi::PropertyBuilder::new() - .component_type(utoipa::openapi::ComponentType::String) - .enum_values::<[&str; 1], &str>(Some([#variant_name])) - } - } + if let Some(tag) = &tag { + Self::tagged_variant_tokens(&tag, variant_name, variant) + } else { + Self::variant_tokens(variant_name, variant) } }) - .for_each(|inline_variant| { - tokens.extend(quote! { + .map(|inline_variant| { + quote! { .item(#inline_variant) - }) - }); + } + }) + .collect(); + + tokens.extend( + quote! { + Into::::into(utoipa::openapi::OneOf::with_capacity(#capacity)) + #items + } + ); if let Some(comment) = CommentAttributes::from_attributes(self.attributes).first() { tokens.extend(quote! { From 24886c6e5fedf464a5b51c067ee07d4dd3a1a075 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Sat, 28 May 2022 22:44:38 +1000 Subject: [PATCH 13/20] Remove todo, it's documented in the PR now --- utoipa-gen/src/schema/component.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/utoipa-gen/src/schema/component.rs b/utoipa-gen/src/schema/component.rs index 2579560e..efc9575e 100644 --- a/utoipa-gen/src/schema/component.rs +++ b/utoipa-gen/src/schema/component.rs @@ -581,7 +581,6 @@ impl ToTokens for ComplexEnum<'_> { let capacity = self.variants.len(); tokens.extend(quote! {}); - // TODO: refactor this to return Option let mut container_rules = serde::parse_container(self.attributes); let tag: Option = if let Some(Serde::Container(serde_container)) = &mut container_rules { From ced1497fbe842d3e88f25b902e5b469958ae7598 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Sun, 29 May 2022 09:57:01 +1000 Subject: [PATCH 14/20] Remove unnecessary Serde enum --- utoipa-gen/src/schema/component.rs | 46 ++++++++++++++---------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/utoipa-gen/src/schema/component.rs b/utoipa-gen/src/schema/component.rs index efc9575e..1becc901 100644 --- a/utoipa-gen/src/schema/component.rs +++ b/utoipa-gen/src/schema/component.rs @@ -1,5 +1,3 @@ -use std::mem; - use proc_macro2::{Ident, TokenStream as TokenStream2}; use proc_macro_error::{abort, ResultExt}; use quote::{quote, ToTokens}; @@ -20,7 +18,7 @@ use self::{ }; use super::{ - serde::{self, RenameRule, Serde}, + serde::{self, RenameRule, SerdeContainer, SerdeValue}, ComponentPart, GenericType, ValueType, }; @@ -455,7 +453,7 @@ impl ToTokens for SimpleEnum<'_> { .collect::>(); tokens.extend(match container_rules { - Some(Serde::Container(serde_container)) if serde_container.tag.is_some() => { + Some(serde_container) if serde_container.tag.is_some() => { let tag = serde_container.tag.expect("Expected tag to be present"); Self::tagged_variants_tokens(tag, enum_values) } @@ -582,12 +580,11 @@ impl ToTokens for ComplexEnum<'_> { tokens.extend(quote! {}); let mut container_rules = serde::parse_container(self.attributes); - let tag: Option = - if let Some(Serde::Container(serde_container)) = &mut container_rules { - serde_container.tag.take() - } else { - None - }; + let tag: Option = if let Some(serde_container) = &mut container_rules { + serde_container.tag.take() + } else { + None + }; // serde, externally tagged format supported by now let items: TokenStream2 = self @@ -794,9 +791,9 @@ where } #[inline] -fn is_not_skipped(rule: &Option) -> bool { +fn is_not_skipped(rule: &Option) -> bool { rule.as_ref() - .map(|rule| matches!(rule, Serde::Value(value) if value.skip == None)) + .map(|value| value.skip.is_none()) .unwrap_or(true) } @@ -806,8 +803,8 @@ fn is_not_skipped(rule: &Option) -> bool { /// rules. #[inline] fn rename_field<'a>( - container_rule: &'a mut Option, - field_rule: &'a mut Option, + container_rule: &'a mut Option, + field_rule: &'a mut Option, field: &str, ) -> Option { rename(container_rule, field_rule, &|rule| rule.rename(field)) @@ -819,8 +816,8 @@ fn rename_field<'a>( /// rules. #[inline] fn rename_variant<'a>( - container_rule: &'a mut Option, - field_rule: &'a mut Option, + container_rule: &'a mut Option, + field_rule: &'a mut Option, variant: &str, ) -> Option { rename(container_rule, field_rule, &|rule| { @@ -833,19 +830,18 @@ fn rename_variant<'a>( /// `Some` of the result of the `rename_op` if a rename is required by the supplied rules. #[inline] fn rename<'a>( - container_rule: &'a mut Option, - field_rule: &'a mut Option, + container_rule: &'a mut Option, + field_rule: &'a mut Option, rename_op: &impl Fn(&RenameRule) -> String, ) -> Option { - let rename = |rule: &mut Serde| match rule { - Serde::Container(container) => container.rename_all.as_ref().map(rename_op), - Serde::Value(ref mut value) => mem::take(&mut value.rename), - }; - field_rule .as_mut() - .and_then(rename) - .or_else(|| container_rule.as_mut().and_then(rename)) + .and_then(|value| value.rename.take()) + .or_else(|| { + container_rule + .as_mut() + .and_then(|container| container.rename_all.as_ref().map(rename_op)) + }) } #[cfg_attr(feature = "debug", derive(Debug))] From 7c7a9110e4021e41187122e74ceee0b744bf5211 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Sun, 29 May 2022 10:04:45 +1000 Subject: [PATCH 15/20] Remove unecessary Serde enum --- utoipa-gen/src/schema.rs | 123 +++++++++++++++++++++------------------ 1 file changed, 66 insertions(+), 57 deletions(-) diff --git a/utoipa-gen/src/schema.rs b/utoipa-gen/src/schema.rs index 7d38d12e..f4a36bde 100644 --- a/utoipa-gen/src/schema.rs +++ b/utoipa-gen/src/schema.rs @@ -214,47 +214,44 @@ pub mod serde { use std::str::FromStr; - use proc_macro2::{Span, TokenTree}; + use proc_macro2::{Span, TokenTree, Ident}; use proc_macro_error::ResultExt; use syn::{buffer::Cursor, Attribute, Error}; - #[cfg_attr(feature = "debug", derive(Debug))] - pub enum Serde { - Container(SerdeContainer), - Value(SerdeValue), - } - - impl Serde { - #[inline] - fn parse_next_lit_str(next: Cursor) -> Option<(String, Span)> { - match next.token_tree() { - Some((tt, next)) => match tt { - TokenTree::Punct(punct) if punct.as_char() == '=' => { - Serde::parse_next_lit_str(next) - } - TokenTree::Literal(literal) => { - Some((literal.to_string().replace('\"', ""), literal.span())) - } - _ => None, - }, + fn parse_next_lit_str(next: Cursor) -> Option<(String, Span)> { + match next.token_tree() { + Some((tt, next)) => match tt { + TokenTree::Punct(punct) if punct.as_char() == '=' => { + parse_next_lit_str(next) + } + TokenTree::Literal(literal) => { + Some((literal.to_string().replace('\"', ""), literal.span())) + } _ => None, - } + }, + _ => None, } + } + + #[derive(Default)] + #[cfg_attr(feature = "debug", derive(Debug))] + pub struct SerdeValue { + pub skip: Option, + pub rename: Option, + } - fn parse_container(input: syn::parse::ParseStream) -> syn::Result { - let mut container = SerdeContainer::default(); + impl SerdeValue { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let mut value = Self::default(); input.step(|cursor| { let mut rest = *cursor; while let Some((tt, next)) = rest.token_tree() { match tt { - TokenTree::Ident(ident) if ident == "rename_all" => { - if let Some((literal, span)) = Serde::parse_next_lit_str(next) { - container.rename_all = Some( - literal - .parse::() - .map_err(|error| Error::new(span, error.to_string()))?, - ); + TokenTree::Ident(ident) if ident == "skip" => value.skip = Some(true), + TokenTree::Ident(ident) if ident == "rename" => { + if let Some((literal, _)) = parse_next_lit_str(next) { + value.rename = Some(literal) }; } _ => (), @@ -265,22 +262,47 @@ pub mod serde { Ok(((), rest)) })?; - Ok(Serde::Container(container)) + Ok(value) + } + } + + #[derive(Default)] + #[cfg_attr(feature = "debug", derive(Debug))] + pub struct SerdeContainer { + pub rename_all: Option, + pub tag: Option, + } + + impl SerdeContainer { + fn parse_ident(ident: Ident, next: Cursor, container: &mut SerdeContainer) -> syn::Result<()> { + match ident.to_string().as_str() { + "rename_all" => { + if let Some((literal, span)) = parse_next_lit_str(next) { + container.rename_all = Some( + literal + .parse::() + .map_err(|error| Error::new(span, error.to_string()))?, + ); + }; + } + "tag" => { + if let Some((literal, _span)) = parse_next_lit_str(next) { + container.tag = Some(literal) + } + } + _ => {}, + } + Ok(()) } - fn parse_value(input: syn::parse::ParseStream) -> syn::Result { - let mut value = SerdeValue::default(); + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let mut container = Self::default(); input.step(|cursor| { let mut rest = *cursor; while let Some((tt, next)) = rest.token_tree() { match tt { - TokenTree::Ident(ident) if ident == "skip" => value.skip = Some(true), - TokenTree::Ident(ident) if ident == "rename" => { - if let Some((literal, _)) = Serde::parse_next_lit_str(next) { - value.rename = Some(literal) - }; - } + TokenTree::Ident(ident) => Self::parse_ident(ident, next, &mut container)?, _ => (), } @@ -289,41 +311,28 @@ pub mod serde { Ok(((), rest)) })?; - Ok(Serde::Value(value)) + Ok(container) } } - #[derive(Default)] - #[cfg_attr(feature = "debug", derive(Debug))] - pub struct SerdeValue { - pub skip: Option, - pub rename: Option, - } - - #[derive(Default)] - #[cfg_attr(feature = "debug", derive(Debug))] - pub struct SerdeContainer { - pub rename_all: Option, - } - - pub fn parse_value(attributes: &[Attribute]) -> Option { + pub fn parse_value(attributes: &[Attribute]) -> Option { attributes .iter() .find(|attribute| attribute.path.is_ident("serde")) .map(|serde_attribute| { serde_attribute - .parse_args_with(Serde::parse_value) + .parse_args_with(SerdeValue::parse) .unwrap_or_abort() }) } - pub fn parse_container(attributes: &[Attribute]) -> Option { + pub fn parse_container(attributes: &[Attribute]) -> Option { attributes .iter() .find(|attribute| attribute.path.is_ident("serde")) .map(|serde_attribute| { serde_attribute - .parse_args_with(Serde::parse_container) + .parse_args_with(SerdeContainer::parse) .unwrap_or_abort() }) } From 9fd6759dd98884b3e0e13a24ce6547d3b112733e Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Sun, 29 May 2022 10:06:52 +1000 Subject: [PATCH 16/20] Reformat code in schema.rs --- utoipa-gen/src/schema.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/utoipa-gen/src/schema.rs b/utoipa-gen/src/schema.rs index f4a36bde..a788d31d 100644 --- a/utoipa-gen/src/schema.rs +++ b/utoipa-gen/src/schema.rs @@ -214,16 +214,14 @@ pub mod serde { use std::str::FromStr; - use proc_macro2::{Span, TokenTree, Ident}; + use proc_macro2::{Ident, Span, TokenTree}; use proc_macro_error::ResultExt; use syn::{buffer::Cursor, Attribute, Error}; fn parse_next_lit_str(next: Cursor) -> Option<(String, Span)> { match next.token_tree() { Some((tt, next)) => match tt { - TokenTree::Punct(punct) if punct.as_char() == '=' => { - parse_next_lit_str(next) - } + TokenTree::Punct(punct) if punct.as_char() == '=' => parse_next_lit_str(next), TokenTree::Literal(literal) => { Some((literal.to_string().replace('\"', ""), literal.span())) } @@ -274,7 +272,11 @@ pub mod serde { } impl SerdeContainer { - fn parse_ident(ident: Ident, next: Cursor, container: &mut SerdeContainer) -> syn::Result<()> { + fn parse_ident( + ident: Ident, + next: Cursor, + container: &mut SerdeContainer, + ) -> syn::Result<()> { match ident.to_string().as_str() { "rename_all" => { if let Some((literal, span)) = parse_next_lit_str(next) { @@ -290,7 +292,7 @@ pub mod serde { container.tag = Some(literal) } } - _ => {}, + _ => {} } Ok(()) } From 824041b4416c57dc0806e14d3b506b5bd4d8c816 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Sun, 29 May 2022 10:11:35 +1000 Subject: [PATCH 17/20] Remove an unecessary test from component_derive_tests.rs --- tests/component_derive_test.rs | 30 +++--------------------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/tests/component_derive_test.rs b/tests/component_derive_test.rs index bb2de287..e7b0ee63 100644 --- a/tests/component_derive_test.rs +++ b/tests/component_derive_test.rs @@ -507,33 +507,6 @@ fn derive_with_box_and_refcell() { }; } -#[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> - }, - 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" - } -} - #[test] fn derive_simple_enum() { let value: Value = api_doc! { @@ -621,6 +594,7 @@ fn derive_simple_enum_serde_tag() { ); } +/// Derive a complex enum with named and unnamed fields. #[test] fn derive_complex_enum() { #[derive(Serialize)] @@ -813,6 +787,8 @@ fn derive_complex_enum_serde_rename_variant() { ); } +/// Derive a complex enum with the serde `tag` container attribute applied for internal tagging. +/// Note that tuple fields are not supported. #[test] fn derive_complex_enum_serde_tag() { #[derive(Serialize)] From 4dce7e94316842f041dee04bdf6c81159baa2aab Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Wed, 15 Jun 2022 11:41:31 +1000 Subject: [PATCH 18/20] Refactored SerdeContainer parse_ident method for PR --- utoipa-gen/src/schema.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/utoipa-gen/src/schema.rs b/utoipa-gen/src/schema.rs index a788d31d..31c1b12f 100644 --- a/utoipa-gen/src/schema.rs +++ b/utoipa-gen/src/schema.rs @@ -218,6 +218,7 @@ pub mod serde { use proc_macro_error::ResultExt; use syn::{buffer::Cursor, Attribute, Error}; + #[inline] fn parse_next_lit_str(next: Cursor) -> Option<(String, Span)> { match next.token_tree() { Some((tt, next)) => match tt { @@ -264,6 +265,7 @@ pub mod serde { } } + /// Attributes defined within a `#[serde(...)]` container attribute. #[derive(Default)] #[cfg_attr(feature = "debug", derive(Debug))] pub struct SerdeContainer { @@ -272,15 +274,13 @@ pub mod serde { } impl SerdeContainer { - fn parse_ident( - ident: Ident, - next: Cursor, - container: &mut SerdeContainer, - ) -> syn::Result<()> { + /// Parse a single serde attribute, currently `rename_all = ...` and `tag = ...` attributes + /// are supported. + fn parse_attribute(&mut self, ident: Ident, next: Cursor) -> syn::Result<()> { match ident.to_string().as_str() { "rename_all" => { if let Some((literal, span)) = parse_next_lit_str(next) { - container.rename_all = Some( + self.rename_all = Some( literal .parse::() .map_err(|error| Error::new(span, error.to_string()))?, @@ -289,7 +289,7 @@ pub mod serde { } "tag" => { if let Some((literal, _span)) = parse_next_lit_str(next) { - container.tag = Some(literal) + self.tag = Some(literal) } } _ => {} @@ -297,6 +297,7 @@ pub mod serde { Ok(()) } + /// Parse the attributes inside a `#[serde(...)]` container attribute. fn parse(input: syn::parse::ParseStream) -> syn::Result { let mut container = Self::default(); @@ -304,7 +305,7 @@ pub mod serde { let mut rest = *cursor; while let Some((tt, next)) = rest.token_tree() { match tt { - TokenTree::Ident(ident) => Self::parse_ident(ident, next, &mut container)?, + TokenTree::Ident(ident) => container.parse_attribute(ident, next)?, _ => (), } From 2c13eb72a3a0fd7af344bc2a0b7aa455c1cefa14 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Wed, 15 Jun 2022 11:56:04 +1000 Subject: [PATCH 19/20] Remove useless tokens.extend() --- utoipa-gen/src/schema/component.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/utoipa-gen/src/schema/component.rs b/utoipa-gen/src/schema/component.rs index 1becc901..6a678aed 100644 --- a/utoipa-gen/src/schema/component.rs +++ b/utoipa-gen/src/schema/component.rs @@ -577,7 +577,6 @@ impl ToTokens for ComplexEnum<'_> { } let capacity = self.variants.len(); - tokens.extend(quote! {}); let mut container_rules = serde::parse_container(self.attributes); let tag: Option = if let Some(serde_container) = &mut container_rules { From f3eba2bd96dbea0e92e4db7bdc35edb0741a03b7 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Wed, 15 Jun 2022 12:03:31 +1000 Subject: [PATCH 20/20] Remove unnecessary mut references from serde rename attribute implementation --- utoipa-gen/src/schema/component.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/utoipa-gen/src/schema/component.rs b/utoipa-gen/src/schema/component.rs index 6a678aed..5a164ad6 100644 --- a/utoipa-gen/src/schema/component.rs +++ b/utoipa-gen/src/schema/component.rs @@ -802,8 +802,8 @@ fn is_not_skipped(rule: &Option) -> bool { /// rules. #[inline] fn rename_field<'a>( - container_rule: &'a mut Option, - field_rule: &'a mut Option, + container_rule: &'a Option, + field_rule: &'a Option, field: &str, ) -> Option { rename(container_rule, field_rule, &|rule| rule.rename(field)) @@ -815,8 +815,8 @@ fn rename_field<'a>( /// rules. #[inline] fn rename_variant<'a>( - container_rule: &'a mut Option, - field_rule: &'a mut Option, + container_rule: &'a Option, + field_rule: &'a Option, variant: &str, ) -> Option { rename(container_rule, field_rule, &|rule| { @@ -829,16 +829,16 @@ fn rename_variant<'a>( /// `Some` of the result of the `rename_op` if a rename is required by the supplied rules. #[inline] fn rename<'a>( - container_rule: &'a mut Option, - field_rule: &'a mut Option, + container_rule: &'a Option, + field_rule: &'a Option, rename_op: &impl Fn(&RenameRule) -> String, ) -> Option { field_rule - .as_mut() - .and_then(|value| value.rename.take()) + .as_ref() + .and_then(|value| value.rename.clone()) .or_else(|| { container_rule - .as_mut() + .as_ref() .and_then(|container| container.rename_all.as_ref().map(rename_op)) }) }