use std::borrow::Cow;
use proc_macro2::{Ident, Span, TokenStream};
use proc_macro_error::abort;
use quote::{format_ident, quote, ToTokens};
use syn::{
parse::Parse, parse_quote, punctuated::Punctuated, spanned::Spanned, token::Comma, Attribute,
Data, Field, Fields, FieldsNamed, FieldsUnnamed, GenericArgument, GenericParam, Generics,
Lifetime, LifetimeParam, Path, PathArguments, Token, Type, Variant, Visibility,
};
use crate::{
component::features::{Example, Rename},
doc_comment::CommentAttributes,
Array, Deprecated, ResultExt,
};
use self::{
enum_variant::{
AdjacentlyTaggedEnum, CustomEnum, Enum, ObjectVariant, SimpleEnumVariant, TaggedEnum,
UntaggedEnum,
},
features::{
ComplexEnumFeatures, EnumFeatures, EnumNamedFieldVariantFeatures,
EnumUnnamedFieldVariantFeatures, FromAttributes, NamedFieldFeatures,
NamedFieldStructFeatures, UnnamedFieldStructFeatures,
},
};
use super::{
features::{
parse_features, pop_feature, pop_feature_as_inner, As, Feature, FeaturesExt, IntoInner,
RenameAll, ToTokensExt,
},
serde::{self, SerdeContainer, SerdeEnumRepr, SerdeValue},
ComponentSchema, FieldRename, TypeTree, ValueType, VariantRename,
};
mod enum_variant;
mod features;
pub mod xml;
pub struct Schema<'a> {
ident: &'a Ident,
attributes: &'a [Attribute],
generics: &'a Generics,
aliases: Option<Punctuated<AliasSchema, Comma>>,
data: &'a Data,
vis: &'a Visibility,
}
impl<'a> Schema<'a> {
const TO_SCHEMA_LIFETIME: &'static str = "'__s";
pub fn new(
data: &'a Data,
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,
attributes,
generics,
aliases,
vis,
}
}
}
impl ToTokens for Schema<'_> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let ident = self.ident;
let variant = SchemaVariant::new(
self.data,
self.attributes,
ident,
self.generics,
None::<Vec<(TypeTree, &TypeTree)>>,
);
let (_, ty_generics, where_clause) = self.generics.split_for_impl();
let life = &Lifetime::new(Schema::TO_SCHEMA_LIFETIME, Span::call_site());
let schema_ty: Type = parse_quote!(#ident #ty_generics);
let schema_children = &*TypeTree::from_type(&schema_ty).children.unwrap_or_default();
let aliases = self.aliases.as_ref().map(|aliases| {
let alias_schemas = aliases
.iter()
.map(|alias| {
let name = &*alias.name;
let alias_type_tree = TypeTree::from_type(&alias.ty);
let variant = SchemaVariant::new(
self.data,
self.attributes,
ident,
self.generics,
alias_type_tree
.children
.map(|children| children.into_iter().zip(schema_children)),
);
quote! { (#name, #variant.into()) }
})
.collect::<Array<TokenStream>>();
quote! {
fn aliases() -> Vec<(& #life str, utoipa::openapi::schema::Schema)> {
#alias_schemas.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 vis = self.vis;
let name_generics = alias.get_lifetimes().fold(
Punctuated::<&GenericArgument, Comma>::new(),
|mut acc, lifetime| {
acc.push(lifetime);
acc
},
);
quote! {
#vis type #name < #name_generics > = #ty;
}
})
.collect::<TokenStream>()
});
let name = if let Some(schema_as) = variant.get_schema_as() {
format_path_ref(&schema_as.0.path)
} else {
ident.to_string()
};
let schema_lifetime: GenericParam = LifetimeParam::new(life.clone()).into();
let schema_generics = Generics {
params: [schema_lifetime.clone()].into_iter().collect(),
..Default::default()
};
let mut impl_generics = self.generics.clone();
impl_generics.params.push(schema_lifetime);
let (impl_generics, _, _) = impl_generics.split_for_impl();
tokens.extend(quote! {
impl #impl_generics utoipa::ToSchema #schema_generics for #ident #ty_generics #where_clause {
fn schema() -> (& #life str, utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>) {
(#name, #variant.into())
}
#aliases
}
#type_aliases
})
}
}
#[cfg_attr(feature = "debug", derive(Debug))]
enum SchemaVariant<'a> {
Named(NamedStructSchema<'a>),
Unnamed(UnnamedStructSchema<'a>),
Enum(EnumSchema<'a>),
Unit(UnitStructVariant),
}
impl<'a> SchemaVariant<'a> {
pub fn new<I: IntoIterator<Item = (TypeTree<'a>, &'a TypeTree<'a>)>>(
data: &'a Data,
attributes: &'a [Attribute],
ident: &'a Ident,
generics: &'a Generics,
aliases: Option<I>,
) -> SchemaVariant<'a> {
match data {
Data::Struct(content) => match &content.fields {
Fields::Unnamed(fields) => {
let FieldsUnnamed { unnamed, .. } = fields;
let mut unnamed_features = attributes
.parse_features::<UnnamedFieldStructFeatures>()
.into_inner();
let schema_as = pop_feature_as_inner!(unnamed_features => Feature::As(_v));
Self::Unnamed(UnnamedStructSchema {
struct_name: Cow::Owned(ident.to_string()),
attributes,
features: unnamed_features,
fields: unnamed,
schema_as,
})
}
Fields::Named(fields) => {
let FieldsNamed { named, .. } = fields;
let mut named_features = attributes
.parse_features::<NamedFieldStructFeatures>()
.into_inner();
let schema_as = pop_feature_as_inner!(named_features => Feature::As(_v));
Self::Named(NamedStructSchema {
struct_name: Cow::Owned(ident.to_string()),
attributes,
rename_all: named_features.pop_rename_all_feature(),
features: named_features,
fields: named,
generics: Some(generics),
schema_as,
aliases: aliases.map(|aliases| aliases.into_iter().collect()),
})
}
Fields::Unit => Self::Unit(UnitStructVariant),
},
Data::Enum(content) => Self::Enum(EnumSchema::new(
Cow::Owned(ident.to_string()),
&content.variants,
attributes,
)),
_ => abort!(
ident.span(),
"unexpected data type, expected syn::Data::Struct or syn::Data::Enum"
),
}
}
fn get_schema_as(&self) -> &Option<As> {
match self {
Self::Enum(schema) => &schema.schema_as,
Self::Named(schema) => &schema.schema_as,
Self::Unnamed(schema) => &schema.schema_as,
_ => &None,
}
}
}
impl ToTokens for SchemaVariant<'_> {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
Self::Enum(schema) => schema.to_tokens(tokens),
Self::Named(schema) => schema.to_tokens(tokens),
Self::Unnamed(schema) => schema.to_tokens(tokens),
Self::Unit(unit) => unit.to_tokens(tokens),
}
}
}
#[cfg_attr(feature = "debug", derive(Debug))]
struct UnitStructVariant;
impl ToTokens for UnitStructVariant {
fn to_tokens(&self, tokens: &mut TokenStream) {
tokens.extend(quote! {
utoipa::openapi::schema::empty()
});
}
}
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct NamedStructSchema<'a> {
pub struct_name: Cow<'a, str>,
pub fields: &'a Punctuated<Field, Comma>,
pub attributes: &'a [Attribute],
pub features: Option<Vec<Feature>>,
pub rename_all: Option<RenameAll>,
pub generics: Option<&'a Generics>,
pub aliases: Option<Vec<(TypeTree<'a>, &'a TypeTree<'a>)>>,
pub schema_as: Option<As>,
}
struct NamedStructFieldOptions<'a> {
property: Property,
rename_field_value: Option<Cow<'a, str>>,
required: Option<super::features::Required>,
is_option: bool,
}
impl NamedStructSchema<'_> {
fn field_as_schema_property<R>(
&self,
field: &Field,
container_rules: &Option<SerdeContainer>,
yield_: impl FnOnce(NamedStructFieldOptions<'_>) -> R,
) -> R {
let type_tree = &mut TypeTree::from_type(&field.ty);
if let Some(aliases) = &self.aliases {
for (new_generic, old_generic_matcher) in aliases.iter() {
if let Some(generic_match) = type_tree.find_mut(old_generic_matcher) {
*generic_match = new_generic.clone();
}
}
}
let mut field_features = field
.attrs
.parse_features::<NamedFieldFeatures>()
.into_inner();
let schema_default = self
.features
.as_ref()
.map(|features| features.iter().any(|f| matches!(f, Feature::Default(_))))
.unwrap_or(false);
let serde_default = container_rules
.as_ref()
.map(|rules| rules.default)
.unwrap_or(false);
if schema_default || serde_default {
let features_inner = field_features.get_or_insert(vec![]);
if !features_inner
.iter()
.any(|f| matches!(f, Feature::Default(_)))
{
let field_ident = field.ident.as_ref().unwrap().to_owned();
let struct_ident = format_ident!("{}", &self.struct_name);
features_inner.push(Feature::Default(
crate::features::Default::new_default_trait(struct_ident, field_ident.into()),
));
}
}
let deprecated = super::get_deprecated(&field.attrs).or_else(|| {
pop_feature!(field_features => Feature::Deprecated(_)).and_then(|feature| match feature
{
Feature::Deprecated(_) => Some(Deprecated::True),
_ => None,
})
});
let rename_field =
pop_feature!(field_features => Feature::Rename(_)).and_then(|feature| match feature {
Feature::Rename(rename) => Some(Cow::Owned(rename.into_value())),
_ => None,
});
let value_type = field_features
.as_mut()
.and_then(|features| features.pop_value_type_feature());
let override_type_tree = value_type
.as_ref()
.map(|value_type| value_type.as_type_tree());
let comments = CommentAttributes::from_attributes(&field.attrs);
let schema_with = pop_feature!(field_features => Feature::SchemaWith(_));
let required = pop_feature_as_inner!(field_features => Feature::Required(_v));
let type_tree = override_type_tree.as_ref().unwrap_or(type_tree);
let is_option = type_tree.is_option();
yield_(NamedStructFieldOptions {
property: if let Some(schema_with) = schema_with {
Property::SchemaWith(schema_with)
} else {
Property::Schema(ComponentSchema::new(super::ComponentSchemaProps {
type_tree,
features: field_features,
description: Some(&comments),
deprecated: deprecated.as_ref(),
object_name: self.struct_name.as_ref(),
}))
},
rename_field_value: rename_field,
required,
is_option,
})
}
}
impl ToTokens for NamedStructSchema<'_> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let container_rules = serde::parse_container(self.attributes);
let object_tokens = self
.fields
.iter()
.filter_map(|field| {
let field_rule = serde::parse_value(&field.attrs);
if is_not_skipped(&field_rule) && !is_flatten(&field_rule) {
Some((field, field_rule))
} else {
None
}
})
.fold(
quote! { utoipa::openapi::ObjectBuilder::new() },
|mut object_tokens, (field, field_rule)| {
let mut field_name = &*field.ident.as_ref().unwrap().to_string();
if field_name.starts_with("r#") {
field_name = &field_name[2..];
}
self.field_as_schema_property(
field,
&container_rules,
|NamedStructFieldOptions {
property,
rename_field_value,
required,
is_option,
}| {
let rename_to = field_rule
.as_ref()
.and_then(|field_rule| {
field_rule.rename.as_deref().map(Cow::Borrowed)
})
.or(rename_field_value);
let rename_all = container_rules
.as_ref()
.and_then(|container_rule| container_rule.rename_all.as_ref())
.or_else(|| {
self.rename_all
.as_ref()
.map(|rename_all| rename_all.as_rename_rule())
});
let name =
super::rename::<FieldRename>(field_name, rename_to, rename_all)
.unwrap_or(Cow::Borrowed(field_name));
object_tokens.extend(quote! {
.property(#name, #property)
});
if let Property::Schema(_) = property {
if (!is_option
&& super::is_required(
field_rule.as_ref(),
container_rules.as_ref(),
))
|| required
.as_ref()
.map(super::features::Required::is_true)
.unwrap_or(false)
{
object_tokens.extend(quote! {
.required(#name)
})
}
}
object_tokens
},
)
},
);
let flatten_fields: Vec<&Field> = self
.fields
.iter()
.filter(|field| {
let field_rule = serde::parse_value(&field.attrs);
is_flatten(&field_rule)
})
.collect();
if !flatten_fields.is_empty() {
tokens.extend(quote! {
utoipa::openapi::AllOfBuilder::new()
});
for field in flatten_fields {
self.field_as_schema_property(
field,
&container_rules,
|NamedStructFieldOptions { property, .. }| {
tokens.extend(quote! { .item(#property) });
},
)
}
tokens.extend(quote! {
.item(#object_tokens)
})
} else {
tokens.extend(object_tokens)
}
if let Some(deprecated) = super::get_deprecated(self.attributes) {
tokens.extend(quote! { .deprecated(Some(#deprecated)) });
}
if let Some(struct_features) = self.features.as_ref() {
tokens.extend(struct_features.to_token_stream())
}
let description = CommentAttributes::from_attributes(self.attributes).as_formatted_string();
if !description.is_empty() {
tokens.extend(quote! {
.description(Some(#description))
})
}
}
}
#[cfg_attr(feature = "debug", derive(Debug))]
struct UnnamedStructSchema<'a> {
struct_name: Cow<'a, str>,
fields: &'a Punctuated<Field, Comma>,
attributes: &'a [Attribute],
features: Option<Vec<Feature>>,
schema_as: Option<As>,
}
impl ToTokens for UnnamedStructSchema<'_> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let fields_len = self.fields.len();
let first_field = self.fields.first().unwrap();
let first_part = &TypeTree::from_type(&first_field.ty);
let all_fields_are_same = fields_len == 1
|| self.fields.iter().skip(1).all(|field| {
let schema_part = &TypeTree::from_type(&field.ty);
first_part == schema_part
});
let deprecated = super::get_deprecated(self.attributes);
if all_fields_are_same {
let mut unnamed_struct_features = self.features.clone();
let value_type = unnamed_struct_features
.as_mut()
.and_then(|features| features.pop_value_type_feature());
let override_type_tree = value_type
.as_ref()
.map(|value_type| value_type.as_type_tree());
if fields_len == 1 {
if let Some(ref mut features) = unnamed_struct_features {
if pop_feature!(features => Feature::Default(crate::features::Default(None)))
.is_some()
{
let struct_ident = format_ident!("{}", &self.struct_name);
let index: syn::Index = 0.into();
features.push(Feature::Default(
crate::features::Default::new_default_trait(struct_ident, index.into()),
));
}
}
}
tokens.extend(
ComponentSchema::new(super::ComponentSchemaProps {
type_tree: override_type_tree.as_ref().unwrap_or(first_part),
features: unnamed_struct_features,
description: Some(&CommentAttributes::from_attributes(self.attributes)),
deprecated: deprecated.as_ref(),
object_name: self.struct_name.as_ref(),
})
.to_token_stream(),
);
} else {
tokens.extend(quote! {
utoipa::openapi::ObjectBuilder::new()
});
if let Some(deprecated) = deprecated {
tokens.extend(quote! { .deprecated(Some(#deprecated)) });
}
if let Some(ref attrs) = self.features {
tokens.extend(attrs.to_token_stream())
}
}
if fields_len > 1 {
let description =
CommentAttributes::from_attributes(self.attributes).as_formatted_string();
tokens.extend(
quote! { .to_array_builder().description(Some(#description)).max_items(Some(#fields_len)).min_items(Some(#fields_len)) },
)
}
}
}
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct EnumSchema<'a> {
schema_type: EnumSchemaType<'a>,
schema_as: Option<As>,
}
impl<'e> EnumSchema<'e> {
pub fn new(
enum_name: Cow<'e, str>,
variants: &'e Punctuated<Variant, Comma>,
attributes: &'e [Attribute],
) -> Self {
if variants
.iter()
.all(|variant| matches!(variant.fields, Fields::Unit))
{
#[cfg(feature = "repr")]
{
attributes
.iter()
.find_map(|attribute| {
if attribute.path().is_ident("repr") {
attribute.parse_args::<syn::TypePath>().ok()
} else {
None
}
})
.map(|enum_type| {
let mut repr_enum_features =
features::parse_schema_features_with(attributes, |input| {
Ok(parse_features!(
input as super::features::Example,
super::features::Default,
super::features::Title,
As
))
})
.unwrap_or_default();
let schema_as =
pop_feature_as_inner!(repr_enum_features => Feature::As(_v));
Self {
schema_type: EnumSchemaType::Repr(ReprEnum {
variants,
attributes,
enum_type,
enum_features: repr_enum_features,
}),
schema_as,
}
})
.unwrap_or_else(|| {
let mut simple_enum_features = attributes
.parse_features::<EnumFeatures>()
.into_inner()
.unwrap_or_default();
let schema_as =
pop_feature_as_inner!(simple_enum_features => Feature::As(_v));
let rename_all = simple_enum_features.pop_rename_all_feature();
Self {
schema_type: EnumSchemaType::Simple(SimpleEnum {
attributes,
variants,
enum_features: simple_enum_features,
rename_all,
}),
schema_as,
}
})
}
#[cfg(not(feature = "repr"))]
{
let mut simple_enum_features = attributes
.parse_features::<EnumFeatures>()
.into_inner()
.unwrap_or_default();
let schema_as = pop_feature_as_inner!(simple_enum_features => Feature::As(_v));
let rename_all = simple_enum_features.pop_rename_all_feature();
Self {
schema_type: EnumSchemaType::Simple(SimpleEnum {
attributes,
variants,
enum_features: simple_enum_features,
rename_all,
}),
schema_as,
}
}
} else {
let mut enum_features = attributes
.parse_features::<ComplexEnumFeatures>()
.into_inner()
.unwrap_or_default();
let schema_as = pop_feature_as_inner!(enum_features => Feature::As(_v));
let rename_all = enum_features.pop_rename_all_feature();
Self {
schema_type: EnumSchemaType::Complex(ComplexEnum {
enum_name,
attributes,
variants,
rename_all,
enum_features,
}),
schema_as,
}
}
}
}
impl ToTokens for EnumSchema<'_> {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.schema_type.to_tokens(tokens);
}
}
#[cfg_attr(feature = "debug", derive(Debug))]
enum EnumSchemaType<'e> {
Simple(SimpleEnum<'e>),
#[cfg(feature = "repr")]
Repr(ReprEnum<'e>),
Complex(ComplexEnum<'e>),
}
impl ToTokens for EnumSchemaType<'_> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let attributes = match self {
Self::Simple(simple) => {
simple.to_tokens(tokens);
simple.attributes
}
#[cfg(feature = "repr")]
Self::Repr(repr) => {
repr.to_tokens(tokens);
repr.attributes
}
Self::Complex(complex) => {
complex.to_tokens(tokens);
complex.attributes
}
};
if let Some(deprecated) = super::get_deprecated(attributes) {
tokens.extend(quote! { .deprecated(Some(#deprecated)) });
}
let description = CommentAttributes::from_attributes(attributes).as_formatted_string();
if !description.is_empty() {
tokens.extend(quote! {
.description(Some(#description))
})
}
}
}
#[cfg(feature = "repr")]
#[cfg_attr(feature = "debug", derive(Debug))]
struct ReprEnum<'a> {
variants: &'a Punctuated<Variant, Comma>,
attributes: &'a [Attribute],
enum_type: syn::TypePath,
enum_features: Vec<Feature>,
}
#[cfg(feature = "repr")]
impl ToTokens for ReprEnum<'_> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let container_rules = serde::parse_container(self.attributes);
regular_enum_to_tokens(tokens, &container_rules, &self.enum_features, || {
self.variants
.iter()
.filter_map(|variant| {
let variant_type = &variant.ident;
let variant_rules = serde::parse_value(&variant.attrs);
if is_not_skipped(&variant_rules) {
let repr_type = &self.enum_type;
Some(enum_variant::ReprVariant {
value: quote! { Self::#variant_type as #repr_type },
type_path: repr_type,
})
} else {
None
}
})
.collect::<Vec<enum_variant::ReprVariant<TokenStream>>>()
});
}
}
fn rename_enum_variant<'a>(
name: &'a str,
features: &mut Vec<Feature>,
variant_rules: &'a Option<SerdeValue>,
container_rules: &'a Option<SerdeContainer>,
rename_all: &'a Option<RenameAll>,
) -> Option<Cow<'a, str>> {
let rename = features
.pop_rename_feature()
.map(|rename| rename.into_value());
let rename_to = variant_rules
.as_ref()
.and_then(|variant_rules| variant_rules.rename.as_deref().map(Cow::Borrowed))
.or_else(|| rename.map(Cow::Owned));
let rename_all = container_rules
.as_ref()
.and_then(|container_rules| container_rules.rename_all.as_ref())
.or_else(|| {
rename_all
.as_ref()
.map(|rename_all| rename_all.as_rename_rule())
});
super::rename::<VariantRename>(name, rename_to, rename_all)
}
#[cfg_attr(feature = "debug", derive(Debug))]
struct SimpleEnum<'a> {
variants: &'a Punctuated<Variant, Comma>,
attributes: &'a [Attribute],
enum_features: Vec<Feature>,
rename_all: Option<RenameAll>,
}
impl ToTokens for SimpleEnum<'_> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let container_rules = serde::parse_container(self.attributes);
regular_enum_to_tokens(tokens, &container_rules, &self.enum_features, || {
self.variants
.iter()
.filter_map(|variant| {
let variant_rules = serde::parse_value(&variant.attrs);
if is_not_skipped(&variant_rules) {
Some((variant, variant_rules))
} else {
None
}
})
.flat_map(|(variant, variant_rules)| {
let name = &*variant.ident.to_string();
let mut variant_features =
features::parse_schema_features_with(&variant.attrs, |input| {
Ok(parse_features!(input as Rename))
})
.unwrap_or_default();
let variant_name = rename_enum_variant(
name,
&mut variant_features,
&variant_rules,
&container_rules,
&self.rename_all,
);
variant_name
.map(|name| SimpleEnumVariant {
value: name.to_token_stream(),
})
.or_else(|| {
Some(SimpleEnumVariant {
value: name.to_token_stream(),
})
})
})
.collect::<Vec<SimpleEnumVariant<TokenStream>>>()
});
}
}
fn regular_enum_to_tokens<T: self::enum_variant::Variant>(
tokens: &mut TokenStream,
container_rules: &Option<SerdeContainer>,
enum_variant_features: &Vec<Feature>,
get_variants_tokens_vec: impl FnOnce() -> Vec<T>,
) {
let enum_values = get_variants_tokens_vec();
tokens.extend(match container_rules {
Some(serde_container) => match &serde_container.enum_repr {
SerdeEnumRepr::ExternallyTagged => Enum::new(enum_values).to_token_stream(),
SerdeEnumRepr::InternallyTagged { tag } => TaggedEnum::new(
enum_values
.into_iter()
.map(|variant| (Cow::Borrowed(tag.as_str()), variant)),
)
.to_token_stream(),
SerdeEnumRepr::Untagged => UntaggedEnum::new().to_token_stream(),
SerdeEnumRepr::AdjacentlyTagged { tag, content } => {
AdjacentlyTaggedEnum::new(enum_values.into_iter().map(|variant| {
(
Cow::Borrowed(tag.as_str()),
Cow::Borrowed(content.as_str()),
variant,
)
}))
.to_token_stream()
}
SerdeEnumRepr::UnfinishedAdjacentlyTagged { .. } => panic!("Invalid serde enum repr"),
},
_ => Enum::new(enum_values).to_token_stream(),
});
tokens.extend(enum_variant_features.to_token_stream());
}
#[cfg_attr(feature = "debug", derive(Debug))]
struct ComplexEnum<'a> {
variants: &'a Punctuated<Variant, Comma>,
attributes: &'a [Attribute],
enum_name: Cow<'a, str>,
enum_features: Vec<Feature>,
rename_all: Option<RenameAll>,
}
impl ComplexEnum<'_> {
fn variant_tokens(
&self,
name: Cow<'_, str>,
variant: &Variant,
variant_rules: &Option<SerdeValue>,
container_rules: &Option<SerdeContainer>,
rename_all: &Option<RenameAll>,
) -> TokenStream {
match &variant.fields {
Fields::Named(named_fields) => {
let (title_features, mut named_struct_features) = variant
.attrs
.parse_features::<EnumNamedFieldVariantFeatures>()
.into_inner()
.map(|features| features.split_for_title())
.unwrap_or_default();
let variant_name = rename_enum_variant(
name.as_ref(),
&mut named_struct_features,
variant_rules,
container_rules,
rename_all,
);
let example = pop_feature!(named_struct_features => Feature::Example(_));
self::enum_variant::Variant::to_tokens(&ObjectVariant {
name: variant_name.unwrap_or(Cow::Borrowed(&name)),
title: title_features.first().map(ToTokens::to_token_stream),
example: example.as_ref().map(ToTokens::to_token_stream),
item: NamedStructSchema {
struct_name: Cow::Borrowed(&*self.enum_name),
attributes: &variant.attrs,
rename_all: named_struct_features.pop_rename_all_feature(),
features: Some(named_struct_features),
fields: &named_fields.named,
generics: None,
aliases: None,
schema_as: None,
},
})
}
Fields::Unnamed(unnamed_fields) => {
let (title_features, mut unnamed_struct_features) = variant
.attrs
.parse_features::<EnumUnnamedFieldVariantFeatures>()
.into_inner()
.map(|features| features.split_for_title())
.unwrap_or_default();
let variant_name = rename_enum_variant(
name.as_ref(),
&mut unnamed_struct_features,
variant_rules,
container_rules,
rename_all,
);
let example = pop_feature!(unnamed_struct_features => Feature::Example(_));
self::enum_variant::Variant::to_tokens(&ObjectVariant {
name: variant_name.unwrap_or(Cow::Borrowed(&name)),
title: title_features.first().map(ToTokens::to_token_stream),
example: example.as_ref().map(ToTokens::to_token_stream),
item: UnnamedStructSchema {
struct_name: Cow::Borrowed(&*self.enum_name),
attributes: &variant.attrs,
features: Some(unnamed_struct_features),
fields: &unnamed_fields.unnamed,
schema_as: None,
},
})
}
Fields::Unit => {
let mut unit_features =
features::parse_schema_features_with(&variant.attrs, |input| {
Ok(parse_features!(
input as super::features::Title,
RenameAll,
Rename,
Example
))
})
.unwrap_or_default();
let title = pop_feature!(unit_features => Feature::Title(_));
let variant_name = rename_enum_variant(
name.as_ref(),
&mut unit_features,
variant_rules,
container_rules,
rename_all,
);
let example: Option<Feature> = pop_feature!(unit_features => Feature::Example(_));
let description =
CommentAttributes::from_attributes(&variant.attrs).as_formatted_string();
let description =
(!description.is_empty()).then(|| Feature::Description(description.into()));
Enum::new([SimpleEnumVariant {
value: variant_name
.unwrap_or(Cow::Borrowed(&name))
.to_token_stream(),
}])
.with_title(title.as_ref().map(ToTokens::to_token_stream))
.with_example(example.as_ref().map(ToTokens::to_token_stream))
.with_description(description.as_ref().map(ToTokens::to_token_stream))
.to_token_stream()
}
}
}
fn untagged_variant_tokens(&self, variant: &Variant) -> TokenStream {
match &variant.fields {
Fields::Named(named_fields) => {
let mut named_struct_features = variant
.attrs
.parse_features::<EnumNamedFieldVariantFeatures>()
.into_inner()
.unwrap_or_default();
NamedStructSchema {
struct_name: Cow::Borrowed(&*self.enum_name),
attributes: &variant.attrs,
rename_all: named_struct_features.pop_rename_all_feature(),
features: Some(named_struct_features),
fields: &named_fields.named,
generics: None,
aliases: None,
schema_as: None,
}
.to_token_stream()
}
Fields::Unnamed(unnamed_fields) => {
let unnamed_struct_features = variant
.attrs
.parse_features::<EnumUnnamedFieldVariantFeatures>()
.into_inner()
.unwrap_or_default();
UnnamedStructSchema {
struct_name: Cow::Borrowed(&*self.enum_name),
attributes: &variant.attrs,
features: Some(unnamed_struct_features),
fields: &unnamed_fields.unnamed,
schema_as: None,
}
.to_token_stream()
}
Fields::Unit => {
let mut unit_features =
features::parse_schema_features_with(&variant.attrs, |input| {
Ok(parse_features!(input as super::features::Title))
})
.unwrap_or_default();
let title = pop_feature!(unit_features => Feature::Title(_));
UntaggedEnum::with_title(title).to_token_stream()
}
}
}
fn tagged_variant_tokens(
&self,
tag: &str,
name: Cow<'_, str>,
variant: &Variant,
variant_rules: &Option<SerdeValue>,
container_rules: &Option<SerdeContainer>,
rename_all: &Option<RenameAll>,
) -> TokenStream {
match &variant.fields {
Fields::Named(named_fields) => {
let (title_features, mut named_struct_features) = variant
.attrs
.parse_features::<EnumNamedFieldVariantFeatures>()
.into_inner()
.map(|features| features.split_for_title())
.unwrap_or_default();
let variant_name = rename_enum_variant(
name.as_ref(),
&mut named_struct_features,
variant_rules,
container_rules,
rename_all,
);
let named_enum = NamedStructSchema {
struct_name: Cow::Borrowed(&*self.enum_name),
attributes: &variant.attrs,
rename_all: named_struct_features.pop_rename_all_feature(),
features: Some(named_struct_features),
fields: &named_fields.named,
generics: None,
aliases: None,
schema_as: None,
};
let title = title_features.first().map(ToTokens::to_token_stream);
let variant_name_tokens = Enum::new([SimpleEnumVariant {
value: variant_name
.unwrap_or(Cow::Borrowed(&name))
.to_token_stream(),
}]);
quote! {
#named_enum
#title
.property(#tag, #variant_name_tokens)
.required(#tag)
}
}
Fields::Unnamed(unnamed_fields) => {
if unnamed_fields.unnamed.len() == 1 {
let (title_features, mut unnamed_struct_features) = variant
.attrs
.parse_features::<EnumUnnamedFieldVariantFeatures>()
.into_inner()
.map(|features| features.split_for_title())
.unwrap_or_default();
let variant_name = rename_enum_variant(
name.as_ref(),
&mut unnamed_struct_features,
variant_rules,
container_rules,
rename_all,
);
let unnamed_enum = UnnamedStructSchema {
struct_name: Cow::Borrowed(&*self.enum_name),
attributes: &variant.attrs,
features: Some(unnamed_struct_features),
fields: &unnamed_fields.unnamed,
schema_as: None,
};
let title = title_features.first().map(ToTokens::to_token_stream);
let variant_name_tokens = Enum::new([SimpleEnumVariant {
value: variant_name
.unwrap_or(Cow::Borrowed(&name))
.to_token_stream(),
}]);
let is_reference = unnamed_fields.unnamed.iter().any(|field| {
let ty = TypeTree::from_type(&field.ty);
ty.value_type == ValueType::Object
});
if is_reference {
quote! {
utoipa::openapi::schema::AllOfBuilder::new()
#title
.item(#unnamed_enum)
.item(utoipa::openapi::schema::ObjectBuilder::new()
.schema_type(utoipa::openapi::schema::SchemaType::Object)
.property(#tag, #variant_name_tokens)
.required(#tag)
)
}
} else {
quote! {
#unnamed_enum
#title
.schema_type(utoipa::openapi::schema::SchemaType::Object)
.property(#tag, #variant_name_tokens)
.required(#tag)
}
}
} else {
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";
note = "See more about enum limitations here: `https://serde.rs/enum-representations.html#internally-tagged`"
);
}
}
Fields::Unit => {
let mut unit_features =
features::parse_schema_features_with(&variant.attrs, |input| {
Ok(parse_features!(input as super::features::Title, Rename))
})
.unwrap_or_default();
let title = pop_feature!(unit_features => Feature::Title(_));
let variant_name = rename_enum_variant(
name.as_ref(),
&mut unit_features,
variant_rules,
container_rules,
rename_all,
);
let variant_tokens = Enum::new([SimpleEnumVariant {
value: variant_name
.unwrap_or(Cow::Borrowed(&name))
.to_token_stream(),
}]);
quote! {
utoipa::openapi::schema::ObjectBuilder::new()
#title
.property(#tag, #variant_tokens)
.required(#tag)
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn adjacently_tagged_variant_tokens(
&self,
tag: &str,
content: &str,
name: Cow<'_, str>,
variant: &Variant,
variant_rules: &Option<SerdeValue>,
container_rules: &Option<SerdeContainer>,
rename_all: &Option<RenameAll>,
) -> TokenStream {
match &variant.fields {
Fields::Named(named_fields) => {
let (title_features, mut named_struct_features) = variant
.attrs
.parse_features::<EnumNamedFieldVariantFeatures>()
.into_inner()
.map(|features| features.split_for_title())
.unwrap_or_default();
let variant_name = rename_enum_variant(
name.as_ref(),
&mut named_struct_features,
variant_rules,
container_rules,
rename_all,
);
let named_enum = NamedStructSchema {
struct_name: Cow::Borrowed(&*self.enum_name),
attributes: &variant.attrs,
rename_all: named_struct_features.pop_rename_all_feature(),
features: Some(named_struct_features),
fields: &named_fields.named,
generics: None,
aliases: None,
schema_as: None,
};
let title = title_features.first().map(ToTokens::to_token_stream);
let variant_name_tokens = Enum::new([SimpleEnumVariant {
value: variant_name
.unwrap_or(Cow::Borrowed(&name))
.to_token_stream(),
}]);
quote! {
utoipa::openapi::schema::ObjectBuilder::new()
#title
.schema_type(utoipa::openapi::schema::SchemaType::Object)
.property(#tag, #variant_name_tokens)
.required(#tag)
.property(#content, #named_enum)
.required(#content)
}
}
Fields::Unnamed(unnamed_fields) => {
if unnamed_fields.unnamed.len() == 1 {
let (title_features, mut unnamed_struct_features) = variant
.attrs
.parse_features::<EnumUnnamedFieldVariantFeatures>()
.into_inner()
.map(|features| features.split_for_title())
.unwrap_or_default();
let variant_name = rename_enum_variant(
name.as_ref(),
&mut unnamed_struct_features,
variant_rules,
container_rules,
rename_all,
);
let unnamed_enum = UnnamedStructSchema {
struct_name: Cow::Borrowed(&*self.enum_name),
attributes: &variant.attrs,
features: Some(unnamed_struct_features),
fields: &unnamed_fields.unnamed,
schema_as: None,
};
let title = title_features.first().map(ToTokens::to_token_stream);
let variant_name_tokens = Enum::new([SimpleEnumVariant {
value: variant_name
.unwrap_or(Cow::Borrowed(&name))
.to_token_stream(),
}]);
quote! {
utoipa::openapi::schema::ObjectBuilder::new()
#title
.schema_type(utoipa::openapi::schema::SchemaType::Object)
.property(#tag, #variant_name_tokens)
.required(#tag)
.property(#content, #unnamed_enum)
.required(#content)
}
} else {
abort!(
variant,
"Unnamed (tuple) enum variants are unsupported for adjacently tagged enums using the `tag = <tag>, content = <content>` serde attribute";
help = "Try using a different serde enum representation";
note = "See more about enum limitations here: `https://serde.rs/enum-representations.html#adjacently-tagged`"
);
}
}
Fields::Unit => {
let mut unit_features =
features::parse_schema_features_with(&variant.attrs, |input| {
Ok(parse_features!(input as super::features::Title, Rename))
})
.unwrap_or_default();
let title = pop_feature!(unit_features => Feature::Title(_));
let variant_name = rename_enum_variant(
name.as_ref(),
&mut unit_features,
variant_rules,
container_rules,
rename_all,
);
let variant_tokens = Enum::new([SimpleEnumVariant {
value: variant_name
.unwrap_or(Cow::Borrowed(&name))
.to_token_stream(),
}]);
quote! {
utoipa::openapi::schema::ObjectBuilder::new()
#title
.property(#tag, #variant_tokens)
.required(#tag)
}
}
}
}
}
impl ToTokens for ComplexEnum<'_> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let attributes = &self.attributes;
let container_rules = serde::parse_container(attributes);
let enum_repr = container_rules
.as_ref()
.map(|rules| rules.enum_repr.clone())
.unwrap_or_default();
let tag = match &enum_repr {
SerdeEnumRepr::AdjacentlyTagged { tag, .. }
| SerdeEnumRepr::InternallyTagged { tag } => Some(tag),
SerdeEnumRepr::ExternallyTagged
| SerdeEnumRepr::Untagged
| SerdeEnumRepr::UnfinishedAdjacentlyTagged { .. } => None,
};
self.variants
.iter()
.filter_map(|variant: &Variant| {
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, variant_serde_rules)| {
let variant_name = &*variant.ident.to_string();
match &enum_repr {
SerdeEnumRepr::ExternallyTagged => self.variant_tokens(
Cow::Borrowed(variant_name),
variant,
&variant_serde_rules,
&container_rules,
&self.rename_all,
),
SerdeEnumRepr::InternallyTagged { tag } => self.tagged_variant_tokens(
tag,
Cow::Borrowed(variant_name),
variant,
&variant_serde_rules,
&container_rules,
&self.rename_all,
),
SerdeEnumRepr::Untagged => self.untagged_variant_tokens(variant),
SerdeEnumRepr::AdjacentlyTagged { tag, content } => self
.adjacently_tagged_variant_tokens(
tag,
content,
Cow::Borrowed(variant_name),
variant,
&variant_serde_rules,
&container_rules,
&self.rename_all,
),
SerdeEnumRepr::UnfinishedAdjacentlyTagged { .. } => {
unreachable!("Serde should not have parsed an UnfinishedAdjacentlyTagged")
}
}
})
.collect::<CustomEnum<'_, TokenStream>>()
.with_discriminator(tag.map(|t| Cow::Borrowed(t.as_str())))
.to_tokens(tokens);
tokens.extend(self.enum_features.to_token_stream());
}
}
#[cfg_attr(feature = "debug", derive(Debug))]
#[derive(PartialEq)]
struct TypeTuple<'a, T>(T, &'a Ident);
#[cfg_attr(feature = "debug", derive(Debug))]
enum Property {
Schema(ComponentSchema),
SchemaWith(Feature),
}
impl ToTokens for Property {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
Self::Schema(schema) => schema.to_tokens(tokens),
Self::SchemaWith(schema_with) => schema_with.to_tokens(tokens),
}
}
}
trait SchemaFeatureExt {
fn split_for_title(self) -> (Vec<Feature>, Vec<Feature>);
}
impl SchemaFeatureExt for Vec<Feature> {
fn split_for_title(self) -> (Vec<Feature>, Vec<Feature>) {
self.into_iter()
.partition(|feature| matches!(feature, Feature::Title(_)))
}
}
pub(crate) fn format_path_ref(path: &Path) -> String {
let mut path = path.clone();
if let Some(last_segment) = path.segments.last_mut() {
last_segment.arguments = PathArguments::None;
}
path.to_token_stream().to_string().replace(" :: ", ".")
}
#[inline]
fn is_not_skipped(rule: &Option<SerdeValue>) -> bool {
rule.as_ref().map(|value| !value.skip).unwrap_or(true)
}
#[inline]
fn is_flatten(rule: &Option<SerdeValue>) -> bool {
rule.as_ref().map(|value| value.flatten).unwrap_or(false)
}
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct AliasSchema {
pub name: String,
pub ty: Type,
}
impl AliasSchema {
fn get_lifetimes(&self) -> impl Iterator<Item = &GenericArgument> {
fn lifetimes_from_type(ty: &Type) -> impl Iterator<Item = &GenericArgument> {
match ty {
Type::Path(type_path) => type_path
.path
.segments
.iter()
.flat_map(|segment| match &segment.arguments {
PathArguments::AngleBracketed(angle_bracketed_args) => {
Some(angle_bracketed_args.args.iter())
}
_ => None,
})
.flatten()
.flat_map(|arg| match arg {
GenericArgument::Type(type_argument) => {
lifetimes_from_type(type_argument).collect::<Vec<_>>()
}
_ => vec![arg],
})
.filter(|generic_arg| matches!(generic_arg, syn::GenericArgument::Lifetime(lifetime) if lifetime.ident != "'static")),
_ => abort!(
&ty.span(),
"AliasSchema `get_lifetimes` only supports syn::TypePath types"
),
}
}
lifetimes_from_type(&self.ty)
}
}
impl Parse for AliasSchema {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let name = input.parse::<Ident>()?;
input.parse::<Token![=]>()?;
Ok(Self {
name: name.to_string(),
ty: input.parse::<Type>()?,
})
}
}
fn parse_aliases(attributes: &[Attribute]) -> Option<Punctuated<AliasSchema, Comma>> {
attributes
.iter()
.find(|attribute| attribute.path().is_ident("aliases"))
.map(|aliases| {
aliases
.parse_args_with(Punctuated::<AliasSchema, Comma>::parse_terminated)
.unwrap_or_abort()
})
}