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,
};
use crate::{
component::{
self,
features::{
self, AdditionalProperties, AllowReserved, Example, ExclusiveMaximum, ExclusiveMinimum,
Explode, Format, Inline, MaxItems, MaxLength, Maximum, MinItems, MinLength, Minimum,
MultipleOf, Names, Nullable, Pattern, ReadOnly, Rename, RenameAll, SchemaWith, Style,
WriteOnly, XmlAttr,
},
FieldRename,
},
doc_comment::CommentAttributes,
Array, Required, ResultExt,
};
use super::{
features::{
impl_into_inner, impl_merge, parse_features, pop_feature, pop_feature_as_inner, Feature,
FeaturesExt, IntoInner, Merge, ToTokensExt,
},
serde::{self, SerdeContainer, SerdeValue},
ComponentSchema, TypeTree,
};
impl_merge!(IntoParamsFeatures, FieldFeatures);
pub struct IntoParamsFeatures(Vec<Feature>);
impl Parse for IntoParamsFeatures {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
Ok(Self(parse_features!(
input as Style,
features::ParameterIn,
Names,
RenameAll
)))
}
}
impl_into_inner!(IntoParamsFeatures);
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct IntoParams {
pub attrs: Vec<Attribute>,
pub generics: Generics,
pub data: Data,
pub ident: Ident,
}
impl ToTokens for IntoParams {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let ident = &self.ident;
let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();
let mut into_params_features = self
.attrs
.iter()
.filter(|attr| attr.path().is_ident("into_params"))
.map(|attribute| {
attribute
.parse_args::<IntoParamsFeatures>()
.unwrap_or_abort()
.into_inner()
})
.reduce(|acc, item| acc.merge(item));
let serde_container = serde::parse_container(&self.attrs);
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`?",
}
}
let names = into_params_features.as_mut().and_then(|features| {
features
.pop_by(|feature| matches!(feature, Feature::IntoParamsNames(_)))
.and_then(|feature| match feature {
Feature::IntoParamsNames(names) => Some(names.into_values()),
_ => None,
})
});
let style = pop_feature!(into_params_features => Feature::Style(_));
let parameter_in = pop_feature!(into_params_features => Feature::ParameterIn(_));
let rename_all = pop_feature!(into_params_features => Feature::RenameAll(_));
let params = self
.get_struct_fields(&names.as_ref())
.enumerate()
.filter_map(|(index, field)| {
let field_params = serde::parse_value(&field.attrs);
if matches!(&field_params, Some(params) if !params.skip) {
Some((index, field, field_params))
} else {
None
}
})
.map(|(index, field, field_serde_params)| {
Param {
field,
field_serde_params,
container_attributes: FieldParamContainerAttributes {
rename_all: rename_all.as_ref().and_then(|feature| {
match feature {
Feature::RenameAll(rename_all) => Some(rename_all),
_ => None
}
}),
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
))),
},
serde_container: serde_container.as_ref(),
}
})
.collect::<Array<Param>>();
tokens.extend(quote! {
impl #impl_generics utoipa::IntoParams for #ident #ty_generics #where_clause {
fn into_params(parameter_in_provider: impl Fn() -> Option<utoipa::openapi::path::ParameterIn>) -> Vec<utoipa::openapi::path::Parameter> {
#params.to_vec()
}
}
});
}
}
impl IntoParams {
fn get_struct_fields(
&self,
field_names: &Option<&Vec<String>>,
) -> impl Iterator<Item = &Field> {
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"}
}
named_fields.named.iter()
}
syn::Fields::Unnamed(unnamed_fields) => {
self.validate_unnamed_field_names(&unnamed_fields.unnamed, field_names);
unnamed_fields.unnamed.iter()
}
_ => abort("Unit type struct is not supported"),
},
_ => abort("Only struct type is supported"),
}
}
fn validate_unnamed_field_names(
&self,
unnamed_fields: &Punctuated<Field, Comma>,
field_names: &Option<&Vec<String>>,
) {
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,
}
}
}
}
}
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct FieldParamContainerAttributes<'a> {
style: &'a Option<Feature>,
name: Option<&'a String>,
parameter_in: &'a Option<Feature>,
rename_all: Option<&'a RenameAll>,
}
struct FieldFeatures(Vec<Feature>);
impl_into_inner!(FieldFeatures);
impl Parse for FieldFeatures {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
Ok(Self(parse_features!(
input as component::features::ValueType,
Rename,
Style,
AllowReserved,
Example,
Explode,
SchemaWith,
component::features::Required,
Inline,
Format,
component::features::Default,
WriteOnly,
ReadOnly,
Nullable,
XmlAttr,
MultipleOf,
Maximum,
Minimum,
ExclusiveMaximum,
ExclusiveMinimum,
MaxLength,
MinLength,
Pattern,
MaxItems,
MinItems,
AdditionalProperties
)))
}
}
#[cfg_attr(feature = "debug", derive(Debug))]
struct Param<'a> {
field: &'a Field,
field_serde_params: Option<SerdeValue>,
container_attributes: FieldParamContainerAttributes<'a>,
serde_container: Option<&'a SerdeContainer>,
}
impl Param<'_> {
fn resolve_field_features(&self) -> (Vec<Feature>, Vec<Feature>) {
let mut field_features = self
.field
.attrs
.iter()
.filter(|attribute| attribute.path().is_ident("param"))
.map(|attribute| {
attribute
.parse_args::<FieldFeatures>()
.unwrap_or_abort()
.into_inner()
})
.reduce(|acc, item| acc.merge(item))
.unwrap_or_default();
if let Some(ref style) = self.container_attributes.style {
if !field_features
.iter()
.any(|feature| matches!(&feature, Feature::Style(_)))
{
field_features.push(style.clone()); };
}
field_features.into_iter().fold(
(Vec::<Feature>::new(), Vec::<Feature>::new()),
|(mut schema_features, mut param_features), feature| {
match feature {
Feature::Inline(_)
| Feature::Format(_)
| Feature::Default(_)
| Feature::WriteOnly(_)
| Feature::ReadOnly(_)
| Feature::Nullable(_)
| Feature::XmlAttr(_)
| Feature::MultipleOf(_)
| Feature::Maximum(_)
| Feature::Minimum(_)
| Feature::ExclusiveMaximum(_)
| Feature::ExclusiveMinimum(_)
| Feature::MaxLength(_)
| Feature::MinLength(_)
| Feature::Pattern(_)
| Feature::MaxItems(_)
| Feature::MinItems(_)
| Feature::AdditionalProperties(_) => {
schema_features.push(feature);
}
_ => {
param_features.push(feature);
}
};
(schema_features, param_features)
},
)
}
}
impl ToTokens for Param<'_> {
fn to_tokens(&self, tokens: &mut TokenStream) {
let field = self.field;
let field_serde_params = &self.field_serde_params;
let ident = &field.ident;
let mut name = &*ident
.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"
));
if name.starts_with("r#") {
name = &name[2..];
}
let (schema_features, mut param_features) = self.resolve_field_features();
let rename = param_features
.pop_rename_feature()
.map(|rename| rename.into_value());
let rename_to = field_serde_params
.as_ref()
.and_then(|field_param_serde| field_param_serde.rename.as_deref().map(Cow::Borrowed))
.or_else(|| rename.map(Cow::Owned));
let rename_all = self
.serde_container
.as_ref()
.and_then(|serde_container| serde_container.rename_all.as_ref())
.or_else(|| {
self.container_attributes
.rename_all
.map(|rename_all| rename_all.as_rename_rule())
});
let name = super::rename::<FieldRename>(name, rename_to, rename_all)
.unwrap_or(Cow::Borrowed(name));
let type_tree = TypeTree::from_type(&field.ty);
tokens.extend(quote! { utoipa::openapi::path::ParameterBuilder::new()
.name(#name)
});
tokens.extend(
if let Some(ref parameter_in) = self.container_attributes.parameter_in {
parameter_in.into_token_stream()
} else {
quote! {
.parameter_in(parameter_in_provider().unwrap_or_default())
}
},
);
if let Some(deprecated) = super::get_deprecated(&field.attrs) {
tokens.extend(quote! { .deprecated(Some(#deprecated)) });
}
let schema_with = pop_feature!(param_features => Feature::SchemaWith(_));
if let Some(schema_with) = schema_with {
tokens.extend(quote! { .schema(Some(#schema_with)).build() });
} else {
let description =
CommentAttributes::from_attributes(&field.attrs).as_formatted_string();
if !description.is_empty() {
tokens.extend(quote! { .description(Some(#description))})
}
let value_type = param_features.pop_value_type_feature();
let component = value_type
.as_ref()
.map(|value_type| value_type.as_type_tree())
.unwrap_or(type_tree);
let required = pop_feature_as_inner!(param_features => Feature::Required(_v))
.as_ref()
.map(super::features::Required::is_true)
.unwrap_or(false);
let non_required = (component.is_option() && !required)
|| !component::is_required(field_serde_params.as_ref(), self.serde_container);
let required: Required = (!non_required).into();
tokens.extend(quote! {
.required(#required)
});
tokens.extend(param_features.to_token_stream());
let schema = ComponentSchema::new(component::ComponentSchemaProps {
type_tree: &component,
features: Some(schema_features),
description: None,
deprecated: None,
object_name: "",
});
tokens.extend(quote! { .schema(Some(#schema)).build() });
}
}
}