From 3af2b2bb83e28427ef096773c5f14efa4b256528 Mon Sep 17 00:00:00 2001 From: Juha Kukkonen Date: Fri, 30 Aug 2024 18:57:34 +0300 Subject: [PATCH] Chore refactor features Split features parsing to multiple modules and make pop features usage consistent across the `utoipa-gen` crate. --- utoipa-gen/src/component.rs | 5 +- utoipa-gen/src/component/features.rs | 1898 +++-------------- .../src/component/features/attributes.rs | 806 +++++++ .../src/component/features/validation.rs | 440 ++++ .../src/component/features/validators.rs | 111 + utoipa-gen/src/component/into_params.rs | 29 +- utoipa-gen/src/component/schema.rs | 93 +- utoipa-gen/src/component/schema/features.rs | 28 +- utoipa-gen/src/lib.rs | 6 +- utoipa-gen/src/path/parameter.rs | 16 +- utoipa-gen/src/path/request_body.rs | 2 +- utoipa-gen/src/path/response.rs | 2 +- 12 files changed, 1728 insertions(+), 1708 deletions(-) create mode 100644 utoipa-gen/src/component/features/attributes.rs create mode 100644 utoipa-gen/src/component/features/validation.rs create mode 100644 utoipa-gen/src/component/features/validators.rs diff --git a/utoipa-gen/src/component.rs b/utoipa-gen/src/component.rs index fcee3ee9..1a3a5038 100644 --- a/utoipa-gen/src/component.rs +++ b/utoipa-gen/src/component.rs @@ -10,9 +10,10 @@ use crate::schema_type::{SchemaFormat, SchemaTypeInner}; use crate::{as_tokens_or_diagnostics, AttributesExt, Diagnostics, OptionExt, ToTokensDiagnostics}; use crate::{schema_type::SchemaType, Deprecated}; +use self::features::attributes::{Description, Nullable}; +use self::features::validation::Minimum; use self::features::{ - pop_feature, Description, Feature, FeaturesExt, IntoInner, IsInline, Minimum, Nullable, - ToTokensExt, Validatable, + pop_feature, Feature, FeaturesExt, IntoInner, IsInline, ToTokensExt, Validatable, }; use self::schema::format_path_ref; use self::serde::{RenameRule, SerdeContainer, SerdeValue}; diff --git a/utoipa-gen/src/component/features.rs b/utoipa-gen/src/component/features.rs index c5d7ca3e..0d895c1c 100644 --- a/utoipa-gen/src/component/features.rs +++ b/utoipa-gen/src/component/features.rs @@ -1,20 +1,21 @@ use std::{fmt::Display, mem, str::FromStr}; -use proc_macro2::{Ident, Span, TokenStream}; +use proc_macro2::{Ident, TokenStream}; use quote::{quote, ToTokens}; -use syn::{ - parenthesized, parse::ParseStream, punctuated::Punctuated, LitFloat, LitInt, LitStr, Token, - TypePath, -}; +use syn::{parse::ParseStream, LitFloat, LitInt}; use crate::{ - as_tokens_or_diagnostics, parse_utils, - path::parameter::{self, ParameterStyle}, - schema_type::{SchemaFormat, SchemaType}, - AnyValue, Array, Diagnostics, OptionExt, ToTokensDiagnostics, + as_tokens_or_diagnostics, parse_utils, schema_type::SchemaType, Diagnostics, OptionExt, + ToTokensDiagnostics, }; -use super::{schema, serde::RenameRule, GenericType, TypeTree}; +use self::validators::{AboveZeroF64, AboveZeroUsize, IsNumber, IsString, IsVec, ValidatorChain}; + +use super::TypeTree; + +pub mod attributes; +pub mod validation; +pub mod validators; /// Parse `LitInt` from parse stream fn parse_integer(input: ParseStream) -> syn::Result @@ -50,20 +51,21 @@ pub trait Name { macro_rules! name { ( $ident:ident = $name:literal ) => { - impl Name for $ident { + impl $crate::features::Name for $ident { fn get_name() -> &'static str { $name } } - impl Display for $ident { + impl std::fmt::Display for $ident { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let name = ::get_name(); + let name = ::get_name(); write!(f, "{name}") } } }; } +use name; /// Define whether [`Feature`] variant is validatable or not pub trait Validatable { @@ -74,7 +76,7 @@ pub trait Validatable { pub trait Validate: Validatable { /// Perform validation check against schema type. - fn validate(&self, validator: impl Validator) -> Option; + fn validate(&self, validator: impl validators::Validator) -> Option; } pub trait Parse { @@ -86,44 +88,44 @@ pub trait Parse { #[cfg_attr(feature = "debug", derive(Debug))] #[derive(Clone)] pub enum Feature { - Example(Example), - Examples(Examples), - Default(Default), - Inline(Inline), - XmlAttr(XmlAttr), - Format(Format), - ValueType(ValueType), - WriteOnly(WriteOnly), - ReadOnly(ReadOnly), - Title(Title), - Nullable(Nullable), - Rename(Rename), - RenameAll(RenameAll), - Style(Style), - AllowReserved(AllowReserved), - Explode(Explode), - ParameterIn(ParameterIn), - IntoParamsNames(IntoParamsNames), - MultipleOf(MultipleOf), - Maximum(Maximum), - Minimum(Minimum), - ExclusiveMaximum(ExclusiveMaximum), - ExclusiveMinimum(ExclusiveMinimum), - MaxLength(MaxLength), - MinLength(MinLength), - Pattern(Pattern), - MaxItems(MaxItems), - MinItems(MinItems), - MaxProperties(MaxProperties), - MinProperties(MinProperties), - SchemaWith(SchemaWith), - Description(Description), - Deprecated(Deprecated), - As(As), - AdditionalProperties(AdditionalProperties), - Required(Required), - ContentEncoding(ContentEncoding), - ContentMediaType(ContentMediaType), + Example(attributes::Example), + Examples(attributes::Examples), + Default(attributes::Default), + Inline(attributes::Inline), + XmlAttr(attributes::XmlAttr), + Format(attributes::Format), + ValueType(attributes::ValueType), + WriteOnly(attributes::WriteOnly), + ReadOnly(attributes::ReadOnly), + Title(attributes::Title), + Nullable(attributes::Nullable), + Rename(attributes::Rename), + RenameAll(attributes::RenameAll), + Style(attributes::Style), + AllowReserved(attributes::AllowReserved), + Explode(attributes::Explode), + ParameterIn(attributes::ParameterIn), + IntoParamsNames(attributes::IntoParamsNames), + SchemaWith(attributes::SchemaWith), + Description(attributes::Description), + Deprecated(attributes::Deprecated), + As(attributes::As), + AdditionalProperties(attributes::AdditionalProperties), + Required(attributes::Required), + ContentEncoding(attributes::ContentEncoding), + ContentMediaType(attributes::ContentMediaType), + MultipleOf(validation::MultipleOf), + Maximum(validation::Maximum), + Minimum(validation::Minimum), + ExclusiveMaximum(validation::ExclusiveMaximum), + ExclusiveMinimum(validation::ExclusiveMinimum), + MaxLength(validation::MaxLength), + MinLength(validation::MinLength), + Pattern(validation::Pattern), + MaxItems(validation::MaxItems), + MinItems(validation::MinItems), + MaxProperties(validation::MaxProperties), + MinProperties(validation::MinProperties), } impl Feature { @@ -153,7 +155,7 @@ impl Feature { Feature::MinItems(min_items) => min_items.validate( ValidatorChain::new(&AboveZeroUsize(min_items.0)).next(&IsVec(type_tree)), ), - _unsupported_variant => { + unsupported => { const SUPPORTED_VARIANTS: [&str; 10] = [ "multiple_of", "maximum", @@ -167,8 +169,7 @@ impl Feature { "min_items", ]; panic!( - "Unsupported variant: `{variant}` for Validate::validate, expected one of: {variants}", - variant = _unsupported_variant, + "Unsupported variant: `{unsupported}` for Validate::validate, expected one of: {variants}", variants = SUPPORTED_VARIANTS.join(", ") ) } @@ -242,7 +243,7 @@ impl ToTokensDiagnostics for Feature { return Err(Diagnostics::new("As does not support `ToTokens`")) } Feature::Required(required) => { - let name = ::get_name(); + let name = ::get_name(); quote! { .#name(#required) } } }; @@ -356,1607 +357,239 @@ impl Validatable for Feature { } macro_rules! is_validatable { - ( $( $ident:ident => $validatable:literal ),* ) => { + ( $( $ty:path $( = $validatable:literal )? ),* ) => { $( - impl Validatable for $ident { + impl Validatable for $ty { + $( fn is_validatable(&self) -> bool { $validatable } + )? } )* }; } is_validatable! { - Default => false, - Example => false, - Examples => false, - XmlAttr => false, - Format => false, - WriteOnly => false, - ReadOnly => false, - Title => false, - Nullable => false, - Rename => false, - Style => false, - ParameterIn => false, - AllowReserved => false, - Explode => false, - RenameAll => false, - ValueType => false, - Inline => false, - IntoParamsNames => false, - MultipleOf => true, - Maximum => true, - Minimum => true, - ExclusiveMaximum => true, - ExclusiveMinimum => true, - MaxLength => true, - MinLength => true, - Pattern => true, - MaxItems => true, - MinItems => true, - MaxProperties => false, - MinProperties => false, - SchemaWith => false, - Description => false, - Deprecated => false, - As => false, - AdditionalProperties => false, - Required => false, - ContentEncoding => false, - ContentMediaType => false -} - -#[derive(Clone)] -#[cfg_attr(feature = "debug", derive(Debug))] -pub struct Example(AnyValue); - -impl Parse for Example { - fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result { - parse_utils::parse_next(input, || AnyValue::parse_any(input)).map(Self) - } -} - -impl ToTokens for Example { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - tokens.extend(self.0.to_token_stream()) - } -} - -impl From for Feature { - fn from(value: Example) -> Self { - Feature::Example(value) - } + attributes::Default, + attributes::Example, + attributes::Examples, + attributes::XmlAttr, + attributes::Format, + attributes::WriteOnly, + attributes::ReadOnly, + attributes::Title, + attributes::Nullable, + attributes::Rename, + attributes::RenameAll, + attributes::Style, + attributes::ParameterIn, + attributes::AllowReserved, + attributes::Explode, + attributes::ValueType, + attributes::Inline, + attributes::IntoParamsNames, + attributes::SchemaWith, + attributes::Description, + attributes::Deprecated, + attributes::As, + attributes::AdditionalProperties, + attributes::Required, + attributes::ContentEncoding, + attributes::ContentMediaType, + validation::MultipleOf = true, + validation::Maximum = true, + validation::Minimum = true, + validation::ExclusiveMaximum = true, + validation::ExclusiveMinimum = true, + validation::MaxLength = true, + validation::MinLength = true, + validation::Pattern = true, + validation::MaxItems = true, + validation::MinItems = true, + validation::MaxProperties, + validation::MinProperties } -name!(Example = "example"); - -#[derive(Clone)] -#[cfg_attr(feature = "debug", derive(Debug))] -pub struct Examples(Vec); - -impl Parse for Examples { - fn parse(input: ParseStream, _: Ident) -> syn::Result - where - Self: std::marker::Sized, - { - let examples; - parenthesized!(examples in input); - - Ok(Self( - Punctuated::::parse_terminated_with( - &examples, - AnyValue::parse_any, - )? - .into_iter() - .collect(), - )) - } -} - -impl ToTokens for Examples { - fn to_tokens(&self, tokens: &mut TokenStream) { - if !self.0.is_empty() { - let examples = Array::Borrowed(&self.0).to_token_stream(); - examples.to_tokens(tokens); - } - } -} +macro_rules! parse_features { + ($ident:ident as $( $feature:path ),*) => { + { + fn parse(input: syn::parse::ParseStream) -> syn::Result> { + let names = [$( ::get_name(), )* ]; + let mut features = Vec::::new(); + let attributes = names.join(", "); -impl From for Feature { - fn from(value: Examples) -> Self { - Feature::Examples(value) - } -} + while !input.is_empty() { + let ident = input.parse::().or_else(|_| { + input.parse::().map(|as_| syn::Ident::new("as", as_.span)) + }).map_err(|error| { + syn::Error::new( + error.span(), + format!("unexpected attribute, expected any of: {attributes}, {error}"), + ) + })?; + let name = &*ident.to_string(); -name!(Examples = "examples"); + $( + if name == ::get_name() { + features.push(<$feature as crate::component::features::Parse>::parse(input, ident)?.into()); + if !input.is_empty() { + input.parse::()?; + } + continue; + } + )* -#[derive(Clone)] -#[cfg_attr(feature = "debug", derive(Debug))] -pub struct Default(pub(crate) Option); + if !names.contains(&name) { + return Err(syn::Error::new(ident.span(), format!("unexpected attribute: {name}, expected any of: {attributes}"))) + } + } -impl Default { - pub fn new_default_trait(struct_ident: Ident, field_ident: syn::Member) -> Self { - Self(Some(AnyValue::new_default_trait(struct_ident, field_ident))) - } -} + Ok(features) + } -impl Parse for Default { - fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result { - if input.peek(syn::Token![=]) { - parse_utils::parse_next(input, || AnyValue::parse_any(input)).map(|any| Self(Some(any))) - } else { - Ok(Self(None)) + parse($ident)? } + }; + (@as_ident $( $tt:tt )* ) => { + $( $tt )* } } -impl ToTokens for Default { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - match &self.0 { - Some(inner) => tokens.extend(quote! {Some(#inner)}), - None => tokens.extend(quote! {None}), - } - } -} +pub(crate) use parse_features; -impl From for Feature { - fn from(value: self::Default) -> Self { - Feature::Default(value) - } +pub trait IsInline { + fn is_inline(&self) -> bool; } -name!(Default = "default"); - -#[derive(Clone)] -#[cfg_attr(feature = "debug", derive(Debug))] -pub struct Inline(bool); - -impl Parse for Inline { - fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result { - parse_utils::parse_bool_or_true(input).map(Self) +impl IsInline for Vec { + fn is_inline(&self) -> bool { + self.iter() + .find_map(|feature| match feature { + Feature::Inline(inline) if inline.0 => Some(inline), + _ => None, + }) + .is_some() } } -impl From for Inline { - fn from(value: bool) -> Self { - Inline(value) - } +pub trait ToTokensExt { + fn to_token_stream(&self) -> Result; } -impl From for Feature { - fn from(value: Inline) -> Self { - Feature::Inline(value) +impl ToTokensExt for Vec { + fn to_token_stream(&self) -> Result { + Ok(self + .iter() + .map(|feature| Ok(as_tokens_or_diagnostics!(feature))) + .collect::, Diagnostics>>()? + .into_iter() + .fold(TokenStream::new(), |mut tokens, item| { + item.to_tokens(&mut tokens); + tokens + })) } } -name!(Inline = "inline"); - -#[derive(Default, Clone)] -#[cfg_attr(feature = "debug", derive(Debug))] -pub struct XmlAttr(schema::xml::XmlAttr); +pub trait FeaturesExt { + fn pop_by(&mut self, op: impl FnMut(&Feature) -> bool) -> Option; -impl XmlAttr { - /// Split [`XmlAttr`] for [`GenericType::Vec`] returning tuple of [`XmlAttr`]s where first - /// one is for a vec and second one is for object field. - pub fn split_for_vec( + /// Extract [`XmlAttr`] feature for given `type_tree` if it has generic type [`GenericType::Vec`] + fn extract_vec_xml_feature( &mut self, type_tree: &TypeTree, - ) -> Result<(Option, Option), Diagnostics> { - if matches!(type_tree.generic_type, Some(GenericType::Vec)) { - let mut value_xml = mem::take(self); - let vec_xml = schema::xml::XmlAttr::with_wrapped( - mem::take(&mut value_xml.0.is_wrapped), - mem::take(&mut value_xml.0.wrap_name), - ); - - Ok((Some(XmlAttr(vec_xml)), Some(value_xml))) - } else { - self.validate_xml(&self.0)?; - - Ok((None, Some(mem::take(self)))) - } - } - - #[inline] - fn validate_xml(&self, xml: &schema::xml::XmlAttr) -> Result<(), Diagnostics> { - if let Some(wrapped_ident) = xml.is_wrapped.as_ref() { - Err(Diagnostics::with_span( - wrapped_ident.span(), - "cannot use `wrapped` attribute in non slice field type", - ) - .help("Try removing `wrapped` attribute or make your field `Vec`")) - } else { - Ok(()) - } - } -} - -impl Parse for XmlAttr { - fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result { - let xml; - parenthesized!(xml in input); - xml.parse::().map(Self) - } -} - -impl ToTokens for XmlAttr { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - tokens.extend(self.0.to_token_stream()) - } -} - -impl From for Feature { - fn from(value: XmlAttr) -> Self { - Feature::XmlAttr(value) - } -} - -name!(XmlAttr = "xml"); - -#[derive(Clone)] -#[cfg_attr(feature = "debug", derive(Debug))] -pub struct Format(SchemaFormat<'static>); - -impl Parse for Format { - fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result { - parse_utils::parse_next(input, || input.parse::()).map(Self) - } -} - -impl ToTokens for Format { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - tokens.extend(self.0.to_token_stream()) - } -} - -impl From for Feature { - fn from(value: Format) -> Self { - Feature::Format(value) - } -} - -name!(Format = "format"); - -#[derive(Clone)] -#[cfg_attr(feature = "debug", derive(Debug))] -pub struct ValueType(syn::Type); - -impl ValueType { - /// Create [`TypeTree`] from current [`syn::Type`]. - pub fn as_type_tree(&self) -> Result { - TypeTree::from_type(&self.0) - } -} - -impl Parse for ValueType { - fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result { - parse_utils::parse_next(input, || input.parse::()).map(Self) - } -} - -impl From for Feature { - fn from(value: ValueType) -> Self { - Feature::ValueType(value) - } -} - -name!(ValueType = "value_type"); - -#[derive(Clone, Copy)] -#[cfg_attr(feature = "debug", derive(Debug))] -pub struct WriteOnly(bool); - -impl Parse for WriteOnly { - fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result { - parse_utils::parse_bool_or_true(input).map(Self) - } -} - -impl ToTokens for WriteOnly { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - tokens.extend(self.0.to_token_stream()) - } -} - -impl From for Feature { - fn from(value: WriteOnly) -> Self { - Feature::WriteOnly(value) - } -} - -name!(WriteOnly = "write_only"); - -#[derive(Clone, Copy)] -#[cfg_attr(feature = "debug", derive(Debug))] -pub struct ReadOnly(bool); - -impl Parse for ReadOnly { - fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result { - parse_utils::parse_bool_or_true(input).map(Self) - } -} - -impl ToTokens for ReadOnly { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - tokens.extend(self.0.to_token_stream()) - } + ) -> Result, Diagnostics>; } -impl From for Feature { - fn from(value: ReadOnly) -> Self { - Feature::ReadOnly(value) +impl FeaturesExt for Vec { + fn pop_by(&mut self, op: impl FnMut(&Feature) -> bool) -> Option { + self.iter() + .position(op) + .map(|index| self.swap_remove(index)) } -} - -name!(ReadOnly = "read_only"); -#[derive(Clone)] -#[cfg_attr(feature = "debug", derive(Debug))] -pub struct Title(String); + fn extract_vec_xml_feature( + &mut self, + type_tree: &TypeTree, + ) -> Result, Diagnostics> { + self.iter_mut() + .find_map(|feature| match feature { + Feature::XmlAttr(xml_feature) => { + match xml_feature.split_for_vec(type_tree) { + Ok((vec_xml, value_xml)) => { + // replace the original xml attribute with split value xml + if let Some(mut xml) = value_xml { + mem::swap(xml_feature, &mut xml) + } -impl Parse for Title { - fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result { - parse_utils::parse_next_literal_str(input).map(Self) + Some(Ok(vec_xml.map(Feature::XmlAttr))) + } + Err(diagnostics) => Some(Err(diagnostics)), + } + } + _ => None, + }) + .and_then_try(|value| value) } } -impl ToTokens for Title { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - tokens.extend(self.0.to_token_stream()) +impl FeaturesExt for Option> { + fn pop_by(&mut self, op: impl FnMut(&Feature) -> bool) -> Option { + self.as_mut().and_then(|features| features.pop_by(op)) } -} -impl From for Feature { - fn from(value: Title) -> Self { - Feature::Title(value) + fn extract_vec_xml_feature( + &mut self, + type_tree: &TypeTree, + ) -> Result<Option<Feature>, Diagnostics> { + self.as_mut() + .and_then_try(|features| features.extract_vec_xml_feature(type_tree)) } } -name!(Title = "title"); - -#[derive(Clone, Copy)] -#[cfg_attr(feature = "debug", derive(Debug))] -pub struct Nullable(bool); - -impl Nullable { - pub fn new() -> Self { - Self(true) - } - - pub fn value(&self) -> bool { - self.0 - } - - pub fn into_schema_type_token_stream(self) -> proc_macro2::TokenStream { - if self.0 { - quote! {utoipa::openapi::schema::Type::Null} - } else { - proc_macro2::TokenStream::new() +/// Pull out a `Feature` from `Vec` of features by given match predicate. +/// This macro can be called in two forms demonstrated below. +/// ```text +/// let _: Option<Feature> = pop_feature!(features => Feature::Inline(_)); +/// let _: Option<Inline> = pop_feature!(feature => Feature::Inline(_) as Option<Inline>); +/// ``` +/// +/// The `as ...` syntax can be used to directly convert the `Feature` instance to it's inner form. +macro_rules! pop_feature { + ($features:ident => $( $ty:tt )* ) => {{ + pop_feature!( @inner $features $( $ty )* ) + }}; + ( @inner $features:ident $ty:tt :: $tv:tt ( $t:pat ) $( $tt:tt)* ) => { + { + let f = $features.pop_by(|feature| matches!(feature, $ty :: $tv ($t) ) ); + pop_feature!( @rest f $( $tt )* ) + } + }; + ( @rest $feature:ident as $ty:ty ) => { + { + let inner: $ty = $feature.into_inner(); + inner } - } -} - -impl Parse for Nullable { - fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result<Self> { - parse_utils::parse_bool_or_true(input).map(Self) - } -} - -impl ToTokens for Nullable { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - tokens.extend(self.0.to_token_stream()) - } -} - -impl From<Nullable> for Feature { - fn from(value: Nullable) -> Self { - Feature::Nullable(value) - } -} - -name!(Nullable = "nullable"); - -#[cfg_attr(feature = "debug", derive(Debug))] -#[derive(Clone)] -pub struct Rename(String); - -impl Rename { - pub fn into_value(self) -> String { - self.0 - } -} -impl Parse for Rename { - fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result<Self> { - parse_utils::parse_next_literal_str(input).map(Self) - } -} - -impl ToTokens for Rename { - fn to_tokens(&self, tokens: &mut TokenStream) { - tokens.extend(self.0.to_token_stream()) - } -} - -impl From<Rename> for Feature { - fn from(value: Rename) -> Self { - Feature::Rename(value) - } -} - -name!(Rename = "rename"); - -#[cfg_attr(feature = "debug", derive(Debug))] -#[derive(Clone)] -pub struct RenameAll(RenameRule); - -impl RenameAll { - pub fn as_rename_rule(&self) -> &RenameRule { - &self.0 - } -} - -impl Parse for RenameAll { - fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result<Self> { - let litstr = parse_utils::parse_next(input, || input.parse::<LitStr>())?; - - litstr - .value() - .parse::<RenameRule>() - .map_err(|error| syn::Error::new(litstr.span(), error.to_string())) - .map(Self) - } -} - -impl From<RenameAll> for Feature { - fn from(value: RenameAll) -> Self { - Feature::RenameAll(value) - } -} - -name!(RenameAll = "rename_all"); - -#[cfg_attr(feature = "debug", derive(Debug))] -#[derive(Clone)] -pub struct Style(ParameterStyle); - -impl From<ParameterStyle> for Style { - fn from(style: ParameterStyle) -> Self { - Self(style) - } -} - -impl Parse for Style { - fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result<Self> { - parse_utils::parse_next(input, || input.parse::<ParameterStyle>().map(Self)) - } -} - -impl ToTokens for Style { - fn to_tokens(&self, tokens: &mut TokenStream) { - self.0.to_tokens(tokens) - } -} - -impl From<Style> for Feature { - fn from(value: Style) -> Self { - Feature::Style(value) - } -} - -name!(Style = "style"); - -#[cfg_attr(feature = "debug", derive(Debug))] -#[derive(Clone)] -pub struct AllowReserved(bool); - -impl Parse for AllowReserved { - fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result<Self> { - parse_utils::parse_bool_or_true(input).map(Self) - } -} - -impl ToTokens for AllowReserved { - fn to_tokens(&self, tokens: &mut TokenStream) { - self.0.to_tokens(tokens) - } -} - -impl From<AllowReserved> for Feature { - fn from(value: AllowReserved) -> Self { - Feature::AllowReserved(value) - } -} - -name!(AllowReserved = "allow_reserved"); - -#[cfg_attr(feature = "debug", derive(Debug))] -#[derive(Clone)] -pub struct Explode(bool); - -impl Parse for Explode { - fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result<Self> { - parse_utils::parse_bool_or_true(input).map(Self) - } -} - -impl ToTokens for Explode { - fn to_tokens(&self, tokens: &mut TokenStream) { - self.0.to_tokens(tokens) - } -} - -impl From<Explode> for Feature { - fn from(value: Explode) -> Self { - Feature::Explode(value) - } -} - -name!(Explode = "explode"); - -#[cfg_attr(feature = "debug", derive(Debug))] -#[derive(Clone)] -pub struct ParameterIn(parameter::ParameterIn); - -impl Parse for ParameterIn { - fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result<Self> { - parse_utils::parse_next(input, || input.parse::<parameter::ParameterIn>().map(Self)) - } -} - -impl ToTokens for ParameterIn { - fn to_tokens(&self, tokens: &mut TokenStream) { - self.0.to_tokens(tokens); - } -} - -impl From<ParameterIn> for Feature { - fn from(value: ParameterIn) -> Self { - Feature::ParameterIn(value) - } -} - -name!(ParameterIn = "parameter_in"); - -/// Specify names of unnamed fields with `names(...) attribute for `IntoParams` derive. -#[cfg_attr(feature = "debug", derive(Debug))] -#[derive(Clone)] -pub struct IntoParamsNames(Vec<String>); - -impl IntoParamsNames { - pub fn into_values(self) -> Vec<String> { - self.0 - } -} - -impl Parse for IntoParamsNames { - fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result<Self> { - Ok(Self( - parse_utils::parse_punctuated_within_parenthesis::<LitStr>(input)? - .iter() - .map(LitStr::value) - .collect(), - )) - } -} - -impl From<IntoParamsNames> for Feature { - fn from(value: IntoParamsNames) -> Self { - Feature::IntoParamsNames(value) - } -} - -name!(IntoParamsNames = "names"); - -#[cfg_attr(feature = "debug", derive(Debug))] -#[derive(Clone)] -pub struct MultipleOf(f64, Ident); - -impl Validate for MultipleOf { - fn validate(&self, validator: impl Validator) -> Option<Diagnostics> { - match validator.is_valid() { - Err(error) => Some(Diagnostics::with_span(self.1.span(), format!( "`multiple_of` error: {}", error)) - .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-multipleof`")), - _ => None - } - } -} - -impl Parse for MultipleOf { - fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self> { - parse_number(input).map(|multiple_of| Self(multiple_of, ident)) - } -} - -impl ToTokens for MultipleOf { - fn to_tokens(&self, tokens: &mut TokenStream) { - self.0.to_tokens(tokens); - } -} - -impl From<MultipleOf> for Feature { - fn from(value: MultipleOf) -> Self { - Feature::MultipleOf(value) - } -} - -name!(MultipleOf = "multiple_of"); - -#[cfg_attr(feature = "debug", derive(Debug))] -#[derive(Clone)] -pub struct Maximum(f64, Ident); - -impl Validate for Maximum { - fn validate(&self, validator: impl Validator) -> Option<Diagnostics> { - match validator.is_valid() { - Err(error) => Some(Diagnostics::with_span(self.1.span(), format!("`maximum` error: {}", error)) - .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-maximum`")), - _ => None, - } - } -} - -impl Parse for Maximum { - fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self> - where - Self: Sized, - { - parse_number(input).map(|maximum| Self(maximum, ident)) - } -} - -impl ToTokens for Maximum { - fn to_tokens(&self, tokens: &mut TokenStream) { - self.0.to_tokens(tokens); - } -} - -impl From<Maximum> for Feature { - fn from(value: Maximum) -> Self { - Feature::Maximum(value) - } -} - -name!(Maximum = "maximum"); - -#[cfg_attr(feature = "debug", derive(Debug))] -#[derive(Clone)] -pub struct Minimum(f64, Ident); - -impl Minimum { - pub fn new(value: f64, span: Span) -> Self { - Self(value, Ident::new("empty", span)) - } -} - -impl Validate for Minimum { - fn validate(&self, validator: impl Validator) -> Option<Diagnostics> { - match validator.is_valid() { - Err(error) => Some( - Diagnostics::with_span(self.1.span(), format!("`minimum` error: {}", error)) - .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-minimum`") - ), - _ => None, - } - } -} - -impl Parse for Minimum { - fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self> - where - Self: Sized, - { - parse_number(input).map(|maximum| Self(maximum, ident)) - } -} - -impl ToTokens for Minimum { - fn to_tokens(&self, tokens: &mut TokenStream) { - self.0.to_tokens(tokens); - } -} - -impl From<Minimum> for Feature { - fn from(value: Minimum) -> Self { - Feature::Minimum(value) - } -} - -name!(Minimum = "minimum"); - -#[cfg_attr(feature = "debug", derive(Debug))] -#[derive(Clone)] -pub struct ExclusiveMaximum(f64, Ident); - -impl Validate for ExclusiveMaximum { - fn validate(&self, validator: impl Validator) -> Option<Diagnostics> { - match validator.is_valid() { - Err(error) => Some(Diagnostics::with_span(self.1.span(), format!("`exclusive_maximum` error: {}", error)) - .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-exclusivemaximum`")), - _ => None, - } - } -} - -impl Parse for ExclusiveMaximum { - fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self> - where - Self: Sized, - { - parse_number(input).map(|max| Self(max, ident)) - } -} - -impl ToTokens for ExclusiveMaximum { - fn to_tokens(&self, tokens: &mut TokenStream) { - self.0.to_tokens(tokens); - } -} - -impl From<ExclusiveMaximum> for Feature { - fn from(value: ExclusiveMaximum) -> Self { - Feature::ExclusiveMaximum(value) - } -} - -name!(ExclusiveMaximum = "exclusive_maximum"); - -#[cfg_attr(feature = "debug", derive(Debug))] -#[derive(Clone)] -pub struct ExclusiveMinimum(f64, Ident); - -impl Validate for ExclusiveMinimum { - fn validate(&self, validator: impl Validator) -> Option<Diagnostics> { - match validator.is_valid() { - Err(error) => Some(Diagnostics::with_span(self.1.span(), format!("`exclusive_minimum` error: {}", error)) - .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-exclusiveminimum`")), - _ => None, - } - } -} - -impl Parse for ExclusiveMinimum { - fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self> - where - Self: Sized, - { - parse_number(input).map(|min| Self(min, ident)) - } -} - -impl ToTokens for ExclusiveMinimum { - fn to_tokens(&self, tokens: &mut TokenStream) { - self.0.to_tokens(tokens); - } -} - -impl From<ExclusiveMinimum> for Feature { - fn from(value: ExclusiveMinimum) -> Self { - Feature::ExclusiveMinimum(value) - } -} - -name!(ExclusiveMinimum = "exclusive_minimum"); - -#[cfg_attr(feature = "debug", derive(Debug))] -#[derive(Clone)] -pub struct MaxLength(usize, Ident); - -impl Validate for MaxLength { - fn validate(&self, validator: impl Validator) -> Option<Diagnostics> { - match validator.is_valid() { - Err(error) => Some(Diagnostics::with_span(self.1.span(), format!("`max_length` error: {}", error)) - .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-maxlength`")), - _ => None, - } - } -} - -impl Parse for MaxLength { - fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self> - where - Self: Sized, - { - parse_integer(input).map(|max_length| Self(max_length, ident)) - } -} - -impl ToTokens for MaxLength { - fn to_tokens(&self, tokens: &mut TokenStream) { - self.0.to_tokens(tokens); - } -} - -impl From<MaxLength> for Feature { - fn from(value: MaxLength) -> Self { - Feature::MaxLength(value) - } -} - -name!(MaxLength = "max_length"); - -#[cfg_attr(feature = "debug", derive(Debug))] -#[derive(Clone)] -pub struct MinLength(usize, Ident); - -impl Validate for MinLength { - fn validate(&self, validator: impl Validator) -> Option<Diagnostics> { - match validator.is_valid() { - Err(error) => Some(Diagnostics::with_span(self.1.span(), format!("`min_length` error: {}", error)) - .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-minlength`")), - _ => None, - } - } -} - -impl Parse for MinLength { - fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self> - where - Self: Sized, - { - parse_integer(input).map(|max_length| Self(max_length, ident)) - } -} - -impl ToTokens for MinLength { - fn to_tokens(&self, tokens: &mut TokenStream) { - self.0.to_tokens(tokens); - } -} - -impl From<MinLength> for Feature { - fn from(value: MinLength) -> Self { - Feature::MinLength(value) - } -} - -name!(MinLength = "min_length"); - -#[cfg_attr(feature = "debug", derive(Debug))] -#[derive(Clone)] -pub struct Pattern(String, Ident); - -impl Validate for Pattern { - fn validate(&self, validator: impl Validator) -> Option<Diagnostics> { - match validator.is_valid() { - Err(error) => Some(Diagnostics::with_span(self.1.span(), format!("`pattern` error: {}", error)) - .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-pattern`") - ), - _ => None, - } - } -} - -impl Parse for Pattern { - fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self> - where - Self: Sized, - { - parse_utils::parse_next(input, || input.parse::<LitStr>()) - .map(|pattern| Self(pattern.value(), ident)) - } -} - -impl ToTokens for Pattern { - fn to_tokens(&self, tokens: &mut TokenStream) { - self.0.to_tokens(tokens); - } -} - -impl From<Pattern> for Feature { - fn from(value: Pattern) -> Self { - Feature::Pattern(value) - } -} - -name!(Pattern = "pattern"); - -#[cfg_attr(feature = "debug", derive(Debug))] -#[derive(Clone)] -pub struct MaxItems(usize, Ident); - -impl Validate for MaxItems { - fn validate(&self, validator: impl Validator) -> Option<Diagnostics> { - match validator.is_valid() { - Err(error) => Some(Diagnostics::with_span(self.1.span(), format!("`max_items` error: {}", error)) - .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-maxitems")), - _ => None, - } - } -} - -impl Parse for MaxItems { - fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self> - where - Self: Sized, - { - parse_number(input).map(|max_items| Self(max_items, ident)) - } -} - -impl ToTokens for MaxItems { - fn to_tokens(&self, tokens: &mut TokenStream) { - self.0.to_tokens(tokens); - } -} - -impl From<MaxItems> for Feature { - fn from(value: MaxItems) -> Self { - Feature::MaxItems(value) - } -} - -name!(MaxItems = "max_items"); - -#[cfg_attr(feature = "debug", derive(Debug))] -#[derive(Clone)] -pub struct MinItems(usize, Ident); - -impl Validate for MinItems { - fn validate(&self, validator: impl Validator) -> Option<Diagnostics> { - match validator.is_valid() { - Err(error) => Some(Diagnostics::with_span(self.1.span(), format!("`min_items` error: {}", error)) - .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-minitems")), - _ => None, - } - } -} - -impl Parse for MinItems { - fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self> - where - Self: Sized, - { - parse_number(input).map(|max_items| Self(max_items, ident)) - } -} - -impl ToTokens for MinItems { - fn to_tokens(&self, tokens: &mut TokenStream) { - self.0.to_tokens(tokens); - } -} - -impl From<MinItems> for Feature { - fn from(value: MinItems) -> Self { - Feature::MinItems(value) - } -} - -name!(MinItems = "min_items"); - -#[cfg_attr(feature = "debug", derive(Debug))] -#[derive(Clone)] -pub struct MaxProperties(usize, ()); - -impl Parse for MaxProperties { - fn parse(input: ParseStream, _ident: Ident) -> syn::Result<Self> - where - Self: Sized, - { - parse_integer(input).map(|max_properties| Self(max_properties, ())) - } -} - -impl ToTokens for MaxProperties { - fn to_tokens(&self, tokens: &mut TokenStream) { - self.0.to_tokens(tokens); - } -} - -impl From<MaxProperties> for Feature { - fn from(value: MaxProperties) -> Self { - Feature::MaxProperties(value) - } -} - -name!(MaxProperties = "max_properties"); - -#[cfg_attr(feature = "debug", derive(Debug))] -#[derive(Clone)] -pub struct MinProperties(usize, ()); - -impl Parse for MinProperties { - fn parse(input: ParseStream, _ident: Ident) -> syn::Result<Self> - where - Self: Sized, - { - parse_integer(input).map(|min_properties| Self(min_properties, ())) - } -} - -impl ToTokens for MinProperties { - fn to_tokens(&self, tokens: &mut TokenStream) { - self.0.to_tokens(tokens); - } -} - -impl From<MinProperties> for Feature { - fn from(value: MinProperties) -> Self { - Feature::MinProperties(value) - } -} - -name!(MinProperties = "min_properties"); - -#[cfg_attr(feature = "debug", derive(Debug))] -#[derive(Clone)] -pub struct SchemaWith(TypePath); - -impl Parse for SchemaWith { - fn parse(input: ParseStream, _: Ident) -> syn::Result<Self> { - parse_utils::parse_next(input, || input.parse::<TypePath>().map(Self)) - } -} - -impl ToTokens for SchemaWith { - fn to_tokens(&self, tokens: &mut TokenStream) { - let path = &self.0; - tokens.extend(quote! { - #path() - }) - } -} - -impl From<SchemaWith> for Feature { - fn from(value: SchemaWith) -> Self { - Feature::SchemaWith(value) - } -} - -name!(SchemaWith = "schema_with"); - -#[cfg_attr(feature = "debug", derive(Debug))] -#[derive(Clone)] -pub struct Description(parse_utils::Value); - -impl Parse for Description { - fn parse(input: ParseStream, _: Ident) -> syn::Result<Self> - where - Self: std::marker::Sized, - { - parse_utils::parse_next_literal_str_or_expr(input).map(Self) - } -} - -impl ToTokens for Description { - fn to_tokens(&self, tokens: &mut TokenStream) { - self.0.to_tokens(tokens); - } -} - -impl From<String> for Description { - fn from(value: String) -> Self { - Self(value.into()) - } -} - -impl From<Description> for Feature { - fn from(value: Description) -> Self { - Self::Description(value) - } -} - -name!(Description = "description"); - -/// Deprecated feature parsed from macro attributes. -/// -/// This feature supports only syntax parsed from utoipa specific macro attributes, it does not -/// support Rust `#[deprecated]` attribute. -#[cfg_attr(feature = "debug", derive(Debug))] -#[derive(Clone)] -pub struct Deprecated(bool); - -impl Parse for Deprecated { - fn parse(input: ParseStream, _: Ident) -> syn::Result<Self> - where - Self: std::marker::Sized, - { - parse_utils::parse_bool_or_true(input).map(Self) - } -} - -impl ToTokens for Deprecated { - fn to_tokens(&self, tokens: &mut TokenStream) { - let deprecated: crate::Deprecated = self.0.into(); - deprecated.to_tokens(tokens); - } -} - -impl From<Deprecated> for Feature { - fn from(value: Deprecated) -> Self { - Self::Deprecated(value) - } -} - -name!(Deprecated = "deprecated"); - -#[cfg_attr(feature = "debug", derive(Debug))] -#[derive(Clone)] -pub struct As(pub TypePath); - -impl Parse for As { - fn parse(input: ParseStream, _: Ident) -> syn::Result<Self> - where - Self: std::marker::Sized, - { - parse_utils::parse_next(input, || input.parse()).map(Self) - } -} - -impl From<As> for Feature { - fn from(value: As) -> Self { - Self::As(value) - } -} - -name!(As = "as"); - -#[cfg_attr(feature = "debug", derive(Debug))] -#[derive(Clone)] -pub struct AdditionalProperties(bool); - -impl Parse for AdditionalProperties { - fn parse(input: ParseStream, _: Ident) -> syn::Result<Self> - where - Self: std::marker::Sized, - { - parse_utils::parse_bool_or_true(input).map(Self) - } -} - -impl ToTokens for AdditionalProperties { - fn to_tokens(&self, tokens: &mut TokenStream) { - let additional_properties = &self.0; - tokens.extend(quote!( - utoipa::openapi::schema::AdditionalProperties::FreeForm( - #additional_properties - ) - )) - } -} - -name!(AdditionalProperties = "additional_properties"); - -impl From<AdditionalProperties> for Feature { - fn from(value: AdditionalProperties) -> Self { - Self::AdditionalProperties(value) - } -} - -#[derive(Clone)] -#[cfg_attr(feature = "debug", derive(Debug))] -pub struct Required(pub bool); - -impl Required { - pub fn is_true(&self) -> bool { - self.0 - } -} - -impl Parse for Required { - fn parse(input: ParseStream, _: Ident) -> syn::Result<Self> - where - Self: std::marker::Sized, - { - parse_utils::parse_bool_or_true(input).map(Self) - } -} - -impl ToTokens for Required { - fn to_tokens(&self, tokens: &mut TokenStream) { - self.0.to_tokens(tokens) - } -} - -impl From<crate::Required> for Required { - fn from(value: crate::Required) -> Self { - if value == crate::Required::True { - Self(true) - } else { - Self(false) - } - } -} - -impl From<bool> for Required { - fn from(value: bool) -> Self { - Self(value) - } -} - -impl From<Required> for Feature { - fn from(value: Required) -> Self { - Self::Required(value) - } -} - -name!(Required = "required"); - -#[derive(Clone)] -#[cfg_attr(feature = "debug", derive(Debug))] -pub struct ContentEncoding(String); - -impl Parse for ContentEncoding { - fn parse(input: ParseStream, _: Ident) -> syn::Result<Self> - where - Self: std::marker::Sized, - { - parse_utils::parse_next_literal_str(input).map(Self) - } -} - -impl ToTokens for ContentEncoding { - fn to_tokens(&self, tokens: &mut TokenStream) { - self.0.to_tokens(tokens); - } -} - -name!(ContentEncoding = "content_encoding"); - -impl From<ContentEncoding> for Feature { - fn from(value: ContentEncoding) -> Self { - Self::ContentEncoding(value) - } -} - -#[derive(Clone)] -#[cfg_attr(feature = "debug", derive(Debug))] -pub struct ContentMediaType(String); - -impl Parse for ContentMediaType { - fn parse(input: ParseStream, _: Ident) -> syn::Result<Self> - where - Self: std::marker::Sized, - { - parse_utils::parse_next_literal_str(input).map(Self) - } -} - -impl ToTokens for ContentMediaType { - fn to_tokens(&self, tokens: &mut TokenStream) { - self.0.to_tokens(tokens); - } -} - -impl From<ContentMediaType> for Feature { - fn from(value: ContentMediaType) -> Self { - Self::ContentMediaType(value) - } -} - -name!(ContentMediaType = "content_media_type"); - -pub trait Validator { - fn is_valid(&self) -> Result<(), &'static str>; -} - -pub struct IsNumber<'a>(pub &'a SchemaType<'a>); - -impl Validator for IsNumber<'_> { - fn is_valid(&self) -> Result<(), &'static str> { - if self.0.is_number() { - Ok(()) - } else { - Err("can only be used with `number` type") - } - } -} - -pub struct IsString<'a>(&'a SchemaType<'a>); - -impl Validator for IsString<'_> { - fn is_valid(&self) -> Result<(), &'static str> { - if self.0.is_string() { - Ok(()) - } else { - Err("can only be used with `string` type") - } - } -} - -pub struct IsInteger<'a>(&'a SchemaType<'a>); - -impl Validator for IsInteger<'_> { - fn is_valid(&self) -> Result<(), &'static str> { - if self.0.is_integer() { - Ok(()) - } else { - Err("can only be used with `integer` type") - } - } -} - -pub struct IsVec<'a>(&'a TypeTree<'a>); - -impl Validator for IsVec<'_> { - fn is_valid(&self) -> Result<(), &'static str> { - if self.0.generic_type == Some(GenericType::Vec) { - Ok(()) - } else { - Err("can only be used with `Vec`, `array` or `slice` types") - } - } -} - -pub struct AboveZeroUsize(usize); - -impl Validator for AboveZeroUsize { - fn is_valid(&self) -> Result<(), &'static str> { - if self.0 != 0 { - Ok(()) - } else { - Err("can only be above zero value") - } - } -} - -pub struct AboveZeroF64(f64); - -impl Validator for AboveZeroF64 { - fn is_valid(&self) -> Result<(), &'static str> { - if self.0 > 0.0 { - Ok(()) - } else { - Err("can only be above zero value") - } - } -} - -pub struct ValidatorChain<'c> { - inner: &'c dyn Validator, - next: Option<&'c dyn Validator>, -} - -impl Validator for ValidatorChain<'_> { - fn is_valid(&self) -> Result<(), &'static str> { - self.inner.is_valid().and_then(|_| { - if let Some(validator) = self.next.as_ref() { - validator.is_valid() - } else { - // if there is no next validator consider it valid - Ok(()) - } - }) - } -} - -impl<'c> ValidatorChain<'c> { - pub fn new(validator: &'c dyn Validator) -> Self { - Self { - inner: validator, - next: None, - } - } - - pub fn next(mut self, validator: &'c dyn Validator) -> Self { - self.next = Some(validator); - - self - } -} - -macro_rules! parse_features { - ($ident:ident as $( $feature:path ),*) => { - { - fn parse(input: syn::parse::ParseStream) -> syn::Result<Vec<crate::component::features::Feature>> { - let names = [$( <crate::component::features::parse_features!(@as_ident $feature) as crate::component::features::Name>::get_name(), )* ]; - let mut features = Vec::<crate::component::features::Feature>::new(); - let attributes = names.join(", "); - - while !input.is_empty() { - let ident = input.parse::<syn::Ident>().or_else(|_| { - input.parse::<syn::Token![as]>().map(|as_| syn::Ident::new("as", as_.span)) - }).map_err(|error| { - syn::Error::new( - error.span(), - format!("unexpected attribute, expected any of: {attributes}, {error}"), - ) - })?; - let name = &*ident.to_string(); - - $( - if name == <crate::component::features::parse_features!(@as_ident $feature) as crate::component::features::Name>::get_name() { - features.push(<$feature as crate::component::features::Parse>::parse(input, ident)?.into()); - if !input.is_empty() { - input.parse::<syn::Token![,]>()?; - } - continue; - } - )* - - if !names.contains(&name) { - return Err(syn::Error::new(ident.span(), format!("unexpected attribute: {name}, expected any of: {attributes}"))) - } - } - - Ok(features) - } - - parse($ident)? - } - }; - (@as_ident $( $tt:tt )* ) => { - $( $tt )* - } -} - -pub(crate) use parse_features; - -pub trait IsInline { - fn is_inline(&self) -> bool; -} - -impl IsInline for Vec<Feature> { - fn is_inline(&self) -> bool { - self.iter() - .find_map(|feature| match feature { - Feature::Inline(inline) if inline.0 => Some(inline), - _ => None, - }) - .is_some() - } -} - -pub trait ToTokensExt { - fn to_token_stream(&self) -> Result<TokenStream, Diagnostics>; -} - -impl ToTokensExt for Vec<Feature> { - fn to_token_stream(&self) -> Result<TokenStream, Diagnostics> { - Ok(self - .iter() - .map(|feature| Ok(as_tokens_or_diagnostics!(feature))) - .collect::<Result<Vec<TokenStream>, Diagnostics>>()? - .into_iter() - .fold(TokenStream::new(), |mut tokens, item| { - item.to_tokens(&mut tokens); - tokens - })) - } -} - -pub trait FeaturesExt { - fn pop_by(&mut self, op: impl FnMut(&Feature) -> bool) -> Option<Feature>; - - fn pop_value_type_feature(&mut self) -> Option<super::features::ValueType>; - - /// Pop [`Rename`] feature if exists in [`Vec<Feature>`] list. - fn pop_rename_feature(&mut self) -> Option<Rename>; - - /// Pop [`RenameAll`] feature if exists in [`Vec<Feature>`] list. - fn pop_rename_all_feature(&mut self) -> Option<RenameAll>; - - /// Extract [`XmlAttr`] feature for given `type_tree` if it has generic type [`GenericType::Vec`] - fn extract_vec_xml_feature( - &mut self, - type_tree: &TypeTree, - ) -> Result<Option<Feature>, Diagnostics>; -} - -impl FeaturesExt for Vec<Feature> { - fn pop_by(&mut self, op: impl FnMut(&Feature) -> bool) -> Option<Feature> { - self.iter() - .position(op) - .map(|index| self.swap_remove(index)) - } - - fn pop_value_type_feature(&mut self) -> Option<super::features::ValueType> { - self.pop_by(|feature| matches!(feature, Feature::ValueType(_))) - .and_then(|feature| match feature { - Feature::ValueType(value_type) => Some(value_type), - _ => None, - }) - } - - fn pop_rename_feature(&mut self) -> Option<Rename> { - self.pop_by(|feature| matches!(feature, Feature::Rename(_))) - .and_then(|feature| match feature { - Feature::Rename(rename) => Some(rename), - _ => None, - }) - } - - fn pop_rename_all_feature(&mut self) -> Option<RenameAll> { - self.pop_by(|feature| matches!(feature, Feature::RenameAll(_))) - .and_then(|feature| match feature { - Feature::RenameAll(rename_all) => Some(rename_all), - _ => None, - }) - } - - fn extract_vec_xml_feature( - &mut self, - type_tree: &TypeTree, - ) -> Result<Option<Feature>, Diagnostics> { - self.iter_mut() - .find_map(|feature| match feature { - Feature::XmlAttr(xml_feature) => { - match xml_feature.split_for_vec(type_tree) { - Ok((vec_xml, value_xml)) => { - // replace the original xml attribute with split value xml - if let Some(mut xml) = value_xml { - mem::swap(xml_feature, &mut xml) - } - - Some(Ok(vec_xml.map(Feature::XmlAttr))) - } - Err(diagnostics) => Some(Err(diagnostics)), - } - } - _ => None, - }) - .and_then_try(|value| value) - } -} - -impl FeaturesExt for Option<Vec<Feature>> { - fn pop_by(&mut self, op: impl FnMut(&Feature) -> bool) -> Option<Feature> { - self.as_mut().and_then(|features| features.pop_by(op)) - } - - fn pop_value_type_feature(&mut self) -> Option<super::features::ValueType> { - self.as_mut() - .and_then(|features| features.pop_value_type_feature()) - } - - fn pop_rename_feature(&mut self) -> Option<Rename> { - self.as_mut() - .and_then(|features| features.pop_rename_feature()) - } - - fn pop_rename_all_feature(&mut self) -> Option<RenameAll> { - self.as_mut() - .and_then(|features| features.pop_rename_all_feature()) - } - - fn extract_vec_xml_feature( - &mut self, - type_tree: &TypeTree, - ) -> Result<Option<Feature>, Diagnostics> { - self.as_mut() - .and_then_try(|features| features.extract_vec_xml_feature(type_tree)) - } -} - -macro_rules! pop_feature { - ($features:ident => $value:pat_param) => {{ - $features.pop_by(|feature| matches!(feature, $value)) - }}; + }; + ( @rest $($tt:tt)* ) => { + $($tt)* + }; } pub(crate) use pop_feature; -macro_rules! pop_feature_as_inner { - ( $features:ident => $($value:tt)* ) => { - crate::component::features::pop_feature!($features => $( $value )* ) - .map(|f| match f { - $( $value )* => { - crate::component::features::pop_feature_as_inner!( @as_value $( $value )* ) - }, - _ => unreachable!() - }) - }; - ( @as_value $tt:tt :: $tr:tt ( $v:tt ) ) => { - $v - } -} - -pub(crate) use pop_feature_as_inner; - pub trait IntoInner<T> { fn into_inner(self) -> T; } macro_rules! impl_feature_into_inner { - ( $( $feat:ident , )* ) => { + ( $( $feat:ident :: $impl:ident , )* ) => { $( - impl IntoInner<Option<$feat>> for Option<Feature> { - fn into_inner(self) -> Option<$feat> { + impl IntoInner<Option<$feat::$impl>> for Option<Feature> { + fn into_inner(self) -> Option<$feat::$impl> { self.and_then(|feature| match feature { - Feature::$feat(value) => Some(value), + Feature::$impl(value) => Some(value), _ => None, }) } @@ -1966,42 +599,42 @@ macro_rules! impl_feature_into_inner { } impl_feature_into_inner! { - Example, - Examples, - Default, - Inline, - XmlAttr, - Format, - ValueType, - WriteOnly, - ReadOnly, - Title, - Nullable, - Rename, - RenameAll, - Style, - AllowReserved, - Explode, - ParameterIn, - IntoParamsNames, - MultipleOf, - Maximum, - Minimum, - ExclusiveMaximum, - ExclusiveMinimum, - MaxLength, - MinLength, - Pattern, - MaxItems, - MinItems, - MaxProperties, - MinProperties, - SchemaWith, - Description, - Deprecated, - As, - AdditionalProperties, - Required, + attributes::Example, + attributes::Examples, + attributes::Default, + attributes::Inline, + attributes::XmlAttr, + attributes::Format, + attributes::ValueType, + attributes::WriteOnly, + attributes::ReadOnly, + attributes::Title, + attributes::Nullable, + attributes::Rename, + attributes::RenameAll, + attributes::Style, + attributes::AllowReserved, + attributes::Explode, + attributes::ParameterIn, + attributes::IntoParamsNames, + attributes::SchemaWith, + attributes::Description, + attributes::Deprecated, + attributes::As, + attributes::AdditionalProperties, + attributes::Required, + validation::MultipleOf, + validation::Maximum, + validation::Minimum, + validation::ExclusiveMaximum, + validation::ExclusiveMinimum, + validation::MaxLength, + validation::MinLength, + validation::Pattern, + validation::MaxItems, + validation::MinItems, + validation::MaxProperties, + validation::MinProperties, } macro_rules! impl_into_inner { @@ -2037,6 +670,7 @@ macro_rules! impl_merge { impl crate::component::features::Merge<$ident> for $ident { fn merge(mut self, from: $ident) -> Self { + use $crate::component::features::IntoInner; let a = self.as_mut(); let mut b = from.into_inner(); diff --git a/utoipa-gen/src/component/features/attributes.rs b/utoipa-gen/src/component/features/attributes.rs new file mode 100644 index 00000000..4648a99a --- /dev/null +++ b/utoipa-gen/src/component/features/attributes.rs @@ -0,0 +1,806 @@ +use std::mem; + +use proc_macro2::{Ident, TokenStream}; +use quote::ToTokens; +use syn::parse::ParseStream; +use syn::punctuated::Punctuated; +use syn::{LitStr, Token, TypePath}; + +use crate::component::serde::RenameRule; +use crate::component::{schema, GenericType, TypeTree}; +use crate::path::parameter::{self, ParameterStyle}; +use crate::schema_type::SchemaFormat; +use crate::{parse_utils, AnyValue, Array, Diagnostics}; + +use super::{name, Feature, Parse}; +use quote::quote; + +#[derive(Clone)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct Default(pub(crate) Option<AnyValue>); + +impl Default { + pub fn new_default_trait(struct_ident: Ident, field_ident: syn::Member) -> Self { + Self(Some(AnyValue::new_default_trait(struct_ident, field_ident))) + } +} + +impl Parse for Default { + fn parse(input: syn::parse::ParseStream, _: proc_macro2::Ident) -> syn::Result<Self> { + if input.peek(syn::Token![=]) { + parse_utils::parse_next(input, || AnyValue::parse_any(input)).map(|any| Self(Some(any))) + } else { + Ok(Self(None)) + } + } +} + +impl ToTokens for Default { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + match &self.0 { + Some(inner) => tokens.extend(quote! {Some(#inner)}), + None => tokens.extend(quote! {None}), + } + } +} + +impl From<self::Default> for Feature { + fn from(value: self::Default) -> Self { + Feature::Default(value) + } +} + +name!(Default = "default"); + +#[derive(Clone)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct Example(AnyValue); + +impl Parse for Example { + fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result<Self> { + parse_utils::parse_next(input, || AnyValue::parse_any(input)).map(Self) + } +} + +impl ToTokens for Example { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + tokens.extend(self.0.to_token_stream()) + } +} + +impl From<Example> for Feature { + fn from(value: Example) -> Self { + Feature::Example(value) + } +} + +name!(Example = "example"); + +#[derive(Clone)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct Examples(Vec<AnyValue>); + +impl Parse for Examples { + fn parse(input: ParseStream, _: Ident) -> syn::Result<Self> + where + Self: std::marker::Sized, + { + let examples; + syn::parenthesized!(examples in input); + + Ok(Self( + Punctuated::<AnyValue, Token![,]>::parse_terminated_with( + &examples, + AnyValue::parse_any, + )? + .into_iter() + .collect(), + )) + } +} + +impl ToTokens for Examples { + fn to_tokens(&self, tokens: &mut TokenStream) { + if !self.0.is_empty() { + let examples = Array::Borrowed(&self.0).to_token_stream(); + examples.to_tokens(tokens); + } + } +} + +impl From<Examples> for Feature { + fn from(value: Examples) -> Self { + Feature::Examples(value) + } +} + +name!(Examples = "examples"); + +#[derive(Default, Clone)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct XmlAttr(schema::xml::XmlAttr); + +impl XmlAttr { + /// Split [`XmlAttr`] for [`GenericType::Vec`] returning tuple of [`XmlAttr`]s where first + /// one is for a vec and second one is for object field. + pub fn split_for_vec( + &mut self, + type_tree: &TypeTree, + ) -> Result<(Option<XmlAttr>, Option<XmlAttr>), Diagnostics> { + if matches!(type_tree.generic_type, Some(GenericType::Vec)) { + let mut value_xml = mem::take(self); + let vec_xml = schema::xml::XmlAttr::with_wrapped( + mem::take(&mut value_xml.0.is_wrapped), + mem::take(&mut value_xml.0.wrap_name), + ); + + Ok((Some(XmlAttr(vec_xml)), Some(value_xml))) + } else { + self.validate_xml(&self.0)?; + + Ok((None, Some(mem::take(self)))) + } + } + + #[inline] + fn validate_xml(&self, xml: &schema::xml::XmlAttr) -> Result<(), Diagnostics> { + if let Some(wrapped_ident) = xml.is_wrapped.as_ref() { + Err(Diagnostics::with_span( + wrapped_ident.span(), + "cannot use `wrapped` attribute in non slice field type", + ) + .help("Try removing `wrapped` attribute or make your field `Vec`")) + } else { + Ok(()) + } + } +} + +impl Parse for XmlAttr { + fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result<Self> { + let xml; + syn::parenthesized!(xml in input); + xml.parse::<schema::xml::XmlAttr>().map(Self) + } +} + +impl ToTokens for XmlAttr { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + tokens.extend(self.0.to_token_stream()) + } +} + +impl From<XmlAttr> for Feature { + fn from(value: XmlAttr) -> Self { + Feature::XmlAttr(value) + } +} + +name!(XmlAttr = "xml"); + +#[derive(Clone)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct Format(SchemaFormat<'static>); + +impl Parse for Format { + fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result<Self> { + parse_utils::parse_next(input, || input.parse::<SchemaFormat>()).map(Self) + } +} + +impl ToTokens for Format { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + tokens.extend(self.0.to_token_stream()) + } +} + +impl From<Format> for Feature { + fn from(value: Format) -> Self { + Feature::Format(value) + } +} + +name!(Format = "format"); + +#[derive(Clone, Copy)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct WriteOnly(bool); + +impl Parse for WriteOnly { + fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result<Self> { + parse_utils::parse_bool_or_true(input).map(Self) + } +} + +impl ToTokens for WriteOnly { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + tokens.extend(self.0.to_token_stream()) + } +} + +impl From<WriteOnly> for Feature { + fn from(value: WriteOnly) -> Self { + Feature::WriteOnly(value) + } +} + +name!(WriteOnly = "write_only"); + +#[derive(Clone, Copy)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct ReadOnly(bool); + +impl Parse for ReadOnly { + fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result<Self> { + parse_utils::parse_bool_or_true(input).map(Self) + } +} + +impl ToTokens for ReadOnly { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + tokens.extend(self.0.to_token_stream()) + } +} + +impl From<ReadOnly> for Feature { + fn from(value: ReadOnly) -> Self { + Feature::ReadOnly(value) + } +} + +name!(ReadOnly = "read_only"); + +#[derive(Clone)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct Title(String); + +impl Parse for Title { + fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result<Self> { + parse_utils::parse_next_literal_str(input).map(Self) + } +} + +impl ToTokens for Title { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + tokens.extend(self.0.to_token_stream()) + } +} + +impl From<Title> for Feature { + fn from(value: Title) -> Self { + Feature::Title(value) + } +} + +name!(Title = "title"); + +#[derive(Clone, Copy)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct Nullable(bool); + +impl Nullable { + pub fn new() -> Self { + Self(true) + } + + pub fn value(&self) -> bool { + self.0 + } + + pub fn into_schema_type_token_stream(self) -> proc_macro2::TokenStream { + if self.0 { + quote! {utoipa::openapi::schema::Type::Null} + } else { + proc_macro2::TokenStream::new() + } + } +} + +impl Parse for Nullable { + fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result<Self> { + parse_utils::parse_bool_or_true(input).map(Self) + } +} + +impl ToTokens for Nullable { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + tokens.extend(self.0.to_token_stream()) + } +} + +impl From<Nullable> for Feature { + fn from(value: Nullable) -> Self { + Feature::Nullable(value) + } +} + +name!(Nullable = "nullable"); + +#[cfg_attr(feature = "debug", derive(Debug))] +#[derive(Clone)] +pub struct Rename(String); + +impl Rename { + pub fn into_value(self) -> String { + self.0 + } +} + +impl Parse for Rename { + fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result<Self> { + parse_utils::parse_next_literal_str(input).map(Self) + } +} + +impl ToTokens for Rename { + fn to_tokens(&self, tokens: &mut TokenStream) { + tokens.extend(self.0.to_token_stream()) + } +} + +impl From<Rename> for Feature { + fn from(value: Rename) -> Self { + Feature::Rename(value) + } +} + +name!(Rename = "rename"); + +#[cfg_attr(feature = "debug", derive(Debug))] +#[derive(Clone)] +pub struct RenameAll(RenameRule); + +impl RenameAll { + pub fn as_rename_rule(&self) -> &RenameRule { + &self.0 + } +} + +impl Parse for RenameAll { + fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result<Self> { + let litstr = parse_utils::parse_next(input, || input.parse::<LitStr>())?; + + litstr + .value() + .parse::<RenameRule>() + .map_err(|error| syn::Error::new(litstr.span(), error.to_string())) + .map(Self) + } +} + +impl From<RenameAll> for Feature { + fn from(value: RenameAll) -> Self { + Feature::RenameAll(value) + } +} + +name!(RenameAll = "rename_all"); + +#[cfg_attr(feature = "debug", derive(Debug))] +#[derive(Clone)] +pub struct Style(ParameterStyle); + +impl From<ParameterStyle> for Style { + fn from(style: ParameterStyle) -> Self { + Self(style) + } +} + +impl Parse for Style { + fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result<Self> { + parse_utils::parse_next(input, || input.parse::<ParameterStyle>().map(Self)) + } +} + +impl ToTokens for Style { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.to_tokens(tokens) + } +} + +impl From<Style> for Feature { + fn from(value: Style) -> Self { + Feature::Style(value) + } +} + +name!(Style = "style"); + +#[cfg_attr(feature = "debug", derive(Debug))] +#[derive(Clone)] +pub struct ParameterIn(parameter::ParameterIn); + +impl Parse for ParameterIn { + fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result<Self> { + parse_utils::parse_next(input, || input.parse::<parameter::ParameterIn>().map(Self)) + } +} + +impl ToTokens for ParameterIn { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.to_tokens(tokens); + } +} + +impl From<ParameterIn> for Feature { + fn from(value: ParameterIn) -> Self { + Feature::ParameterIn(value) + } +} + +name!(ParameterIn = "parameter_in"); + +#[cfg_attr(feature = "debug", derive(Debug))] +#[derive(Clone)] +pub struct AllowReserved(bool); + +impl Parse for AllowReserved { + fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result<Self> { + parse_utils::parse_bool_or_true(input).map(Self) + } +} + +impl ToTokens for AllowReserved { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.to_tokens(tokens) + } +} + +impl From<AllowReserved> for Feature { + fn from(value: AllowReserved) -> Self { + Feature::AllowReserved(value) + } +} + +name!(AllowReserved = "allow_reserved"); + +#[cfg_attr(feature = "debug", derive(Debug))] +#[derive(Clone)] +pub struct Explode(bool); + +impl Parse for Explode { + fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result<Self> { + parse_utils::parse_bool_or_true(input).map(Self) + } +} + +impl ToTokens for Explode { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.to_tokens(tokens) + } +} + +impl From<Explode> for Feature { + fn from(value: Explode) -> Self { + Feature::Explode(value) + } +} + +name!(Explode = "explode"); + +#[derive(Clone)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct ValueType(syn::Type); + +impl ValueType { + /// Create [`TypeTree`] from current [`syn::Type`]. + pub fn as_type_tree(&self) -> Result<TypeTree, Diagnostics> { + TypeTree::from_type(&self.0) + } +} + +impl Parse for ValueType { + fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result<Self> { + parse_utils::parse_next(input, || input.parse::<syn::Type>()).map(Self) + } +} + +impl From<ValueType> for Feature { + fn from(value: ValueType) -> Self { + Feature::ValueType(value) + } +} + +name!(ValueType = "value_type"); + +#[derive(Clone)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct Inline(pub(super) bool); + +impl Parse for Inline { + fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result<Self> { + parse_utils::parse_bool_or_true(input).map(Self) + } +} + +impl From<bool> for Inline { + fn from(value: bool) -> Self { + Inline(value) + } +} + +impl From<Inline> for Feature { + fn from(value: Inline) -> Self { + Feature::Inline(value) + } +} + +name!(Inline = "inline"); + +/// Specify names of unnamed fields with `names(...) attribute for `IntoParams` derive. +#[cfg_attr(feature = "debug", derive(Debug))] +#[derive(Clone)] +pub struct IntoParamsNames(Vec<String>); + +impl IntoParamsNames { + pub fn into_values(self) -> Vec<String> { + self.0 + } +} + +impl Parse for IntoParamsNames { + fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result<Self> { + Ok(Self( + parse_utils::parse_punctuated_within_parenthesis::<LitStr>(input)? + .iter() + .map(LitStr::value) + .collect(), + )) + } +} + +impl From<IntoParamsNames> for Feature { + fn from(value: IntoParamsNames) -> Self { + Feature::IntoParamsNames(value) + } +} + +name!(IntoParamsNames = "names"); + +#[cfg_attr(feature = "debug", derive(Debug))] +#[derive(Clone)] +pub struct SchemaWith(TypePath); + +impl Parse for SchemaWith { + fn parse(input: ParseStream, _: Ident) -> syn::Result<Self> { + parse_utils::parse_next(input, || input.parse::<TypePath>().map(Self)) + } +} + +impl ToTokens for SchemaWith { + fn to_tokens(&self, tokens: &mut TokenStream) { + let path = &self.0; + tokens.extend(quote! { + #path() + }) + } +} + +impl From<SchemaWith> for Feature { + fn from(value: SchemaWith) -> Self { + Feature::SchemaWith(value) + } +} + +name!(SchemaWith = "schema_with"); + +#[cfg_attr(feature = "debug", derive(Debug))] +#[derive(Clone)] +pub struct Description(parse_utils::Value); + +impl Parse for Description { + fn parse(input: ParseStream, _: Ident) -> syn::Result<Self> + where + Self: std::marker::Sized, + { + parse_utils::parse_next_literal_str_or_expr(input).map(Self) + } +} + +impl ToTokens for Description { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.to_tokens(tokens); + } +} + +impl From<String> for Description { + fn from(value: String) -> Self { + Self(value.into()) + } +} + +impl From<Description> for Feature { + fn from(value: Description) -> Self { + Self::Description(value) + } +} + +name!(Description = "description"); + +/// Deprecated feature parsed from macro attributes. +/// +/// This feature supports only syntax parsed from utoipa specific macro attributes, it does not +/// support Rust `#[deprecated]` attribute. +#[cfg_attr(feature = "debug", derive(Debug))] +#[derive(Clone)] +pub struct Deprecated(bool); + +impl Parse for Deprecated { + fn parse(input: ParseStream, _: Ident) -> syn::Result<Self> + where + Self: std::marker::Sized, + { + parse_utils::parse_bool_or_true(input).map(Self) + } +} + +impl ToTokens for Deprecated { + fn to_tokens(&self, tokens: &mut TokenStream) { + let deprecated: crate::Deprecated = self.0.into(); + deprecated.to_tokens(tokens); + } +} + +impl From<Deprecated> for Feature { + fn from(value: Deprecated) -> Self { + Self::Deprecated(value) + } +} + +name!(Deprecated = "deprecated"); + +#[cfg_attr(feature = "debug", derive(Debug))] +#[derive(Clone)] +pub struct As(pub TypePath); + +impl Parse for As { + fn parse(input: ParseStream, _: Ident) -> syn::Result<Self> + where + Self: std::marker::Sized, + { + parse_utils::parse_next(input, || input.parse()).map(Self) + } +} + +impl From<As> for Feature { + fn from(value: As) -> Self { + Self::As(value) + } +} + +name!(As = "as"); + +#[cfg_attr(feature = "debug", derive(Debug))] +#[derive(Clone)] +pub struct AdditionalProperties(bool); + +impl Parse for AdditionalProperties { + fn parse(input: ParseStream, _: Ident) -> syn::Result<Self> + where + Self: std::marker::Sized, + { + parse_utils::parse_bool_or_true(input).map(Self) + } +} + +impl ToTokens for AdditionalProperties { + fn to_tokens(&self, tokens: &mut TokenStream) { + let additional_properties = &self.0; + tokens.extend(quote!( + utoipa::openapi::schema::AdditionalProperties::FreeForm( + #additional_properties + ) + )) + } +} + +name!(AdditionalProperties = "additional_properties"); + +impl From<AdditionalProperties> for Feature { + fn from(value: AdditionalProperties) -> Self { + Self::AdditionalProperties(value) + } +} + +#[derive(Clone)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct Required(pub bool); + +impl Required { + pub fn is_true(&self) -> bool { + self.0 + } +} + +impl Parse for Required { + fn parse(input: ParseStream, _: Ident) -> syn::Result<Self> + where + Self: std::marker::Sized, + { + parse_utils::parse_bool_or_true(input).map(Self) + } +} + +impl ToTokens for Required { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.to_tokens(tokens) + } +} + +impl From<crate::Required> for Required { + fn from(value: crate::Required) -> Self { + if value == crate::Required::True { + Self(true) + } else { + Self(false) + } + } +} + +impl From<bool> for Required { + fn from(value: bool) -> Self { + Self(value) + } +} + +impl From<Required> for Feature { + fn from(value: Required) -> Self { + Self::Required(value) + } +} + +name!(Required = "required"); + +#[derive(Clone)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct ContentEncoding(String); + +impl Parse for ContentEncoding { + fn parse(input: ParseStream, _: Ident) -> syn::Result<Self> + where + Self: std::marker::Sized, + { + parse_utils::parse_next_literal_str(input).map(Self) + } +} + +impl ToTokens for ContentEncoding { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.to_tokens(tokens); + } +} + +name!(ContentEncoding = "content_encoding"); + +impl From<ContentEncoding> for Feature { + fn from(value: ContentEncoding) -> Self { + Self::ContentEncoding(value) + } +} + +#[derive(Clone)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct ContentMediaType(String); + +impl Parse for ContentMediaType { + fn parse(input: ParseStream, _: Ident) -> syn::Result<Self> + where + Self: std::marker::Sized, + { + parse_utils::parse_next_literal_str(input).map(Self) + } +} + +impl ToTokens for ContentMediaType { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.to_tokens(tokens); + } +} + +impl From<ContentMediaType> for Feature { + fn from(value: ContentMediaType) -> Self { + Self::ContentMediaType(value) + } +} + +name!(ContentMediaType = "content_media_type"); diff --git a/utoipa-gen/src/component/features/validation.rs b/utoipa-gen/src/component/features/validation.rs new file mode 100644 index 00000000..eedfbb32 --- /dev/null +++ b/utoipa-gen/src/component/features/validation.rs @@ -0,0 +1,440 @@ +use proc_macro2::{Ident, Span, TokenStream}; +use quote::ToTokens; +use syn::parse::ParseStream; +use syn::LitStr; + +use crate::{parse_utils, Diagnostics}; + +use super::validators::Validator; +use super::{name, parse_integer, parse_number, Feature, Parse, Validate}; + +#[cfg_attr(feature = "debug", derive(Debug))] +#[derive(Clone)] +pub struct MultipleOf(pub(super) f64, Ident); + +impl Validate for MultipleOf { + fn validate(&self, validator: impl Validator) -> Option<Diagnostics> { + match validator.is_valid() { + Err(error) => Some(Diagnostics::with_span(self.1.span(), format!( "`multiple_of` error: {}", error)) + .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-multipleof`")), + _ => None + } + } +} + +impl Parse for MultipleOf { + fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self> { + parse_number(input).map(|multiple_of| Self(multiple_of, ident)) + } +} + +impl ToTokens for MultipleOf { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.to_tokens(tokens); + } +} + +impl From<MultipleOf> for Feature { + fn from(value: MultipleOf) -> Self { + Feature::MultipleOf(value) + } +} + +name!(MultipleOf = "multiple_of"); + +#[cfg_attr(feature = "debug", derive(Debug))] +#[derive(Clone)] +pub struct Maximum(pub(super) f64, Ident); + +impl Validate for Maximum { + fn validate(&self, validator: impl Validator) -> Option<Diagnostics> { + match validator.is_valid() { + Err(error) => Some(Diagnostics::with_span(self.1.span(), format!("`maximum` error: {}", error)) + .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-maximum`")), + _ => None, + } + } +} + +impl Parse for Maximum { + fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self> + where + Self: Sized, + { + parse_number(input).map(|maximum| Self(maximum, ident)) + } +} + +impl ToTokens for Maximum { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.to_tokens(tokens); + } +} + +impl From<Maximum> for Feature { + fn from(value: Maximum) -> Self { + Feature::Maximum(value) + } +} + +name!(Maximum = "maximum"); + +#[cfg_attr(feature = "debug", derive(Debug))] +#[derive(Clone)] +pub struct Minimum(f64, Ident); + +impl Minimum { + pub fn new(value: f64, span: Span) -> Self { + Self(value, Ident::new("empty", span)) + } +} + +impl Validate for Minimum { + fn validate(&self, validator: impl Validator) -> Option<Diagnostics> { + match validator.is_valid() { + Err(error) => Some( + Diagnostics::with_span(self.1.span(), format!("`minimum` error: {}", error)) + .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-minimum`") + ), + _ => None, + } + } +} + +impl Parse for Minimum { + fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self> + where + Self: Sized, + { + parse_number(input).map(|maximum| Self(maximum, ident)) + } +} + +impl ToTokens for Minimum { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.to_tokens(tokens); + } +} + +impl From<Minimum> for Feature { + fn from(value: Minimum) -> Self { + Feature::Minimum(value) + } +} + +name!(Minimum = "minimum"); + +#[cfg_attr(feature = "debug", derive(Debug))] +#[derive(Clone)] +pub struct ExclusiveMaximum(f64, Ident); + +impl Validate for ExclusiveMaximum { + fn validate(&self, validator: impl Validator) -> Option<Diagnostics> { + match validator.is_valid() { + Err(error) => Some(Diagnostics::with_span(self.1.span(), format!("`exclusive_maximum` error: {}", error)) + .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-exclusivemaximum`")), + _ => None, + } + } +} + +impl Parse for ExclusiveMaximum { + fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self> + where + Self: Sized, + { + parse_number(input).map(|max| Self(max, ident)) + } +} + +impl ToTokens for ExclusiveMaximum { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.to_tokens(tokens); + } +} + +impl From<ExclusiveMaximum> for Feature { + fn from(value: ExclusiveMaximum) -> Self { + Feature::ExclusiveMaximum(value) + } +} + +name!(ExclusiveMaximum = "exclusive_maximum"); + +#[cfg_attr(feature = "debug", derive(Debug))] +#[derive(Clone)] +pub struct ExclusiveMinimum(f64, Ident); + +impl Validate for ExclusiveMinimum { + fn validate(&self, validator: impl Validator) -> Option<Diagnostics> { + match validator.is_valid() { + Err(error) => Some(Diagnostics::with_span(self.1.span(), format!("`exclusive_minimum` error: {}", error)) + .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-exclusiveminimum`")), + _ => None, + } + } +} + +impl Parse for ExclusiveMinimum { + fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self> + where + Self: Sized, + { + parse_number(input).map(|min| Self(min, ident)) + } +} + +impl ToTokens for ExclusiveMinimum { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.to_tokens(tokens); + } +} + +impl From<ExclusiveMinimum> for Feature { + fn from(value: ExclusiveMinimum) -> Self { + Feature::ExclusiveMinimum(value) + } +} + +name!(ExclusiveMinimum = "exclusive_minimum"); + +#[cfg_attr(feature = "debug", derive(Debug))] +#[derive(Clone)] +pub struct MaxLength(pub(super) usize, Ident); + +impl Validate for MaxLength { + fn validate(&self, validator: impl Validator) -> Option<Diagnostics> { + match validator.is_valid() { + Err(error) => Some(Diagnostics::with_span(self.1.span(), format!("`max_length` error: {}", error)) + .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-maxlength`")), + _ => None, + } + } +} + +impl Parse for MaxLength { + fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self> + where + Self: Sized, + { + parse_integer(input).map(|max_length| Self(max_length, ident)) + } +} + +impl ToTokens for MaxLength { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.to_tokens(tokens); + } +} + +impl From<MaxLength> for Feature { + fn from(value: MaxLength) -> Self { + Feature::MaxLength(value) + } +} + +name!(MaxLength = "max_length"); + +#[cfg_attr(feature = "debug", derive(Debug))] +#[derive(Clone)] +pub struct MinLength(pub(super) usize, Ident); + +impl Validate for MinLength { + fn validate(&self, validator: impl Validator) -> Option<Diagnostics> { + match validator.is_valid() { + Err(error) => Some(Diagnostics::with_span(self.1.span(), format!("`min_length` error: {}", error)) + .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-minlength`")), + _ => None, + } + } +} + +impl Parse for MinLength { + fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self> + where + Self: Sized, + { + parse_integer(input).map(|max_length| Self(max_length, ident)) + } +} + +impl ToTokens for MinLength { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.to_tokens(tokens); + } +} + +impl From<MinLength> for Feature { + fn from(value: MinLength) -> Self { + Feature::MinLength(value) + } +} + +name!(MinLength = "min_length"); + +#[cfg_attr(feature = "debug", derive(Debug))] +#[derive(Clone)] +pub struct Pattern(String, Ident); + +impl Validate for Pattern { + fn validate(&self, validator: impl Validator) -> Option<Diagnostics> { + match validator.is_valid() { + Err(error) => Some(Diagnostics::with_span(self.1.span(), format!("`pattern` error: {}", error)) + .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-pattern`") + ), + _ => None, + } + } +} + +impl Parse for Pattern { + fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self> + where + Self: Sized, + { + parse_utils::parse_next(input, || input.parse::<LitStr>()) + .map(|pattern| Self(pattern.value(), ident)) + } +} + +impl ToTokens for Pattern { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.to_tokens(tokens); + } +} + +impl From<Pattern> for Feature { + fn from(value: Pattern) -> Self { + Feature::Pattern(value) + } +} + +name!(Pattern = "pattern"); + +#[cfg_attr(feature = "debug", derive(Debug))] +#[derive(Clone)] +pub struct MaxItems(pub(super) usize, Ident); + +impl Validate for MaxItems { + fn validate(&self, validator: impl Validator) -> Option<Diagnostics> { + match validator.is_valid() { + Err(error) => Some(Diagnostics::with_span(self.1.span(), format!("`max_items` error: {}", error)) + .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-maxitems")), + _ => None, + } + } +} + +impl Parse for MaxItems { + fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self> + where + Self: Sized, + { + parse_number(input).map(|max_items| Self(max_items, ident)) + } +} + +impl ToTokens for MaxItems { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.to_tokens(tokens); + } +} + +impl From<MaxItems> for Feature { + fn from(value: MaxItems) -> Self { + Feature::MaxItems(value) + } +} + +name!(MaxItems = "max_items"); + +#[cfg_attr(feature = "debug", derive(Debug))] +#[derive(Clone)] +pub struct MinItems(pub(super) usize, Ident); + +impl Validate for MinItems { + fn validate(&self, validator: impl Validator) -> Option<Diagnostics> { + match validator.is_valid() { + Err(error) => Some(Diagnostics::with_span(self.1.span(), format!("`min_items` error: {}", error)) + .help("See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-minitems")), + _ => None, + } + } +} + +impl Parse for MinItems { + fn parse(input: ParseStream, ident: Ident) -> syn::Result<Self> + where + Self: Sized, + { + parse_number(input).map(|max_items| Self(max_items, ident)) + } +} + +impl ToTokens for MinItems { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.to_tokens(tokens); + } +} + +impl From<MinItems> for Feature { + fn from(value: MinItems) -> Self { + Feature::MinItems(value) + } +} + +name!(MinItems = "min_items"); + +#[cfg_attr(feature = "debug", derive(Debug))] +#[derive(Clone)] +pub struct MaxProperties(usize, ()); + +impl Parse for MaxProperties { + fn parse(input: ParseStream, _ident: Ident) -> syn::Result<Self> + where + Self: Sized, + { + parse_integer(input).map(|max_properties| Self(max_properties, ())) + } +} + +impl ToTokens for MaxProperties { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.to_tokens(tokens); + } +} + +impl From<MaxProperties> for Feature { + fn from(value: MaxProperties) -> Self { + Feature::MaxProperties(value) + } +} + +name!(MaxProperties = "max_properties"); + +#[cfg_attr(feature = "debug", derive(Debug))] +#[derive(Clone)] +pub struct MinProperties(usize, ()); + +impl Parse for MinProperties { + fn parse(input: ParseStream, _ident: Ident) -> syn::Result<Self> + where + Self: Sized, + { + parse_integer(input).map(|min_properties| Self(min_properties, ())) + } +} + +impl ToTokens for MinProperties { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.to_tokens(tokens); + } +} + +impl From<MinProperties> for Feature { + fn from(value: MinProperties) -> Self { + Feature::MinProperties(value) + } +} + +name!(MinProperties = "min_properties"); diff --git a/utoipa-gen/src/component/features/validators.rs b/utoipa-gen/src/component/features/validators.rs new file mode 100644 index 00000000..01c21784 --- /dev/null +++ b/utoipa-gen/src/component/features/validators.rs @@ -0,0 +1,111 @@ +use crate::component::{GenericType, TypeTree}; +use crate::schema_type::SchemaType; + +pub trait Validator { + fn is_valid(&self) -> Result<(), &'static str>; +} + +pub struct IsNumber<'a>(pub &'a SchemaType<'a>); + +impl Validator for IsNumber<'_> { + fn is_valid(&self) -> Result<(), &'static str> { + if self.0.is_number() { + Ok(()) + } else { + Err("can only be used with `number` type") + } + } +} + +pub struct IsString<'a>(pub(super) &'a SchemaType<'a>); + +impl Validator for IsString<'_> { + fn is_valid(&self) -> Result<(), &'static str> { + if self.0.is_string() { + Ok(()) + } else { + Err("can only be used with `string` type") + } + } +} + +pub struct IsInteger<'a>(&'a SchemaType<'a>); + +impl Validator for IsInteger<'_> { + fn is_valid(&self) -> Result<(), &'static str> { + if self.0.is_integer() { + Ok(()) + } else { + Err("can only be used with `integer` type") + } + } +} + +pub struct IsVec<'a>(pub(super) &'a TypeTree<'a>); + +impl Validator for IsVec<'_> { + fn is_valid(&self) -> Result<(), &'static str> { + if self.0.generic_type == Some(GenericType::Vec) { + Ok(()) + } else { + Err("can only be used with `Vec`, `array` or `slice` types") + } + } +} + +pub struct AboveZeroUsize(pub(super) usize); + +impl Validator for AboveZeroUsize { + fn is_valid(&self) -> Result<(), &'static str> { + if self.0 != 0 { + Ok(()) + } else { + Err("can only be above zero value") + } + } +} + +pub struct AboveZeroF64(pub(super) f64); + +impl Validator for AboveZeroF64 { + fn is_valid(&self) -> Result<(), &'static str> { + if self.0 > 0.0 { + Ok(()) + } else { + Err("can only be above zero value") + } + } +} + +pub struct ValidatorChain<'c> { + inner: &'c dyn Validator, + next: Option<&'c dyn Validator>, +} + +impl Validator for ValidatorChain<'_> { + fn is_valid(&self) -> Result<(), &'static str> { + self.inner.is_valid().and_then(|_| { + if let Some(validator) = self.next.as_ref() { + validator.is_valid() + } else { + // if there is no next validator consider it valid + Ok(()) + } + }) + } +} + +impl<'c> ValidatorChain<'c> { + pub fn new(validator: &'c dyn Validator) -> Self { + Self { + inner: validator, + next: None, + } + } + + pub fn next(mut self, validator: &'c dyn Validator) -> Self { + self.next = Some(validator); + + self + } +} diff --git a/utoipa-gen/src/component/into_params.rs b/utoipa-gen/src/component/into_params.rs index c0a9b5fe..ffbef46f 100644 --- a/utoipa-gen/src/component/into_params.rs +++ b/utoipa-gen/src/component/into_params.rs @@ -11,10 +11,16 @@ use crate::{ component::{ self, features::{ - self, AdditionalProperties, AllowReserved, Example, ExclusiveMaximum, ExclusiveMinimum, - Explode, Format, Inline, IntoParamsNames, MaxItems, MaxLength, Maximum, MinItems, - MinLength, Minimum, MultipleOf, Nullable, Pattern, ReadOnly, Rename, RenameAll, - SchemaWith, Style, WriteOnly, XmlAttr, + self, + attributes::{ + AdditionalProperties, AllowReserved, Example, Explode, Format, Inline, + IntoParamsNames, Nullable, ReadOnly, Rename, RenameAll, SchemaWith, Style, + WriteOnly, XmlAttr, + }, + validation::{ + ExclusiveMaximum, ExclusiveMinimum, MaxItems, MaxLength, Maximum, MinItems, + MinLength, Minimum, MultipleOf, Pattern, + }, }, FieldRename, }, @@ -40,7 +46,7 @@ impl Parse for IntoParamsFeatures { fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { Ok(Self(parse_features!( input as Style, - features::ParameterIn, + features::attributes::ParameterIn, IntoParamsNames, RenameAll ))) @@ -253,18 +259,18 @@ impl Parse for FieldFeatures { fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { Ok(Self(parse_features!( // param features - input as component::features::ValueType, + input as component::features::attributes::ValueType, Rename, Style, AllowReserved, Example, Explode, SchemaWith, - component::features::Required, + component::features::attributes::Required, // param schema features Inline, Format, - component::features::Default, + component::features::attributes::Default, WriteOnly, ReadOnly, Nullable, @@ -382,8 +388,7 @@ impl ToTokensDiagnostics for Param<'_> { let (schema_features, mut param_features) = self.resolve_field_features().map_err(Diagnostics::from)?; - let rename = param_features - .pop_rename_feature() + let rename = pop_feature!(param_features => Feature::Rename(_) as Option<Rename>) .map(|rename| rename.into_value()); let rename_to = field_serde_params .rename @@ -426,13 +431,13 @@ impl ToTokensDiagnostics for Param<'_> { tokens.extend(quote! { .description(Some(#description))}) } - let value_type = param_features.pop_value_type_feature(); + let value_type = pop_feature!(param_features => Feature::ValueType(_) as Option<features::attributes::ValueType>); let component = value_type .as_ref() .map_try(|value_type| value_type.as_type_tree())? .unwrap_or(type_tree); - let required: Option<features::Required> = + let required: Option<features::attributes::Required> = pop_feature!(param_features => Feature::Required(_)).into_inner(); let component_required = !component.is_option() && super::is_required(field_serde_params, self.serde_container); diff --git a/utoipa-gen/src/component/schema.rs b/utoipa-gen/src/component/schema.rs index 8aa4e95e..4322cbbc 100644 --- a/utoipa-gen/src/component/schema.rs +++ b/utoipa-gen/src/component/schema.rs @@ -10,7 +10,7 @@ use syn::{ use crate::{ as_tokens_or_diagnostics, - component::features::{Example, Rename}, + component::features::attributes::{Example, Rename, ValueType}, doc_comment::CommentAttributes, Array, Deprecated, Diagnostics, OptionExt, ToTokensDiagnostics, }; @@ -29,11 +29,11 @@ use self::{ use super::{ features::{ - parse_features, pop_feature, pop_feature_as_inner, As, Description, Feature, FeaturesExt, - IntoInner, RenameAll, ToTokensExt, + attributes::{As, Description, RenameAll}, + parse_features, pop_feature, Feature, FeaturesExt, IntoInner, ToTokensExt, }, serde::{self, SerdeContainer, SerdeEnumRepr, SerdeValue}, - ComponentDescription, ComponentSchema, FieldRename, FlattenedMapSchema, TypeTree, ValueType, + ComponentDescription, ComponentSchema, FieldRename, FlattenedMapSchema, TypeTree, VariantRename, }; @@ -206,7 +206,7 @@ impl<'a> SchemaVariant<'a> { .parse_features::<UnnamedFieldStructFeatures>()? .into_inner(); - let schema_as = pop_feature_as_inner!(unnamed_features => Feature::As(_v)); + let schema_as = pop_feature!(unnamed_features => Feature::As(_) as Option<As>); let description = pop_feature!(unnamed_features => Feature::Description(_)).into_inner(); Ok(Self::Unnamed(UnnamedStructSchema { @@ -223,7 +223,7 @@ impl<'a> SchemaVariant<'a> { let mut named_features = attributes .parse_features::<NamedFieldStructFeatures>()? .into_inner(); - let schema_as = pop_feature_as_inner!(named_features => Feature::As(_v)); + let schema_as = pop_feature!(named_features => Feature::As(_) as Option<As>); let description = pop_feature!(named_features => Feature::Description(_)).into_inner(); @@ -231,7 +231,7 @@ impl<'a> SchemaVariant<'a> { struct_name: Cow::Owned(ident.to_string()), attributes, description, - rename_all: named_features.pop_rename_all_feature(), + rename_all: pop_feature!(named_features => Feature::RenameAll(_) as Option<RenameAll>), features: named_features, fields: named, schema_as, @@ -303,7 +303,7 @@ pub struct NamedStructSchema<'a> { struct NamedStructFieldOptions<'a> { property: Property, rename_field_value: Option<Cow<'a, str>>, - required: Option<super::features::Required>, + required: Option<super::features::attributes::Required>, is_option: bool, } @@ -344,7 +344,10 @@ impl NamedStructSchema<'_> { 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()), + crate::features::attributes::Default::new_default_trait( + struct_ident, + field_ident.into(), + ), )); } } @@ -364,9 +367,9 @@ impl NamedStructSchema<'_> { _ => None, }); - let value_type = field_features - .as_mut() - .and_then(|features| features.pop_value_type_feature()); + let value_type = field_features.as_mut().and_then( + |features| pop_feature!(features => Feature::ValueType(_) as Option<ValueType>), + ); let override_type_tree = value_type .as_ref() .map_try(|value_type| value_type.as_type_tree())?; @@ -374,7 +377,7 @@ impl NamedStructSchema<'_> { let description = &ComponentDescription::CommentAttributes(&comments); let schema_with = pop_feature!(field_features => Feature::SchemaWith(_)); - let required = pop_feature_as_inner!(field_features => Feature::Required(_v)); + let required = pop_feature!(field_features => Feature::Required(_) as Option<crate::component::features::attributes::Required>); let type_tree = override_type_tree.as_ref().unwrap_or(type_tree); let is_option = type_tree.is_option(); @@ -606,9 +609,9 @@ impl ToTokensDiagnostics for UnnamedStructSchema<'_> { 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 value_type = unnamed_struct_features.as_mut().and_then( + |features| pop_feature!(features => Feature::ValueType(_) as Option<ValueType>), + ); let override_type_tree = value_type .as_ref() .map_try(|value_type| value_type.as_type_tree())?; @@ -617,19 +620,21 @@ impl ToTokensDiagnostics for UnnamedStructSchema<'_> { if let Some(ref mut features) = unnamed_struct_features { let inline = features::parse_schema_features_with(&first_field.attrs, |input| { - Ok(parse_features!(input as super::features::Inline)) + Ok(parse_features!( + input as super::features::attributes::Inline + )) })? .unwrap_or_default(); features.extend(inline); - if pop_feature!(features => Feature::Default(crate::features::Default(None))) + if pop_feature!(features => Feature::Default(crate::features::attributes::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()), + crate::features::attributes::Default::new_default_trait(struct_ident, index.into()), )); } } @@ -720,17 +725,17 @@ impl<'e> EnumSchema<'e> { let mut repr_enum_features = features::parse_schema_features_with(attributes, |input| { Ok(parse_features!( - input as super::features::Example, - super::features::Examples, - super::features::Default, - super::features::Title, + input as super::features::attributes::Example, + super::features::attributes::Examples, + super::features::attributes::Default, + super::features::attributes::Title, As )) })? .unwrap_or_default(); let schema_as = - pop_feature_as_inner!(repr_enum_features => Feature::As(_v)); + pop_feature!(repr_enum_features => Feature::As(_) as Option<As>); let description = pop_feature!(repr_enum_features => Feature::Description(_)) .into_inner(); @@ -755,8 +760,8 @@ impl<'e> EnumSchema<'e> { .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(); + pop_feature!(simple_enum_features => Feature::As(_) as Option<As>); + let rename_all = pop_feature!(simple_enum_features => Feature::RenameAll(_) as Option<RenameAll>); let description = pop_feature!(simple_enum_features => Feature::Description(_)) .into_inner(); @@ -781,8 +786,8 @@ impl<'e> EnumSchema<'e> { .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(); + let schema_as = pop_feature!(simple_enum_features => Feature::As(_) as Option<As>); + let rename_all = pop_feature!(simple_enum_features => Feature::RenameAll(_) as Option<RenameAll>); let description = pop_feature!(simple_enum_features => Feature::Description(_)).into_inner(); @@ -802,8 +807,9 @@ impl<'e> EnumSchema<'e> { .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(); + let schema_as = pop_feature!(enum_features => Feature::As(_) as Option<As>); + let rename_all = + pop_feature!(enum_features => Feature::RenameAll(_) as Option<RenameAll>); let description = pop_feature!(enum_features => Feature::Description(_)).into_inner(); Ok(Self { @@ -924,8 +930,7 @@ fn rename_enum_variant<'a>( container_rules: &'a SerdeContainer, rename_all: &'a Option<RenameAll>, ) -> Option<Cow<'a, str>> { - let rename = features - .pop_rename_feature() + let rename = pop_feature!(features => Feature::Rename(_) as Option<Rename>) .map(|rename| rename.into_value()); let rename_to = variant_rules .rename @@ -1099,7 +1104,7 @@ impl ComplexEnum<'_> { struct_name: Cow::Borrowed(&*self.enum_name), attributes: &variant.attrs, description: None, - rename_all: named_struct_features.pop_rename_all_feature(), + rename_all: pop_feature!(named_struct_features => Feature::RenameAll(_) as Option<RenameAll>), features: Some(named_struct_features), fields: &named_fields.named, aliases: None, @@ -1144,7 +1149,7 @@ impl ComplexEnum<'_> { let mut unit_features = features::parse_schema_features_with(&variant.attrs, |input| { Ok(parse_features!( - input as super::features::Title, + input as super::features::attributes::Title, RenameAll, Rename, Example @@ -1200,7 +1205,7 @@ impl ComplexEnum<'_> { struct_name: Cow::Borrowed(&*self.enum_name), attributes: &variant.attrs, description: None, - rename_all: named_struct_features.pop_rename_all_feature(), + rename_all: pop_feature!(named_struct_features => Feature::RenameAll(_) as Option<RenameAll>), features: Some(named_struct_features), fields: &named_fields.named, aliases: None, @@ -1226,7 +1231,7 @@ impl ComplexEnum<'_> { Fields::Unit => { let mut unit_features = features::parse_schema_features_with(&variant.attrs, |input| { - Ok(parse_features!(input as super::features::Title)) + Ok(parse_features!(input as super::features::attributes::Title)) }) .unwrap_or_default(); let title = pop_feature!(unit_features => Feature::Title(_)); @@ -1267,7 +1272,7 @@ impl ComplexEnum<'_> { struct_name: Cow::Borrowed(&*self.enum_name), attributes: &variant.attrs, description: None, - rename_all: named_struct_features.pop_rename_all_feature(), + rename_all: pop_feature!(named_struct_features => Feature::RenameAll(_) as Option<RenameAll>), features: Some(named_struct_features), fields: &named_fields.named, aliases: None, @@ -1331,7 +1336,7 @@ impl ComplexEnum<'_> { .map(|field| TypeTree::from_type(&field.ty)) .collect::<Result<Vec<TypeTree>, Diagnostics>>()? .iter() - .any(|type_tree| type_tree.value_type == ValueType::Object); + .any(|type_tree| type_tree.value_type == super::ValueType::Object); if is_reference { Ok(quote! { @@ -1364,7 +1369,10 @@ impl ComplexEnum<'_> { Fields::Unit => { let mut unit_features = features::parse_schema_features_with(&variant.attrs, |input| { - Ok(parse_features!(input as super::features::Title, Rename)) + Ok(parse_features!( + input as super::features::attributes::Title, + Rename + )) })? .unwrap_or_default(); let title = pop_feature!(unit_features => Feature::Title(_)); @@ -1427,7 +1435,7 @@ impl ComplexEnum<'_> { struct_name: Cow::Borrowed(&*self.enum_name), attributes: &variant.attrs, description: None, - rename_all: named_struct_features.pop_rename_all_feature(), + rename_all: pop_feature!(named_struct_features => Feature::RenameAll(_) as Option<RenameAll>), features: Some(named_struct_features), fields: &named_fields.named, aliases: None, @@ -1511,7 +1519,10 @@ impl ComplexEnum<'_> { let mut unit_features = features::parse_schema_features_with(&variant.attrs, |input| { - Ok(parse_features!(input as super::features::Title, Rename)) + Ok(parse_features!( + input as super::features::attributes::Title, + Rename + )) })? .unwrap_or_default(); let title = pop_feature!(unit_features => Feature::Title(_)); diff --git a/utoipa-gen/src/component/schema/features.rs b/utoipa-gen/src/component/schema/features.rs index 8211ddf9..dddf7370 100644 --- a/utoipa-gen/src/component/schema/features.rs +++ b/utoipa-gen/src/component/schema/features.rs @@ -5,11 +5,17 @@ use syn::{ use crate::{ component::features::{ - impl_into_inner, impl_merge, parse_features, AdditionalProperties, As, ContentEncoding, - ContentMediaType, Default, Deprecated, Description, Example, Examples, ExclusiveMaximum, - ExclusiveMinimum, Feature, Format, Inline, IntoInner, MaxItems, MaxLength, MaxProperties, - Maximum, Merge, MinItems, MinLength, MinProperties, Minimum, MultipleOf, Nullable, Pattern, - ReadOnly, Rename, RenameAll, Required, SchemaWith, Title, ValueType, WriteOnly, XmlAttr, + attributes::{ + AdditionalProperties, As, ContentEncoding, ContentMediaType, Deprecated, Description, + Example, Examples, Format, Inline, Nullable, ReadOnly, Rename, RenameAll, Required, + SchemaWith, Title, ValueType, WriteOnly, XmlAttr, + }, + impl_into_inner, impl_merge, parse_features, + validation::{ + ExclusiveMaximum, ExclusiveMinimum, MaxItems, MaxLength, MaxProperties, Maximum, + MinItems, MinLength, MinProperties, Minimum, MultipleOf, Pattern, + }, + Feature, Merge, }, Diagnostics, }; @@ -28,7 +34,7 @@ impl Parse for NamedFieldStructFeatures { MaxProperties, MinProperties, As, - Default, + crate::component::features::attributes::Default, Deprecated, Description ))) @@ -45,7 +51,7 @@ impl Parse for UnnamedFieldStructFeatures { Ok(UnnamedFieldStructFeatures(parse_features!( input as Example, Examples, - Default, + crate::component::features::attributes::Default, Title, Format, ValueType, @@ -65,7 +71,7 @@ impl Parse for EnumFeatures { Ok(EnumFeatures(parse_features!( input as Example, Examples, - Default, + crate::component::features::attributes::Default, Title, RenameAll, As, @@ -84,7 +90,7 @@ impl Parse for ComplexEnumFeatures { Ok(ComplexEnumFeatures(parse_features!( input as Example, Examples, - Default, + crate::component::features::attributes::Default, RenameAll, As, Deprecated, @@ -104,7 +110,7 @@ impl Parse for NamedFieldFeatures { Examples, ValueType, Format, - Default, + crate::component::features::attributes::Default, WriteOnly, ReadOnly, XmlAttr, @@ -158,7 +164,7 @@ impl Parse for EnumUnnamedFieldVariantFeatures { Ok(EnumUnnamedFieldVariantFeatures(parse_features!( input as Example, Examples, - Default, + crate::component::features::attributes::Default, Title, Format, ValueType, diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs index dced7f61..589408d2 100644 --- a/utoipa-gen/src/lib.rs +++ b/utoipa-gen/src/lib.rs @@ -2699,9 +2699,9 @@ impl From<bool> for Required { } } -impl From<features::Required> for Required { - fn from(value: features::Required) -> Self { - let features::Required(required) = value; +impl From<features::attributes::Required> for Required { + fn from(value: features::attributes::Required) -> Self { + let features::attributes::Required(required) = value; crate::Required::from(required) } } diff --git a/utoipa-gen/src/path/parameter.rs b/utoipa-gen/src/path/parameter.rs index 1a981849..98adc622 100644 --- a/utoipa-gen/src/path/parameter.rs +++ b/utoipa-gen/src/path/parameter.rs @@ -13,10 +13,16 @@ use crate::{ component::{ self, features::{ - impl_into_inner, parse_features, AllowReserved, Description, Example, ExclusiveMaximum, - ExclusiveMinimum, Explode, Feature, Format, MaxItems, MaxLength, Maximum, MinItems, - MinLength, Minimum, MultipleOf, Nullable, Pattern, ReadOnly, Style, ToTokensExt, - WriteOnly, XmlAttr, + attributes::{ + AllowReserved, Description, Example, Explode, Format, Nullable, ReadOnly, Style, + WriteOnly, XmlAttr, + }, + impl_into_inner, parse_features, + validation::{ + ExclusiveMaximum, ExclusiveMinimum, MaxItems, MaxLength, Maximum, MinItems, + MinLength, Minimum, MultipleOf, Pattern, + }, + Feature, ToTokensExt, }, ComponentSchema, }, @@ -296,7 +302,7 @@ impl Parse for ParameterFeatures { Explode, AllowReserved, Example, - crate::component::features::Deprecated, + crate::component::features::attributes::Deprecated, Description, // param schema features Format, diff --git a/utoipa-gen/src/path/request_body.rs b/utoipa-gen/src/path/request_body.rs index 70d5fa9a..74601364 100644 --- a/utoipa-gen/src/path/request_body.rs +++ b/utoipa-gen/src/path/request_body.rs @@ -4,7 +4,7 @@ use syn::punctuated::Punctuated; use syn::token::Comma; use syn::{parenthesized, parse::Parse, token::Paren, Error, Token}; -use crate::component::features::Inline; +use crate::component::features::attributes::Inline; use crate::component::ComponentSchema; use crate::{parse_utils, AnyValue, Array, Diagnostics, Required, ToTokensDiagnostics}; diff --git a/utoipa-gen/src/path/response.rs b/utoipa-gen/src/path/response.rs index f8487f1b..c972c84f 100644 --- a/utoipa-gen/src/path/response.rs +++ b/utoipa-gen/src/path/response.rs @@ -11,7 +11,7 @@ use syn::{ }; use crate::{ - component::{features::Inline, ComponentSchema, TypeTree}, + component::{features::attributes::Inline, ComponentSchema, TypeTree}, parse_utils, AnyValue, Array, Diagnostics, ToTokensDiagnostics, };