utoipa_gen/component/
into_params.rs

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