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#[cfg_attr(feature = "debug", derive(Debug))]
46#[derive(PartialEq, Eq)]
47pub enum Parameter<'a> {
48 Value(ValueParameter<'a>),
49 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.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 = "e! { || 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 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 input as Style,
324 Explode,
325 AllowReserved,
326 Example,
327 crate::component::features::attributes::Deprecated,
328 Description,
329 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 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 parameter_in_fn: Option<TokenStream>,
423}
424
425impl 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#[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}