Skip to content

Commit 59270e6

Browse files
committed
Refactor enums processing
This commit refactors enum processing from ground up. This fully unifies the how each enum variant schema is resolved and the way variant schemas are generated. This will result easier debugging, changing and updating the enum processing in future and most of all the enums now will behave consistently due to removing bunch of duplication and adding correct abstractions instead. This commit also unifies previously known `SimpleEnum` and `ReprEnum` to a single enum to furhter simplify the code. Also `ComplexEnum` is now known by `MixedEnum`. This commit implements discriminator with support for custom mapping. Discriminator can only be used with `#[serde(untagged)]` enum having only unnamed field variants with one schema reference implementing `ToSchema` trait. It cannot be used with primitive types nor with inlined schemas. Removed `#[serde(tag = ...)]` as discriminator support. Update docs and add support for missing features for enum variants such as `Title`, `Deprecated`, `MinProperties` and `MaxProperties`. ### Breaking changes * `#[serde(tag = ...)]` will not be used as discriminator.
1 parent 576f6c1 commit 59270e6

File tree

16 files changed

+1978
-1630
lines changed

16 files changed

+1978
-1630
lines changed

utoipa-gen/src/component.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ impl<'t> TypeTree<'t> {
265265
fn convert(path: &'t Path, last_segment: &'t PathSegment) -> TypeTree<'t> {
266266
let generic_type = Self::get_generic_type(last_segment);
267267
let schema_type = SchemaType {
268-
path,
268+
path: Cow::Borrowed(path),
269269
nullable: matches!(generic_type, Some(GenericType::Option)),
270270
};
271271

@@ -500,11 +500,11 @@ trait Rename {
500500
/// * `value` to rename.
501501
/// * `to` Optional rename to value for fields with _`rename`_ property.
502502
/// * `container_rule` which is used to rename containers with _`rename_all`_ property.
503-
fn rename<'r, R: Rename>(
504-
value: &'r str,
505-
to: Option<Cow<'r, str>>,
506-
container_rule: Option<&'r RenameRule>,
507-
) -> Option<Cow<'r, str>> {
503+
fn rename<'s, R: Rename>(
504+
value: &str,
505+
to: Option<Cow<'s, str>>,
506+
container_rule: Option<&RenameRule>,
507+
) -> Option<Cow<'s, str>> {
508508
let rename = to.and_then(|to| if !to.is_empty() { Some(to) } else { None });
509509

