utoipa_gen/component/
into_params.rs

1use std::borrow::Cow;
2
3use proc_macro2::TokenStream;
4use quote::{quote, quote_spanned, ToTokens};
5use syn::{
6    parse::Parse, punctuated::Punctuated, spanned::Spanned, token::Comma, Attribute, Data, Field,
7    Generics, Ident,
8};
9
10use crate::{
11    component::{
12        self,
13        features::{
14            self,
15            attributes::{
16                AdditionalProperties, AllowReserved, Example, Explode, Format, Ignore, Inline,
17                IntoParamsNames, Nullable, ReadOnly, Rename, RenameAll, SchemaWith, Style,
18                WriteOnly, XmlAttr,
19            },
20            validation::{
21                ExclusiveMaximum, ExclusiveMinimum, MaxItems, MaxLength, Maximum, MinItems,
22                MinLength, Minimum, MultipleOf, Pattern,
23            },
24        },
25        FieldRename,
26    },
27    doc_comment::CommentAttributes,
28    parse_utils::LitBoolOrExprPath,
29    Array, Diagnostics, OptionExt, Required, ToTokensDiagnostics,
30};
31
32use super::{
33    features::{
34        impl_into_inner, impl_merge, parse_features, pop_feature, Feature, FeaturesExt, IntoInner,
35        Merge, ToTokensExt,
36    },
37    serde::{self, SerdeContainer, SerdeValue},
38    ComponentSchema, Container, TypeTree,
39};
40
41impl_merge!(IntoParamsFeatures, FieldFeatures);
42
43/// Container attribute `#[into_params(...)]`.
44pub struct IntoParamsFeatures(Vec<Feature>);
45
46impl Parse for IntoParamsFeatures {
47    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
48        Ok(Self(parse_features!(
49            input as Style,
50            features::attributes::ParameterIn,
51            IntoParamsNames,
52            RenameAll
53        )))
54    }
55}
56
57impl_into_inner!(IntoParamsFeatures);
58
59#[cfg_attr(feature = "debug", derive(Debug))]
60pub struct IntoParams {
61    /// Attributes tagged on the whole struct or enum.
62    pub attrs: Vec<Attribute>,
63    /// Generics required to complete the definition.
64    pub generics: Generics,
65    /// Data within the struct or enum.
66    pub data: Data,
67    /// Name of the struct or enum.
68    pub ident: Ident,
69}
70
71impl ToTokensDiagnostics for IntoParams {
72    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) -> Result<(), Diagnostics> {
73        let ident = &self.ident;
74        let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();
75
76        let mut into_params_features = self
77            .attrs
78            .iter()
79            .filter(|attr| attr.path().is_ident("into_params"))
80            .map(|attribute| {
81                attribute
82                    .parse_args::<IntoParamsFeatures>()
83                    .map(IntoParamsFeatures::into_inner)
84                    .map_err(Diagnostics::from)
85            })
86            .collect::<Result<Vec<_>, Diagnostics>>()?
87            .into_iter()
88            .reduce(|acc, item| acc.merge(item));
89        let serde_container = serde::parse_container(&self.attrs)?;
90
91        // #[param] is only supported over fields
92        if self.attrs.iter().any(|attr| attr.path().is_ident("param")) {
93            return Err(Diagnostics::with_span(
94                ident.span(),
95                "found `param` attribute in unsupported context",
96            )
97            .help("Did you mean `into_params`?"));
98        }
99
100        let names = into_params_features.as_mut().and_then(|features| {
101            let into_params_names = pop_feature!(features => Feature::IntoParamsNames(_));
102            IntoInner::<Option<IntoParamsNames>>::into_inner(into_params_names)
103                .map(|names| names.into_values())
104        });
105
106        let style = pop_feature!(into_params_features => Feature::Style(_));
107        let parameter_in = pop_feature!(into_params_features => Feature::ParameterIn(_));
108        let rename_all = pop_feature!(into_params_features => Feature::RenameAll(_));
109
110        let params = self
111            .get_struct_fields(&names.as_ref())?
112            .enumerate()
113            .map(|(index, field)| {
114                let field_features = match parse_field_features(field) {
115                    Ok(features) => features,
116                    Err(error) => return Err(error),
117                };
118                match serde::parse_value(&field.attrs) {
119                    Ok(serde_value) => Ok((index, field, serde_value, field_features)),
120                    Err(diagnostics) => Err(diagnostics)
121                }
122            })
123            .collect::<Result<Vec<_>, Diagnostics>>()?
124            .into_iter()
125            .filter_map(|(index, field, field_serde_params, field_features)| {
126                if field_serde_params.skip {
127                    None
128                } else {
129                    Some((index, field, field_serde_params, field_features))
130                }
131            })
132            .map(|(index, field, field_serde_params, field_features)| {
133                let name = names.as_ref()
134                    .map_try(|names| names.get(index).ok_or_else(|| Diagnostics::with_span(
135                        ident.span(),
136                        format!("There is no name specified in the names(...) container attribute for tuple struct field {}", index),
137                    )));
138                let name = match name {
139                    Ok(name) => name,
140                    Err(diagnostics) => return Err(diagnostics)
141                };
142                let param = Param::new(field, field_serde_params, field_features, FieldParamContainerAttributes {
143                        rename_all: rename_all.as_ref().and_then(|feature| {
144                            match feature {
145                                Feature::RenameAll(rename_all) => Some(rename_all),
146                                _ => None
147                            }
148                        }),
149                        style: &style,
150                        parameter_in: &parameter_in,
151                        name,
152                    }, &serde_container, &self.generics)?;
153
154
155                Ok(param.to_token_stream())
156            })
157            .collect::<Result<Array<TokenStream>, Diagnostics>>()?;
158
159        tokens.extend(quote! {
160            impl #impl_generics utoipa::IntoParams for #ident #ty_generics #where_clause {
161                fn into_params(parameter_in_provider: impl Fn() -> Option<utoipa::openapi::path::ParameterIn>) -> Vec<utoipa::openapi::path::Parameter> {
162                    #params.into_iter().filter(Option::is_some).flatten().collect()
163                }
164            }
165        });
166
167        Ok(())
168    }
169}
170
171fn parse_field_features(field: &Field) -> Result<Vec<Feature>, Diagnostics> {
172    Ok(field
173        .attrs
174        .iter()
175        .filter(|attribute| attribute.path().is_ident("param"))
176        .map(|attribute| {
177            attribute
178                .parse_args::<FieldFeatures>()
179                .map(FieldFeatures::into_inner)
180        })
181        .collect::<Result<Vec<_>, syn::Error>>()?
182        .into_iter()
183        .reduce(|acc, item| acc.merge(item))
184        .unwrap_or_default())
185}
186
187impl IntoParams {
188    fn get_struct_fields(
189        &self,
190        field_names: &Option<&Vec<String>>,
191    ) -> Result<impl Iterator<Item = &Field>, Diagnostics> {
192        let ident = &self.ident;
193        match &self.data {
194            Data::Struct(data_struct) => match &data_struct.fields {
195                syn::Fields::Named(named_fields) => {
196                    if field_names.is_some() {
197                        return Err(Diagnostics::with_span(
198                            ident.span(),
199                            "`#[into_params(names(...))]` is not supported attribute on a struct with named fields")
200                        );
201                    }
202                    Ok(named_fields.named.iter())
203                }
204                syn::Fields::Unnamed(unnamed_fields) => {
205                    match self.validate_unnamed_field_names(&unnamed_fields.unnamed, field_names) {
206                        None => Ok(unnamed_fields.unnamed.iter()),
207                        Some(diagnostics) => Err(diagnostics),
208                    }
209                }
210                _ => Err(Diagnostics::with_span(
211                    ident.span(),
212                    "Unit type struct is not supported",
213                )),
214            },
215            _ => Err(Diagnostics::with_span(
216                ident.span(),
217                "Only struct type is supported",
218            )),
219        }
220    }
221
222    fn validate_unnamed_field_names(
223        &self,
224        unnamed_fields: &Punctuated<Field, Comma>,
225        field_names: &Option<&Vec<String>>,
226    ) -> Option<Diagnostics> {
227        let ident = &self.ident;
228        match field_names {
229            Some(names) => {
230                if names.len() != unnamed_fields.len() {
231                    Some(Diagnostics::with_span(
232                        ident.span(),
233                        format!("declared names amount '{}' does not match to the unnamed fields amount '{}' in type: {}", 
234                            names.len(), unnamed_fields.len(), ident)
235                    )
236                        .help(r#"Did you forget to add a field name to `#[into_params(names(... , "field_name"))]`"#)
237                        .help("Or have you added extra name but haven't defined a type?")
238                    )
239                } else {
240                    None
241                }
242            }
243            None => Some(
244                Diagnostics::with_span(
245                    ident.span(),
246                    "struct with unnamed fields must have explicit name declarations.",
247                )
248                .help(format!(
249                    "Try defining `#[into_params(names(...))]` over your type: {}",
250                    ident
251                )),
252            ),
253        }
254    }
255}
256
257#[cfg_attr(feature = "debug", derive(Debug))]
258pub struct FieldParamContainerAttributes<'a> {
259    /// See [`IntoParamsAttr::style`].
260    style: &'a Option<Feature>,
261    /// See [`IntoParamsAttr::names`]. The name that applies to this field.
262    name: Option<&'a String>,
263    /// See [`IntoParamsAttr::parameter_in`].
264    parameter_in: &'a Option<Feature>,
265    /// Custom rename all if serde attribute is not present.
266    rename_all: Option<&'a RenameAll>,
267}
268
269struct FieldFeatures(Vec<Feature>);
270
271impl_into_inner!(FieldFeatures);
272
273impl Parse for FieldFeatures {
274    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
275        Ok(Self(parse_features!(
276            // param features
277            input as component::features::attributes::ValueType,
278            Rename,
279            Style,
280            AllowReserved,
281            Example,
282            Explode,
283            SchemaWith,
284            component::features::attributes::Required,
285            // param schema features
286            Inline,
287            Format,
288            component::features::attributes::Default,
289            WriteOnly,
290            ReadOnly,
291            Nullable,
292            XmlAttr,
293            MultipleOf,
294            Maximum,
295            Minimum,
296            ExclusiveMaximum,
297            ExclusiveMinimum,
298            MaxLength,
299            MinLength,
300            Pattern,
301            MaxItems,
302            MinItems,
303            AdditionalProperties,
304            Ignore
305        )))
306    }
307}
308
309#[cfg_attr(feature = "debug", derive(Debug))]
310struct Param {
311    tokens: TokenStream,
312}
313
314impl Param {
315    fn new(
316        field: &Field,
317        field_serde_params: SerdeValue,
318        field_features: Vec<Feature>,
319        container_attributes: FieldParamContainerAttributes<'_>,
320        serde_container: &SerdeContainer,
321        generics: &Generics,
322    ) -> Result<Self, Diagnostics> {
323        let mut tokens = TokenStream::new();
324        let field_serde_params = &field_serde_params;
325        let ident = &field.ident;
326        let mut name = &*ident
327            .as_ref()
328            .map(|ident| ident.to_string())
329            .or_else(|| container_attributes.name.cloned())
330            .ok_or_else(||
331                Diagnostics::with_span(field.span(), "No name specified for unnamed field.")
332                    .help("Try adding #[into_params(names(...))] container attribute to specify the name for this field")
333            )?;
334
335        if name.starts_with("r#") {
336            name = &name[2..];
337        }
338
339        let (schema_features, mut param_features) =
340            Param::resolve_field_features(field_features, &container_attributes)
341                .map_err(Diagnostics::from)?;
342
343        let ignore = pop_feature!(param_features => Feature::Ignore(_));
344        let rename = pop_feature!(param_features => Feature::Rename(_) as Option<Rename>)
345            .map(|rename| rename.into_value());
346        let rename_to = field_serde_params
347            .rename
348            .as_deref()
349            .map(Cow::Borrowed)
350            .or(rename.map(Cow::Owned));
351        let rename_all = serde_container.rename_all.as_ref().or(container_attributes
352            .rename_all
353            .map(|rename_all| rename_all.as_rename_rule()));
354        let name = super::rename::<FieldRename>(name, rename_to, rename_all)
355            .unwrap_or(Cow::Borrowed(name));
356        let type_tree = TypeTree::from_type(&field.ty)?;
357
358        tokens.extend(quote! { utoipa::openapi::path::ParameterBuilder::new()
359            .name(#name)
360        });
361        tokens.extend(
362            if let Some(ref parameter_in) = &container_attributes.parameter_in {
363                parameter_in.to_token_stream()
364            } else {
365                quote! {
366                    .parameter_in(parameter_in_provider().unwrap_or_default())
367                }
368            },
369        );
370
371        if let Some(deprecated) = super::get_deprecated(&field.attrs) {
372            tokens.extend(quote! { .deprecated(Some(#deprecated)) });
373        }
374
375        let schema_with = pop_feature!(param_features => Feature::SchemaWith(_));
376        if let Some(schema_with) = schema_with {
377            let schema_with = crate::as_tokens_or_diagnostics!(&schema_with);
378            tokens.extend(quote! { .schema(Some(#schema_with)).build() });
379        } else {
380            let description =
381                CommentAttributes::from_attributes(&field.attrs).as_formatted_string();
382            if !description.is_empty() {
383                tokens.extend(quote! { .description(Some(#description))})
384            }
385
386            let value_type = pop_feature!(param_features => Feature::ValueType(_) as Option<features::attributes::ValueType>);
387            let component = value_type
388                .as_ref()
389                .map_try(|value_type| value_type.as_type_tree())?
390                .unwrap_or(type_tree);
391            let alias_type = component.get_alias_type()?;
392            let alias_type_tree = alias_type.as_ref().map_try(TypeTree::from_type)?;
393            let component = alias_type_tree.as_ref().unwrap_or(&component);
394
395            let required: Option<features::attributes::Required> =
396                pop_feature!(param_features => Feature::Required(_)).into_inner();
397            let component_required =
398                !component.is_option() && super::is_required(field_serde_params, serde_container);
399
400            let required = match (required, component_required) {
401                (Some(required_feature), _) => Into::<Required>::into(required_feature.is_true()),
402                (None, component_required) => Into::<Required>::into(component_required),
403            };
404
405            tokens.extend(quote! {
406                .required(#required)
407            });
408            tokens.extend(param_features.to_token_stream()?);
409
410            let is_query = matches!(container_attributes.parameter_in, Some(Feature::ParameterIn(p)) if p.is_query());
411            let option_is_nullable = !is_query;
412
413            let schema = ComponentSchema::for_params(
414                component::ComponentSchemaProps {
415                    type_tree: component,
416                    features: schema_features,
417                    description: None,
418                    container: &Container { generics },
419                },
420                option_is_nullable,
421            )?;
422            let schema_tokens = schema.to_token_stream();
423
424            tokens.extend(quote! { .schema(Some(#schema_tokens)).build() });
425        }
426
427        let tokens = match ignore {
428            Some(Feature::Ignore(Ignore(LitBoolOrExprPath::LitBool(bool)))) => {
429                quote_spanned! {
430                    bool.span() => if #bool {
431                        None
432                    } else {
433                        Some(#tokens)
434                    }
435                }
436            }
437            Some(Feature::Ignore(Ignore(LitBoolOrExprPath::ExprPath(path)))) => {
438                quote_spanned! {
439                    path.span() => if #path() {
440                        None
441                    } else {
442                        Some(#tokens)
443                    }
444                }
445            }
446            _ => quote! { Some(#tokens) },
447        };
448
449        Ok(Self { tokens })
450    }
451
452    /// Resolve [`Param`] features and split features into two [`Vec`]s. Features are split by
453    /// whether they should be rendered in [`Param`] itself or in [`Param`]s schema.
454    ///
455    /// Method returns a tuple containing two [`Vec`]s of [`Feature`].
456    fn resolve_field_features(
457        mut field_features: Vec<Feature>,
458        container_attributes: &FieldParamContainerAttributes<'_>,
459    ) -> Result<(Vec<Feature>, Vec<Feature>), syn::Error> {
460        if let Some(ref style) = container_attributes.style {
461            if !field_features
462                .iter()
463                .any(|feature| matches!(&feature, Feature::Style(_)))
464            {
465                field_features.push(style.clone()); // could try to use cow to avoid cloning
466            };
467        }
468
469        Ok(field_features.into_iter().fold(
470            (Vec::<Feature>::new(), Vec::<Feature>::new()),
471            |(mut schema_features, mut param_features), feature| {
472                match feature {
473                    Feature::Inline(_)
474                    | Feature::Format(_)
475                    | Feature::Default(_)
476                    | Feature::WriteOnly(_)
477                    | Feature::ReadOnly(_)
478                    | Feature::Nullable(_)
479                    | Feature::XmlAttr(_)
480                    | Feature::MultipleOf(_)
481                    | Feature::Maximum(_)
482                    | Feature::Minimum(_)
483                    | Feature::ExclusiveMaximum(_)
484                    | Feature::ExclusiveMinimum(_)
485                    | Feature::MaxLength(_)
486                    | Feature::MinLength(_)
487                    | Feature::Pattern(_)
488                    | Feature::MaxItems(_)
489                    | Feature::MinItems(_)
490                    | Feature::AdditionalProperties(_) => {
491                        schema_features.push(feature);
492                    }
493                    _ => {
494                        param_features.push(feature);
495                    }
496                };
497
498                (schema_features, param_features)
499            },
500        ))
501    }
502}
503
504impl ToTokens for Param {
505    fn to_tokens(&self, tokens: &mut TokenStream) {
506        self.tokens.to_tokens(tokens)
507    }
508}