utoipa_gen/path/
parameter.rs

1use std::{borrow::Cow, fmt::Display};
2
3use proc_macro2::{Ident, Span, TokenStream};
4use proc_macro_error::abort;
5use quote::{quote, quote_spanned, ToTokens};
6use syn::{
7    parenthesized,
8    parse::{Parse, ParseBuffer, ParseStream},
9    Error, LitStr, Token, TypePath,
10};
11
12use crate::{
13    component::{
14        self,
15        features::{
16            impl_into_inner, parse_features, AllowReserved, Description, Example, ExclusiveMaximum,
17            ExclusiveMinimum, Explode, Feature, Format, MaxItems, MaxLength, Maximum, MinItems,
18            MinLength, Minimum, MultipleOf, Nullable, Pattern, ReadOnly, Style, ToTokensExt,
19            WriteOnly, XmlAttr,
20        },
21        ComponentSchema,
22    },
23    parse_utils, Required,
24};
25
26use super::InlineType;
27
28/// Parameter of request such as in path, header, query or cookie
29///
30/// For example path `/users/{id}` the path parameter is used to define
31/// type, format and other details of the `{id}` parameter within the path
32///
33/// Parse is executed for following formats:
34///
35/// * ("id" = String, path, deprecated, description = "Users database id"),
36/// * ("id", path, deprecated, description = "Users database id"),
37///
38/// The `= String` type statement is optional if automatic resolution is supported.
39#[cfg_attr(feature = "debug", derive(Debug))]
40#[derive(PartialEq, Eq)]
41pub enum Parameter<'a> {
42    Value(ValueParameter<'a>),
43    /// Identifier for a struct that implements `IntoParams` trait.
44    IntoParamsIdent(IntoParamsIdentParameter<'a>),
45}
46
47#[cfg(any(
48    feature = "actix_extras",
49    feature = "rocket_extras",
50    feature = "axum_extras"
51))]
52impl<'p> Parameter<'p> {
53    pub fn merge(&mut self, other: Parameter<'p>) {
54        match (self, other) {
55            (Self::Value(value), Parameter::Value(other)) => {
56                value.parameter_schema = other.parameter_schema;
57            }
58            (Self::IntoParamsIdent(into_params), Parameter::IntoParamsIdent(other)) => {
59                *into_params = other;
60            }
61            _ => (),
62        }
63    }
64}
65
66impl Parse for Parameter<'_> {
67    fn parse(input: ParseStream) -> syn::Result<Self> {
68        if input.fork().parse::<TypePath>().is_ok() {
69            Ok(Self::IntoParamsIdent(IntoParamsIdentParameter {
70                path: Cow::Owned(input.parse::<TypePath>()?.path),
71                parameter_in_fn: None,
72            }))
73        } else {
74            Ok(Self::Value(input.parse()?))
75        }
76    }
77}
78
79impl ToTokens for Parameter<'_> {
80    fn to_tokens(&self, tokens: &mut TokenStream) {
81        match self {
82            Parameter::Value(parameter) => tokens.extend(quote! { .parameter(#parameter) }),
83            Parameter::IntoParamsIdent(IntoParamsIdentParameter {
84                path,
85                parameter_in_fn,
86            }) => {
87                let last_ident = &path.segments.last().unwrap().ident;
88
89                let default_parameter_in_provider = &quote! { || None };
90                let parameter_in_provider = parameter_in_fn
91                    .as_ref()
92                    .unwrap_or(default_parameter_in_provider);
93                tokens.extend(quote_spanned! {last_ident.span()=>
94                    .parameters(
95                        Some(<#path as utoipa::IntoParams>::into_params(#parameter_in_provider))
96                    )
97                })
98            }
99        }
100    }
101}
102
103#[cfg(any(
104    feature = "actix_extras",
105    feature = "rocket_extras",
106    feature = "axum_extras"
107))]
108impl<'a> From<crate::ext::ValueArgument<'a>> for Parameter<'a> {
109    fn from(argument: crate::ext::ValueArgument<'a>) -> Self {
110        Self::Value(ValueParameter {
111            name: argument.name.unwrap_or_else(|| Cow::Owned(String::new())),
112            parameter_in: if argument.argument_in == crate::ext::ArgumentIn::Path {
113                ParameterIn::Path
114            } else {
115                ParameterIn::Query
116            },
117            parameter_schema: argument.type_tree.map(|type_tree| ParameterSchema {
118                parameter_type: ParameterType::External(type_tree),
119                features: Vec::new(),
120            }),
121            ..Default::default()
122        })
123    }
124}
125
126#[cfg(any(
127    feature = "actix_extras",
128    feature = "rocket_extras",
129    feature = "axum_extras"
130))]
131impl<'a> From<crate::ext::IntoParamsType<'a>> for Parameter<'a> {
132    fn from(value: crate::ext::IntoParamsType<'a>) -> Self {
133        Self::IntoParamsIdent(IntoParamsIdentParameter {
134            path: value.type_path.expect("IntoParams type must have a path"),
135            parameter_in_fn: Some(value.parameter_in_provider),
136        })
137    }
138}
139
140#[cfg_attr(feature = "debug", derive(Debug))]
141struct ParameterSchema<'p> {
142    parameter_type: ParameterType<'p>,
143    features: Vec<Feature>,
144}
145
146impl ToTokens for ParameterSchema<'_> {
147    fn to_tokens(&self, tokens: &mut TokenStream) {
148        let mut to_tokens = |param_schema, required| {
149            tokens.extend(quote! { .schema(Some(#param_schema)).required(#required) });
150        };
151
152        match &self.parameter_type {
153            #[cfg(any(
154                feature = "actix_extras",
155                feature = "rocket_extras",
156                feature = "axum_extras"
157            ))]
158            ParameterType::External(type_tree) => {
159                let required: Required = (!type_tree.is_option()).into();
160
161                to_tokens(
162                    ComponentSchema::new(component::ComponentSchemaProps {
163                        type_tree,
164                        features: Some(self.features.clone()),
165                        description: None,
166                        deprecated: None,
167                        object_name: "",
168                    }),
169                    required,
170                )
171            }
172            ParameterType::Parsed(inline_type) => {
173                let type_tree = inline_type.as_type_tree();
174                let required: Required = (!type_tree.is_option()).into();
175                let mut schema_features = Vec::<Feature>::new();
176                schema_features.clone_from(&self.features);
177                schema_features.push(Feature::Inline(inline_type.is_inline.into()));
178
179                to_tokens(
180                    ComponentSchema::new(component::ComponentSchemaProps {
181                        type_tree: &type_tree,
182                        features: Some(schema_features),
183                        description: None,
184                        deprecated: None,
185                        object_name: "",
186                    }),
187                    required,
188                )
189            }
190        }
191    }
192}
193
194#[cfg_attr(feature = "debug", derive(Debug))]
195enum ParameterType<'p> {
196    #[cfg(any(
197        feature = "actix_extras",
198        feature = "rocket_extras",
199        feature = "axum_extras"
200    ))]
201    External(crate::component::TypeTree<'p>),
202    Parsed(InlineType<'p>),
203}
204
205#[derive(Default)]
206#[cfg_attr(feature = "debug", derive(Debug))]
207pub struct ValueParameter<'a> {
208    pub name: Cow<'a, str>,
209    parameter_in: ParameterIn,
210    parameter_schema: Option<ParameterSchema<'a>>,
211    features: (Vec<Feature>, Vec<Feature>),
212}
213
214impl PartialEq for ValueParameter<'_> {
215    fn eq(&self, other: &Self) -> bool {
216        self.name == other.name && self.parameter_in == other.parameter_in
217    }
218}
219
220impl Eq for ValueParameter<'_> {}
221
222impl Parse for ValueParameter<'_> {
223    fn parse(input_with_parens: ParseStream) -> syn::Result<Self> {
224        let input: ParseBuffer;
225        parenthesized!(input in input_with_parens);
226
227        let mut parameter = ValueParameter::default();
228
229        if input.peek(LitStr) {
230            // parse name
231            let name = input.parse::<LitStr>()?.value();
232            parameter.name = Cow::Owned(name);
233
234            if input.peek(Token![=]) {
235                parameter.parameter_schema = Some(ParameterSchema {
236                    parameter_type: ParameterType::Parsed(parse_utils::parse_next(&input, || {
237                        input.parse().map_err(|error| {
238                            Error::new(
239                                error.span(),
240                                format!("unexpected token, expected type such as String, {error}"),
241                            )
242                        })
243                    })?),
244                    features: Vec::new(),
245                });
246            }
247        } else {
248            return Err(input.error("unparseable parameter name, expected literal string"));
249        }
250
251        input.parse::<Token![,]>()?;
252
253        if input.fork().parse::<ParameterIn>().is_ok() {
254            parameter.parameter_in = input.parse()?;
255            input.parse::<Token![,]>()?;
256        }
257
258        let (schema_features, parameter_features) = input
259            .parse::<ParameterFeatures>()?
260            .split_for_parameter_type();
261
262        parameter.features = (schema_features.clone(), parameter_features);
263        if let Some(parameter_schema) = &mut parameter.parameter_schema {
264            parameter_schema.features = schema_features;
265        }
266
267        Ok(parameter)
268    }
269}
270
271#[derive(Default)]
272#[cfg_attr(feature = "debug", derive(Debug))]
273struct ParameterFeatures(Vec<Feature>);
274
275impl Parse for ParameterFeatures {
276    fn parse(input: ParseStream) -> syn::Result<Self> {
277        Ok(Self(parse_features!(
278            // param features
279            input as Style,
280            Explode,
281            AllowReserved,
282            Example,
283            crate::component::features::Deprecated,
284            Description,
285            // param schema features
286            Format,
287            WriteOnly,
288            ReadOnly,
289            Nullable,
290            XmlAttr,
291            MultipleOf,
292            Maximum,
293            Minimum,
294            ExclusiveMaximum,
295            ExclusiveMinimum,
296            MaxLength,
297            MinLength,
298            Pattern,
299            MaxItems,
300            MinItems
301        )))
302    }
303}
304
305impl ParameterFeatures {
306    /// Split parsed features to two `Vec`s of [`Feature`]s.
307    ///
308    /// * First vec contains parameter type schema features.
309    /// * Second vec contains generic parameter features.
310    fn split_for_parameter_type(self) -> (Vec<Feature>, Vec<Feature>) {
311        self.0.into_iter().fold(
312            (Vec::new(), Vec::new()),
313            |(mut schema_features, mut param_features), feature| {
314                match feature {
315                    Feature::Format(_)
316                    | Feature::WriteOnly(_)
317                    | Feature::ReadOnly(_)
318                    | Feature::Nullable(_)
319                    | Feature::XmlAttr(_)
320                    | Feature::MultipleOf(_)
321                    | Feature::Maximum(_)
322                    | Feature::Minimum(_)
323                    | Feature::ExclusiveMaximum(_)
324                    | Feature::ExclusiveMinimum(_)
325                    | Feature::MaxLength(_)
326                    | Feature::MinLength(_)
327                    | Feature::Pattern(_)
328                    | Feature::MaxItems(_)
329                    | Feature::MinItems(_) => {
330                        schema_features.push(feature);
331                    }
332                    _ => {
333                        param_features.push(feature);
334                    }
335                };
336
337                (schema_features, param_features)
338            },
339        )
340    }
341}
342
343impl_into_inner!(ParameterFeatures);
344
345impl ToTokens for ValueParameter<'_> {
346    fn to_tokens(&self, tokens: &mut TokenStream) {
347        let name = &*self.name;
348        tokens.extend(quote! {
349            utoipa::openapi::path::ParameterBuilder::from(utoipa::openapi::path::Parameter::new(#name))
350        });
351        let parameter_in = &self.parameter_in;
352        tokens.extend(quote! { .parameter_in(#parameter_in) });
353
354        let (schema_features, param_features) = &self.features;
355
356        tokens.extend(param_features.to_token_stream());
357
358        if !schema_features.is_empty() && self.parameter_schema.is_none() {
359            abort!(
360                Span::call_site(),
361                "Missing `parameter_type` attribute, cannot define schema features without it.";
362                help = "See docs for more details <https://docs.rs/utoipa/latest/utoipa/attr.path.html#parameter-type-attributes>"
363
364            );
365        }
366
367        if let Some(parameter_schema) = &self.parameter_schema {
368            parameter_schema.to_tokens(tokens);
369        }
370    }
371}
372
373#[cfg_attr(feature = "debug", derive(Debug))]
374pub struct IntoParamsIdentParameter<'i> {
375    pub path: Cow<'i, syn::Path>,
376    /// quote!{ ... } of function which should implement `parameter_in_provider` for [`utoipa::IntoParams::into_param`]
377    parameter_in_fn: Option<TokenStream>,
378}
379
380// Compare paths loosly only by segment idents ignoring possible generics
381impl PartialEq for IntoParamsIdentParameter<'_> {
382    fn eq(&self, other: &Self) -> bool {
383        self.path
384            .segments
385            .iter()
386            .map(|segment| &segment.ident)
387            .collect::<Vec<_>>()
388            == other
389                .path
390                .segments
391                .iter()
392                .map(|segment| &segment.ident)
393                .collect::<Vec<_>>()
394    }
395}
396
397impl Eq for IntoParamsIdentParameter<'_> {}
398
399#[cfg_attr(feature = "debug", derive(Debug))]
400#[derive(PartialEq, Eq, Clone, Copy)]
401pub enum ParameterIn {
402    Query,
403    Path,
404    Header,
405    Cookie,
406}
407
408impl ParameterIn {
409    pub const VARIANTS: &'static [Self] = &[Self::Query, Self::Path, Self::Header, Self::Cookie];
410}
411
412impl Display for ParameterIn {
413    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
414        match self {
415            ParameterIn::Query => write!(f, "Query"),
416            ParameterIn::Path => write!(f, "Path"),
417            ParameterIn::Header => write!(f, "Header"),
418            ParameterIn::Cookie => write!(f, "Cookie"),
419        }
420    }
421}
422
423impl Default for ParameterIn {
424    fn default() -> Self {
425        Self::Path
426    }
427}
428
429impl Parse for ParameterIn {
430    fn parse(input: ParseStream) -> syn::Result<Self> {
431        fn expected_style() -> String {
432            let variants: String = ParameterIn::VARIANTS
433                .iter()
434                .map(ToString::to_string)
435                .collect::<Vec<_>>()
436                .join(", ");
437            format!("unexpected in, expected one of: {variants}")
438        }
439        let style = input.parse::<Ident>()?;
440
441        match &*style.to_string() {
442            "Path" => Ok(Self::Path),
443            "Query" => Ok(Self::Query),
444            "Header" => Ok(Self::Header),
445            "Cookie" => Ok(Self::Cookie),
446            _ => Err(Error::new(style.span(), expected_style())),
447        }
448    }
449}
450
451impl ToTokens for ParameterIn {
452    fn to_tokens(&self, tokens: &mut TokenStream) {
453        tokens.extend(match self {
454            Self::Path => quote! { utoipa::openapi::path::ParameterIn::Path },
455            Self::Query => quote! { utoipa::openapi::path::ParameterIn::Query },
456            Self::Header => quote! { utoipa::openapi::path::ParameterIn::Header },
457            Self::Cookie => quote! { utoipa::openapi::path::ParameterIn::Cookie },
458        })
459    }
460}
461
462/// See definitions from `utoipa` crate path.rs
463#[derive(Copy, Clone)]
464#[cfg_attr(feature = "debug", derive(Debug))]
465pub enum ParameterStyle {
466    Matrix,
467    Label,
468    Form,
469    Simple,
470    SpaceDelimited,
471    PipeDelimited,
472    DeepObject,
473}
474
475impl Parse for ParameterStyle {
476    fn parse(input: ParseStream) -> syn::Result<Self> {
477        const EXPECTED_STYLE: &str =  "unexpected style, expected one of: Matrix, Label, Form, Simple, SpaceDelimited, PipeDelimited, DeepObject";
478        let style = input.parse::<Ident>()?;
479
480        match &*style.to_string() {
481            "Matrix" => Ok(ParameterStyle::Matrix),
482            "Label" => Ok(ParameterStyle::Label),
483            "Form" => Ok(ParameterStyle::Form),
484            "Simple" => Ok(ParameterStyle::Simple),
485            "SpaceDelimited" => Ok(ParameterStyle::SpaceDelimited),
486            "PipeDelimited" => Ok(ParameterStyle::PipeDelimited),
487            "DeepObject" => Ok(ParameterStyle::DeepObject),
488            _ => Err(Error::new(style.span(), EXPECTED_STYLE)),
489        }
490    }
491}
492
493impl ToTokens for ParameterStyle {
494    fn to_tokens(&self, tokens: &mut TokenStream) {
495        match self {
496            ParameterStyle::Matrix => {
497                tokens.extend(quote! { utoipa::openapi::path::ParameterStyle::Matrix })
498            }
499            ParameterStyle::Label => {
500                tokens.extend(quote! { utoipa::openapi::path::ParameterStyle::Label })
501            }
502            ParameterStyle::Form => {
503                tokens.extend(quote! { utoipa::openapi::path::ParameterStyle::Form })
504            }
505            ParameterStyle::Simple => {
506                tokens.extend(quote! { utoipa::openapi::path::ParameterStyle::Simple })
507            }
508            ParameterStyle::SpaceDelimited => {
509                tokens.extend(quote! { utoipa::openapi::path::ParameterStyle::SpaceDelimited })
510            }
511            ParameterStyle::PipeDelimited => {
512                tokens.extend(quote! { utoipa::openapi::path::ParameterStyle::PipeDelimited })
513            }
514            ParameterStyle::DeepObject => {
515                tokens.extend(quote! { utoipa::openapi::path::ParameterStyle::DeepObject })
516            }
517        }
518    }
519}