510510
rename.or_else(|| {
@@ -841,7 +841,7 @@ impl<'c> ComponentSchema {
841841
let validate = |feature: &Feature| {
842842
let type_path = &**type_tree.path.as_ref().unwrap();
843843
let schema_type = SchemaType {
844-
path: type_path,
844+
path: Cow::Borrowed(type_path),
845845
nullable: nullable
846846
.map(|nullable| nullable.value())
847847
.unwrap_or_default(),
@@ -894,7 +894,7 @@ impl<'c> ComponentSchema {
894894
ValueType::Primitive => {
895895
let type_path = &**type_tree.path.as_ref().unwrap();
896896
let schema_type = SchemaType {
897-
path: type_path,
897+
path: Cow::Borrowed(type_path),
898898
nullable,
899899
};
900900
if schema_type.is_unsigned_integer() {

utoipa-gen/src/component/features.rs

Lines changed: 74 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ pub enum Feature {
100100
Required(attributes::Required),
101101
ContentEncoding(attributes::ContentEncoding),
102102
ContentMediaType(attributes::ContentMediaType),
103+
Discriminator(attributes::Discriminator),
103104
MultipleOf(validation::MultipleOf),
104105
Maximum(validation::Maximum),
105106
Minimum(validation::Minimum),
@@ -166,73 +167,74 @@ impl Feature {
166167
impl ToTokensDiagnostics for Feature {
167168
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) -> Result<(), Diagnostics> {
168169
let feature = match &self {
169-
Feature::Default(default) => quote! { .default(#default) },
170-
Feature::Example(example) => quote! { .example(Some(#example)) },
171-
Feature::Examples(examples) => quote! { .examples(#examples) },
172-
Feature::XmlAttr(xml) => quote! { .xml(Some(#xml)) },
173-
Feature::Format(format) => quote! { .format(Some(#format)) },
174-
Feature::WriteOnly(write_only) => quote! { .write_only(Some(#write_only)) },
175-
Feature::ReadOnly(read_only) => quote! { .read_only(Some(#read_only)) },
176-
Feature::Title(title) => quote! { .title(Some(#title)) },
177-
Feature::Nullable(_nullable) => return Err(Diagnostics::new("Nullable does not support `ToTokens`")),
178-
Feature::Rename(rename) => rename.to_token_stream(),
179-
Feature::Style(style) => quote! { .style(Some(#style)) },
180-
Feature::ParameterIn(parameter_in) => quote! { .parameter_in(#parameter_in) },
181-
Feature::MultipleOf(multiple_of) => quote! { .multiple_of(Some(#multiple_of)) },
182-
Feature::AllowReserved(allow_reserved) => {
183-
quote! { .allow_reserved(Some(#allow_reserved)) }
184-
}
185-
Feature::Explode(explode) => quote! { .explode(Some(#explode)) },
186-
Feature::Maximum(maximum) => quote! { .maximum(Some(#maximum)) },
187-
Feature::Minimum(minimum) => quote! { .minimum(Some(#minimum)) },
188-
Feature::ExclusiveMaximum(exclusive_maximum) => {
189-
quote! { .exclusive_maximum(Some(#exclusive_maximum)) }
190-
}
191-
Feature::ExclusiveMinimum(exclusive_minimum) => {
192-
quote! { .exclusive_minimum(Some(#exclusive_minimum)) }
193-
}
194-
Feature::MaxLength(max_length) => quote! { .max_length(Some(#max_length)) },
195-
Feature::MinLength(min_length) => quote! { .min_length(Some(#min_length)) },
196-
Feature::Pattern(pattern) => quote! { .pattern(Some(#pattern)) },
197-
Feature::MaxItems(max_items) => quote! { .max_items(Some(#max_items)) },
198-
Feature::MinItems(min_items) => quote! { .min_items(Some(#min_items)) },
199-
Feature::MaxProperties(max_properties) => {
200-
quote! { .max_properties(Some(#max_properties)) }
201-
}
202-
Feature::MinProperties(min_properties) => {
203-
quote! { .max_properties(Some(#min_properties)) }
204-
}
205-
Feature::SchemaWith(schema_with) => schema_with.to_token_stream(),
206-
Feature::Description(description) => quote! { .description(Some(#description)) },
207-
Feature::Deprecated(deprecated) => quote! { .deprecated(Some(#deprecated)) },
208-
Feature::AdditionalProperties(additional_properties) => {
209-
quote! { .additional_properties(Some(#additional_properties)) }
210-
}
211-
Feature::ContentEncoding(content_encoding) => quote! { .content_encoding(#content_encoding) },
212-
Feature::ContentMediaType(content_media_type) => quote! { .content_media_type(#content_media_type) },
213-
Feature::RenameAll(_) => {
214-
return Err(Diagnostics::new("RenameAll feature does not support `ToTokens`"))
215-
}
216-
Feature::ValueType(_) => {
217-
return Err(Diagnostics::new("ValueType feature does not support `ToTokens`")
218-
.help("ValueType is supposed to be used with `TypeTree` in same manner as a resolved struct/field type."))
219-
}
220-
Feature::Inline(_) => {
221-
// inline feature is ignored by `ToTokens`
222-
TokenStream::new()
223-
}
224-
Feature::IntoParamsNames(_) => {
225-
return Err(Diagnostics::new("Names feature does not support `ToTokens`")
226-
.help("Names is only used with IntoParams to artificially give names for unnamed struct type `IntoParams`."))
227-
}
228-
Feature::As(_) => {
229-
return Err(Diagnostics::new("As does not support `ToTokens`"))
230-
}
231-
Feature::Required(required) => {
232-
let name = <attributes::Required as FeatureLike>::get_name();
233-
quote! { .#name(#required) }
234-
}
235-
};
170+
Feature::Default(default) => quote! { .default(#default) },
171+
Feature::Example(example) => quote! { .example(Some(#example)) },
172+
Feature::Examples(examples) => quote! { .examples(#examples) },
173+
Feature::XmlAttr(xml) => quote! { .xml(Some(#xml)) },
174+
Feature::Format(format) => quote! { .format(Some(#format)) },
175+
Feature::WriteOnly(write_only) => quote! { .write_only(Some(#write_only)) },
176+
Feature::ReadOnly(read_only) => quote! { .read_only(Some(#read_only)) },
177+
Feature::Title(title) => quote! { .title(Some(#title)) },
178+
Feature::Nullable(_nullable) => return Err(Diagnostics::new("Nullable does not support `ToTokens`")),
179+
Feature::Rename(rename) => rename.to_token_stream(),
180+
Feature::Style(style) => quote! { .style(Some(#style)) },
181+
Feature::ParameterIn(parameter_in) => quote! { .parameter_in(#parameter_in) },
182+
Feature::MultipleOf(multiple_of) => quote! { .multiple_of(Some(#multiple_of)) },
183+
Feature::AllowReserved(allow_reserved) => {
184+
quote! { .allow_reserved(Some(#allow_reserved)) }
185+
}
186+
Feature::Explode(explode) => quote! { .explode(Some(#explode)) },
187+
Feature::Maximum(maximum) => quote! { .maximum(Some(#maximum)) },
188+
Feature::Minimum(minimum) => quote! { .minimum(Some(#minimum)) },
189+
Feature::ExclusiveMaximum(exclusive_maximum) => {
190+
quote! { .exclusive_maximum(Some(#exclusive_maximum)) }
191+
}
192+
Feature::ExclusiveMinimum(exclusive_minimum) => {
193+
quote! { .exclusive_minimum(Some(#exclusive_minimum)) }
194+
}
195+
Feature::MaxLength(max_length) => quote! { .max_length(Some(#max_length)) },
196+
Feature::MinLength(min_length) => quote! { .min_length(Some(#min_length)) },
197+
Feature::Pattern(pattern) => quote! { .pattern(Some(#pattern)) },
198+
Feature::MaxItems(max_items) => quote! { .max_items(Some(#max_items)) },
199+
Feature::MinItems(min_items) => quote! { .min_items(Some(#min_items)) },
200+
Feature::MaxProperties(max_properties) => {
201+
quote! { .max_properties(Some(#max_properties)) }
202+
}
203+
Feature::MinProperties(min_properties) => {
204+
quote! { .max_properties(Some(#min_properties)) }
205+
}
206+
Feature::SchemaWith(schema_with) => schema_with.to_token_stream(),
207+
Feature::Description(description) => quote! { .description(Some(#description)) },
208+
Feature::Deprecated(deprecated) => quote! { .deprecated(Some(#deprecated)) },
209+
Feature::AdditionalProperties(additional_properties) => {
210+
quote! { .additional_properties(Some(#additional_properties)) }
211+
}
212+
Feature::ContentEncoding(content_encoding) => quote! { .content_encoding(#content_encoding) },
213+
Feature::ContentMediaType(content_media_type) => quote! { .content_media_type(#content_media_type) },
214+
Feature::Discriminator(discriminator) => quote! { .discriminator(Some(#discriminator)) },
215+
Feature::RenameAll(_) => {
216+
return Err(Diagnostics::new("RenameAll feature does not support `ToTokens`"))
217+
}
218+
Feature::ValueType(_) => {
219+
return Err(Diagnostics::new("ValueType feature does not support `ToTokens`")
220+
.help("ValueType is supposed to be used with `TypeTree` in same manner as a resolved struct/field type."))
221+
}
222+
Feature::Inline(_) => {
223+
// inline feature is ignored by `ToTokens`
224+
TokenStream::new()
225+
}
226+
Feature::IntoParamsNames(_) => {
227+
return Err(Diagnostics::new("Names feature does not support `ToTokens`")
228+
.help("Names is only used with IntoParams to artificially give names for unnamed struct type `IntoParams`."))
229+
}
230+
Feature::As(_) => {
231+
return Err(Diagnostics::new("As does not support `ToTokens`"))
232+
}
233+
Feature::Required(required) => {
234+
let name = <attributes::Required as FeatureLike>::get_name();
235+
quote! { .#name(#required) }
236+
}
237+
};
236238

237239
tokens.extend(feature);
238240

@@ -291,6 +293,7 @@ impl Display for Feature {
291293
Feature::Required(required) => required.fmt(f),
292294
Feature::ContentEncoding(content_encoding) => content_encoding.fmt(f),
293295
Feature::ContentMediaType(content_media_type) => content_media_type.fmt(f),
296+
Feature::Discriminator(discriminator) => discriminator.fmt(f),
294297
}
295298
}
296299
}
@@ -338,6 +341,7 @@ impl Validatable for Feature {
338341
Feature::Required(required) => required.is_validatable(),
339342
Feature::ContentEncoding(content_encoding) => content_encoding.is_validatable(),
340343
Feature::ContentMediaType(content_media_type) => content_media_type.is_validatable(),
344+
Feature::Discriminator(discriminator) => discriminator.is_validatable(),
341345
}
342346
}
343347
}
@@ -383,6 +387,7 @@ is_validatable! {
383387
attributes::Required,
384388
attributes::ContentEncoding,
385389
attributes::ContentMediaType,
390+
attributes::Discriminator,
386391
validation::MultipleOf = true,
387392
validation::Maximum = true,
388393
validation::Minimum = true,
@@ -607,8 +612,9 @@ impl_feature_into_inner! {
607612
attributes::Description,
608613
attributes::Deprecated,
609614
attributes::As,
610-
attributes::AdditionalProperties,
611615
attributes::Required,
616+
attributes::AdditionalProperties,
617+
attributes::Discriminator,
612618
validation::MultipleOf,
613619
validation::Maximum,
614620
validation::Minimum,

utoipa-gen/src/component/features/attributes.rs

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ use proc_macro2::{Ident, TokenStream};
44
use quote::ToTokens;
55
use syn::parse::ParseStream;
66
use syn::punctuated::Punctuated;
7-
use syn::{LitStr, Token, TypePath};
7+
use syn::token::Paren;
8+
use syn::{Error, LitStr, Token, TypePath};
89

910
use crate::component::serde::RenameRule;
1011
use crate::component::{schema, GenericType, TypeTree};
12+
use crate::parse_utils::LitStrOrExpr;
1113
use crate::path::parameter::{self, ParameterStyle};
1214
use crate::schema_type::SchemaFormat;
1315
use crate::{parse_utils, AnyValue, Array, Diagnostics};
@@ -649,6 +651,12 @@ impl From<Deprecated> for Feature {
649651
}
650652
}
651653

654+
impl From<bool> for Deprecated {
655+
fn from(value: bool) -> Self {
656+
Self(value)
657+
}
658+
}
659+
652660
impl_feature! {
653661
#[cfg_attr(feature = "debug", derive(Debug))]
654662
#[derive(Clone)]
@@ -804,3 +812,128 @@ impl From<ContentMediaType> for Feature {
804812
Self::ContentMediaType(value)
805813
}
806814
}
815+
816+
// discriminator = ...
817+
// discriminator(property_name = ..., mapping(
818+
// (value = ...),
819+
// (value2 = ...)
820+
// ))
821+
impl_feature! {
822+
#[derive(Clone)]
823+
#[cfg_attr(feature = "debug", derive(Debug))]
824+
pub struct Discriminator(LitStrOrExpr, Punctuated<(LitStrOrExpr, LitStrOrExpr), Token![,]>, Ident);
825+
}
826+
827+
impl Discriminator {
828+
fn new(attribute: Ident) -> Self {
829+
Self(LitStrOrExpr::default(), Punctuated::default(), attribute)
830+
}
831+
832+
pub fn get_attribute(&self) -> &Ident {
833+
&self.2
834+
}
835+
}
836+
837+
impl Parse for Discriminator {
838+
fn parse(input: ParseStream, attribute: Ident) -> syn::Result<Self>
839+
where
840+
Self: std::marker::Sized,
841+
{
842+
let lookahead = input.lookahead1();
843+
if lookahead.peek(Token![=]) {
844+
parse_utils::parse_next_literal_str_or_expr(input)
845+
.map(|property_name| Self(property_name, Punctuated::new(), attribute))
846+
} else if lookahead.peek(Paren) {
847+
let discriminator_stream;
848+
syn::parenthesized!(discriminator_stream in input);
849+
850+
let mut discriminator = Discriminator::new(attribute);
851+
852+
while !discriminator_stream.is_empty() {
853+
let property = discriminator_stream.parse::<Ident>()?;
854+
let name = &*property.to_string();
855+
856+
match name {
857+
"property_name" => {
858+
discriminator.0 =
859+
parse_utils::parse_next_literal_str_or_expr(&discriminator_stream)?
860+
}
861+
"mapping" => {
862+
let mapping_stream;
863+
syn::parenthesized!(mapping_stream in &discriminator_stream);
864+
let mappings: Punctuated<(LitStrOrExpr, LitStrOrExpr), Token![,]> =
865+
Punctuated::parse_terminated_with(&mapping_stream, |input| {
866+
let inner;
867+
syn::parenthesized!(inner in input);
868+
869+
let key = inner.parse::<LitStrOrExpr>()?;
870+
inner.parse::<Token![=]>()?;
871+
let value = inner.parse::<LitStrOrExpr>()?;
872+
873+
Ok((key, value))
874+
})?;
875+
discriminator.1 = mappings;
876+
}
877+
unexpected => {
878+
return Err(Error::new(
879+
property.span(),
880+
&format!(
881+
"unexpected identifier {}, expected any of: property_name, mapping",
882+
unexpected
883+
),
884+
))
885+
}
886+
}
887+
888+
if !discriminator_stream.is_empty() {
889+
discriminator_stream.parse::<Token![,]>()?;
890+
}
891+
}
892+
893+
Ok(discriminator)
894+
} else {
895+
Err(lookahead.error())
896+
}
897+
}
898+
}
899+
900+
impl ToTokens for Discriminator {
901+
fn to_tokens(&self, tokens: &mut TokenStream) {
902+
let Discriminator(property_name, mapping, _) = self;
903+
904+
struct Mapping<'m>(&'m LitStrOrExpr, &'m LitStrOrExpr);
905+
906+
impl ToTokens for Mapping<'_> {
907+
fn to_tokens(&self, tokens: &mut TokenStream) {
908+
let Mapping(property_name, value) = *self;
909+
910+
tokens.extend(quote! {
911+
(#property_name, #value)
912+
})
913+
}
914+
}
915+
916+
let discriminator = if !mapping.is_empty() {
917+
let mapping = mapping
918+
.iter()
919+
.map(|(key, value)| Mapping(key, value))
920+
.collect::<Array<Mapping>>();
921+
922+
quote! {
923+
utoipa::openapi::schema::Discriminator::with_mapping(#property_name, #mapping)
924+
}
925+
} else {
926+
quote! {
927+
utoipa::openapi::schema::Discriminator::new(#property_name)
928+
}
929+
};
930+
931+
discriminator.to_tokens(tokens);
932+
}
933+
}
934+
935+
impl From<Discriminator> for Feature {
936+
fn from(value: Discriminator) -> Self {
937+
Self::Discriminator(value)
938+
}
939+
}

0 commit comments

Comments
 (0)