From f03e7d5b240e0aacae96c5027d1666a1e92eae88 Mon Sep 17 00:00:00 2001 From: Juha Kukkonen Date: Tue, 14 May 2024 18:19:38 +0300 Subject: [PATCH] Migrate out from proc macro error (#920) Implement `Result` based proc macro error handling instead of out of fashion `proc_macro_error` that still is in _syn_ `1.0`. This allows us to remove the need for `proc_macro_error` crate and makes our dependency tree leaner. The error handling is implemented with custom `ToTokensDiagnostics` trait that is used instead of the `ToTokens` when there is a possibility for error. Error is passed up in the call stack via `Diagnostics` struct that converts to compile error token stream at root of the macro call. The result based approach is the recommended way of handling compile errors in proc macros. Resolves #854 --- utoipa-gen/Cargo.toml | 1 - utoipa-gen/src/component.rs | 257 ++++--- utoipa-gen/src/component/features.rs | 330 +++++---- utoipa-gen/src/component/into_params.rs | 178 +++-- utoipa-gen/src/component/schema.rs | 752 +++++++++++--------- utoipa-gen/src/component/schema/features.rs | 34 +- utoipa-gen/src/component/serde.rs | 31 +- utoipa-gen/src/ext.rs | 100 ++- utoipa-gen/src/ext/actix.rs | 77 +- utoipa-gen/src/ext/axum.rs | 22 +- utoipa-gen/src/ext/rocket.rs | 90 +-- utoipa-gen/src/lib.rs | 288 ++++++-- utoipa-gen/src/openapi.rs | 9 +- utoipa-gen/src/path.rs | 97 +-- utoipa-gen/src/path/parameter.rs | 41 +- utoipa-gen/src/path/request_body.rs | 26 +- utoipa-gen/src/path/response.rs | 71 +- utoipa-gen/src/path/response/derive.rs | 303 +++++--- utoipa-gen/src/schema_type.rs | 130 ++-- utoipa-gen/tests/path_derive_actix.rs | 5 +- 20 files changed, 1725 insertions(+), 1117 deletions(-) diff --git a/utoipa-gen/Cargo.toml b/utoipa-gen/Cargo.toml index a7946e41..4a396935 100644 --- a/utoipa-gen/Cargo.toml +++ b/utoipa-gen/Cargo.toml @@ -16,7 +16,6 @@ proc-macro = true proc-macro2 = "1.0" syn = { version = "2.0", features = ["full", "extra-traits"] } quote = "1.0" -proc-macro-error = "1.0" regex = { version = "1.7", optional = true } uuid = { version = "1", features = ["serde"], optional = true } ulid = { version = "1", optional = true, default-features = false } diff --git a/utoipa-gen/src/component.rs b/utoipa-gen/src/component.rs index 321f6a40..1fd5a2b4 100644 --- a/utoipa-gen/src/component.rs +++ b/utoipa-gen/src/component.rs @@ -1,7 +1,6 @@ use std::borrow::Cow; use proc_macro2::{Ident, Span, TokenStream}; -use proc_macro_error::{abort, abort_call_site}; use quote::{quote, quote_spanned, ToTokens}; use syn::spanned::Spanned; use syn::{Attribute, GenericArgument, Path, PathArguments, PathSegment, Type, TypePath}; @@ -9,6 +8,7 @@ use syn::{Attribute, GenericArgument, Path, PathArguments, PathSegment, Type, Ty use crate::doc_comment::CommentAttributes; use crate::schema_type::SchemaFormat; use crate::{schema_type::SchemaType, Deprecated}; +use crate::{Diagnostics, OptionExt}; use self::features::{ pop_feature, Feature, FeaturesExt, IsInline, Minimum, Nullable, ToTokensExt, Validatable, @@ -104,27 +104,31 @@ pub struct TypeTree<'t> { } impl<'t> TypeTree<'t> { - pub fn from_type(ty: &'t Type) -> TypeTree<'t> { - Self::convert_types(Self::get_type_tree_values(ty)) - .next() - .expect("TypeTree from type should have one TypeTree parent") + pub fn from_type(ty: &'t Type) -> Result, Diagnostics> { + Self::convert_types(Self::get_type_tree_values(ty)?).map(|mut type_tree| { + type_tree + .next() + .expect("TypeTree from type should have one TypeTree parent") + }) } - fn get_type_tree_values(ty: &'t Type) -> Vec { - match ty { + fn get_type_tree_values(ty: &'t Type) -> Result, Diagnostics> { + let type_tree_values = match ty { Type::Path(path) => { vec![TypeTreeValue::TypePath(path)] }, - Type::Reference(reference) => Self::get_type_tree_values(reference.elem.as_ref()), + Type::Reference(reference) => Self::get_type_tree_values(reference.elem.as_ref())?, Type::Tuple(tuple) => { // Detect unit type () - if tuple.elems.is_empty() { return vec![TypeTreeValue::UnitType] } - - vec![TypeTreeValue::Tuple(tuple.elems.iter().flat_map(Self::get_type_tree_values).collect(), tuple.span())] + if tuple.elems.is_empty() { return Ok(vec![TypeTreeValue::UnitType]) } + vec![TypeTreeValue::Tuple( + tuple.elems.iter().map(Self::get_type_tree_values).collect::, Diagnostics>>()?.into_iter().flatten().collect(), + tuple.span() + )] }, - Type::Group(group) => Self::get_type_tree_values(group.elem.as_ref()), - Type::Slice(slice) => vec![TypeTreeValue::Array(Self::get_type_tree_values(&slice.elem), slice.bracket_token.span.join())], - Type::Array(array) => vec![TypeTreeValue::Array(Self::get_type_tree_values(&array.elem), array.bracket_token.span.join())], + Type::Group(group) => Self::get_type_tree_values(group.elem.as_ref())?, + Type::Slice(slice) => vec![TypeTreeValue::Array(Self::get_type_tree_values(&slice.elem)?, slice.bracket_token.span.join())], + Type::Array(array) => vec![TypeTreeValue::Array(Self::get_type_tree_values(&array.elem)?, array.bracket_token.span.join())], Type::TraitObject(trait_object) => { trait_object .bounds @@ -139,68 +143,83 @@ impl<'t> TypeTree<'t> { }) .map(|path| vec![TypeTreeValue::Path(path)]).unwrap_or_else(Vec::new) } - _ => abort_call_site!( - "unexpected type in component part get type path, expected one of: Path, Tuple, Reference, Group, Array, Slice, TraitObject" - ), - } + unexpected => return Err(Diagnostics::with_span(unexpected.span(), "unexpected type in component part get type path, expected one of: Path, Tuple, Reference, Group, Array, Slice, TraitObject")), + }; + + Ok(type_tree_values) } - fn convert_types(paths: Vec>) -> impl Iterator> { - paths.into_iter().map(|value| { - let path = match value { - TypeTreeValue::TypePath(type_path) => &type_path.path, - TypeTreeValue::Path(path) => path, - TypeTreeValue::Array(value, span) => { - let array: Path = Ident::new("Array", span).into(); - return TypeTree { - path: Some(Cow::Owned(array)), - span: Some(span), - value_type: ValueType::Object, - generic_type: Some(GenericType::Vec), - children: Some(Self::convert_types(value).collect()), - }; - } - TypeTreeValue::Tuple(tuple, span) => { - return TypeTree { - path: None, - span: Some(span), - children: Some(Self::convert_types(tuple).collect()), - generic_type: None, - value_type: ValueType::Tuple, + fn convert_types( + paths: Vec>, + ) -> Result>, Diagnostics> { + paths + .into_iter() + .map(|value| { + let path = match value { + TypeTreeValue::TypePath(type_path) => &type_path.path, + TypeTreeValue::Path(path) => path, + TypeTreeValue::Array(value, span) => { + let array: Path = Ident::new("Array", span).into(); + return Ok(TypeTree { + path: Some(Cow::Owned(array)), + span: Some(span), + value_type: ValueType::Object, + generic_type: Some(GenericType::Vec), + children: Some(match Self::convert_types(value) { + Ok(converted_values) => converted_values.collect(), + Err(diagnostics) => return Err(diagnostics), + }), + }); } - } - TypeTreeValue::UnitType => { - return TypeTree { - path: None, - span: None, - value_type: ValueType::Tuple, - generic_type: None, - children: None, + TypeTreeValue::Tuple(tuple, span) => { + return Ok(TypeTree { + path: None, + span: Some(span), + children: Some(match Self::convert_types(tuple) { + Ok(converted_values) => converted_values.collect(), + Err(diagnostics) => return Err(diagnostics), + }), + generic_type: None, + value_type: ValueType::Tuple, + }) } - } - }; + TypeTreeValue::UnitType => { + return Ok(TypeTree { + path: None, + span: None, + value_type: ValueType::Tuple, + generic_type: None, + children: None, + }) + } + }; - // there will always be one segment at least - let last_segment = path - .segments - .last() - .expect("at least one segment within path in TypeTree::convert_types"); + // there will always be one segment at least + let last_segment = path + .segments + .last() + .expect("at least one segment within path in TypeTree::convert_types"); - if last_segment.arguments.is_empty() { - Self::convert(path, last_segment) - } else { - Self::resolve_schema_type(path, last_segment) - } - }) + if last_segment.arguments.is_empty() { + Ok(Self::convert(path, last_segment)) + } else { + Self::resolve_schema_type(path, last_segment) + } + }) + .collect::>, Diagnostics>>() + .map(IntoIterator::into_iter) } // Only when type is a generic type we get to this function. - fn resolve_schema_type(path: &'t Path, last_segment: &'t PathSegment) -> TypeTree<'t> { + fn resolve_schema_type( + path: &'t Path, + last_segment: &'t PathSegment, + ) -> Result, Diagnostics> { if last_segment.arguments.is_empty() { - abort!( - last_segment.ident, - "expected at least one angle bracket argument but was 0" - ); + return Err(Diagnostics::with_span( + last_segment.ident.span(), + "expected at least one angle bracket argument but was 0", + )); }; let mut generic_schema_type = Self::convert(path, last_segment); @@ -227,26 +246,32 @@ impl<'t> TypeTree<'t> { ) }) .map(|arg| match arg { - GenericArgument::Type(arg) => arg, - _ => abort!( - arg, - "expected generic argument type or generic argument lifetime" - ), - }), + GenericArgument::Type(arg) => Ok(arg), + unexpected => Err(Diagnostics::with_span( + unexpected.span(), + "expected generic argument type or generic argument lifetime", + )), + }) + .collect::, Diagnostics>>()? + .into_iter(), ) } } - _ => abort!( - last_segment.ident, - "unexpected path argument, expected angle bracketed path argument" - ), + _ => { + return Err(Diagnostics::with_span( + last_segment.ident.span(), + "unexpected path argument, expected angle bracketed path argument", + )) + } }; - generic_schema_type.children = generic_types - .as_mut() - .map(|generic_type| generic_type.map(Self::from_type).collect()); + generic_schema_type.children = generic_types.as_mut().map_try(|generic_type| { + generic_type + .map(Self::from_type) + .collect::, Diagnostics>>() + })?; - generic_schema_type + Ok(generic_schema_type) } fn convert(path: &'t Path, last_segment: &'t PathSegment) -> TypeTree<'t> { @@ -489,6 +514,12 @@ impl<'c> ComponentSchema { let deprecated_stream = ComponentSchema::get_deprecated(deprecated); let description_stream = ComponentSchema::get_description(description); + let match_diagnostics = + |result: Result<(), Diagnostics>, tokens: &mut TokenStream| match result { + Err(diagnostics) => diagnostics.to_tokens(tokens), + _ => (), + }; + match type_tree.generic_type { Some(GenericType::Map) => ComponentSchema::map_to_tokens( &mut tokens, @@ -498,38 +529,50 @@ impl<'c> ComponentSchema { description_stream, deprecated_stream, ), - Some(GenericType::Vec) => ComponentSchema::vec_to_tokens( + Some(GenericType::Vec) => match_diagnostics( + ComponentSchema::vec_to_tokens( + &mut tokens, + features, + type_tree, + object_name, + description_stream, + deprecated_stream, + ), &mut tokens, - features, - type_tree, - object_name, - description_stream, - deprecated_stream, ), - Some(GenericType::LinkedList) => ComponentSchema::vec_to_tokens( + Some(GenericType::LinkedList) => match_diagnostics( + ComponentSchema::vec_to_tokens( + &mut tokens, + features, + type_tree, + object_name, + description_stream, + deprecated_stream, + ), &mut tokens, - features, - type_tree, - object_name, - description_stream, - deprecated_stream, ), - Some(GenericType::Set) => ComponentSchema::vec_to_tokens( + Some(GenericType::Set) => match_diagnostics( + ComponentSchema::vec_to_tokens( + &mut tokens, + features, + type_tree, + object_name, + description_stream, + deprecated_stream, + ), &mut tokens, - features, - type_tree, - object_name, - description_stream, - deprecated_stream, ), #[cfg(feature = "smallvec")] - Some(GenericType::SmallVec) => ComponentSchema::vec_to_tokens( + Some(GenericType::SmallVec) => match_diagnostics( + ComponentSchema::vec_to_tokens( + &mut tokens, + features, + type_tree, + object_name, + description_stream, + deprecated_stream, + ), &mut tokens, - features, - type_tree, - object_name, - description_stream, - deprecated_stream, ), Some(GenericType::Option) => { // Add nullable feature if not already exists. Option is always nullable @@ -658,9 +701,9 @@ impl<'c> ComponentSchema { object_name: &str, description_stream: Option, deprecated_stream: Option, - ) { + ) -> Result<(), Diagnostics> { let example = pop_feature!(features => Feature::Example(_)); - let xml = features.extract_vec_xml_feature(type_tree); + let xml = features.extract_vec_xml_feature(type_tree)?; let max_items = pop_feature!(features => Feature::MaxItems(_)); let min_items = pop_feature!(features => Feature::MinItems(_)); let nullable = pop_feature!(features => Feature::Nullable(_)); @@ -753,6 +796,8 @@ impl<'c> ComponentSchema { example.to_tokens(tokens); xml.to_tokens(tokens); nullable.to_tokens(tokens); + + Ok(()) } fn non_generic_to_tokens( diff --git a/utoipa-gen/src/component/features.rs b/utoipa-gen/src/component/features.rs index 509f51aa..3dab2888 100644 --- a/utoipa-gen/src/component/features.rs +++ b/utoipa-gen/src/component/features.rs @@ -1,15 +1,14 @@ use std::{fmt::Display, mem, str::FromStr}; use proc_macro2::{Ident, Span, TokenStream}; -use proc_macro_error::abort; use quote::{quote, ToTokens}; use syn::{parenthesized, parse::ParseStream, LitFloat, LitInt, LitStr, TypePath}; use crate::{ - parse_utils, + impl_to_tokens_diagnostics, parse_utils, path::parameter::{self, ParameterStyle}, schema_type::{SchemaFormat, SchemaType}, - AnyValue, + AnyValue, Diagnostics, OptionExt, }; use super::{schema, serde::RenameRule, GenericType, TypeTree}; @@ -72,7 +71,7 @@ pub trait Validatable { pub trait Validate: Validatable { /// Perform validation check against schema type. - fn validate(&self, validator: impl Validator); + fn validate(&self, validator: impl Validator) -> Option; } pub trait Parse { @@ -122,7 +121,7 @@ pub enum Feature { } impl Feature { - pub fn validate(&self, schema_type: &SchemaType, type_tree: &TypeTree) { + pub fn validate(&self, schema_type: &SchemaType, type_tree: &TypeTree) -> Option { match self { Feature::MultipleOf(multiple_of) => multiple_of.validate( ValidatorChain::new(&IsNumber(schema_type)).next(&AboveZeroF64(multiple_of.0)), @@ -169,86 +168,85 @@ impl Feature { } } } -} -impl ToTokens for Feature { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + fn tokens_or_diagnostics(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> { let feature = match &self { - Feature::Default(default) => quote! { .default(#default) }, - Feature::Example(example) => quote! { .example(Some(#example)) }, - Feature::XmlAttr(xml) => quote! { .xml(Some(#xml)) }, - Feature::Format(format) => quote! { .format(Some(#format)) }, - Feature::WriteOnly(write_only) => quote! { .write_only(Some(#write_only)) }, - Feature::ReadOnly(read_only) => quote! { .read_only(Some(#read_only)) }, - Feature::Title(title) => quote! { .title(Some(#title)) }, - Feature::Nullable(nullable) => quote! { .nullable(#nullable) }, - Feature::Rename(rename) => rename.to_token_stream(), - Feature::Style(style) => quote! { .style(Some(#style)) }, - Feature::ParameterIn(parameter_in) => quote! { .parameter_in(#parameter_in) }, - Feature::MultipleOf(multiple_of) => quote! { .multiple_of(Some(#multiple_of)) }, - Feature::AllowReserved(allow_reserved) => { - quote! { .allow_reserved(Some(#allow_reserved)) } - } - Feature::Explode(explode) => quote! { .explode(Some(#explode)) }, - Feature::Maximum(maximum) => quote! { .maximum(Some(#maximum)) }, - Feature::Minimum(minimum) => quote! { .minimum(Some(#minimum)) }, - Feature::ExclusiveMaximum(exclusive_maximum) => { - quote! { .exclusive_maximum(Some(#exclusive_maximum)) } - } - Feature::ExclusiveMinimum(exclusive_minimum) => { - quote! { .exclusive_minimum(Some(#exclusive_minimum)) } - } - Feature::MaxLength(max_length) => quote! { .max_length(Some(#max_length)) }, - Feature::MinLength(min_length) => quote! { .min_length(Some(#min_length)) }, - Feature::Pattern(pattern) => quote! { .pattern(Some(#pattern)) }, - Feature::MaxItems(max_items) => quote! { .max_items(Some(#max_items)) }, - Feature::MinItems(min_items) => quote! { .min_items(Some(#min_items)) }, - Feature::MaxProperties(max_properties) => { - quote! { .max_properties(Some(#max_properties)) } - } - Feature::MinProperties(min_properties) => { - quote! { .max_properties(Some(#min_properties)) } - } - Feature::SchemaWith(schema_with) => schema_with.to_token_stream(), - Feature::Description(description) => quote! { .description(Some(#description)) }, - Feature::Deprecated(deprecated) => quote! { .deprecated(Some(#deprecated)) }, - Feature::AdditionalProperties(additional_properties) => { - quote! { .additional_properties(Some(#additional_properties)) } - } - Feature::RenameAll(_) => { - abort! { - Span::call_site(), - "RenameAll feature does not support `ToTokens`" + Feature::Default(default) => quote! { .default(#default) }, + Feature::Example(example) => quote! { .example(Some(#example)) }, + Feature::XmlAttr(xml) => quote! { .xml(Some(#xml)) }, + Feature::Format(format) => quote! { .format(Some(#format)) }, + Feature::WriteOnly(write_only) => quote! { .write_only(Some(#write_only)) }, + Feature::ReadOnly(read_only) => quote! { .read_only(Some(#read_only)) }, + Feature::Title(title) => quote! { .title(Some(#title)) }, + Feature::Nullable(nullable) => quote! { .nullable(#nullable) }, + Feature::Rename(rename) => rename.to_token_stream(), + Feature::Style(style) => quote! { .style(Some(#style)) }, + Feature::ParameterIn(parameter_in) => quote! { .parameter_in(#parameter_in) }, + Feature::MultipleOf(multiple_of) => quote! { .multiple_of(Some(#multiple_of)) }, + Feature::AllowReserved(allow_reserved) => { + quote! { .allow_reserved(Some(#allow_reserved)) } } - } - Feature::ValueType(_) => { - abort! { - Span::call_site(), - "ValueType feature does not support `ToTokens`"; - help = "ValueType is supposed to be used with `TypeTree` in same manner as a resolved struct/field type."; + Feature::Explode(explode) => quote! { .explode(Some(#explode)) }, + Feature::Maximum(maximum) => quote! { .maximum(Some(#maximum)) }, + Feature::Minimum(minimum) => quote! { .minimum(Some(#minimum)) }, + Feature::ExclusiveMaximum(exclusive_maximum) => { + quote! { .exclusive_maximum(Some(#exclusive_maximum)) } } - } - Feature::Inline(_) => { - // inline feature is ignored by `ToTokens` - TokenStream::new() - } - Feature::IntoParamsNames(_) => { - abort! { - Span::call_site(), - "Names feature does not support `ToTokens`"; - help = "Names is only used with IntoParams to artificially give names for unnamed struct type `IntoParams`." + Feature::ExclusiveMinimum(exclusive_minimum) => { + quote! { .exclusive_minimum(Some(#exclusive_minimum)) } } - } - Feature::As(_) => { - abort!(Span::call_site(), "As does not support `ToTokens`") - } - Feature::Required(required) => { - let name = ::get_name(); - quote! { .#name(#required) } - } - }; + Feature::MaxLength(max_length) => quote! { .max_length(Some(#max_length)) }, + Feature::MinLength(min_length) => quote! { .min_length(Some(#min_length)) }, + Feature::Pattern(pattern) => quote! { .pattern(Some(#pattern)) }, + Feature::MaxItems(max_items) => quote! { .max_items(Some(#max_items)) }, + Feature::MinItems(min_items) => quote! { .min_items(Some(#min_items)) }, + Feature::MaxProperties(max_properties) => { + quote! { .max_properties(Some(#max_properties)) } + } + Feature::MinProperties(min_properties) => { + quote! { .max_properties(Some(#min_properties)) } + } + Feature::SchemaWith(schema_with) => schema_with.to_token_stream(), + Feature::Description(description) => quote! { .description(Some(#description)) }, + Feature::Deprecated(deprecated) => quote! { .deprecated(Some(#deprecated)) }, + Feature::AdditionalProperties(additional_properties) => { + quote! { .additional_properties(Some(#additional_properties)) } + } + Feature::RenameAll(_) => { + return Err(Diagnostics::new("RenameAll feature does not support `ToTokens`")) + } + Feature::ValueType(_) => { + return Err(Diagnostics::new("ValueType feature does not support `ToTokens`") + .help("ValueType is supposed to be used with `TypeTree` in same manner as a resolved struct/field type.")) + } + Feature::Inline(_) => { + // inline feature is ignored by `ToTokens` + TokenStream::new() + } + Feature::IntoParamsNames(_) => { + return Err(Diagnostics::new("Names feature does not support `ToTokens`") + .help("Names is only used with IntoParams to artificially give names for unnamed struct type `IntoParams`.")) + } + Feature::As(_) => { + return Err(Diagnostics::new("As does not support `ToTokens`")) + } + Feature::Required(required) => { + let name = ::get_name(); + quote! { .#name(#required) } + } + }; - tokens.extend(feature) + tokens.extend(feature); + + Ok(()) + } +} + +impl_to_tokens_diagnostics! { + impl ToTokensDiagnostics for Feature { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) -> Result<(), Diagnostics> { + self.tokens_or_diagnostics(tokens) + } } } @@ -480,7 +478,10 @@ 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) -> (Option, Option) { + pub fn split_for_vec( + &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( @@ -488,20 +489,24 @@ impl XmlAttr { mem::take(&mut value_xml.0.wrap_name), ); - (Some(XmlAttr(vec_xml)), Some(value_xml)) + Ok((Some(XmlAttr(vec_xml)), Some(value_xml))) } else { - self.validate_xml(&self.0); + self.validate_xml(&self.0)?; - (None, Some(mem::take(self))) + Ok((None, Some(mem::take(self)))) } } #[inline] - fn validate_xml(&self, xml: &schema::xml::XmlAttr) { + fn validate_xml(&self, xml: &schema::xml::XmlAttr) -> Result<(), Diagnostics> { if let Some(wrapped_ident) = xml.is_wrapped.as_ref() { - abort! {wrapped_ident, "cannot use `wrapped` attribute in non slice field type"; - help = "Try removing `wrapped` attribute or make your field `Vec`" - } + 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(()) } } } @@ -558,7 +563,7 @@ pub struct ValueType(syn::Type); impl ValueType { /// Create [`TypeTree`] from current [`syn::Type`]. - pub fn as_type_tree(&self) -> TypeTree { + pub fn as_type_tree(&self) -> Result { TypeTree::from_type(&self.0) } } @@ -876,12 +881,12 @@ name!(Names = "names"); pub struct MultipleOf(f64, Ident); impl Validate for MultipleOf { - fn validate(&self, validator: impl Validator) { - if let Err(error) = validator.is_valid() { - abort! {self.1, "`multiple_of` error: {}", error; - help = "See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-multipleof`" - } - }; + fn validate(&self, validator: impl Validator) -> Option { + 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 + } } } @@ -910,11 +915,11 @@ name!(MultipleOf = "multiple_of"); pub struct Maximum(f64, Ident); impl Validate for Maximum { - fn validate(&self, validator: impl Validator) { - if let Err(error) = validator.is_valid() { - abort! {self.1, "`maximum` error: {}", error; - help = "See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-maximum`" - } + fn validate(&self, validator: impl Validator) -> Option { + 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, } } } @@ -953,11 +958,13 @@ impl Minimum { } impl Validate for Minimum { - fn validate(&self, validator: impl Validator) { - if let Err(error) = validator.is_valid() { - abort! {self.1, "`minimum` error: {}", error; - help = "See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-minimum`" - } + fn validate(&self, validator: impl Validator) -> Option { + 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, } } } @@ -990,11 +997,11 @@ name!(Minimum = "minimum"); pub struct ExclusiveMaximum(f64, Ident); impl Validate for ExclusiveMaximum { - fn validate(&self, validator: impl Validator) { - if let Err(error) = validator.is_valid() { - abort! {self.1, "`exclusive_maximum` error: {}", error; - help = "See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-exclusivemaximum`" - } + fn validate(&self, validator: impl Validator) -> Option { + 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, } } } @@ -1027,11 +1034,11 @@ name!(ExclusiveMaximum = "exclusive_maximum"); pub struct ExclusiveMinimum(f64, Ident); impl Validate for ExclusiveMinimum { - fn validate(&self, validator: impl Validator) { - if let Err(error) = validator.is_valid() { - abort! {self.1, "`exclusive_minimum` error: {}", error; - help = "See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-exclusiveminimum`" - } + fn validate(&self, validator: impl Validator) -> Option { + 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, } } } @@ -1064,11 +1071,11 @@ name!(ExclusiveMinimum = "exclusive_minimum"); pub struct MaxLength(usize, Ident); impl Validate for MaxLength { - fn validate(&self, validator: impl Validator) { - if let Err(error) = validator.is_valid() { - abort! {self.1, "`max_length` error: {}", error; - help = "See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-maxlength`" - } + fn validate(&self, validator: impl Validator) -> Option { + 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, } } } @@ -1101,11 +1108,11 @@ name!(MaxLength = "max_length"); pub struct MinLength(usize, Ident); impl Validate for MinLength { - fn validate(&self, validator: impl Validator) { - if let Err(error) = validator.is_valid() { - abort! {self.1, "`min_length` error: {}", error; - help = "See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-minlength`" - } + fn validate(&self, validator: impl Validator) -> Option { + 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, } } } @@ -1138,11 +1145,12 @@ name!(MinLength = "min_length"); pub struct Pattern(String, Ident); impl Validate for Pattern { - fn validate(&self, validator: impl Validator) { - if let Err(error) = validator.is_valid() { - abort! {self.1, "`pattern` error: {}", error; - help = "See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-pattern`" - } + fn validate(&self, validator: impl Validator) -> Option { + 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, } } } @@ -1176,11 +1184,11 @@ name!(Pattern = "pattern"); pub struct MaxItems(usize, Ident); impl Validate for MaxItems { - fn validate(&self, validator: impl Validator) { - if let Err(error) = validator.is_valid() { - abort! {self.1, "`max_items` error: {}", error; - help = "See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-maxitems" - } + fn validate(&self, validator: impl Validator) -> Option { + 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, } } } @@ -1213,11 +1221,11 @@ name!(MaxItems = "max_items"); pub struct MinItems(usize, Ident); impl Validate for MinItems { - fn validate(&self, validator: impl Validator) { - if let Err(error) = validator.is_valid() { - abort! {self.1, "`min_items` error: {}", error; - help = "See more details: `http://json-schema.org/draft/2020-12/json-schema-validation.html#name-minitems" - } + fn validate(&self, validator: impl Validator) -> Option { + 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, } } } @@ -1689,7 +1697,10 @@ pub trait FeaturesExt { fn pop_rename_all_feature(&mut self) -> Option; /// Extract [`XmlAttr`] feature for given `type_tree` if it has generic type [`GenericType::Vec`] - fn extract_vec_xml_feature(&mut self, type_tree: &TypeTree) -> Option; + fn extract_vec_xml_feature( + &mut self, + type_tree: &TypeTree, + ) -> Result, Diagnostics>; } impl FeaturesExt for Vec { @@ -1723,20 +1734,28 @@ impl FeaturesExt for Vec { }) } - fn extract_vec_xml_feature(&mut self, type_tree: &TypeTree) -> Option { - self.iter_mut().find_map(|feature| match feature { - Feature::XmlAttr(xml_feature) => { - let (vec_xml, value_xml) = xml_feature.split_for_vec(type_tree); + 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) + } - // 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)), + } } - - vec_xml.map(Feature::XmlAttr) - } - _ => None, - }) + _ => None, + }) + .and_then_try(|value| value) } } @@ -1760,9 +1779,12 @@ impl FeaturesExt for Option> { .and_then(|features| features.pop_rename_all_feature()) } - fn extract_vec_xml_feature(&mut self, type_tree: &TypeTree) -> Option { + fn extract_vec_xml_feature( + &mut self, + type_tree: &TypeTree, + ) -> Result, Diagnostics> { self.as_mut() - .and_then(|features| features.extract_vec_xml_feature(type_tree)) + .and_then_try(|features| features.extract_vec_xml_feature(type_tree)) } } diff --git a/utoipa-gen/src/component/into_params.rs b/utoipa-gen/src/component/into_params.rs index 21702cfc..9d63f000 100644 --- a/utoipa-gen/src/component/into_params.rs +++ b/utoipa-gen/src/component/into_params.rs @@ -1,10 +1,10 @@ use std::borrow::Cow; use proc_macro2::TokenStream; -use proc_macro_error::abort; use quote::{quote, ToTokens}; use syn::{ - parse::Parse, punctuated::Punctuated, token::Comma, Attribute, Data, Field, Generics, Ident, + parse::Parse, punctuated::Punctuated, spanned::Spanned, token::Comma, Attribute, Data, Field, + Generics, Ident, }; use crate::{ @@ -19,7 +19,7 @@ use crate::{ FieldRename, }, doc_comment::CommentAttributes, - Array, Required, ResultExt, + impl_to_tokens_diagnostics, Array, Diagnostics, OptionExt, Required, ToTokensDiagnostics, }; use super::{ @@ -61,8 +61,8 @@ pub struct IntoParams { pub ident: Ident, } -impl ToTokens for IntoParams { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { +impl ToTokensDiagnostics for IntoParams { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) -> Result<(), Diagnostics> { let ident = &self.ident; let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl(); @@ -73,19 +73,21 @@ impl ToTokens for IntoParams { .map(|attribute| { attribute .parse_args::() - .unwrap_or_abort() - .into_inner() + .map(IntoParamsFeatures::into_inner) + .map_err(Diagnostics::from) }) + .collect::, Diagnostics>>()? + .into_iter() .reduce(|acc, item| acc.merge(item)); - let serde_container = serde::parse_container(&self.attrs); + let serde_container = serde::parse_container(&self.attrs)?; // #[param] is only supported over fields if self.attrs.iter().any(|attr| attr.path().is_ident("param")) { - abort! { - ident, - "found `param` attribute in unsupported context"; - help = "Did you mean `into_params`?", - } + return Err(Diagnostics::with_span( + ident.span(), + "found `param` attribute in unsupported context", + ) + .help("Did you mean `into_params`?")); } let names = into_params_features.as_mut().and_then(|features| { @@ -102,10 +104,15 @@ impl ToTokens for IntoParams { let rename_all = pop_feature!(into_params_features => Feature::RenameAll(_)); let params = self - .get_struct_fields(&names.as_ref()) + .get_struct_fields(&names.as_ref())? .enumerate() - .filter_map(|(index, field)| { - let field_params = serde::parse_value(&field.attrs); + .map(|(index, field)| match serde::parse_value(&field.attrs) { + Ok(serde_value) => Ok((index, field, serde_value)), + Err(diagnostics) => Err(diagnostics) + }) + .collect::, Diagnostics>>()? + .into_iter() + .filter_map(|(index, field, field_params)| { if matches!(&field_params, Some(params) if !params.skip) { Some((index, field, field_params)) } else { @@ -113,7 +120,16 @@ impl ToTokens for IntoParams { } }) .map(|(index, field, field_serde_params)| { - Param { + let name = names.as_ref() + .map_try(|names| names.get(index).ok_or_else(|| Diagnostics::with_span( + ident.span(), + format!("There is no name specified in the names(...) container attribute for tuple struct field {}", index), + ))); + let name = match name { + Ok(name) => name, + Err(diagnostics) => return Err(diagnostics) + }; + let param = Param { field, field_serde_params, container_attributes: FieldParamContainerAttributes { @@ -125,17 +141,18 @@ impl ToTokens for IntoParams { }), style: &style, parameter_in: ¶meter_in, - name: names.as_ref() - .map(|names| names.get(index).unwrap_or_else(|| abort!( - ident, - "There is no name specified in the names(...) container attribute for tuple struct field {}", - index - ))), + name, }, serde_container: serde_container.as_ref(), + }; + + let mut param_tokens = TokenStream::new(); + match ToTokensDiagnostics::to_tokens(¶m, &mut param_tokens) { + Ok(_) => Ok(param_tokens), + Err(diagnostics) => Err(diagnostics) } }) - .collect::>(); + .collect::, Diagnostics>>()?; tokens.extend(quote! { impl #impl_generics utoipa::IntoParams for #ident #ty_generics #where_clause { @@ -144,6 +161,8 @@ impl ToTokens for IntoParams { } } }); + + Ok(()) } } @@ -151,33 +170,34 @@ impl IntoParams { fn get_struct_fields( &self, field_names: &Option<&Vec>, - ) -> impl Iterator { + ) -> Result, Diagnostics> { let ident = &self.ident; - let abort = |note: &str| { - abort! { - ident, - "unsupported data type, expected struct with named fields `struct {} {{...}}` or unnamed fields `struct {}(...)`", - ident.to_string(), - ident.to_string(); - note = note - } - }; - match &self.data { Data::Struct(data_struct) => match &data_struct.fields { syn::Fields::Named(named_fields) => { if field_names.is_some() { - abort! {ident, "`#[into_params(names(...))]` is not supported attribute on a struct with named fields"} + return Err(Diagnostics::with_span( + ident.span(), + "`#[into_params(names(...))]` is not supported attribute on a struct with named fields") + ); } - named_fields.named.iter() + Ok(named_fields.named.iter()) } syn::Fields::Unnamed(unnamed_fields) => { - self.validate_unnamed_field_names(&unnamed_fields.unnamed, field_names); - unnamed_fields.unnamed.iter() + match self.validate_unnamed_field_names(&unnamed_fields.unnamed, field_names) { + None => Ok(unnamed_fields.unnamed.iter()), + Some(diagnostics) => Err(diagnostics), + } } - _ => abort("Unit type struct is not supported"), + _ => Err(Diagnostics::with_span( + ident.span(), + "Unit type struct is not supported", + )), }, - _ => abort("Only struct type is supported"), + _ => Err(Diagnostics::with_span( + ident.span(), + "Only struct type is supported", + )), } } @@ -185,27 +205,33 @@ impl IntoParams { &self, unnamed_fields: &Punctuated, field_names: &Option<&Vec>, - ) { + ) -> Option { let ident = &self.ident; match field_names { Some(names) => { if names.len() != unnamed_fields.len() { - abort! { - ident, - "declared names amount '{}' does not match to the unnamed fields amount '{}' in type: {}", - names.len(), unnamed_fields.len(), ident; - help = r#"Did you forget to add a field name to `#[into_params(names(... , "field_name"))]`"#; - help = "Or have you added extra name but haven't defined a type?" - } - } - } - None => { - abort! { - ident, - "struct with unnamed fields must have explicit name declarations."; - help = "Try defining `#[into_params(names(...))]` over your type: {}", ident, + Some(Diagnostics::with_span( + ident.span(), + format!("declared names amount '{}' does not match to the unnamed fields amount '{}' in type: {}", + names.len(), unnamed_fields.len(), ident) + ) + .help(r#"Did you forget to add a field name to `#[into_params(names(... , "field_name"))]`"#) + .help("Or have you added extra name but haven't defined a type?") + ) + } else { + None } } + None => Some( + Diagnostics::with_span( + ident.span(), + "struct with unnamed fields must have explicit name declarations.", + ) + .help(format!( + "Try defining `#[into_params(names(...))]` over your type: {}", + ident + )), + ), } } } @@ -278,7 +304,7 @@ impl Param<'_> { /// whether they should be rendered in [`Param`] itself or in [`Param`]s schema. /// /// Method returns a tuple containing two [`Vec`]s of [`Feature`]. - fn resolve_field_features(&self) -> (Vec, Vec) { + fn resolve_field_features(&self) -> Result<(Vec, Vec), syn::Error> { let mut field_features = self .field .attrs @@ -287,9 +313,10 @@ impl Param<'_> { .map(|attribute| { attribute .parse_args::() - .unwrap_or_abort() - .into_inner() + .map(FieldFeatures::into_inner) }) + .collect::, syn::Error>>()? + .into_iter() .reduce(|acc, item| acc.merge(item)) .unwrap_or_default(); @@ -302,7 +329,7 @@ impl Param<'_> { }; } - field_features.into_iter().fold( + Ok(field_features.into_iter().fold( (Vec::::new(), Vec::::new()), |(mut schema_features, mut param_features), feature| { match feature { @@ -333,12 +360,10 @@ impl Param<'_> { (schema_features, param_features) }, - ) + )) } -} -impl ToTokens for Param<'_> { - fn to_tokens(&self, tokens: &mut TokenStream) { + fn tokens_or_diagnostics(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> { let field = self.field; let field_serde_params = &self.field_serde_params; let ident = &field.ident; @@ -346,16 +371,17 @@ impl ToTokens for Param<'_> { .as_ref() .map(|ident| ident.to_string()) .or_else(|| self.container_attributes.name.cloned()) - .unwrap_or_else(|| abort!( - field, "No name specified for unnamed field."; - help = "Try adding #[into_params(names(...))] container attribute to specify the name for this field" - )); + .ok_or_else(|| + Diagnostics::with_span(field.span(), "No name specified for unnamed field.") + .help("Try adding #[into_params(names(...))] container attribute to specify the name for this field") + )?; if name.starts_with("r#") { name = &name[2..]; } - let (schema_features, mut param_features) = self.resolve_field_features(); + let (schema_features, mut param_features) = + self.resolve_field_features().map_err(Diagnostics::from)?; let rename = param_features .pop_rename_feature() @@ -375,7 +401,7 @@ impl ToTokens for Param<'_> { }); let name = super::rename::(name, rename_to, rename_all) .unwrap_or(Cow::Borrowed(name)); - let type_tree = TypeTree::from_type(&field.ty); + let type_tree = TypeTree::from_type(&field.ty)?; tokens.extend(quote! { utoipa::openapi::path::ParameterBuilder::new() .name(#name) @@ -407,7 +433,7 @@ impl ToTokens for Param<'_> { let value_type = param_features.pop_value_type_feature(); let component = value_type .as_ref() - .map(|value_type| value_type.as_type_tree()) + .map_try(|value_type| value_type.as_type_tree())? .unwrap_or(type_tree); let required = pop_feature_as_inner!(param_features => Feature::Required(_v)) @@ -434,5 +460,15 @@ impl ToTokens for Param<'_> { tokens.extend(quote! { .schema(Some(#schema)).build() }); } + + Ok(()) + } +} + +impl_to_tokens_diagnostics! { + impl ToTokensDiagnostics for Param<'_> { + fn to_tokens(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> { + self.tokens_or_diagnostics(tokens) + } } } diff --git a/utoipa-gen/src/component/schema.rs b/utoipa-gen/src/component/schema.rs index 490d12da..7f37174c 100644 --- a/utoipa-gen/src/component/schema.rs +++ b/utoipa-gen/src/component/schema.rs @@ -1,7 +1,6 @@ -use std::borrow::Cow; +use std::borrow::{Borrow, Cow}; use proc_macro2::{Ident, Span, TokenStream}; -use proc_macro_error::abort; use quote::{format_ident, quote, ToTokens}; use syn::{ parse::Parse, parse_quote, punctuated::Punctuated, spanned::Spanned, token::Comma, Attribute, @@ -12,7 +11,7 @@ use syn::{ use crate::{ component::features::{Example, Rename}, doc_comment::CommentAttributes, - Array, Deprecated, ResultExt, + impl_to_tokens_diagnostics, Array, Deprecated, Diagnostics, OptionExt, ToTokensDiagnostics, }; use self::{ @@ -57,26 +56,26 @@ impl<'a> Schema<'a> { ident: &'a Ident, generics: &'a Generics, vis: &'a Visibility, - ) -> Self { + ) -> Result { let aliases = if generics.type_params().count() > 0 { - parse_aliases(attributes) + parse_aliases(attributes)? } else { None }; - Self { + Ok(Self { data, ident, attributes, generics, aliases, vis, - } + }) } } -impl ToTokens for Schema<'_> { - fn to_tokens(&self, tokens: &mut TokenStream) { +impl ToTokensDiagnostics for Schema<'_> { + fn to_tokens(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> { let ident = self.ident; let variant = SchemaVariant::new( self.data, @@ -84,50 +83,58 @@ impl ToTokens for Schema<'_> { ident, self.generics, None::>, - ); + )?; let (_, ty_generics, where_clause) = self.generics.split_for_impl(); let life = &Lifetime::new(Schema::TO_SCHEMA_LIFETIME, Span::call_site()); let schema_ty: Type = parse_quote!(#ident #ty_generics); - let schema_children = &*TypeTree::from_type(&schema_ty).children.unwrap_or_default(); + let schema_children = &*TypeTree::from_type(&schema_ty)? + .children + .unwrap_or_default(); - let aliases = self.aliases.as_ref().map(|aliases| { + let aliases = self.aliases.as_ref().map_try(|aliases| { let alias_schemas = aliases .iter() .map(|alias| { let name = &*alias.name; let alias_type_tree = TypeTree::from_type(&alias.ty); - let variant = SchemaVariant::new( + SchemaVariant::new( self.data, self.attributes, ident, self.generics, - alias_type_tree + alias_type_tree? .children .map(|children| children.into_iter().zip(schema_children)), - ); - quote! { (#name, #variant.into()) } + ) + .and_then(|variant| { + let mut alias_tokens = TokenStream::new(); + match variant.to_tokens(&mut alias_tokens) { + Ok(_) => Ok(quote! { (#name, #alias_tokens.into()) }), + Err(diagnostics) => Err(diagnostics), + } + }) }) - .collect::>(); + .collect::, Diagnostics>>()?; - quote! { + Result::::Ok(quote! { fn aliases() -> Vec<(& #life str, utoipa::openapi::schema::Schema)> { #alias_schemas.to_vec() } - } - }); + }) + })?; - let type_aliases = self.aliases.as_ref().map(|aliases| { + let type_aliases = self.aliases.as_ref().map_try(|aliases| { aliases .iter() .map(|alias| { let name = quote::format_ident!("{}", alias.name); let ty = &alias.ty; let vis = self.vis; - let name_generics = alias.get_lifetimes().fold( + let name_generics = alias.get_lifetimes()?.fold( Punctuated::<&GenericArgument, Comma>::new(), |mut acc, lifetime| { acc.push(lifetime); @@ -135,12 +142,12 @@ impl ToTokens for Schema<'_> { }, ); - quote! { + Ok(quote! { #vis type #name < #name_generics > = #ty; - } + }) }) - .collect::() - }); + .collect::>() + })?; let name = if let Some(schema_as) = variant.get_schema_as() { format_path_ref(&schema_as.0.path) @@ -158,17 +165,21 @@ impl ToTokens for Schema<'_> { impl_generics.params.push(schema_lifetime); let (impl_generics, _, _) = impl_generics.split_for_impl(); + let mut variant_tokens = TokenStream::new(); + variant.to_tokens(&mut variant_tokens)?; + tokens.extend(quote! { impl #impl_generics utoipa::ToSchema #schema_generics for #ident #ty_generics #where_clause { fn schema() -> (& #life str, utoipa::openapi::RefOr) { - (#name, #variant.into()) + (#name, #variant_tokens.into()) } #aliases } #type_aliases - }) + }); + Ok(()) } } @@ -187,32 +198,32 @@ impl<'a> SchemaVariant<'a> { ident: &'a Ident, generics: &'a Generics, aliases: Option, - ) -> SchemaVariant<'a> { + ) -> Result, Diagnostics> { match data { Data::Struct(content) => match &content.fields { Fields::Unnamed(fields) => { let FieldsUnnamed { unnamed, .. } = fields; let mut unnamed_features = attributes - .parse_features::() + .parse_features::()? .into_inner(); let schema_as = pop_feature_as_inner!(unnamed_features => Feature::As(_v)); - Self::Unnamed(UnnamedStructSchema { + Ok(Self::Unnamed(UnnamedStructSchema { struct_name: Cow::Owned(ident.to_string()), attributes, features: unnamed_features, fields: unnamed, schema_as, - }) + })) } Fields::Named(fields) => { let FieldsNamed { named, .. } = fields; let mut named_features = attributes - .parse_features::() + .parse_features::()? .into_inner(); let schema_as = pop_feature_as_inner!(named_features => Feature::As(_v)); - Self::Named(NamedStructSchema { + Ok(Self::Named(NamedStructSchema { struct_name: Cow::Owned(ident.to_string()), attributes, rename_all: named_features.pop_rename_all_feature(), @@ -221,19 +232,19 @@ impl<'a> SchemaVariant<'a> { generics: Some(generics), schema_as, aliases: aliases.map(|aliases| aliases.into_iter().collect()), - }) + })) } - Fields::Unit => Self::Unit(UnitStructVariant), + Fields::Unit => Ok(Self::Unit(UnitStructVariant)), }, - Data::Enum(content) => Self::Enum(EnumSchema::new( + Data::Enum(content) => Ok(Self::Enum(EnumSchema::new( Cow::Owned(ident.to_string()), &content.variants, attributes, - )), - _ => abort!( + )?)), + _ => Err(Diagnostics::with_span( ident.span(), - "unexpected data type, expected syn::Data::Struct or syn::Data::Enum" - ), + "unexpected data type, expected syn::Data::Struct or syn::Data::Enum", + )), } } @@ -247,13 +258,19 @@ impl<'a> SchemaVariant<'a> { } } -impl ToTokens for SchemaVariant<'_> { - fn to_tokens(&self, tokens: &mut TokenStream) { +impl ToTokensDiagnostics for SchemaVariant<'_> { + fn to_tokens(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> { match self { - Self::Enum(schema) => schema.to_tokens(tokens), - Self::Named(schema) => schema.to_tokens(tokens), - Self::Unnamed(schema) => schema.to_tokens(tokens), - Self::Unit(unit) => unit.to_tokens(tokens), + Self::Enum(schema) => { + schema.to_tokens(tokens); + Ok(()) + } + Self::Named(schema) => ToTokensDiagnostics::to_tokens(schema, tokens), + Self::Unnamed(schema) => ToTokensDiagnostics::to_tokens(schema, tokens), + Self::Unit(unit) => { + unit.to_tokens(tokens); + Ok(()) + } } } } @@ -281,6 +298,7 @@ pub struct NamedStructSchema<'a> { pub schema_as: Option, } +#[cfg_attr(feature = "debug", derive(Debug))] struct NamedStructFieldOptions<'a> { property: Property, rename_field_value: Option>, @@ -289,14 +307,13 @@ struct NamedStructFieldOptions<'a> { } impl NamedStructSchema<'_> { - fn field_as_schema_property( + fn get_named_struct_field_options( &self, field: &Field, - flatten: bool, + field_rules: Option<&SerdeValue>, container_rules: &Option, - yield_: impl FnOnce(NamedStructFieldOptions<'_>) -> R, - ) -> R { - let type_tree = &mut TypeTree::from_type(&field.ty); + ) -> Result, Diagnostics> { + let type_tree = &mut TypeTree::from_type(&field.ty)?; if let Some(aliases) = &self.aliases { for (new_generic, old_generic_matcher) in aliases.iter() { if let Some(generic_match) = type_tree.find_mut(old_generic_matcher) { @@ -307,7 +324,7 @@ impl NamedStructSchema<'_> { let mut field_features = field .attrs - .parse_features::() + .parse_features::()? .into_inner(); let schema_default = self @@ -354,14 +371,14 @@ impl NamedStructSchema<'_> { .and_then(|features| features.pop_value_type_feature()); let override_type_tree = value_type .as_ref() - .map(|value_type| value_type.as_type_tree()); + .map_try(|value_type| value_type.as_type_tree())?; let comments = CommentAttributes::from_attributes(&field.attrs); let schema_with = pop_feature!(field_features => Feature::SchemaWith(_)); let required = pop_feature_as_inner!(field_features => Feature::Required(_v)); let type_tree = override_type_tree.as_ref().unwrap_or(type_tree); let is_option = type_tree.is_option(); - yield_(NamedStructFieldOptions { + Ok(NamedStructFieldOptions { property: if let Some(schema_with) = schema_with { Property::SchemaWith(schema_with) } else { @@ -372,7 +389,7 @@ impl NamedStructSchema<'_> { deprecated: deprecated.as_ref(), object_name: self.struct_name.as_ref(), }; - if flatten && type_tree.is_map() { + if is_flatten(field_rules) && type_tree.is_map() { Property::FlattenedMap(FlattenedMapSchema::new(cs)) } else { Property::Schema(ComponentSchema::new(cs)) @@ -383,124 +400,130 @@ impl NamedStructSchema<'_> { is_option, }) } -} -impl ToTokens for NamedStructSchema<'_> { - fn to_tokens(&self, tokens: &mut TokenStream) { - let container_rules = serde::parse_container(self.attributes); + fn tokens_or_diagnostics(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> { + let container_rules = serde::parse_container(self.attributes)?; - let mut object_tokens = self + let fields = self .fields .iter() - .filter_map(|field| { - let field_rule = serde::parse_value(&field.attrs); + .map(|field| { + let mut field_name = Cow::Owned(field.ident.as_ref().unwrap().to_string()); - if is_not_skipped(&field_rule) && !is_flatten(&field_rule) { - Some((field, field_rule)) - } else { - None + if Borrow::::borrow(&field_name).starts_with("r#") { + field_name = Cow::Owned(field_name[2..].to_string()); + } + + let field_rules = serde::parse_value(&field.attrs); + let field_rules = match field_rules { + Ok(field_rules) => field_rules, + Err(diagnostics) => return Err(diagnostics), + }; + let field_options = self.get_named_struct_field_options( + field, + field_rules.as_ref(), + &container_rules, + ); + + match field_options { + Ok(field_options) => Ok((field_options, field_rules, field_name, field)), + Err(options_diagnostics) => Err(options_diagnostics), } }) + .collect::, Diagnostics>>()?; + + let mut object_tokens = fields + .iter() + .filter(|(_, field_rules, ..)| { + is_not_skipped(field_rules) && !is_flatten(field_rules.as_ref()) + }) .fold( quote! { utoipa::openapi::ObjectBuilder::new() }, - |mut object_tokens, (field, field_rule)| { - let mut field_name = &*field.ident.as_ref().unwrap().to_string(); + |mut object_tokens, + ( + NamedStructFieldOptions { + property, + rename_field_value, + required, + is_option, + }, + field_rules, + field_name, + _field, + )| { + let rename_to = field_rules + .as_ref() + .and_then(|field_rule| field_rule.rename.as_deref().map(Cow::Borrowed)) + .or(rename_field_value.as_ref().cloned()); + let rename_all = container_rules + .as_ref() + .and_then(|container_rule| container_rule.rename_all.as_ref()) + .or_else(|| { + self.rename_all + .as_ref() + .map(|rename_all| rename_all.as_rename_rule()) + }); - if field_name.starts_with("r#") { - field_name = &field_name[2..]; - } + let name = + super::rename::(field_name.borrow(), rename_to, rename_all) + .unwrap_or(Cow::Borrowed(field_name.borrow())); - self.field_as_schema_property( - field, - false, - &container_rules, - |NamedStructFieldOptions { - property, - rename_field_value, - required, - is_option, - }| { - let rename_to = field_rule - .as_ref() - .and_then(|field_rule| { - field_rule.rename.as_deref().map(Cow::Borrowed) - }) - .or(rename_field_value); - let rename_all = container_rules - .as_ref() - .and_then(|container_rule| container_rule.rename_all.as_ref()) - .or_else(|| { - self.rename_all - .as_ref() - .map(|rename_all| rename_all.as_rename_rule()) - }); - - let name = - super::rename::(field_name, rename_to, rename_all) - .unwrap_or(Cow::Borrowed(field_name)); - - object_tokens.extend(quote! { - .property(#name, #property) - }); - - if (!is_option - && super::is_required( - field_rule.as_ref(), - container_rules.as_ref(), - )) - || required - .as_ref() - .map(super::features::Required::is_true) - .unwrap_or(false) - { - object_tokens.extend(quote! { - .required(#name) - }) - } + object_tokens.extend(quote! { + .property(#name, #property) + }); - object_tokens - }, - ) + if (!is_option + && super::is_required(field_rules.as_ref(), container_rules.as_ref())) + || required + .as_ref() + .map(super::features::Required::is_true) + .unwrap_or(false) + { + object_tokens.extend(quote! { + .required(#name) + }) + } + + object_tokens }, ); - let flatten_fields: Vec<&Field> = self - .fields + let flatten_fields = fields .iter() - .filter(|field| { - let field_rule = serde::parse_value(&field.attrs); - is_flatten(&field_rule) - }) - .collect(); + .filter(|(_, field_rules, ..)| is_flatten(field_rules.as_ref())) + .collect::>(); let all_of = if !flatten_fields.is_empty() { let mut flattened_tokens = TokenStream::new(); let mut flattened_map_field = None; - for field in flatten_fields { - self.field_as_schema_property( - field, - true, - &container_rules, - |NamedStructFieldOptions { property, .. }| match property { - Property::Schema(_) | Property::SchemaWith(_) => { - flattened_tokens.extend(quote! { .item(#property) }) - } - Property::FlattenedMap(_) => match flattened_map_field { + + for (options, _, _, field) in flatten_fields { + let NamedStructFieldOptions { property, .. } = options; + + match property { + Property::Schema(_) | Property::SchemaWith(_) => { + flattened_tokens.extend(quote! { .item(#property) }) + } + Property::FlattenedMap(_) => { + match flattened_map_field { None => { object_tokens .extend(quote! { .additional_properties(Some(#property)) }); flattened_map_field = Some(field); } Some(flattened_map_field) => { - abort!(self.fields, - "The structure `{}` contains multiple flattened map fields.", - self.struct_name; - note = flattened_map_field.span() => "first flattened map field was declared here as `{}`", flattened_map_field.ident.as_ref().unwrap(); - note = field.span() => "second flattened map field was declared here as `{}`", field.ident.as_ref().unwrap()); - }, - }, - }, - ) + return Err(Diagnostics::with_span( + self.fields.span(), + format!("The structure `{}` contains multiple flattened map fields.", self.struct_name)) + .note( + format!("first flattened map field was declared here as `{}`", + flattened_map_field.ident.as_ref().unwrap())) + .note(format!("second flattened map field was declared here as `{}`", field.ident.as_ref().unwrap())) + ); + } + } + } + } } if flattened_tokens.is_empty() { @@ -522,7 +545,7 @@ impl ToTokens for NamedStructSchema<'_> { if !all_of && container_rules .as_ref() - .and_then(|container_rule| Some(container_rule.deny_unknown_fields)) + .map(|container_rule| container_rule.deny_unknown_fields) .unwrap_or(false) { tokens.extend(quote! { @@ -544,6 +567,16 @@ impl ToTokens for NamedStructSchema<'_> { .description(Some(#description)) }) } + + Ok(()) + } +} + +impl_to_tokens_diagnostics! { + impl ToTokensDiagnostics for NamedStructSchema<'_> { + fn to_tokens(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> { + self.tokens_or_diagnostics(tokens) + } } } @@ -556,18 +589,21 @@ struct UnnamedStructSchema<'a> { schema_as: Option, } -impl ToTokens for UnnamedStructSchema<'_> { - fn to_tokens(&self, tokens: &mut TokenStream) { +impl UnnamedStructSchema<'_> { + fn tokens_or_diagnostics(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> { let fields_len = self.fields.len(); let first_field = self.fields.first().unwrap(); - let first_part = &TypeTree::from_type(&first_field.ty); + let first_part = &TypeTree::from_type(&first_field.ty)?; let all_fields_are_same = fields_len == 1 - || self.fields.iter().skip(1).all(|field| { - let schema_part = &TypeTree::from_type(&field.ty); - - first_part == schema_part - }); + || self + .fields + .iter() + .skip(1) + .map(|field| TypeTree::from_type(&field.ty)) + .collect::, Diagnostics>>()? + .iter() + .all(|schema_part| first_part == schema_part); let deprecated = super::get_deprecated(self.attributes); if all_fields_are_same { @@ -577,7 +613,7 @@ impl ToTokens for UnnamedStructSchema<'_> { .and_then(|features| features.pop_value_type_feature()); let override_type_tree = value_type .as_ref() - .map(|value_type| value_type.as_type_tree()); + .map_try(|value_type| value_type.as_type_tree())?; if fields_len == 1 { if let Some(ref mut features) = unnamed_struct_features { @@ -628,6 +664,16 @@ impl ToTokens for UnnamedStructSchema<'_> { quote! { .to_array_builder().description(Some(#description)).max_items(Some(#fields_len)).min_items(Some(#fields_len)) }, ) } + + Ok(()) + } +} + +impl_to_tokens_diagnostics! { + impl ToTokensDiagnostics for UnnamedStructSchema<'_> { + fn to_tokens(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> { + self.tokens_or_diagnostics(tokens) + } } } @@ -642,14 +688,14 @@ impl<'e> EnumSchema<'e> { enum_name: Cow<'e, str>, variants: &'e Punctuated, attributes: &'e [Attribute], - ) -> Self { + ) -> Result { if variants .iter() .all(|variant| matches!(variant.fields, Fields::Unit)) { #[cfg(feature = "repr")] { - attributes + let repr_enum = attributes .iter() .find_map(|attribute| { if attribute.path().is_ident("repr") { @@ -658,7 +704,7 @@ impl<'e> EnumSchema<'e> { None } }) - .map(|enum_type| { + .map_try(|enum_type| { let mut repr_enum_features = features::parse_schema_features_with(attributes, |input| { Ok(parse_features!( @@ -667,12 +713,12 @@ impl<'e> EnumSchema<'e> { super::features::Title, As )) - }) + })? .unwrap_or_default(); let schema_as = pop_feature_as_inner!(repr_enum_features => Feature::As(_v)); - Self { + Result::::Ok(Self { schema_type: EnumSchemaType::Repr(ReprEnum { variants, attributes, @@ -680,18 +726,21 @@ impl<'e> EnumSchema<'e> { enum_features: repr_enum_features, }), schema_as, - } - }) - .unwrap_or_else(|| { + }) + })?; + + match repr_enum { + Some(repr) => Ok(repr), + None => { let mut simple_enum_features = attributes - .parse_features::() + .parse_features::()? .into_inner() .unwrap_or_default(); let schema_as = pop_feature_as_inner!(simple_enum_features => Feature::As(_v)); let rename_all = simple_enum_features.pop_rename_all_feature(); - Self { + Ok(Self { schema_type: EnumSchemaType::Simple(SimpleEnum { attributes, variants, @@ -699,20 +748,21 @@ impl<'e> EnumSchema<'e> { rename_all, }), schema_as, - } - }) + }) + } + } } #[cfg(not(feature = "repr"))] { let mut simple_enum_features = attributes - .parse_features::() + .parse_features::()? .into_inner() .unwrap_or_default(); let schema_as = pop_feature_as_inner!(simple_enum_features => Feature::As(_v)); let rename_all = simple_enum_features.pop_rename_all_feature(); - Self { + Ok(Self { schema_type: EnumSchemaType::Simple(SimpleEnum { attributes, variants, @@ -720,17 +770,17 @@ impl<'e> EnumSchema<'e> { rename_all, }), schema_as, - } + }) } } else { let mut enum_features = attributes - .parse_features::() + .parse_features::()? .into_inner() .unwrap_or_default(); let schema_as = pop_feature_as_inner!(enum_features => Feature::As(_v)); let rename_all = enum_features.pop_rename_all_feature(); - Self { + Ok(Self { schema_type: EnumSchemaType::Complex(ComplexEnum { enum_name, attributes, @@ -739,7 +789,7 @@ impl<'e> EnumSchema<'e> { enum_features, }), schema_as, - } + }) } } } @@ -762,16 +812,16 @@ impl ToTokens for EnumSchemaType<'_> { fn to_tokens(&self, tokens: &mut TokenStream) { let attributes = match self { Self::Simple(simple) => { - simple.to_tokens(tokens); + ToTokens::to_tokens(simple, tokens); simple.attributes } #[cfg(feature = "repr")] Self::Repr(repr) => { - repr.to_tokens(tokens); + ToTokens::to_tokens(repr, tokens); repr.attributes } Self::Complex(complex) => { - complex.to_tokens(tokens); + ToTokens::to_tokens(complex, tokens); complex.attributes } }; @@ -799,29 +849,47 @@ struct ReprEnum<'a> { } #[cfg(feature = "repr")] -impl ToTokens for ReprEnum<'_> { - fn to_tokens(&self, tokens: &mut TokenStream) { - let container_rules = serde::parse_container(self.attributes); +impl ReprEnum<'_> { + fn tokens_or_diagnostics(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> { + let container_rules = serde::parse_container(self.attributes)?; + let enum_variants = self + .variants + .iter() + .map(|variant| match serde::parse_value(&variant.attrs) { + Ok(variant_rules) => Ok((variant, variant_rules)), + Err(diagnostics) => Err(diagnostics), + }) + .collect::, Diagnostics>>()? + .into_iter() + .filter_map(|(variant, variant_rules)| { + let variant_type = &variant.ident; + + if is_not_skipped(&variant_rules) { + let repr_type = &self.enum_type; + Some(enum_variant::ReprVariant { + value: quote! { Self::#variant_type as #repr_type }, + type_path: repr_type, + }) + } else { + None + } + }) + .collect::>>(); regular_enum_to_tokens(tokens, &container_rules, &self.enum_features, || { - self.variants - .iter() - .filter_map(|variant| { - let variant_type = &variant.ident; - let variant_rules = serde::parse_value(&variant.attrs); - - if is_not_skipped(&variant_rules) { - let repr_type = &self.enum_type; - Some(enum_variant::ReprVariant { - value: quote! { Self::#variant_type as #repr_type }, - type_path: repr_type, - }) - } else { - None - } - }) - .collect::>>() + enum_variants }); + + Ok(()) + } +} + +#[cfg(feature = "repr")] +impl_to_tokens_diagnostics! { + impl ToTokensDiagnostics for ReprEnum<'_> { + fn to_tokens(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> { + self.tokens_or_diagnostics(tokens) + } } } @@ -860,49 +928,75 @@ struct SimpleEnum<'a> { rename_all: Option, } -impl ToTokens for SimpleEnum<'_> { - fn to_tokens(&self, tokens: &mut TokenStream) { - let container_rules = serde::parse_container(self.attributes); - - regular_enum_to_tokens(tokens, &container_rules, &self.enum_features, || { - self.variants - .iter() - .filter_map(|variant| { - let variant_rules = serde::parse_value(&variant.attrs); +impl SimpleEnum<'_> { + fn tokens_or_diagnostics(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> { + let container_rules = serde::parse_container(self.attributes)?; + let simple_enum_variant = self + .variants + .iter() + .map(|variant| match serde::parse_value(&variant.attrs) { + Ok(variant_rules) => Ok((variant, variant_rules)), + Err(diagnostics) => Err(diagnostics), + }) + .collect::, Diagnostics>>()? + .into_iter() + .filter_map(|(variant, variant_rules)| { + if is_not_skipped(&variant_rules) { + Some((variant, variant_rules)) + } else { + None + } + }) + .map(|(variant, variant_rules)| { + let variant_features = + features::parse_schema_features_with(&variant.attrs, |input| { + Ok(parse_features!(input as Rename)) + }); - if is_not_skipped(&variant_rules) { - Some((variant, variant_rules)) - } else { - None + match variant_features { + Ok(variant_features) => { + Ok((variant, variant_rules, variant_features.unwrap_or_default())) } - }) - .flat_map(|(variant, variant_rules)| { - let name = &*variant.ident.to_string(); - let mut variant_features = - features::parse_schema_features_with(&variant.attrs, |input| { - Ok(parse_features!(input as Rename)) - }) - .unwrap_or_default(); - let variant_name = rename_enum_variant( - name, - &mut variant_features, - &variant_rules, - &container_rules, - &self.rename_all, - ); + Err(diagnostics) => Err(diagnostics), + } + }) + .collect::, Diagnostics>>()? + .into_iter() + .flat_map(|(variant, variant_rules, mut variant_features)| { + let name = &*variant.ident.to_string(); + let variant_name = rename_enum_variant( + name, + &mut variant_features, + &variant_rules, + &container_rules, + &self.rename_all, + ); - variant_name - .map(|name| SimpleEnumVariant { + variant_name + .map(|name| SimpleEnumVariant { + value: name.to_token_stream(), + }) + .or_else(|| { + Some(SimpleEnumVariant { value: name.to_token_stream(), }) - .or_else(|| { - Some(SimpleEnumVariant { - value: name.to_token_stream(), - }) - }) - }) - .collect::>>() + }) + }) + .collect::>>(); + + regular_enum_to_tokens(tokens, &container_rules, &self.enum_features, || { + simple_enum_variant }); + + Ok(()) + } +} + +impl_to_tokens_diagnostics! { + impl ToTokensDiagnostics for SimpleEnum<'_> { + fn to_tokens(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> { + self.tokens_or_diagnostics(tokens) + } } } @@ -961,13 +1055,13 @@ impl ComplexEnum<'_> { variant_rules: &Option, container_rules: &Option, rename_all: &Option, - ) -> TokenStream { + ) -> Result { // TODO need to be able to split variant.attrs for variant and the struct representation! match &variant.fields { Fields::Named(named_fields) => { let (title_features, mut named_struct_features) = variant .attrs - .parse_features::() + .parse_features::()? .into_inner() .map(|features| features.split_for_title()) .unwrap_or_default(); @@ -981,7 +1075,7 @@ impl ComplexEnum<'_> { let example = pop_feature!(named_struct_features => Feature::Example(_)); - self::enum_variant::Variant::to_tokens(&ObjectVariant { + Ok(self::enum_variant::Variant::to_tokens(&ObjectVariant { name: variant_name.unwrap_or(Cow::Borrowed(&name)), title: title_features.first().map(ToTokens::to_token_stream), example: example.as_ref().map(ToTokens::to_token_stream), @@ -995,12 +1089,12 @@ impl ComplexEnum<'_> { aliases: None, schema_as: None, }, - }) + })) } Fields::Unnamed(unnamed_fields) => { let (title_features, mut unnamed_struct_features) = variant .attrs - .parse_features::() + .parse_features::()? .into_inner() .map(|features| features.split_for_title()) .unwrap_or_default(); @@ -1014,7 +1108,7 @@ impl ComplexEnum<'_> { let example = pop_feature!(unnamed_struct_features => Feature::Example(_)); - self::enum_variant::Variant::to_tokens(&ObjectVariant { + Ok(self::enum_variant::Variant::to_tokens(&ObjectVariant { name: variant_name.unwrap_or(Cow::Borrowed(&name)), title: title_features.first().map(ToTokens::to_token_stream), example: example.as_ref().map(ToTokens::to_token_stream), @@ -1025,7 +1119,7 @@ impl ComplexEnum<'_> { fields: &unnamed_fields.unnamed, schema_as: None, }, - }) + })) } Fields::Unit => { let mut unit_features = @@ -1036,7 +1130,7 @@ impl ComplexEnum<'_> { Rename, Example )) - }) + })? .unwrap_or_default(); let title = pop_feature!(unit_features => Feature::Title(_)); let variant_name = rename_enum_variant( @@ -1055,7 +1149,7 @@ impl ComplexEnum<'_> { (!description.is_empty()).then(|| Feature::Description(description.into())); // Unit variant is just simple enum with single variant. - Enum::new([SimpleEnumVariant { + Ok(Enum::new([SimpleEnumVariant { value: variant_name .unwrap_or(Cow::Borrowed(&name)) .to_token_stream(), @@ -1063,23 +1157,23 @@ impl ComplexEnum<'_> { .with_title(title.as_ref().map(ToTokens::to_token_stream)) .with_example(example.as_ref().map(ToTokens::to_token_stream)) .with_description(description.as_ref().map(ToTokens::to_token_stream)) - .to_token_stream() + .to_token_stream()) } } } /// Produce tokens that represent a variant of a [`ComplexEnum`] where serde enum attribute /// `untagged` applies. - fn untagged_variant_tokens(&self, variant: &Variant) -> TokenStream { + fn untagged_variant_tokens(&self, variant: &Variant) -> Result { match &variant.fields { Fields::Named(named_fields) => { let mut named_struct_features = variant .attrs - .parse_features::() + .parse_features::()? .into_inner() .unwrap_or_default(); - NamedStructSchema { + Ok(ToTokens::to_token_stream(&NamedStructSchema { struct_name: Cow::Borrowed(&*self.enum_name), attributes: &variant.attrs, rename_all: named_struct_features.pop_rename_all_feature(), @@ -1088,24 +1182,22 @@ impl ComplexEnum<'_> { generics: None, aliases: None, schema_as: None, - } - .to_token_stream() + })) } Fields::Unnamed(unnamed_fields) => { let unnamed_struct_features = variant .attrs - .parse_features::() + .parse_features::()? .into_inner() .unwrap_or_default(); - UnnamedStructSchema { + Ok(ToTokens::to_token_stream(&UnnamedStructSchema { struct_name: Cow::Borrowed(&*self.enum_name), attributes: &variant.attrs, features: Some(unnamed_struct_features), fields: &unnamed_fields.unnamed, schema_as: None, - } - .to_token_stream() + })) } Fields::Unit => { let mut unit_features = @@ -1115,7 +1207,7 @@ impl ComplexEnum<'_> { .unwrap_or_default(); let title = pop_feature!(unit_features => Feature::Title(_)); - UntaggedEnum::with_title(title).to_token_stream() + Ok(UntaggedEnum::with_title(title).to_token_stream()) } } } @@ -1130,12 +1222,12 @@ impl ComplexEnum<'_> { variant_rules: &Option, container_rules: &Option, rename_all: &Option, - ) -> TokenStream { + ) -> Result { match &variant.fields { Fields::Named(named_fields) => { let (title_features, mut named_struct_features) = variant .attrs - .parse_features::() + .parse_features::()? .into_inner() .map(|features| features.split_for_title()) .unwrap_or_default(); @@ -1164,18 +1256,18 @@ impl ComplexEnum<'_> { .unwrap_or(Cow::Borrowed(&name)) .to_token_stream(), }]); - quote! { + Ok(quote! { #named_enum #title .property(#tag, #variant_name_tokens) .required(#tag) - } + }) } Fields::Unnamed(unnamed_fields) => { if unnamed_fields.unnamed.len() == 1 { let (title_features, mut unnamed_struct_features) = variant .attrs - .parse_features::() + .parse_features::()? .into_inner() .map(|features| features.split_for_title()) .unwrap_or_default(); @@ -1202,14 +1294,16 @@ impl ComplexEnum<'_> { .to_token_stream(), }]); - let is_reference = unnamed_fields.unnamed.iter().any(|field| { - let ty = TypeTree::from_type(&field.ty); - - ty.value_type == ValueType::Object - }); + let is_reference = unnamed_fields + .unnamed + .iter() + .map(|field| TypeTree::from_type(&field.ty)) + .collect::, Diagnostics>>()? + .iter() + .any(|type_tree| type_tree.value_type == ValueType::Object); if is_reference { - quote! { + Ok(quote! { utoipa::openapi::schema::AllOfBuilder::new() #title .item(#unnamed_enum) @@ -1218,31 +1312,29 @@ impl ComplexEnum<'_> { .property(#tag, #variant_name_tokens) .required(#tag) ) - } + }) } else { - quote! { + Ok(quote! { #unnamed_enum #title .schema_type(utoipa::openapi::schema::SchemaType::Object) .property(#tag, #variant_name_tokens) .required(#tag) - } + }) } } else { - abort!( - variant, - "Unnamed (tuple) enum variants are unsupported for internally tagged enums using the `tag = ` serde attribute"; - - help = "Try using a different serde enum representation"; - note = "See more about enum limitations here: `https://serde.rs/enum-representations.html#internally-tagged`" - ); + Err(Diagnostics::with_span(variant.span(), + "Unnamed (tuple) enum variants are unsupported for internally tagged enums using the `tag = ` serde attribute") + .help("Try using a different serde enum representation") + .note("See more about enum limitations here: `https://serde.rs/enum-representations.html#internally-tagged`") + ) } } Fields::Unit => { let mut unit_features = features::parse_schema_features_with(&variant.attrs, |input| { Ok(parse_features!(input as super::features::Title, Rename)) - }) + })? .unwrap_or_default(); let title = pop_feature!(unit_features => Feature::Title(_)); @@ -1261,12 +1353,12 @@ impl ComplexEnum<'_> { .to_token_stream(), }]); - quote! { + Ok(quote! { utoipa::openapi::schema::ObjectBuilder::new() #title .property(#tag, #variant_tokens) .required(#tag) - } + }) } } } @@ -1282,12 +1374,12 @@ impl ComplexEnum<'_> { variant_rules: &Option, container_rules: &Option, rename_all: &Option, - ) -> TokenStream { + ) -> Result { match &variant.fields { Fields::Named(named_fields) => { let (title_features, mut named_struct_features) = variant .attrs - .parse_features::() + .parse_features::()? .into_inner() .map(|features| features.split_for_title()) .unwrap_or_default(); @@ -1316,7 +1408,7 @@ impl ComplexEnum<'_> { .unwrap_or(Cow::Borrowed(&name)) .to_token_stream(), }]); - quote! { + Ok(quote! { utoipa::openapi::schema::ObjectBuilder::new() #title .schema_type(utoipa::openapi::schema::SchemaType::Object) @@ -1324,13 +1416,13 @@ impl ComplexEnum<'_> { .required(#tag) .property(#content, #named_enum) .required(#content) - } + }) } Fields::Unnamed(unnamed_fields) => { if unnamed_fields.unnamed.len() == 1 { let (title_features, mut unnamed_struct_features) = variant .attrs - .parse_features::() + .parse_features::()? .into_inner() .map(|features| features.split_for_title()) .unwrap_or_default(); @@ -1357,7 +1449,7 @@ impl ComplexEnum<'_> { .to_token_stream(), }]); - quote! { + Ok(quote! { utoipa::openapi::schema::ObjectBuilder::new() #title .schema_type(utoipa::openapi::schema::SchemaType::Object) @@ -1365,15 +1457,14 @@ impl ComplexEnum<'_> { .required(#tag) .property(#content, #unnamed_enum) .required(#content) - } + }) } else { - abort!( - variant, - "Unnamed (tuple) enum variants are unsupported for adjacently tagged enums using the `tag = , content = ` serde attribute"; - - help = "Try using a different serde enum representation"; - note = "See more about enum limitations here: `https://serde.rs/enum-representations.html#adjacently-tagged`" - ); + Err( + Diagnostics::with_span(variant.span(), + "Unnamed (tuple) enum variants are unsupported for adjacently tagged enums using the `tag = , content = ` serde attribute") + .help("Try using a different serde enum representation") + .note("See more about enum limitations here: `https://serde.rs/enum-representations.html#adjacently-tagged`") + ) } } Fields::Unit => { @@ -1382,7 +1473,7 @@ impl ComplexEnum<'_> { let mut unit_features = features::parse_schema_features_with(&variant.attrs, |input| { Ok(parse_features!(input as super::features::Title, Rename)) - }) + })? .unwrap_or_default(); let title = pop_feature!(unit_features => Feature::Title(_)); @@ -1401,21 +1492,19 @@ impl ComplexEnum<'_> { .to_token_stream(), }]); - quote! { + Ok(quote! { utoipa::openapi::schema::ObjectBuilder::new() #title .property(#tag, #variant_tokens) .required(#tag) - } + }) } } } -} -impl ToTokens for ComplexEnum<'_> { - fn to_tokens(&self, tokens: &mut TokenStream) { + fn tokens_or_diagnostics(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> { let attributes = &self.attributes; - let container_rules = serde::parse_container(attributes); + let container_rules = serde::parse_container(attributes)?; let enum_repr = container_rules .as_ref() @@ -1431,10 +1520,15 @@ impl ToTokens for ComplexEnum<'_> { self.variants .iter() - .filter_map(|variant: &Variant| { - let variant_serde_rules = serde::parse_value(&variant.attrs); - if is_not_skipped(&variant_serde_rules) { - Some((variant, variant_serde_rules)) + .map(|variant| match serde::parse_value(&variant.attrs) { + Ok(variant_rules) => Ok((variant, variant_rules)), + Err(diagnostics) => Err(diagnostics), + }) + .collect::, Diagnostics>>()? + .into_iter() + .filter_map(|(variant, variant_rules)| { + if is_not_skipped(&variant_rules) { + Some((variant, variant_rules)) } else { None } @@ -1474,17 +1568,22 @@ impl ToTokens for ComplexEnum<'_> { } } }) - .collect::>() + .collect::, Diagnostics>>()? .with_discriminator(tag.map(|t| Cow::Borrowed(t.as_str()))) .to_tokens(tokens); tokens.extend(self.enum_features.to_token_stream()); + Ok(()) } } -#[cfg_attr(feature = "debug", derive(Debug))] -#[derive(PartialEq)] -struct TypeTuple<'a, T>(T, &'a Ident); +impl_to_tokens_diagnostics! { + impl ToTokensDiagnostics for ComplexEnum<'_> { + fn to_tokens(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> { + self.tokens_or_diagnostics(tokens) + } + } +} #[cfg_attr(feature = "debug", derive(Debug))] enum Property { @@ -1498,7 +1597,7 @@ impl ToTokens for Property { match self { Self::Schema(schema) => schema.to_tokens(tokens), Self::FlattenedMap(schema) => schema.to_tokens(tokens), - Self::SchemaWith(schema_with) => schema_with.to_tokens(tokens), + Self::SchemaWith(schema_with) => ToTokens::to_tokens(schema_with, tokens), } } } @@ -1534,7 +1633,7 @@ fn is_not_skipped(rule: &Option) -> bool { } #[inline] -fn is_flatten(rule: &Option) -> bool { +fn is_flatten(rule: Option<&SerdeValue>) -> bool { rule.as_ref().map(|value| value.flatten).unwrap_or(false) } @@ -1545,10 +1644,12 @@ pub struct AliasSchema { } impl AliasSchema { - fn get_lifetimes(&self) -> impl Iterator { - fn lifetimes_from_type(ty: &Type) -> impl Iterator { + fn get_lifetimes(&self) -> Result, Diagnostics> { + fn lifetimes_from_type( + ty: &Type, + ) -> Result, Diagnostics> { match ty { - Type::Path(type_path) => type_path + Type::Path(type_path) => Ok(type_path .path .segments .iter() @@ -1561,15 +1662,13 @@ impl AliasSchema { .flatten() .flat_map(|arg| match arg { GenericArgument::Type(type_argument) => { - lifetimes_from_type(type_argument).collect::>() + lifetimes_from_type(type_argument).map(|iter| iter.collect::>()) } - _ => vec![arg], + _ => Ok(vec![arg]), }) - .filter(|generic_arg| matches!(generic_arg, syn::GenericArgument::Lifetime(lifetime) if lifetime.ident != "'static")), - _ => abort!( - &ty.span(), - "AliasSchema `get_lifetimes` only supports syn::TypePath types" - ), + .flat_map(|args| args.into_iter().filter(|generic_arg| matches!(generic_arg, syn::GenericArgument::Lifetime(lifetime) if lifetime.ident != "'static"))), + ), + _ => Err(Diagnostics::with_span(ty.span(), "AliasSchema `get_lifetimes` only supports syn::TypePath types")) } } @@ -1589,13 +1688,14 @@ impl Parse for AliasSchema { } } -fn parse_aliases(attributes: &[Attribute]) -> Option> { +fn parse_aliases( + attributes: &[Attribute], +) -> Result>, Diagnostics> { attributes .iter() .find(|attribute| attribute.path().is_ident("aliases")) - .map(|aliases| { - aliases - .parse_args_with(Punctuated::::parse_terminated) - .unwrap_or_abort() + .map_try(|aliases| { + aliases.parse_args_with(Punctuated::::parse_terminated) }) + .map_err(Into::into) } diff --git a/utoipa-gen/src/component/schema/features.rs b/utoipa-gen/src/component/schema/features.rs index c1445f5a..2eea3d09 100644 --- a/utoipa-gen/src/component/schema/features.rs +++ b/utoipa-gen/src/component/schema/features.rs @@ -11,7 +11,7 @@ use crate::{ MultipleOf, Nullable, Pattern, ReadOnly, Rename, RenameAll, Required, SchemaWith, Title, ValueType, WriteOnly, XmlAttr, }, - ResultExt, + Diagnostics, }; #[cfg_attr(feature = "debug", derive(Debug))] @@ -158,13 +158,13 @@ impl Parse for EnumUnnamedFieldVariantFeatures { impl_into_inner!(EnumUnnamedFieldVariantFeatures); pub trait FromAttributes { - fn parse_features(&self) -> Option + fn parse_features(&self) -> Result, Diagnostics> where T: Parse + Merge; } impl FromAttributes for &'_ [Attribute] { - fn parse_features(&self) -> Option + fn parse_features(&self) -> Result, Diagnostics> where T: Parse + Merge, { @@ -173,7 +173,7 @@ impl FromAttributes for &'_ [Attribute] { } impl FromAttributes for Vec { - fn parse_features(&self) -> Option + fn parse_features(&self) -> Result, Diagnostics> where T: Parse + Merge, { @@ -191,8 +191,10 @@ impl_merge!( EnumUnnamedFieldVariantFeatures ); -pub fn parse_schema_features>(attributes: &[Attribute]) -> Option { - attributes +pub fn parse_schema_features>( + attributes: &[Attribute], +) -> Result, Diagnostics> { + Ok(attributes .iter() .filter(|attribute| { attribute @@ -201,8 +203,10 @@ pub fn parse_schema_features>(attributes: &[Attribut .map(|ident| *ident == "schema") .unwrap_or(false) }) - .map(|attribute| attribute.parse_args::().unwrap_or_abort()) - .reduce(|acc, item| acc.merge(item)) + .map(|attribute| attribute.parse_args::().map_err(Diagnostics::from)) + .collect::, Diagnostics>>()? + .into_iter() + .reduce(|acc, item| acc.merge(item))) } pub fn parse_schema_features_with< @@ -211,8 +215,8 @@ pub fn parse_schema_features_with< >( attributes: &[Attribute], parser: P, -) -> Option { - attributes +) -> Result, Diagnostics> { + Ok(attributes .iter() .filter(|attribute| { attribute @@ -221,6 +225,12 @@ pub fn parse_schema_features_with< .map(|ident| *ident == "schema") .unwrap_or(false) }) - .map(|attributes| attributes.parse_args_with(parser).unwrap_or_abort()) - .reduce(|acc, item| acc.merge(item)) + .map(|attributes| { + attributes + .parse_args_with(parser) + .map_err(Diagnostics::from) + }) + .collect::, Diagnostics>>()? + .into_iter() + .reduce(|acc, item| acc.merge(item))) } diff --git a/utoipa-gen/src/component/serde.rs b/utoipa-gen/src/component/serde.rs index 4f5efb6c..70ca1f0e 100644 --- a/utoipa-gen/src/component/serde.rs +++ b/utoipa-gen/src/component/serde.rs @@ -3,10 +3,9 @@ use std::str::FromStr; use proc_macro2::{Ident, Span, TokenTree}; -use proc_macro_error::abort; use syn::{buffer::Cursor, Attribute, Error}; -use crate::ResultExt; +use crate::Diagnostics; #[inline] fn parse_next_lit_str(next: Cursor) -> Option<(String, Span)> { @@ -159,9 +158,11 @@ impl SerdeContainer { } SerdeEnumRepr::InternallyTagged { .. } | SerdeEnumRepr::AdjacentlyTagged { .. } => { - abort!(span, "Duplicate serde tag argument") + return Err(syn::Error::new(span, "Duplicate serde tag argument")); + } + SerdeEnumRepr::Untagged => { + return Err(syn::Error::new(span, "Untagged enum cannot have tag")) } - SerdeEnumRepr::Untagged => abort!(span, "Untagged enum cannot have tag"), }; } } @@ -179,10 +180,10 @@ impl SerdeContainer { } SerdeEnumRepr::AdjacentlyTagged { .. } | SerdeEnumRepr::UnfinishedAdjacentlyTagged { .. } => { - abort!(span, "Duplicate serde content argument") + return Err(syn::Error::new(span, "Duplicate serde content argument")) } SerdeEnumRepr::Untagged => { - abort!(span, "Untagged enum cannot have content") + return Err(syn::Error::new(span, "Untagged enum cannot have content")) } }; } @@ -221,15 +222,17 @@ impl SerdeContainer { } } -pub fn parse_value(attributes: &[Attribute]) -> Option { +pub fn parse_value(attributes: &[Attribute]) -> Result, Diagnostics> { attributes .iter() .filter(|attribute| attribute.path().is_ident("serde")) .map(|serde_attribute| { serde_attribute .parse_args_with(SerdeValue::parse) - .unwrap_or_abort() + .map_err(Diagnostics::from) }) + .collect::, Diagnostics>>()? + .into_iter() .fold(Some(SerdeValue::default()), |acc, value| { acc.map(|mut acc| { if value.skip { @@ -254,17 +257,21 @@ pub fn parse_value(attributes: &[Attribute]) -> Option { acc }) }) + .map(Ok) + .transpose() } -pub fn parse_container(attributes: &[Attribute]) -> Option { +pub fn parse_container(attributes: &[Attribute]) -> Result, Diagnostics> { attributes .iter() .filter(|attribute| attribute.path().is_ident("serde")) .map(|serde_attribute| { serde_attribute .parse_args_with(SerdeContainer::parse) - .unwrap_or_abort() + .map_err(Diagnostics::from) }) + .collect::, Diagnostics>>()? + .into_iter() .fold(Some(SerdeContainer::default()), |acc, value| { acc.map(|mut acc| { if value.default { @@ -289,6 +296,8 @@ pub fn parse_container(attributes: &[Attribute]) -> Option { acc }) }) + .map(Ok) + .transpose() } #[derive(Clone)] @@ -508,7 +517,7 @@ mod tests { ..Default::default() }; - let result = parse_container(attributes).unwrap(); + let result = parse_container(attributes).expect("parse succes").unwrap(); assert_eq!(expected, result); } } diff --git a/utoipa-gen/src/ext.rs b/utoipa-gen/src/ext.rs index 59dc678c..083b8431 100644 --- a/utoipa-gen/src/ext.rs +++ b/utoipa-gen/src/ext.rs @@ -4,13 +4,14 @@ use std::borrow::Cow; use std::cmp::Ordering; use proc_macro2::TokenStream; -use quote::{quote, quote_spanned, ToTokens}; +use quote::{quote, quote_spanned}; use syn::parse_quote; use syn::spanned::Spanned; use syn::{punctuated::Punctuated, token::Comma, ItemFn}; use crate::component::{ComponentSchema, ComponentSchemaProps, TypeTree}; use crate::path::{PathOperation, PathTypeTree}; +use crate::{impl_to_tokens_diagnostics, Diagnostics}; #[cfg(feature = "auto_into_responses")] pub mod auto_types; @@ -107,8 +108,8 @@ impl<'t> From> for RequestBody<'t> { } } -impl ToTokens for RequestBody<'_> { - fn to_tokens(&self, tokens: &mut TokenStream) { +impl RequestBody<'_> { + fn tokens_or_diagnostics(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> { let mut actual_body = get_actual_body_type(&self.ty).unwrap().clone(); if let Some(option) = find_option_type_tree(&self.ty) { @@ -150,13 +151,23 @@ impl ToTokens for RequestBody<'_> { if self.ty.is("Bytes") { let bytes_as_bytes_vec = parse_quote!(Vec); - let ty = TypeTree::from_type(&bytes_as_bytes_vec); + let ty = TypeTree::from_type(&bytes_as_bytes_vec)?; create_body_tokens("application/octet-stream", &ty); } else if self.ty.is("Form") { create_body_tokens("application/x-www-form-urlencoded", &actual_body); } else { create_body_tokens(actual_body.get_default_content_type(), &actual_body); }; + + Ok(()) + } +} + +impl_to_tokens_diagnostics! { + impl ToTokensDiagnostics for RequestBody<'_> { + fn to_tokens(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> { + self.tokens_or_diagnostics(tokens) + } } } @@ -252,17 +263,19 @@ pub struct ResolvedOperation { pub body: String, } +pub type Arguments<'a> = ( + Option>>, + Option>>, + Option>, +); + pub trait ArgumentResolver { fn resolve_arguments( _: &'_ Punctuated, _: Option>, _: String, - ) -> ( - Option>>, - Option>>, - Option>, - ) { - (None, None, None) + ) -> Result { + Ok((None, None, None)) } } @@ -273,8 +286,8 @@ pub trait PathResolver { } pub trait PathOperationResolver { - fn resolve_operation(_: &ItemFn) -> Option { - None + fn resolve_operation(_: &ItemFn) -> Result, Diagnostics> { + Ok(None) } } @@ -305,15 +318,17 @@ impl PathOperationResolver for PathOperations {} pub mod fn_arg { use proc_macro2::Ident; - use proc_macro_error::abort; + // use proc_macro_error::abort; #[cfg(any(feature = "actix_extras", feature = "axum_extras"))] use quote::quote; + use syn::spanned::Spanned; use syn::PatStruct; use syn::{punctuated::Punctuated, token::Comma, Pat, PatType}; use crate::component::TypeTree; #[cfg(any(feature = "actix_extras", feature = "axum_extras"))] use crate::component::ValueType; + use crate::Diagnostics; /// Http operation handler functions fn argument. #[cfg_attr(feature = "debug", derive(Debug))] @@ -358,7 +373,7 @@ pub mod fn_arg { impl<'a> PartialOrd for FnArg<'a> { fn partial_cmp(&self, other: &Self) -> Option { - self.arg_type.partial_cmp(&other.arg_type) + Some(self.arg_type.cmp(&other.arg_type)) } } @@ -370,34 +385,47 @@ pub mod fn_arg { impl<'a> Eq for FnArg<'a> {} - pub fn get_fn_args(fn_args: &Punctuated) -> impl Iterator> { + pub fn get_fn_args( + fn_args: &Punctuated, + ) -> Result, Diagnostics> { fn_args .iter() .filter_map(|arg| { - let pat_type = get_fn_arg_pat_type(arg); + let pat_type = match get_fn_arg_pat_type(arg) { + Ok(pat_type) => pat_type, + Err(diagnostics) => return Some(Err(diagnostics)), + }; match pat_type.pat.as_ref() { syn::Pat::Wild(_) => None, _ => { - let arg_name = get_pat_fn_arg_type(pat_type.pat.as_ref()); - Some((TypeTree::from_type(&pat_type.ty), arg_name)) + let arg_name = match get_pat_fn_arg_type(pat_type.pat.as_ref()) { + Ok(arg_type) => arg_type, + Err(diagnostics) => return Some(Err(diagnostics)), + }; + match TypeTree::from_type(&pat_type.ty) { + Ok(type_tree) => Some(Ok((type_tree, arg_name))), + Err(diagnostics) => Some(Err(diagnostics)), + } } } }) - .map(FnArg::from) + .map(|value| value.map(FnArg::from)) + .collect::, Diagnostics>>() + .map(IntoIterator::into_iter) } #[inline] - fn get_pat_fn_arg_type(pat: &Pat) -> FnArgType { + fn get_pat_fn_arg_type(pat: &Pat) -> Result, Diagnostics> { let arg_name = match pat { - syn::Pat::Ident(ident) => FnArgType::Single(&ident.ident), + syn::Pat::Ident(ident) => Ok(FnArgType::Single(&ident.ident)), syn::Pat::Tuple(tuple) => { - FnArgType::Destructed(tuple.elems.iter().map(|item| { + tuple.elems.iter().map(|item| { match item { - syn::Pat::Ident(ident) => &ident.ident, - _ => abort!(item, "expected syn::Ident in get_pat_fn_arg_type Pat::Tuple") + syn::Pat::Ident(ident) => Ok(&ident.ident), + _ => Err(Diagnostics::with_span(item.span(), "expected syn::Ident in get_pat_fn_arg_type Pat::Tuple")) } - }).collect::>()) + }).collect::, Diagnostics>>().map(FnArgType::Destructed) }, syn::Pat::TupleStruct(tuple_struct) => { get_pat_fn_arg_type(tuple_struct.elems.first().as_ref().expect( @@ -406,7 +434,10 @@ pub mod fn_arg { }, syn::Pat::Struct(PatStruct { fields, ..}) => { let idents = fields.iter() - .map(|field| get_pat_fn_arg_type(&field.pat)) + .flat_map(|field| Ok(match get_pat_fn_arg_type(&field.pat) { + Ok(field_type) => field_type, + Err(diagnostics) => return Err(diagnostics), + })) .fold(Vec::<&'_ Ident>::new(), |mut idents, field_type| { if let FnArgType::Single(ident) = field_type { idents.push(ident) @@ -414,20 +445,21 @@ pub mod fn_arg { idents }); - FnArgType::Destructed(idents) + Ok(FnArgType::Destructed(idents)) } - _ => abort!(pat, - "unexpected syn::Pat, expected syn::Pat::Ident,in get_fn_args, cannot get fn argument name" - ), + _ => Err(Diagnostics::with_span(pat.span(), "unexpected syn::Pat, expected syn::Pat::Ident,in get_fn_args, cannot get fn argument name")), }; arg_name } #[inline] - fn get_fn_arg_pat_type(fn_arg: &syn::FnArg) -> &PatType { + fn get_fn_arg_pat_type(fn_arg: &syn::FnArg) -> Result<&PatType, Diagnostics> { match fn_arg { - syn::FnArg::Typed(value) => value, - _ => abort!(fn_arg, "unexpected fn argument type, expected FnArg::Typed"), + syn::FnArg::Typed(value) => Ok(value), + _ => Err(Diagnostics::with_span( + fn_arg.span(), + "unexpected fn argument type, expected FnArg::Typed", + )), } } @@ -481,7 +513,7 @@ pub mod fn_arg { .map(|children| { children.iter().all(|child| { matches!(child.value_type, ValueType::Object) - && matches!(child.generic_type, None) + && child.generic_type.is_none() }) }) .unwrap_or(false) diff --git a/utoipa-gen/src/ext/actix.rs b/utoipa-gen/src/ext/actix.rs index 3a87b8f8..f7cc085c 100644 --- a/utoipa-gen/src/ext/actix.rs +++ b/utoipa-gen/src/ext/actix.rs @@ -1,7 +1,6 @@ use std::borrow::Cow; use proc_macro2::Ident; -use proc_macro_error::abort; use regex::{Captures, Regex}; use syn::{parse::Parse, punctuated::Punctuated, token::Comma, ItemFn, LitStr}; @@ -9,6 +8,7 @@ use crate::{ component::{TypeTree, ValueType}, ext::ArgValue, path::PathOperation, + Diagnostics, }; use super::{ @@ -22,18 +22,21 @@ impl ArgumentResolver for PathOperations { fn_args: &Punctuated, macro_args: Option>, _: String, - ) -> ( - Option>>, - Option>>, - Option>, - ) { + ) -> Result< + ( + Option>>, + Option>>, + Option>, + ), + Diagnostics, + > { let (into_params_args, value_args): (Vec, Vec) = - fn_arg::get_fn_args(fn_args).partition(fn_arg::is_into_params); + fn_arg::get_fn_args(fn_args)?.partition(fn_arg::is_into_params); if let Some(macro_args) = macro_args { let (primitive_args, body) = split_path_args_and_request(value_args); - ( + Ok(( Some( macro_args .into_iter() @@ -49,10 +52,10 @@ impl ArgumentResolver for PathOperations { .collect(), ), body.into_iter().next().map(Into::into), - ) + )) } else { let (_, body) = split_path_args_and_request(value_args); - ( + Ok(( None, Some( into_params_args @@ -62,7 +65,7 @@ impl ArgumentResolver for PathOperations { .collect(), ), body.into_iter().next().map(Into::into), - ) + )) } } } @@ -113,27 +116,39 @@ fn into_value_argument((macro_arg, primitive_arg): (MacroArg, TypeTree)) -> Valu } impl PathOperationResolver for PathOperations { - fn resolve_operation(item_fn: &ItemFn) -> Option { - item_fn.attrs.iter().find_map(|attribute| { - if is_valid_request_type(attribute.path().get_ident()) { - match attribute.parse_args::() { - Ok(path) => Some(ResolvedOperation { - path: path.0, - path_operation: PathOperation::from_ident( - attribute.path().get_ident().unwrap(), - ), - body: String::new(), - }), - Err(error) => abort!( - error.span(), - "parse path of path operation attribute: {}", - error - ), + fn resolve_operation(item_fn: &ItemFn) -> Result, Diagnostics> { + item_fn + .attrs + .iter() + .find_map(|attribute| { + if is_valid_request_type(attribute.path().get_ident()) { + match attribute.parse_args::() { + Ok(path) => { + let path_operation = match PathOperation::from_ident( + attribute.path().get_ident().unwrap(), + ) { + Ok(path_operation) => path_operation, + Err(diagnostics) => return Some(Err(diagnostics)), + }; + + Some(Ok(ResolvedOperation { + path: path.0, + path_operation, + body: String::new(), + })) + } + Err(error) => Some(Err(Into::::into(error))), + // Err(error) => abort!( + // error.span(), + // "parse path of path operation attribute: {}", + // error + // ), + } + } else { + None } - } else { - None - } - }) + }) + .transpose() } } diff --git a/utoipa-gen/src/ext/axum.rs b/utoipa-gen/src/ext/axum.rs index e482a390..36cf9054 100644 --- a/utoipa-gen/src/ext/axum.rs +++ b/utoipa-gen/src/ext/axum.rs @@ -3,11 +3,15 @@ use std::borrow::Cow; use regex::Captures; use syn::{punctuated::Punctuated, token::Comma}; -use crate::component::{TypeTree, ValueType}; +use crate::{ + component::{TypeTree, ValueType}, + Diagnostics, +}; use super::{ fn_arg::{self, FnArg, FnArgType}, - ArgValue, ArgumentResolver, MacroArg, MacroPath, PathOperations, PathResolver, ValueArgument, + ArgValue, ArgumentResolver, Arguments, MacroArg, MacroPath, PathOperations, PathResolver, + ValueArgument, }; // axum framework is only able to resolve handler function arguments. @@ -17,20 +21,16 @@ impl ArgumentResolver for PathOperations { args: &'_ Punctuated, macro_args: Option>, _: String, - ) -> ( - Option>>, - Option>>, - Option>, - ) { + ) -> Result, Diagnostics> { let (into_params_args, value_args): (Vec, Vec) = - fn_arg::get_fn_args(args).partition(fn_arg::is_into_params); + fn_arg::get_fn_args(args)?.partition(fn_arg::is_into_params); let (value_args, body) = split_value_args_and_request_body(value_args); - ( + Ok(( Some( value_args - .zip(macro_args.unwrap_or_default().into_iter()) + .zip(macro_args.unwrap_or_default()) .map(|(value_arg, macro_arg)| ValueArgument { name: match macro_arg { MacroArg::Path(path) => Some(Cow::Owned(path.name)), @@ -48,7 +48,7 @@ impl ArgumentResolver for PathOperations { .collect(), ), body.into_iter().next().map(Into::into), - ) + )) } } diff --git a/utoipa-gen/src/ext/rocket.rs b/utoipa-gen/src/ext/rocket.rs index fe0b6538..76cf84ee 100644 --- a/utoipa-gen/src/ext/rocket.rs +++ b/utoipa-gen/src/ext/rocket.rs @@ -1,20 +1,20 @@ use std::{borrow::Cow, str::FromStr}; use proc_macro2::{Ident, TokenStream}; -use proc_macro_error::abort; use quote::quote; use regex::{Captures, Regex}; use syn::{parse::Parse, LitStr, Token}; use crate::{ component::ValueType, - ext::{ArgValue, ArgumentIn, IntoParamsType, MacroArg, ValueArgument}, + ext::{ArgValue, ArgumentIn, MacroArg, ValueArgument}, path::PathOperation, + Diagnostics, OptionExt, }; use super::{ fn_arg::{self, FnArg}, - ArgumentResolver, MacroPath, PathOperationResolver, PathOperations, PathResolver, RequestBody, + ArgumentResolver, Arguments, MacroPath, PathOperationResolver, PathOperations, PathResolver, ResolvedOperation, }; @@ -25,17 +25,13 @@ impl ArgumentResolver for PathOperations { fn_args: &syn::punctuated::Punctuated, macro_args: Option>, body: String, - ) -> ( - Option>>, - Option>>, - Option>, - ) { - let mut args = fn_arg::get_fn_args(fn_args).collect::>(); + ) -> Result, Diagnostics> { + let mut args = fn_arg::get_fn_args(fn_args)?.collect::>(); args.sort_unstable(); let (into_params_args, value_args): (Vec, Vec) = args.into_iter().partition(is_into_params); - macro_args + Ok(macro_args .map(|args| { let (anonymous_args, named_args): (Vec, Vec) = args.into_iter().partition(is_anonymous_arg); @@ -65,7 +61,7 @@ impl ArgumentResolver for PathOperations { body, ) }) - .unwrap_or_else(|| (None, None, None)) + .unwrap_or_else(|| (None, None, None))) } } @@ -140,7 +136,7 @@ fn with_argument_in(named_args: &[MacroArg]) -> impl Fn(FnArg) -> Option<(FnArg, #[inline] fn is_into_params(fn_arg: &FnArg) -> bool { - matches!(fn_arg.ty.value_type, ValueType::Object) && matches!(fn_arg.ty.generic_type, None) + matches!(fn_arg.ty.value_type, ValueType::Object) && fn_arg.ty.generic_type.is_none() } #[inline] @@ -150,41 +146,45 @@ fn is_anonymous_arg(arg: &MacroArg) -> bool { } impl PathOperationResolver for PathOperations { - fn resolve_operation(ast_fn: &syn::ItemFn) -> Option { - ast_fn.attrs.iter().find_map(|attribute| { - if is_valid_route_type(attribute.path().get_ident()) { - let Path { - path, - operation, - body, - } = match attribute.parse_args::() { - Ok(path) => path, - Err(error) => abort!( - error.span(), - "parse path of path operation attribute: {}", - error - ), - }; - - if !operation.is_empty() { - Some(ResolvedOperation { - path_operation: PathOperation::from_str(&operation).unwrap(), - path, - body, - }) - } else { - Some(ResolvedOperation { - path_operation: PathOperation::from_ident( - attribute.path().get_ident().unwrap(), - ), + fn resolve_operation( + ast_fn: &syn::ItemFn, + ) -> Result, Diagnostics> { + ast_fn + .attrs + .iter() + .find(|attribute| is_valid_route_type(attribute.path().get_ident())) + .map_try( + |attribute| match attribute.parse_args::().map_err(Diagnostics::from) { + Ok(path) => Ok((path, attribute)), + Err(diagnostics) => Err(diagnostics), + }, + )? + .map_try( + |( + Path { path, + operation, body, - }) - } - } else { - None - } - }) + }, + attribute, + )| { + if !operation.is_empty() { + Ok(ResolvedOperation { + path_operation: PathOperation::from_str(&operation).unwrap(), + path, + body, + }) + } else { + Ok(ResolvedOperation { + path_operation: PathOperation::from_ident( + attribute.path().get_ident().unwrap(), + )?, + path, + body, + }) + } + }, + ) } } diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs index c4353d23..8c0892ea 100644 --- a/utoipa-gen/src/lib.rs +++ b/utoipa-gen/src/lib.rs @@ -10,7 +10,13 @@ #[cfg(all(feature = "decimal", feature = "decimal_float"))] compile_error!("`decimal` and `decimal_float` are mutually exclusive feature flags"); -use std::{mem, ops::Deref}; +use std::{ + borrow::{Borrow, Cow}, + error::Error, + fmt::Display, + mem, + ops::Deref, +}; use component::schema::Schema; use doc_comment::CommentAttributes; @@ -19,8 +25,7 @@ use component::into_params::IntoParams; use ext::{PathOperationResolver, PathOperations, PathResolver}; use openapi::OpenApi; use proc_macro::TokenStream; -use proc_macro_error::{abort, proc_macro_error}; -use quote::{quote, ToTokens, TokenStreamExt}; +use quote::{quote, quote_spanned, ToTokens, TokenStreamExt}; use proc_macro2::{Group, Ident, Punct, Span, TokenStream as TokenStream2}; use syn::{ @@ -46,10 +51,10 @@ use self::{ features::{self, Feature}, ComponentSchema, ComponentSchemaProps, TypeTree, }, + openapi::parse_openapi_attrs, path::response::derive::{IntoResponses, ToResponse}, }; -#[proc_macro_error] #[proc_macro_derive(ToSchema, attributes(schema, aliases))] /// Generate reusable OpenAPI schema to be used /// together with [`OpenApi`][openapi_derive]. @@ -637,12 +642,12 @@ pub fn derive_to_schema(input: TokenStream) -> TokenStream { vis, } = syn::parse_macro_input!(input); - let schema = Schema::new(&data, &attrs, &ident, &generics, &vis); - - schema.to_token_stream().into() + Schema::new(&data, &attrs, &ident, &generics, &vis) + .as_ref() + .map_or_else(Diagnostics::to_token_stream, Schema::to_token_stream) + .into() } -#[proc_macro_error] #[proc_macro_attribute] /// Path attribute macro implements OpenAPI path for the decorated function. /// @@ -1335,7 +1340,10 @@ pub fn path(attr: TokenStream, item: TokenStream) -> TokenStream { ))] let mut path_attribute = path_attribute; - let ast_fn = syn::parse::(item).unwrap_or_abort(); + let ast_fn = match syn::parse::(item) { + Ok(ast_fn) => ast_fn, + Err(error) => return error.into_compile_error().into_token_stream().into(), + }; let fn_name = &*ast_fn.sig.ident.to_string(); #[cfg(feature = "auto_into_responses")] @@ -1345,7 +1353,10 @@ pub fn path(attr: TokenStream, item: TokenStream) -> TokenStream { }; } - let mut resolved_operation = PathOperations::resolve_operation(&ast_fn); + let mut resolved_operation = match PathOperations::resolve_operation(&ast_fn) { + Ok(operation) => operation, + Err(diagnostics) => return diagnostics.into_token_stream().into(), + }; let resolved_path = PathOperations::resolve_path( &resolved_operation .as_mut() @@ -1375,7 +1386,10 @@ pub fn path(attr: TokenStream, item: TokenStream) -> TokenStream { .unwrap_or_default(); let (arguments, into_params_types, body) = - PathOperations::resolve_arguments(&ast_fn.sig.inputs, args, body); + match PathOperations::resolve_arguments(&ast_fn.sig.inputs, args, body) { + Ok(args) => args, + Err(diagnostics) => return diagnostics.into_token_stream().into(), + }; let parameters = arguments .into_iter() @@ -1408,7 +1422,6 @@ pub fn path(attr: TokenStream, item: TokenStream) -> TokenStream { .into() } -#[proc_macro_error] #[proc_macro_derive(OpenApi, attributes(openapi))] /// Generate OpenApi base object with defaults from /// project settings. @@ -1608,16 +1621,20 @@ pub fn path(attr: TokenStream, item: TokenStream) -> TokenStream { pub fn openapi(input: TokenStream) -> TokenStream { let DeriveInput { attrs, ident, .. } = syn::parse_macro_input!(input); - let openapi_attributes = openapi::parse_openapi_attrs(&attrs).expect_or_abort( - "expected #[openapi(...)] attribute to be present when used with OpenApi derive trait", - ); - - let openapi = OpenApi(openapi_attributes, ident); - - openapi.to_token_stream().into() + parse_openapi_attrs(&attrs) + .and_then(|openapi_attr| { + openapi_attr.ok_or( + syn::Error::new( + ident.span(), + "expected #[openapi(...)] attribute to be present when used with OpenApi derive trait") + ) + }) + .map_or_else(syn::Error::into_compile_error, |attrs| { + OpenApi(attrs, ident).into_token_stream() + }) + .into() } -#[proc_macro_error] #[proc_macro_derive(IntoParams, attributes(param, into_params))] /// Generate [path parameters][path_params] from struct's /// fields. @@ -1982,7 +1999,6 @@ pub fn into_params(input: TokenStream) -> TokenStream { into_params.to_token_stream().into() } -#[proc_macro_error] #[proc_macro_derive(ToResponse, attributes(response, content, to_schema))] /// Generate reusable OpenAPI response that can be used /// in [`utoipa::path`][path] or in [`OpenApi`][openapi]. @@ -2189,12 +2205,12 @@ pub fn to_response(input: TokenStream) -> TokenStream { .. } = syn::parse_macro_input!(input); - let response = ToResponse::new(attrs, &data, generics, ident); - - response.to_token_stream().into() + ToResponse::new(attrs, &data, generics, ident) + .as_ref() + .map_or_else(Diagnostics::to_token_stream, ToResponse::to_token_stream) + .into() } -#[proc_macro_error] #[proc_macro_derive( IntoResponses, attributes(response, to_schema, ref_response, to_response) @@ -2385,7 +2401,7 @@ pub fn into_responses(input: TokenStream) -> TokenStream { data, }; - into_responses.to_token_stream().into() + ToTokens::into_token_stream(into_responses).into() } /// Create OpenAPI Schema from arbitrary type. @@ -2465,7 +2481,10 @@ pub fn schema(input: TokenStream) -> TokenStream { } let schema = syn::parse_macro_input!(input as Schema); - let type_tree = TypeTree::from_type(&schema.ty); + let type_tree = match TypeTree::from_type(&schema.ty) { + Ok(type_tree) => type_tree, + Err(diagnostics) => return diagnostics.into_token_stream().into(), + }; let schema = ComponentSchema::new(ComponentSchemaProps { features: Some(vec![Feature::Inline(schema.inline.into())]), @@ -2656,7 +2675,7 @@ impl ToTokens for ExternalDocs { /// Represents OpenAPI Any value used in example and default fields. #[derive(Clone)] #[cfg_attr(feature = "debug", derive(Debug))] -pub(self) enum AnyValue { +enum AnyValue { String(TokenStream2), Json(TokenStream2), DefaultTrait { @@ -2743,37 +2762,81 @@ impl ToTokens for AnyValue { } } -trait ResultExt { - fn unwrap_or_abort(self) -> T; - fn expect_or_abort(self, message: &str) -> T; +trait OptionExt { + fn map_try(self, f: F) -> Result, E> + where + F: FnOnce(T) -> Result; + fn and_then_try(self, f: F) -> Result, E> + where + F: FnOnce(T) -> Result, E>; } -impl ResultExt for Result { - fn unwrap_or_abort(self) -> T { - match self { - Ok(value) => value, - Err(error) => abort!(error.span(), format!("{error}")), +impl OptionExt for Option { + fn map_try(self, f: F) -> Result, E> + where + F: FnOnce(T) -> Result, + { + if let Some(v) = self { + f(v).map(Some) + } else { + Ok(None) } } - fn expect_or_abort(self, message: &str) -> T { - match self { - Ok(value) => value, - Err(error) => abort!(error.span(), format!("{error}: {message}")), + fn and_then_try(self, f: F) -> Result, E> + where + F: FnOnce(T) -> Result, E>, + { + if let Some(v) = self { + match f(v) { + Ok(inner) => Ok(inner), + Err(error) => Err(error), + } + } else { + Ok(None) } } } -trait OptionExt { - fn expect_or_abort(self, message: &str) -> T; -} +trait ToTokensDiagnostics { + fn to_tokens(&self, tokens: &mut TokenStream2) -> Result<(), Diagnostics>; -impl OptionExt for Option { - fn expect_or_abort(self, message: &str) -> T { - self.unwrap_or_else(|| abort!(Span::call_site(), message)) + #[allow(unused)] + fn into_token_stream(self) -> TokenStream2 + where + Self: std::marker::Sized, + { + ToTokensDiagnostics::to_token_stream(&self) + } + + fn to_token_stream(&self) -> TokenStream2 { + let mut tokens = TokenStream2::new(); + match ToTokensDiagnostics::to_tokens(self, &mut tokens) { + Ok(_) => tokens, + Err(error_stream) => Into::::into(error_stream).into_token_stream(), + } } } +macro_rules! impl_to_tokens_diagnostics { + ( impl $(< $life:lifetime >)? ToTokensDiagnostics for $e:path { $($tt:tt)* } ) => { + impl $(< $life >)? quote::ToTokens for $e + where + Self: crate::ToTokensDiagnostics, + { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + tokens.extend(crate::ToTokensDiagnostics::to_token_stream(self)); + } + } + + impl $(< $life >)? crate::ToTokensDiagnostics for $e { + $($tt)* + } + }; +} + +use impl_to_tokens_diagnostics; + /// Parsing utils mod parse_utils { use std::fmt::Display; @@ -2788,8 +2851,6 @@ mod parse_utils { Error, Expr, LitBool, LitStr, Token, }; - use crate::ResultExt; - #[cfg_attr(feature = "debug", derive(Debug))] pub enum Value { LitStr(LitStr), @@ -2836,10 +2897,11 @@ mod parse_utils { } } - pub fn parse_next(input: ParseStream, next: impl FnOnce() -> T) -> T { - input - .parse::() - .expect_or_abort("expected equals token before value assignment"); + pub fn parse_next Result, R: Sized>( + input: ParseStream, + next: T, + ) -> Result { + input.parse::()?; next() } @@ -2955,3 +3017,125 @@ mod parse_utils { } } } + +#[derive(Debug)] +struct Diagnostics { + diagnostics: Vec, +} + +#[derive(Debug)] +struct DiangosticsInner { + span: Span, + message: Cow<'static, str>, + suggestions: Vec, +} + +#[derive(Debug)] +enum Suggestion { + Help(Cow<'static, str>), + Note(Cow<'static, str>), +} + +impl Display for Diagnostics { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message()) + } +} + +impl Display for Suggestion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Help(help) => { + let s: &str = help.borrow(); + write!(f, "help = {}", s) + } + Self::Note(note) => { + let s: &str = note.borrow(); + write!(f, "note = {}", s) + } + } + } +} + +impl Diagnostics { + fn message(&self) -> Cow<'static, str> { + self.diagnostics + .first() + .as_ref() + .map(|diagnostics| diagnostics.message.clone()) + .unwrap_or_else(|| Cow::Borrowed("")) + } + + pub fn new>>(message: S) -> Self { + Self::with_span(Span::call_site(), message) + } + + pub fn with_span>>(span: Span, message: S) -> Self { + Self { + diagnostics: vec![DiangosticsInner { + span, + message: message.into(), + suggestions: Vec::new(), + }], + } + } + + pub fn help>>(mut self, help: S) -> Self { + if let Some(diagnostics) = self.diagnostics.first_mut() { + diagnostics.suggestions.push(Suggestion::Help(help.into())); + } + + self + } + + pub fn note>>(mut self, note: S) -> Self { + if let Some(diagnostics) = self.diagnostics.first_mut() { + diagnostics.suggestions.push(Suggestion::Note(note.into())); + } + + self + } +} + +impl From for Diagnostics { + fn from(value: syn::Error) -> Self { + Self::with_span(value.span(), value.to_string()) + } +} + +impl ToTokens for Diagnostics { + fn to_tokens(&self, tokens: &mut TokenStream2) { + for diagnostics in &self.diagnostics { + let span = diagnostics.span; + let message: &str = diagnostics.message.borrow(); + + let suggestions = diagnostics + .suggestions + .iter() + .map(Suggestion::to_string) + .collect::>() + .join("\n"); + + let diagnostics = if !suggestions.is_empty() { + Cow::Owned(format!("{message}\n\n{suggestions}")) + } else { + Cow::Borrowed(message) + }; + + tokens.extend(quote_spanned! {span=> + ::core::compile_error!(#diagnostics); + }) + } + } +} + +impl Error for Diagnostics {} + +impl FromIterator for Option { + fn from_iter>(iter: T) -> Self { + iter.into_iter().reduce(|mut acc, diagnostics| { + acc.diagnostics.extend(diagnostics.diagnostics); + acc + }) + } +} diff --git a/utoipa-gen/src/openapi.rs b/utoipa-gen/src/openapi.rs index 958067f3..d6b4f90a 100644 --- a/utoipa-gen/src/openapi.rs +++ b/utoipa-gen/src/openapi.rs @@ -14,7 +14,7 @@ use quote::{format_ident, quote, quote_spanned, ToTokens}; use crate::parse_utils::Str; use crate::{ parse_utils, path::PATH_STRUCT_PREFIX, security_requirement::SecurityRequirementsAttr, Array, - ExternalDocs, ResultExt, + ExternalDocs, }; use self::info::Info; @@ -65,12 +65,13 @@ impl<'o> OpenApiAttr<'o> { } } -pub fn parse_openapi_attrs(attrs: &[Attribute]) -> Option { +pub fn parse_openapi_attrs(attrs: &[Attribute]) -> Result, Error> { attrs .iter() .filter(|attribute| attribute.path().is_ident("openapi")) - .map(|attribute| attribute.parse_args::().unwrap_or_abort()) - .reduce(|acc, item| acc.merge(item)) + .map(|attribute| attribute.parse_args::()) + .collect::, _>>() + .map(|attrs| attrs.into_iter().reduce(|acc, item| acc.merge(item))) } impl Parse for OpenApiAttr<'_> { diff --git a/utoipa-gen/src/path.rs b/utoipa-gen/src/path.rs index 989c7ec0..054dd57b 100644 --- a/utoipa-gen/src/path.rs +++ b/utoipa-gen/src/path.rs @@ -3,7 +3,6 @@ use std::ops::Deref; use std::{io::Error, str::FromStr}; use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; -use proc_macro_error::abort; use quote::{format_ident, quote, quote_spanned, ToTokens}; use syn::punctuated::Punctuated; use syn::spanned::Spanned; @@ -13,7 +12,7 @@ use syn::{Expr, ExprLit, Lit, LitStr, Type}; use crate::component::{GenericType, TypeTree}; use crate::path::request_body::RequestBody; -use crate::{parse_utils, Deprecated}; +use crate::{impl_to_tokens_diagnostics, parse_utils, Deprecated, Diagnostics}; use crate::{schema_type::SchemaType, security_requirement::SecurityRequirementsAttr, Array}; use self::response::Response; @@ -209,11 +208,12 @@ impl PathOperation { /// /// Ident must have value of http request type as lower case string such as `get`. #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))] - pub fn from_ident(ident: &Ident) -> Self { - match ident.to_string().as_str().parse::() { - Ok(operation) => operation, - Err(error) => abort!(ident.span(), format!("{error}")), - } + pub fn from_ident(ident: &Ident) -> Result { + ident + .to_string() + .as_str() + .parse::() + .map_err(|error| Diagnostics::with_span(ident.span(), error.to_string())) } } @@ -300,25 +300,28 @@ impl<'p> Path<'p> { self } -} -impl<'p> ToTokens for Path<'p> { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + fn tokens_or_diagnostics(&self, tokens: &mut TokenStream2) -> Result<(), Diagnostics> { let operation_id = self .path_attr .operation_id .clone() - .or(Some(ExprLit { - attrs: vec![], - lit: Lit::Str(LitStr::new(&self.fn_name, Span::call_site())) - }.into())) - .unwrap_or_else(|| { - abort! { - Span::call_site(), "operation id is not defined for path"; - help = r###"Try to define it in #[utoipa::path(operation_id = {})]"###, &self.fn_name; - help = "Did you define the #[utoipa::path(...)] over function?" + .or(Some( + ExprLit { + attrs: vec![], + lit: Lit::Str(LitStr::new(&self.fn_name, Span::call_site())), } - }); + .into(), + )) + .ok_or_else(|| { + Diagnostics::new("operation id is not defined for path") + .help(format!( + "Try to define it in #[utoipa::path(operation_id = {})]", + &self.fn_name + )) + .help("Did you define the #[utoipa::path(...)] over function?") + })?; + let tags = if !self.path_attr.tags.is_empty() { let tags = self.path_attr.tags.as_slice().iter().collect::>(); Some(quote! { @@ -338,20 +341,17 @@ impl<'p> ToTokens for Path<'p> { .path_operation .as_ref() .or(self.path_operation.as_ref()) - .unwrap_or_else(|| { - #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))] - let help = - Some("Did you forget to define operation path attribute macro e.g #[get(...)]"); + .ok_or_else(|| { + let diagnostics = Diagnostics::new("path operation is not defined for path") + .help("Did you forget to define it, e.g. #[utoipa::path(get, ...)]"); - #[cfg(not(any(feature = "actix_extras", feature = "rocket_extras")))] - let help = None::<&str>; + #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))] + let diagnostics = diagnostics.help( + "Did you forget to define operation path attribute macro e.g #[get(...)]", + ); - abort! { - Span::call_site(), "path operation is not defined for path"; - help = "Did you forget to define it in #[utoipa::path(get,...)]"; - help =? help - } - }); + diagnostics + })?; let path = self .path_attr @@ -359,20 +359,17 @@ impl<'p> ToTokens for Path<'p> { .as_ref() .map(|path| path.to_token_stream()) .or(Some(self.path.to_token_stream())) - .unwrap_or_else(|| { - #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))] - let help = - Some("Did you forget to define operation path attribute macro e.g #[get(...)]"); + .ok_or_else(|| { + let diagnostics = Diagnostics::new("path is not defined for path") + .help(r#"Did you forget to define it in #[utoipa::path(path = "...")]"#); - #[cfg(not(any(feature = "actix_extras", feature = "rocket_extras")))] - let help = None::<&str>; + #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))] + let diagnostics = diagnostics.help( + "Did you forget to define operation path attribute macro e.g #[get(...)]", + ); - abort! { - Span::call_site(), "path is not defined for path"; - help = r#"Did you forget to define it in #[utoipa::path(path = "...")]"#; - help =? help - } - }); + diagnostics + })?; let path_with_context_path = self .path_attr @@ -455,6 +452,16 @@ impl<'p> ToTokens for Path<'p> { } } }); + + Ok(()) + } +} + +impl_to_tokens_diagnostics! { + impl<'p> ToTokensDiagnostics for Path<'p> { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) -> Result<(), Diagnostics> { + self.tokens_or_diagnostics(tokens) + } } } @@ -557,7 +564,7 @@ struct InlineType<'i> { impl InlineType<'_> { /// Get's the underlying [`syn::Type`] as [`TypeTree`]. - fn as_type_tree(&self) -> TypeTree { + fn as_type_tree(&self) -> Result { TypeTree::from_type(&self.ty) } } diff --git a/utoipa-gen/src/path/parameter.rs b/utoipa-gen/src/path/parameter.rs index 09a29b6e..f8ed6c03 100644 --- a/utoipa-gen/src/path/parameter.rs +++ b/utoipa-gen/src/path/parameter.rs @@ -1,7 +1,6 @@ use std::{borrow::Cow, fmt::Display}; use proc_macro2::{Ident, Span, TokenStream}; -use proc_macro_error::abort; use quote::{quote, quote_spanned, ToTokens}; use syn::{ parenthesized, @@ -20,7 +19,7 @@ use crate::{ }, ComponentSchema, }, - parse_utils, Required, + impl_to_tokens_diagnostics, parse_utils, Diagnostics, Required, ToTokensDiagnostics, }; use super::InlineType; @@ -143,8 +142,8 @@ struct ParameterSchema<'p> { features: Vec, } -impl ToTokens for ParameterSchema<'_> { - fn to_tokens(&self, tokens: &mut TokenStream) { +impl ToTokensDiagnostics for ParameterSchema<'_> { + fn to_tokens(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> { let mut to_tokens = |param_schema, required| { tokens.extend(quote! { .schema(Some(#param_schema)).required(#required) }); }; @@ -158,7 +157,7 @@ impl ToTokens for ParameterSchema<'_> { ParameterType::External(type_tree) => { let required: Required = (!type_tree.is_option()).into(); - to_tokens( + Ok(to_tokens( ComponentSchema::new(component::ComponentSchemaProps { type_tree, features: Some(self.features.clone()), @@ -167,16 +166,16 @@ impl ToTokens for ParameterSchema<'_> { object_name: "", }), required, - ) + )) } ParameterType::Parsed(inline_type) => { - let type_tree = inline_type.as_type_tree(); + let type_tree = inline_type.as_type_tree()?; let required: Required = (!type_tree.is_option()).into(); let mut schema_features = Vec::::new(); schema_features.clone_from(&self.features); schema_features.push(Feature::Inline(inline_type.is_inline.into())); - to_tokens( + Ok(to_tokens( ComponentSchema::new(component::ComponentSchemaProps { type_tree: &type_tree, features: Some(schema_features), @@ -185,7 +184,7 @@ impl ToTokens for ParameterSchema<'_> { object_name: "", }), required, - ) + )) } } } @@ -342,8 +341,8 @@ impl ParameterFeatures { impl_into_inner!(ParameterFeatures); -impl ToTokens for ValueParameter<'_> { - fn to_tokens(&self, tokens: &mut TokenStream) { +impl ValueParameter<'_> { + fn tokens_or_diagnostics(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> { let name = &*self.name; tokens.extend(quote! { utoipa::openapi::path::ParameterBuilder::from(utoipa::openapi::path::Parameter::new(#name)) @@ -356,16 +355,24 @@ impl ToTokens for ValueParameter<'_> { tokens.extend(param_features.to_token_stream()); if !schema_features.is_empty() && self.parameter_schema.is_none() { - abort!( - Span::call_site(), - "Missing `parameter_type` attribute, cannot define schema features without it."; - help = "See docs for more details " - + return Err( + Diagnostics::new("Missing `parameter_type` attribute, cannot define schema features without it.") + .help("See docs for more details ") ); } if let Some(parameter_schema) = &self.parameter_schema { - parameter_schema.to_tokens(tokens); + parameter_schema.to_tokens(tokens)?; + } + + Ok(()) + } +} + +impl_to_tokens_diagnostics! { + impl ToTokensDiagnostics for ValueParameter<'_> { + fn to_tokens(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> { + self.tokens_or_diagnostics(tokens) } } } diff --git a/utoipa-gen/src/path/request_body.rs b/utoipa-gen/src/path/request_body.rs index 910cefd5..22e044b4 100644 --- a/utoipa-gen/src/path/request_body.rs +++ b/utoipa-gen/src/path/request_body.rs @@ -6,7 +6,7 @@ use syn::{parenthesized, parse::Parse, token::Paren, Error, Token}; use crate::component::features::Inline; use crate::component::ComponentSchema; -use crate::{parse_utils, AnyValue, Array, Required}; +use crate::{impl_to_tokens_diagnostics, parse_utils, AnyValue, Array, Diagnostics, Required}; use super::example::Example; use super::{PathType, PathTypeTree}; @@ -25,13 +25,13 @@ pub enum RequestBody<'r> { impl ToTokens for RequestBody<'_> { fn to_tokens(&self, tokens: &mut TokenStream2) { match self { - Self::Parsed(parsed) => parsed.to_tokens(tokens), + Self::Parsed(parsed) => ToTokens::to_tokens(parsed, tokens), #[cfg(any( feature = "actix_extras", feature = "rocket_extras", feature = "axum_extras" ))] - Self::Ext(ext) => ext.to_tokens(tokens), + Self::Ext(ext) => ToTokens::to_tokens(ext, tokens), } } } @@ -157,15 +157,15 @@ impl Parse for RequestBodyAttr<'_> { } } -impl ToTokens for RequestBodyAttr<'_> { - fn to_tokens(&self, tokens: &mut TokenStream2) { +impl RequestBodyAttr<'_> { + fn tokens_or_diagnostics(&self, tokens: &mut TokenStream2) -> Result<(), Diagnostics> { if let Some(body_type) = &self.content { let media_type_schema = match body_type { PathType::Ref(ref_type) => quote! { utoipa::openapi::schema::Ref::new(#ref_type) }, PathType::MediaType(body_type) => { - let type_tree = body_type.as_type_tree(); + let type_tree = body_type.as_type_tree()?; ComponentSchema::new(crate::component::ComponentSchemaProps { type_tree: &type_tree, features: Some(vec![Inline::from(body_type.is_inline).into()]), @@ -208,7 +208,7 @@ impl ToTokens for RequestBodyAttr<'_> { }); } PathType::MediaType(body_type) => { - let type_tree = body_type.as_type_tree(); + let type_tree = body_type.as_type_tree()?; let required: Required = (!type_tree.is_option()).into(); let content_type = match &self.content_type { Some(content_type) => content_type.to_token_stream(), @@ -235,6 +235,16 @@ impl ToTokens for RequestBodyAttr<'_> { }) } - tokens.extend(quote! { .build() }) + tokens.extend(quote! { .build() }); + + Ok(()) + } +} + +impl_to_tokens_diagnostics! { + impl ToTokensDiagnostics for RequestBodyAttr<'_> { + fn to_tokens(&self, tokens: &mut TokenStream2) -> Result<(), Diagnostics> { + self.tokens_or_diagnostics(tokens) + } } } diff --git a/utoipa-gen/src/path/response.rs b/utoipa-gen/src/path/response.rs index a4dc8489..b013d754 100644 --- a/utoipa-gen/src/path/response.rs +++ b/utoipa-gen/src/path/response.rs @@ -11,8 +11,11 @@ use syn::{ }; use crate::{ - component::{features::Inline, ComponentSchema, TypeTree}, - parse_utils, AnyValue, Array, ResultExt, + component::{ + features::{impl_merge, Inline}, + ComponentSchema, TypeTree, + }, + impl_to_tokens_diagnostics, parse_utils, AnyValue, Array, Diagnostics, }; use super::{example::Example, status::STATUS_CODES, InlineType, PathType, PathTypeTree}; @@ -251,8 +254,8 @@ impl<'r> ResponseValue<'r> { } } -impl ToTokens for ResponseTuple<'_> { - fn to_tokens(&self, tokens: &mut TokenStream2) { +impl ResponseTuple<'_> { + fn tokens_or_diagnostics(&self, tokens: &mut TokenStream2) -> Result<(), Diagnostics> { match self.inner.as_ref().unwrap() { ResponseTupleInner::Ref(res) => { let path = &res.ty; @@ -275,14 +278,14 @@ impl ToTokens for ResponseTuple<'_> { let create_content = |path_type: &PathType, example: &Option, examples: &Option>| - -> TokenStream2 { + -> Result { let content_schema = match path_type { PathType::Ref(ref_type) => quote! { utoipa::openapi::schema::Ref::new(#ref_type) } .to_token_stream(), PathType::MediaType(ref path_type) => { - let type_tree = path_type.as_type_tree(); + let type_tree = path_type.as_type_tree()?; ComponentSchema::new(crate::component::ComponentSchemaProps { type_tree: &type_tree, @@ -317,13 +320,13 @@ impl ToTokens for ResponseTuple<'_> { )) } - quote! { + Ok(quote! { #content.build() - } + }) }; if let Some(response_type) = &val.response_type { - let content = create_content(response_type, &val.example, &val.examples); + let content = create_content(response_type, &val.example, &val.examples)?; if let Some(content_types) = val.content_type.as_ref() { content_types.iter().for_each(|content_type| { @@ -339,14 +342,14 @@ impl ToTokens for ResponseTuple<'_> { }); } PathType::MediaType(path_type) => { - let type_tree = path_type.as_type_tree(); + let type_tree = path_type.as_type_tree()?; let default_type = type_tree.get_default_content_type(); tokens.extend(quote! { .content(#default_type, #content) }) } PathType::InlineSchema(_, ty) => { - let type_tree = TypeTree::from_type(ty); + let type_tree = TypeTree::from_type(ty)?; let default_type = type_tree.get_default_content_type(); tokens.extend(quote! { .content(#default_type, #content) @@ -359,9 +362,13 @@ impl ToTokens for ResponseTuple<'_> { val.content .iter() .map(|Content(content_type, body, example, examples)| { - let content = create_content(body, example, examples); - (Cow::Borrowed(&**content_type), content) + match create_content(body, example, examples) { + Ok(content) => Ok((Cow::Borrowed(&**content_type), content)), + Err(diagnostics) => Err(diagnostics), + } }) + .collect::, Diagnostics>>()? + .into_iter() .for_each(|(content_type, content)| { tokens.extend(quote! { .content(#content_type, #content) }) }); @@ -376,18 +383,30 @@ impl ToTokens for ResponseTuple<'_> { tokens.extend(quote! { .build() }); } } + + Ok(()) + } +} + +impl_to_tokens_diagnostics! { + impl ToTokensDiagnostics for ResponseTuple<'_> { + fn to_tokens(&self, tokens: &mut TokenStream2) -> Result<(), Diagnostics> { + self.tokens_or_diagnostics(tokens) + } } } trait DeriveResponseValue: Parse { fn merge_from(self, other: Self) -> Self; - fn from_attributes(attributes: &[Attribute]) -> Option { - attributes + fn from_attributes(attributes: &[Attribute]) -> Result, Diagnostics> { + Ok(attributes .iter() .filter(|attribute| attribute.path().get_ident().unwrap() == "response") - .map(|attribute| attribute.parse_args::().unwrap_or_abort()) - .reduce(|acc, item| acc.merge_from(item)) + .map(|attribute| attribute.parse_args::().map_err(Diagnostics::from)) + .collect::, Diagnostics>>()? + .into_iter() + .reduce(|acc, item| acc.merge_from(item))) } } @@ -831,11 +850,11 @@ impl Parse for Header { } } -impl ToTokens for Header { - fn to_tokens(&self, tokens: &mut TokenStream2) { +impl Header { + fn tokens_or_diagnostics(&self, tokens: &mut TokenStream2) -> Result<(), Diagnostics> { if let Some(header_type) = &self.value_type { // header property with custom type - let type_tree = header_type.as_type_tree(); + let type_tree = header_type.as_type_tree()?; let media_type_schema = ComponentSchema::new(crate::component::ComponentSchemaProps { type_tree: &type_tree, @@ -862,7 +881,17 @@ impl ToTokens for Header { }) } - tokens.extend(quote! { .build() }) + tokens.extend(quote! { .build() }); + + Ok(()) + } +} + +impl_to_tokens_diagnostics! { + impl ToTokensDiagnostics for Header { + fn to_tokens(&self, tokens: &mut TokenStream2) -> Result<(), Diagnostics> { + self.tokens_or_diagnostics(tokens) + } } } diff --git a/utoipa-gen/src/path/response/derive.rs b/utoipa-gen/src/path/response/derive.rs index 8c093246..88a27642 100644 --- a/utoipa-gen/src/path/response/derive.rs +++ b/utoipa-gen/src/path/response/derive.rs @@ -2,7 +2,6 @@ use std::borrow::Cow; use std::{iter, mem}; use proc_macro2::{Ident, Span, TokenStream}; -use proc_macro_error::{abort, emit_error}; use quote::{quote, ToTokens}; use syn::parse::ParseStream; use syn::punctuated::Punctuated; @@ -16,7 +15,7 @@ use syn::{ use crate::component::schema::{EnumSchema, NamedStructSchema}; use crate::doc_comment::CommentAttributes; use crate::path::{InlineType, PathType}; -use crate::{parse_utils, Array, ResultExt}; +use crate::{impl_to_tokens_diagnostics, parse_utils, Array, Diagnostics, OptionExt}; use super::{ Content, DeriveIntoResponsesValue, DeriveResponseValue, DeriveResponsesAttributes, @@ -38,11 +37,11 @@ impl<'r> ToResponse<'r> { data: &'r Data, generics: Generics, ident: Ident, - ) -> ToResponse<'r> { + ) -> Result, Diagnostics> { let response = match &data { Data::Struct(struct_value) => match &struct_value.fields { Fields::Named(fields) => { - ToResponseNamedStructResponse::new(&attributes, &ident, &fields.named).0 + ToResponseNamedStructResponse::new(&attributes, &ident, &fields.named)?.0 } Fields::Unnamed(fields) => { let field = fields @@ -51,24 +50,29 @@ impl<'r> ToResponse<'r> { .next() .expect("Unnamed struct must have 1 field"); - ToResponseUnnamedStructResponse::new(&attributes, &field.ty, &field.attrs).0 + ToResponseUnnamedStructResponse::new(&attributes, &field.ty, &field.attrs)?.0 } - Fields::Unit => ToResponseUnitStructResponse::new(&attributes).0, + Fields::Unit => ToResponseUnitStructResponse::new(&attributes)?.0, }, Data::Enum(enum_value) => { - EnumResponse::new(&ident, &enum_value.variants, &attributes).0 + EnumResponse::new(&ident, &enum_value.variants, &attributes)?.0 + } + Data::Union(_) => { + return Err(Diagnostics::with_span( + ident.span(), + "`ToResponse` does not support `Union` type", + )) } - Data::Union(_) => abort!(ident, "`ToResponse` does not support `Union` type"), }; let lifetime = Lifetime::new(ToResponse::LIFETIME, Span::call_site()); - Self { + Ok(Self { ident, lifetime, generics, response, - } + }) } } @@ -106,13 +110,13 @@ pub struct IntoResponses { pub ident: Ident, } -impl ToTokens for IntoResponses { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { +impl IntoResponses { + fn tokens_or_diagnostics(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> { let responses = match &self.data { Data::Struct(struct_value) => match &struct_value.fields { Fields::Named(fields) => { let response = - NamedStructResponse::new(&self.attributes, &self.ident, &fields.named).0; + NamedStructResponse::new(&self.attributes, &self.ident, &fields.named)?.0; let status = &response.status_code; Array::from_iter(iter::once(quote!((#status, #response)))) @@ -125,13 +129,13 @@ impl ToTokens for IntoResponses { .expect("Unnamed struct must have 1 field"); let response = - UnnamedStructResponse::new(&self.attributes, &field.ty, &field.attrs).0; + UnnamedStructResponse::new(&self.attributes, &field.ty, &field.attrs)?.0; let status = &response.status_code; Array::from_iter(iter::once(quote!((#status, #response)))) } Fields::Unit => { - let response = UnitStructResponse::new(&self.attributes).0; + let response = UnitStructResponse::new(&self.attributes)?.0; let status = &response.status_code; Array::from_iter(iter::once(quote!((#status, #response)))) @@ -141,25 +145,38 @@ impl ToTokens for IntoResponses { .variants .iter() .map(|variant| match &variant.fields { - Fields::Named(fields) => { - NamedStructResponse::new(&variant.attrs, &variant.ident, &fields.named).0 - } + Fields::Named(fields) => Ok(NamedStructResponse::new( + &variant.attrs, + &variant.ident, + &fields.named, + )? + .0), Fields::Unnamed(fields) => { let field = fields .unnamed .iter() .next() .expect("Unnamed enum variant must have 1 field"); - UnnamedStructResponse::new(&variant.attrs, &field.ty, &field.attrs).0 + match UnnamedStructResponse::new(&variant.attrs, &field.ty, &field.attrs) { + Ok(response) => Ok(response.0), + Err(diagnostics) => Err(diagnostics), + } } - Fields::Unit => UnitStructResponse::new(&variant.attrs).0, + Fields::Unit => Ok(UnitStructResponse::new(&variant.attrs)?.0), }) + .collect::, Diagnostics>>()? + .iter() .map(|response| { let status = &response.status_code; quote!((#status, utoipa::openapi::RefOr::from(#response))) }) - .collect::>(), - Data::Union(_) => abort!(self.ident, "`IntoResponses` does not support `Union` type"), + .collect(), + Data::Union(_) => { + return Err(Diagnostics::with_span( + self.ident.span(), + "`IntoResponses` does not support `Union` type", + )) + } }; let ident = &self.ident; @@ -171,15 +188,25 @@ impl ToTokens for IntoResponses { None }; tokens.extend(quote!{ - impl #impl_generics utoipa::IntoResponses for #ident #ty_generics #where_clause { - fn responses() -> std::collections::BTreeMap> { - utoipa::openapi::response::ResponsesBuilder::new() - #responses - .build() - .into() + impl #impl_generics utoipa::IntoResponses for #ident #ty_generics #where_clause { + fn responses() -> std::collections::BTreeMap> { + utoipa::openapi::response::ResponsesBuilder::new() + #responses + .build() + .into() + } } - } - }) + }); + + Ok(()) + } +} + +impl_to_tokens_diagnostics! { + impl ToTokensDiagnostics for IntoResponses { + fn to_tokens(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> { + self.tokens_or_diagnostics(tokens) + } } } @@ -206,14 +233,16 @@ trait Response { fn validate_attributes<'a, I: IntoIterator>( attributes: I, - validate: impl Fn(&Attribute) -> (bool, &'static str), - ) { - for attribute in attributes { - let (valid, message) = validate(attribute); + validate: impl Fn(&Attribute) -> (bool, &'static str) + 'a, + ) -> impl Iterator { + attributes.into_iter().filter_map(move |attribute| { + let (valid, error_message) = validate(attribute); if !valid { - emit_error!(attribute, message) + Some(Diagnostics::with_span(attribute.span(), error_message)) + } else { + None } - } + }) } } @@ -222,7 +251,11 @@ struct UnnamedStructResponse<'u>(ResponseTuple<'u>); impl Response for UnnamedStructResponse<'_> {} impl<'u> UnnamedStructResponse<'u> { - fn new(attributes: &[Attribute], ty: &'u Type, inner_attributes: &[Attribute]) -> Self { + fn new( + attributes: &[Attribute], + ty: &'u Type, + inner_attributes: &[Attribute], + ) -> Result { let is_inline = inner_attributes .iter() .any(|attribute| attribute.path().get_ident().unwrap() == "to_schema"); @@ -234,12 +267,9 @@ impl<'u> UnnamedStructResponse<'u> { .any(|attribute| attribute.path().get_ident().unwrap() == "to_response"); if is_inline && (ref_response || to_response) { - abort!( - ty.span(), - "Attribute `to_schema` cannot be used with `ref_response` and `to_response` attribute" - ) + return Err(Diagnostics::with_span(ty.span(), "Attribute `to_schema` cannot be used with `ref_response` and `to_response` attribute")); } - let mut derive_value = DeriveIntoResponsesValue::from_attributes(attributes) + let mut derive_value = DeriveIntoResponsesValue::from_attributes(attributes)? .expect("`IntoResponses` must have `#[response(...)]` attribute"); let description = { let s = CommentAttributes::from_attributes(attributes).as_formatted_string(); @@ -247,7 +277,7 @@ impl<'u> UnnamedStructResponse<'u> { }; let status_code = mem::take(&mut derive_value.status); - match (ref_response, to_response) { + let response = match (ref_response, to_response) { (false, false) => Self( ( status_code, @@ -274,12 +304,14 @@ impl<'u> UnnamedStructResponse<'u> { status_code, }), (true, true) => { - abort!( + return Err(Diagnostics::with_span( ty.span(), - "Cannot define `ref_response` and `to_response` attribute simultaneously" - ); + "Cannot define `ref_response` and `to_response` attribute simultaneously", + )) } - } + }; + + Ok(response) } } @@ -288,21 +320,29 @@ struct NamedStructResponse<'n>(ResponseTuple<'n>); impl Response for NamedStructResponse<'_> {} impl NamedStructResponse<'_> { - fn new(attributes: &[Attribute], ident: &Ident, fields: &Punctuated) -> Self { - Self::validate_attributes(attributes, Self::has_no_field_attributes); - Self::validate_attributes( - fields.iter().flat_map(|field| &field.attrs), - Self::has_no_field_attributes, - ); - - let mut derive_value = DeriveIntoResponsesValue::from_attributes(attributes) + fn new( + attributes: &[Attribute], + ident: &Ident, + fields: &Punctuated, + ) -> Result { + if let Some(diagnostics) = + Self::validate_attributes(attributes, Self::has_no_field_attributes) + .chain(Self::validate_attributes( + fields.iter().flat_map(|field| &field.attrs), + Self::has_no_field_attributes, + )) + .collect::>() + { + return Err(diagnostics); + } + + let mut derive_value = DeriveIntoResponsesValue::from_attributes(attributes)? .expect("`IntoResponses` must have `#[response(...)]` attribute"); let description = { let s = CommentAttributes::from_attributes(attributes).as_formatted_string(); parse_utils::Value::LitStr(LitStr::new(&s, Span::call_site())) }; let status_code = mem::take(&mut derive_value.status); - let inline_schema = NamedStructSchema { attributes, fields, @@ -316,7 +356,7 @@ impl NamedStructResponse<'_> { let ty = Self::to_type(ident); - Self( + Ok(Self( ( status_code, ResponseValue::from_derive_into_responses_value(derive_value, description) @@ -326,7 +366,7 @@ impl NamedStructResponse<'_> { ))), ) .into(), - ) + )) } } @@ -335,10 +375,15 @@ struct UnitStructResponse<'u>(ResponseTuple<'u>); impl Response for UnitStructResponse<'_> {} impl UnitStructResponse<'_> { - fn new(attributes: &[Attribute]) -> Self { - Self::validate_attributes(attributes, Self::has_no_field_attributes); + fn new(attributes: &[Attribute]) -> Result { + if let Some(diagnostics) = + Self::validate_attributes(attributes, Self::has_no_field_attributes) + .collect::>() + { + return Err(diagnostics); + } - let mut derive_value = DeriveIntoResponsesValue::from_attributes(attributes) + let mut derive_value = DeriveIntoResponsesValue::from_attributes(attributes)? .expect("`IntoResponses` must have `#[response(...)]` attribute"); let status_code = mem::take(&mut derive_value.status); let description = { @@ -346,13 +391,13 @@ impl UnitStructResponse<'_> { parse_utils::Value::LitStr(LitStr::new(&s, Span::call_site())) }; - Self( + Ok(Self( ( status_code, ResponseValue::from_derive_into_responses_value(derive_value, description), ) .into(), - ) + )) } } @@ -361,14 +406,23 @@ struct ToResponseNamedStructResponse<'p>(ResponseTuple<'p>); impl Response for ToResponseNamedStructResponse<'_> {} impl<'p> ToResponseNamedStructResponse<'p> { - fn new(attributes: &[Attribute], ident: &Ident, fields: &Punctuated) -> Self { - Self::validate_attributes(attributes, Self::has_no_field_attributes); - Self::validate_attributes( - fields.iter().flat_map(|field| &field.attrs), - Self::has_no_field_attributes, - ); - - let derive_value = DeriveToResponseValue::from_attributes(attributes); + fn new( + attributes: &[Attribute], + ident: &Ident, + fields: &Punctuated, + ) -> Result { + if let Some(diagnostics) = + Self::validate_attributes(attributes, Self::has_no_field_attributes) + .chain(Self::validate_attributes( + fields.iter().flat_map(|field| &field.attrs), + Self::has_no_field_attributes, + )) + .collect::>() + { + return Err(diagnostics); + } + + let derive_value = DeriveToResponseValue::from_attributes(attributes)?; let description = { let s = CommentAttributes::from_attributes(attributes).as_formatted_string(); parse_utils::Value::LitStr(LitStr::new(&s, Span::call_site())) @@ -393,7 +447,7 @@ impl<'p> ToResponseNamedStructResponse<'p> { }); response_value.response_type = Some(response_type); - Self(response_value.into()) + Ok(Self(response_value.into())) } } @@ -402,18 +456,27 @@ struct ToResponseUnnamedStructResponse<'c>(ResponseTuple<'c>); impl Response for ToResponseUnnamedStructResponse<'_> {} impl<'u> ToResponseUnnamedStructResponse<'u> { - fn new(attributes: &[Attribute], ty: &'u Type, inner_attributes: &[Attribute]) -> Self { - Self::validate_attributes(attributes, Self::has_no_field_attributes); - Self::validate_attributes(inner_attributes, |attribute| { - const ERROR: &str = + fn new( + attributes: &[Attribute], + ty: &'u Type, + inner_attributes: &[Attribute], + ) -> Result { + if let Some(diagnostics) = + Self::validate_attributes(attributes, Self::has_no_field_attributes) + .chain(Self::validate_attributes(inner_attributes, |attribute| { + const ERROR: &str = "Unexpected attribute, `content` is only supported on unnamed field enum variant"; - if attribute.path().get_ident().unwrap() == "content" { - (false, ERROR) - } else { - (true, ERROR) - } - }); - let derive_value = DeriveToResponseValue::from_attributes(attributes); + if attribute.path().get_ident().unwrap() == "content" { + (false, ERROR) + } else { + (true, ERROR) + } + })) + .collect::>() + { + return Err(diagnostics); + } + let derive_value = DeriveToResponseValue::from_attributes(attributes)?; let description = { let s = CommentAttributes::from_attributes(attributes).as_formatted_string(); parse_utils::Value::LitStr(LitStr::new(&s, Span::call_site())) @@ -432,7 +495,7 @@ impl<'u> ToResponseUnnamedStructResponse<'u> { is_inline, })); - Self(response_value.into()) + Ok(Self(response_value.into())) } } @@ -451,12 +514,17 @@ impl<'r> EnumResponse<'r> { ident: &Ident, variants: &'r Punctuated, attributes: &[Attribute], - ) -> Self { - Self::validate_attributes(attributes, Self::has_no_field_attributes); - Self::validate_attributes( - variants.iter().flat_map(|variant| &variant.attrs), - Self::has_no_field_attributes, - ); + ) -> Result { + if let Some(diagnostics) = + Self::validate_attributes(attributes, Self::has_no_field_attributes) + .chain(Self::validate_attributes( + variants.iter().flat_map(|variant| &variant.attrs), + Self::has_no_field_attributes, + )) + .collect::>() + { + return Err(diagnostics); + } let ty = Self::to_type(ident); let description = { @@ -467,10 +535,12 @@ impl<'r> EnumResponse<'r> { let variants_content = variants .into_iter() .map(Self::parse_variant_attributes) + .collect::, Diagnostics>>()? + .into_iter() .filter_map(Self::to_content); let content: Punctuated = Punctuated::from_iter(variants_content); - let derive_value = DeriveToResponseValue::from_attributes(attributes); + let derive_value = DeriveToResponseValue::from_attributes(attributes)?; if let Some(derive_value) = &derive_value { if (!content.is_empty() && derive_value.example.is_some()) || (!content.is_empty() && derive_value.examples.is_some()) @@ -481,11 +551,11 @@ impl<'r> EnumResponse<'r> { .map(|(_, ident)| ident) .or_else(|| derive_value.examples.as_ref().map(|(_, ident)| ident)) .expect("Expected `example` or `examples` to be present"); - abort! { - ident, - "Enum with `#[content]` attribute in variant cannot have enum level `example` or `examples` defined"; - help = "Try defining `{}` on the enum variant", ident.to_string(), - } + return Err( + Diagnostics::with_span(ident.span(), + "Enum with `#[content]` attribute in variant cannot have enum level `example` or `examples` defined") + .help(format!("Try defining `{}` on the enum variant", ident)) + ); } } @@ -495,7 +565,7 @@ impl<'r> EnumResponse<'r> { }); response_value.response_type = if content.is_empty() { let inline_schema = - EnumSchema::new(Cow::Owned(ident.to_string()), variants, attributes); + EnumSchema::new(Cow::Owned(ident.to_string()), variants, attributes)?; Some(PathType::InlineSchema( inline_schema.into_token_stream(), @@ -506,34 +576,38 @@ impl<'r> EnumResponse<'r> { }; response_value.content = content; - Self(response_value.into()) + Ok(Self(response_value.into())) } - fn parse_variant_attributes(variant: &Variant) -> VariantAttributes { + fn parse_variant_attributes(variant: &Variant) -> Result { let variant_derive_response_value = - DeriveToResponseValue::from_attributes(variant.attrs.as_slice()); + DeriveToResponseValue::from_attributes(variant.attrs.as_slice())?; // named enum variant should not have field attributes if let Fields::Named(named_fields) = &variant.fields { - Self::validate_attributes( + if let Some(diagnostics) = Self::validate_attributes( named_fields.named.iter().flat_map(|field| &field.attrs), Self::has_no_field_attributes, ) + .collect::>() + { + return Err(diagnostics); + } }; let field = variant.fields.iter().next(); - let content_type = field.and_then(|field| { + let content_type = field.and_then_try(|field| { field .attrs .iter() .find(|attribute| attribute.path().get_ident().unwrap() == "content") - .map(|attribute| { + .map_try(|attribute| { attribute .parse_args_with(|input: ParseStream| input.parse::()) - .unwrap_or_abort() + .map(|content| content.value()) + .map_err(Diagnostics::from) }) - .map(|content| content.value()) - }); + })?; let is_inline = field .map(|field| { @@ -544,11 +618,11 @@ impl<'r> EnumResponse<'r> { }) .unwrap_or(false); - VariantAttributes { + Ok(VariantAttributes { type_and_content: field.map(|field| &field.ty).zip(content_type), derive_value: variant_derive_response_value, is_inline, - } + }) } fn to_content( @@ -586,10 +660,15 @@ struct ToResponseUnitStructResponse<'u>(ResponseTuple<'u>); impl Response for ToResponseUnitStructResponse<'_> {} impl ToResponseUnitStructResponse<'_> { - fn new(attributes: &[Attribute]) -> Self { - Self::validate_attributes(attributes, Self::has_no_field_attributes); + fn new(attributes: &[Attribute]) -> Result { + if let Some(diagnostics) = + Self::validate_attributes(attributes, Self::has_no_field_attributes) + .collect::>() + { + return Err(diagnostics); + } - let derive_value = DeriveToResponseValue::from_attributes(attributes); + let derive_value = DeriveToResponseValue::from_attributes(attributes)?; let description = { let s = CommentAttributes::from_attributes(attributes).as_formatted_string(); parse_utils::Value::LitStr(LitStr::new(&s, Span::call_site())) @@ -599,6 +678,6 @@ impl ToResponseUnitStructResponse<'_> { description, }); - Self(response_value.into()) + Ok(Self(response_value.into())) } } diff --git a/utoipa-gen/src/schema_type.rs b/utoipa-gen/src/schema_type.rs index d70d319e..0c8a6a3e 100644 --- a/utoipa-gen/src/schema_type.rs +++ b/utoipa-gen/src/schema_type.rs @@ -1,8 +1,10 @@ use proc_macro2::TokenStream; -use proc_macro_error::abort_call_site; use quote::{quote, ToTokens}; +use syn::spanned::Spanned; use syn::{parse::Parse, Error, Ident, LitStr, Path}; +use crate::{impl_to_tokens_diagnostics, Diagnostics, ToTokensDiagnostics}; + /// Tokenizes OpenAPI data type correctly according to the Rust type pub struct SchemaType<'a>(pub &'a syn::Path); @@ -137,53 +139,14 @@ impl SchemaType<'_> { pub fn is_byte(&self) -> bool { matches!(&*self.last_segment_to_string(), "u8") } -} - -#[inline] -fn is_primitive(name: &str) -> bool { - matches!( - name, - "String" - | "str" - | "char" - | "bool" - | "usize" - | "u8" - | "u16" - | "u32" - | "u64" - | "u128" - | "isize" - | "i8" - | "i16" - | "i32" - | "i64" - | "i128" - | "f32" - | "f64" - ) -} -#[inline] -#[cfg(feature = "chrono")] -fn is_primitive_chrono(name: &str) -> bool { - matches!( - name, - "DateTime" | "Date" | "NaiveDate" | "NaiveTime" | "Duration" | "NaiveDateTime" - ) -} - -#[inline] -#[cfg(any(feature = "decimal", feature = "decimal_float"))] -fn is_primitive_rust_decimal(name: &str) -> bool { - matches!(name, "Decimal") -} - -impl ToTokens for SchemaType<'_> { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - let last_segment = self.0.segments.last().unwrap_or_else(|| { - abort_call_site!("expected there to be at least one segment in the path") - }); + fn tokens_or_diagnostics(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> { + let last_segment = self.0.segments.last().ok_or_else(|| { + Diagnostics::with_span( + self.0.span(), + "schema type should have at least one segment in the path", + ) + })?; let name = &*last_segment.ident.to_string(); match name { @@ -228,6 +191,56 @@ impl ToTokens for SchemaType<'_> { tokens.extend(quote! { utoipa::openapi::SchemaType::String }) } _ => tokens.extend(quote! { utoipa::openapi::SchemaType::Object }), + }; + + Ok(()) + } +} + +#[inline] +fn is_primitive(name: &str) -> bool { + matches!( + name, + "String" + | "str" + | "char" + | "bool" + | "usize" + | "u8" + | "u16" + | "u32" + | "u64" + | "u128" + | "isize" + | "i8" + | "i16" + | "i32" + | "i64" + | "i128" + | "f32" + | "f64" + ) +} + +#[inline] +#[cfg(feature = "chrono")] +fn is_primitive_chrono(name: &str) -> bool { + matches!( + name, + "DateTime" | "Date" | "NaiveDate" | "NaiveTime" | "Duration" | "NaiveDateTime" + ) +} + +#[inline] +#[cfg(any(feature = "decimal", feature = "decimal_float"))] +fn is_primitive_rust_decimal(name: &str) -> bool { + matches!(name, "Decimal") +} + +impl_to_tokens_diagnostics! { + impl ToTokensDiagnostics for SchemaType<'_> { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) -> Result<(), Diagnostics> { + self.tokens_or_diagnostics(tokens) } } } @@ -266,7 +279,11 @@ impl Parse for SchemaFormat<'_> { impl ToTokens for SchemaFormat<'_> { fn to_tokens(&self, tokens: &mut TokenStream) { match self { - Self::Type(ty) => ty.to_tokens(tokens), + Self::Type(ty) => { + if let Err(diagnostics) = ty.to_tokens(tokens) { + diagnostics.to_tokens(tokens) + } + } Self::Variant(variant) => variant.to_tokens(tokens), } } @@ -352,11 +369,14 @@ fn is_known_format(name: &str) -> bool { ) } -impl ToTokens for Type<'_> { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - let last_segment = self.0.segments.last().unwrap_or_else(|| { - abort_call_site!("expected there to be at least one segment in the path") - }); +impl ToTokensDiagnostics for Type<'_> { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) -> Result<(), Diagnostics> { + let last_segment = self.0.segments.last().ok_or_else(|| { + Diagnostics::with_span( + self.0.span(), + "type should have at least one segment in the path", + ) + })?; let name = &*last_segment.ident.to_string(); match name { @@ -416,7 +436,9 @@ impl ToTokens for Type<'_> { tokens.extend(quote! { utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::DateTime) }) } _ => (), - } + }; + + Ok(()) } } diff --git a/utoipa-gen/tests/path_derive_actix.rs b/utoipa-gen/tests/path_derive_actix.rs index caa189d2..fe88014f 100644 --- a/utoipa-gen/tests/path_derive_actix.rs +++ b/utoipa-gen/tests/path_derive_actix.rs @@ -17,7 +17,6 @@ use utoipa::{ }, IntoParams, OpenApi, ToSchema, }; -use uuid::Uuid; mod common; @@ -866,6 +865,7 @@ fn path_with_all_args() { } #[test] +#[cfg(feature = "uuid")] fn path_with_all_args_using_uuid() { #[derive(utoipa::ToSchema, serde::Serialize, serde::Deserialize)] struct Item(String); @@ -926,6 +926,7 @@ fn path_with_all_args_using_uuid() { } #[test] +#[cfg(feature = "uuid")] fn path_with_all_args_using_custom_uuid() { #[derive(utoipa::ToSchema, serde::Serialize, serde::Deserialize)] struct Item(String); @@ -944,7 +945,7 @@ fn path_with_all_args_using_custom_uuid() { #[derive(Serialize, Deserialize, IntoParams)] #[into_params(names("custom_uuid"))] - struct Id(Uuid); + struct Id(uuid::Uuid); impl FromRequest for Id { type Error = Error;