utoipa_gen/path/
parameter.rs

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