utoipa_gen/
path.rs

1use std::borrow::Cow;
2use std::ops::Deref;
3use std::{io::Error, str::FromStr};
4
5use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
6use proc_macro_error::abort;
7use quote::{format_ident, quote, quote_spanned, ToTokens};
8use syn::punctuated::Punctuated;
9use syn::spanned::Spanned;
10use syn::token::Paren;
11use syn::{parenthesized, parse::Parse, Token};
12use syn::{Expr, ExprLit, Lit, LitStr, Type};
13
14use crate::component::{GenericType, TypeTree};
15use crate::path::request_body::RequestBody;
16use crate::{parse_utils, Deprecated};
17use crate::{schema_type::SchemaType, security_requirement::SecurityRequirementAttr, Array};
18
19use self::response::Response;
20use self::{parameter::Parameter, request_body::RequestBodyAttr, response::Responses};
21
22pub mod example;
23pub mod parameter;
24mod request_body;
25pub mod response;
26mod status;
27
28pub(crate) const PATH_STRUCT_PREFIX: &str = "__path_";
29
30#[derive(Default)]
31#[cfg_attr(feature = "debug", derive(Debug))]
32pub struct PathAttr<'p> {
33    path_operation: Option<PathOperation>,
34    request_body: Option<RequestBody<'p>>,
35    responses: Vec<Response<'p>>,
36    pub(super) path: Option<String>,
37    operation_id: Option<Expr>,
38    tag: Option<String>,
39    params: Vec<Parameter<'p>>,
40    security: Option<Array<'p, SecurityRequirementAttr>>,
41    context_path: Option<String>,
42}
43
44impl<'p> PathAttr<'p> {
45    #[cfg(feature = "auto_into_responses")]
46    pub fn responses_from_into_responses(&mut self, ty: &'p syn::TypePath) {
47        self.responses
48            .push(Response::IntoResponses(Cow::Borrowed(ty)))
49    }
50
51    #[cfg(any(
52        feature = "actix_extras",
53        feature = "rocket_extras",
54        feature = "axum_extras"
55    ))]
56    pub fn update_request_body(&mut self, request_body: Option<crate::ext::RequestBody<'p>>) {
57        use std::mem;
58
59        if self.request_body.is_none() {
60            self.request_body = request_body
61                .map(RequestBody::Ext)
62                .or(mem::take(&mut self.request_body));
63        }
64    }
65
66    /// Update path with external parameters from extensions.
67    #[cfg(any(
68        feature = "actix_extras",
69        feature = "rocket_extras",
70        feature = "axum_extras"
71    ))]
72    pub fn update_parameters_ext<I: IntoIterator<Item = Parameter<'p>>>(
73        &mut self,
74        ext_parameters: I,
75    ) {
76        let ext_params = ext_parameters.into_iter();
77
78        let (existing_params, new_params): (Vec<Parameter>, Vec<Parameter>) =
79            ext_params.partition(|param| self.params.iter().any(|p| p == param));
80
81        for existing in existing_params {
82            if let Some(param) = self.params.iter_mut().find(|p| **p == existing) {
83                param.merge(existing);
84            }
85        }
86
87        self.params.extend(
88            new_params
89                .into_iter()
90                .filter(|param| !matches!(param, Parameter::IntoParamsIdent(_))),
91        );
92    }
93}
94
95impl Parse for PathAttr<'_> {
96    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
97        const EXPECTED_ATTRIBUTE_MESSAGE: &str = "unexpected identifier, expected any of: operation_id, path, get, post, put, delete, options, head, patch, trace, connect, request_body, responses, params, tag, security, context_path";
98        let mut path_attr = PathAttr::default();
99
100        while !input.is_empty() {
101            let ident = input.parse::<Ident>().map_err(|error| {
102                syn::Error::new(
103                    error.span(),
104                    format!("{EXPECTED_ATTRIBUTE_MESSAGE}, {error}"),
105                )
106            })?;
107            let attribute_name = &*ident.to_string();
108
109            match attribute_name {
110                "operation_id" => {
111                    path_attr.operation_id =
112                        Some(parse_utils::parse_next(input, || Expr::parse(input))?);
113                }
114                "path" => {
115                    path_attr.path = Some(parse_utils::parse_next_literal_str(input)?);
116                }
117                "request_body" => {
118                    path_attr.request_body =
119                        Some(RequestBody::Parsed(input.parse::<RequestBodyAttr>()?));
120                }
121                "responses" => {
122                    let responses;
123                    parenthesized!(responses in input);
124                    path_attr.responses =
125                        Punctuated::<Response, Token![,]>::parse_terminated(&responses)
126                            .map(|punctuated| punctuated.into_iter().collect::<Vec<Response>>())?;
127                }
128                "params" => {
129                    let params;
130                    parenthesized!(params in input);
131                    path_attr.params =
132                        Punctuated::<Parameter, Token![,]>::parse_terminated(&params)
133                            .map(|punctuated| punctuated.into_iter().collect::<Vec<Parameter>>())?;
134                }
135                "tag" => {
136                    path_attr.tag = Some(parse_utils::parse_next_literal_str(input)?);
137                }
138                "security" => {
139                    let security;
140                    parenthesized!(security in input);
141                    path_attr.security = Some(parse_utils::parse_groups(&security)?)
142                }
143                "context_path" => {
144                    path_attr.context_path = Some(parse_utils::parse_next_literal_str(input)?)
145                }
146                _ => {
147                    // any other case it is expected to be path operation
148                    if let Some(path_operation) =
149                        attribute_name.parse::<PathOperation>().into_iter().next()
150                    {
151                        path_attr.path_operation = Some(path_operation)
152                    } else {
153                        return Err(syn::Error::new(ident.span(), EXPECTED_ATTRIBUTE_MESSAGE));
154                    }
155                }
156            }
157
158            if !input.is_empty() {
159                input.parse::<Token![,]>()?;
160            }
161        }
162
163        Ok(path_attr)
164    }
165}
166
167/// Path operation type of response
168///
169/// Instance of path operation can be formed from str parsing with following supported values:
170///   * "get"
171///   * "post"
172///   * "put"
173///   * "delete"
174///   * "options"
175///   * "head"
176///   * "patch"
177///   * "trace"
178#[cfg_attr(feature = "debug", derive(Debug))]
179pub enum PathOperation {
180    Get,
181    Post,
182    Put,
183    Delete,
184    Options,
185    Head,
186    Patch,
187    Trace,
188    Connect,
189}
190
191impl PathOperation {
192    /// Create path operation from ident
193    ///
194    /// Ident must have value of http request type as lower case string such as `get`.
195    #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))]
196    pub fn from_ident(ident: &Ident) -> Self {
197        match ident.to_string().as_str().parse::<PathOperation>() {
198            Ok(operation) => operation,
199            Err(error) => abort!(ident.span(), format!("{error}")),
200        }
201    }
202}
203
204impl FromStr for PathOperation {
205    type Err = Error;
206
207    fn from_str(s: &str) -> Result<Self, Self::Err> {
208        match s {
209            "get" => Ok(Self::Get),
210            "post" => Ok(Self::Post),
211            "put" => Ok(Self::Put),
212            "delete" => Ok(Self::Delete),
213            "options" => Ok(Self::Options),
214            "head" => Ok(Self::Head),
215            "patch" => Ok(Self::Patch),
216            "trace" => Ok(Self::Trace),
217            "connect" => Ok(Self::Connect),
218            _ => Err(Error::new(
219                std::io::ErrorKind::Other,
220                "invalid PathOperation expected one of: get, post, put, delete, options, head, patch, trace, connect",
221            )),
222        }
223    }
224}
225
226impl ToTokens for PathOperation {
227    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
228        let path_item_type = match self {
229            Self::Get => quote! { utoipa::openapi::PathItemType::Get },
230            Self::Post => quote! { utoipa::openapi::PathItemType::Post },
231            Self::Put => quote! { utoipa::openapi::PathItemType::Put },
232            Self::Delete => quote! { utoipa::openapi::PathItemType::Delete },
233            Self::Options => quote! { utoipa::openapi::PathItemType::Options },
234            Self::Head => quote! { utoipa::openapi::PathItemType::Head },
235            Self::Patch => quote! { utoipa::openapi::PathItemType::Patch },
236            Self::Trace => quote! { utoipa::openapi::PathItemType::Trace },
237            Self::Connect => quote! { utoipa::openapi::PathItemType::Connect },
238        };
239
240        tokens.extend(path_item_type);
241    }
242}
243pub struct Path<'p> {
244    path_attr: PathAttr<'p>,
245    fn_name: String,
246    path_operation: Option<PathOperation>,
247    path: Option<String>,
248    doc_comments: Option<Vec<String>>,
249    deprecated: Option<bool>,
250}
251
252impl<'p> Path<'p> {
253    pub fn new(path_attr: PathAttr<'p>, fn_name: &str) -> Self {
254        Self {
255            path_attr,
256            fn_name: fn_name.to_string(),
257            path_operation: None,
258            path: None,
259            doc_comments: None,
260            deprecated: None,
261        }
262    }
263
264    pub fn path_operation(mut self, path_operation: Option<PathOperation>) -> Self {
265        self.path_operation = path_operation;
266
267        self
268    }
269
270    pub fn path(mut self, path_provider: impl FnOnce() -> Option<String>) -> Self {
271        self.path = path_provider();
272
273        self
274    }
275
276    pub fn doc_comments(mut self, doc_comments: Vec<String>) -> Self {
277        self.doc_comments = Some(doc_comments);
278
279        self
280    }
281
282    pub fn deprecated(mut self, deprecated: Option<bool>) -> Self {
283        self.deprecated = deprecated;
284
285        self
286    }
287}
288
289impl<'p> ToTokens for Path<'p> {
290    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
291        let path_struct = format_ident!("{}{}", PATH_STRUCT_PREFIX, self.fn_name);
292        let operation_id = self
293            .path_attr
294            .operation_id
295            .clone()
296            .or(Some(ExprLit {
297                attrs: vec![],
298                lit: Lit::Str(LitStr::new(&self.fn_name, Span::call_site()))
299            }.into()))
300            .unwrap_or_else(|| {
301                abort! {
302                    Span::call_site(), "operation id is not defined for path";
303                    help = r###"Try to define it in #[utoipa::path(operation_id = {})]"###, &self.fn_name;
304                    help = "Did you define the #[utoipa::path(...)] over function?"
305                }
306            });
307        let tag = &*self
308            .path_attr
309            .tag
310            .as_ref()
311            .map(ToOwned::to_owned)
312            .unwrap_or_default();
313        let path_operation = self
314            .path_attr
315            .path_operation
316            .as_ref()
317            .or(self.path_operation.as_ref())
318            .unwrap_or_else(|| {
319                #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))]
320                let help =
321                    Some("Did you forget to define operation path attribute macro e.g #[get(...)]");
322
323                #[cfg(not(any(feature = "actix_extras", feature = "rocket_extras")))]
324                let help = None::<&str>;
325
326                abort! {
327                    Span::call_site(), "path operation is not defined for path";
328                    help = "Did you forget to define it in #[utoipa::path(get,...)]";
329                    help =? help
330                }
331            });
332
333        let path = self
334            .path_attr
335            .path
336            .as_ref()
337            .or(self.path.as_ref())
338            .unwrap_or_else(|| {
339                #[cfg(any(feature = "actix_extras", feature = "rocket_extras"))]
340                let help =
341                    Some("Did you forget to define operation path attribute macro e.g #[get(...)]");
342
343                #[cfg(not(any(feature = "actix_extras", feature = "rocket_extras")))]
344                let help = None::<&str>;
345
346                abort! {
347                    Span::call_site(), "path is not defined for path";
348                    help = r###"Did you forget to define it in #[utoipa::path(path = "...")]"###;
349                    help =? help
350                }
351            });
352
353        let path_with_context_path = self
354            .path_attr
355            .context_path
356            .as_ref()
357            .map(|context_path| format!("{context_path}{path}"))
358            .unwrap_or_else(|| path.to_string());
359
360        let operation: Operation = Operation {
361            deprecated: &self.deprecated,
362            operation_id,
363            summary: self
364                .doc_comments
365                .as_ref()
366                .and_then(|comments| comments.iter().next()),
367            description: self.doc_comments.as_ref(),
368            parameters: self.path_attr.params.as_ref(),
369            request_body: self.path_attr.request_body.as_ref(),
370            responses: self.path_attr.responses.as_ref(),
371            security: self.path_attr.security.as_ref(),
372        };
373
374        tokens.extend(quote! {
375            #[allow(non_camel_case_types)]
376            #[doc(hidden)]
377            pub struct #path_struct;
378
379            impl utoipa::Path for #path_struct {
380                fn path() -> &'static str {
381                    #path_with_context_path
382                }
383
384                fn path_item(default_tag: Option<&str>) -> utoipa::openapi::path::PathItem {
385                    use utoipa::openapi::ToArray;
386                    use std::iter::FromIterator;
387                    utoipa::openapi::PathItem::new(
388                        #path_operation,
389                        #operation.tag(*[Some(#tag), default_tag, Some("crate")].iter()
390                            .flatten()
391                            .find(|t| !t.is_empty()).unwrap()
392                        )
393                    )
394                }
395            }
396        });
397    }
398}
399
400#[cfg_attr(feature = "debug", derive(Debug))]
401struct Operation<'a> {
402    operation_id: Expr,
403    summary: Option<&'a String>,
404    description: Option<&'a Vec<String>>,
405    deprecated: &'a Option<bool>,
406    parameters: &'a Vec<Parameter<'a>>,
407    request_body: Option<&'a RequestBody<'a>>,
408    responses: &'a Vec<Response<'a>>,
409    security: Option<&'a Array<'a, SecurityRequirementAttr>>,
410}
411
412impl ToTokens for Operation<'_> {
413    fn to_tokens(&self, tokens: &mut TokenStream2) {
414        tokens.extend(quote! { utoipa::openapi::path::OperationBuilder::new() });
415
416        if let Some(request_body) = self.request_body {
417            tokens.extend(quote! {
418                .request_body(Some(#request_body))
419            })
420        }
421
422        let responses = Responses(self.responses);
423        tokens.extend(quote! {
424            .responses(#responses)
425        });
426        if let Some(security_requirements) = self.security {
427            tokens.extend(quote! {
428                .securities(Some(#security_requirements))
429            })
430        }
431        let operation_id = &self.operation_id;
432        tokens.extend(quote_spanned! { operation_id.span() =>
433            .operation_id(Some(#operation_id))
434        });
435
436        if let Some(deprecated) = self.deprecated.map(Into::<Deprecated>::into) {
437            tokens.extend(quote!( .deprecated(Some(#deprecated))))
438        }
439
440        if let Some(summary) = self.summary {
441            tokens.extend(quote! {
442                .summary(Some(#summary))
443            })
444        }
445
446        if let Some(description) = self.description {
447            let description = description.join("\n");
448
449            if !description.is_empty() {
450                tokens.extend(quote! {
451                    .description(Some(#description))
452                })
453            }
454        }
455
456        self.parameters
457            .iter()
458            .for_each(|parameter| parameter.to_tokens(tokens));
459    }
460}
461
462/// Represents either `ref("...")` or `Type` that can be optionally inlined with `inline(Type)`.
463#[cfg_attr(feature = "debug", derive(Debug))]
464enum PathType<'p> {
465    Ref(String),
466    MediaType(InlineType<'p>),
467    InlineSchema(TokenStream2, Type),
468}
469
470impl Parse for PathType<'_> {
471    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
472        let fork = input.fork();
473        let is_ref = if (fork.parse::<Option<Token![ref]>>()?).is_some() {
474            fork.peek(Paren)
475        } else {
476            false
477        };
478
479        if is_ref {
480            input.parse::<Token![ref]>()?;
481            let ref_stream;
482            parenthesized!(ref_stream in input);
483            Ok(Self::Ref(ref_stream.parse::<LitStr>()?.value()))
484        } else {
485            Ok(Self::MediaType(input.parse()?))
486        }
487    }
488}
489
490// inline(syn::Type) | syn::Type
491#[cfg_attr(feature = "debug", derive(Debug))]
492struct InlineType<'i> {
493    ty: Cow<'i, Type>,
494    is_inline: bool,
495}
496
497impl InlineType<'_> {
498    /// Get's the underlying [`syn::Type`] as [`TypeTree`].
499    fn as_type_tree(&self) -> TypeTree {
500        TypeTree::from_type(&self.ty)
501    }
502}
503
504impl Parse for InlineType<'_> {
505    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
506        let fork = input.fork();
507        let is_inline = if let Some(ident) = fork.parse::<Option<Ident>>()? {
508            ident == "inline" && fork.peek(Paren)
509        } else {
510            false
511        };
512
513        let ty = if is_inline {
514            input.parse::<Ident>()?;
515            let inlined;
516            parenthesized!(inlined in input);
517
518            inlined.parse::<Type>()?
519        } else {
520            input.parse::<Type>()?
521        };
522
523        Ok(InlineType {
524            ty: Cow::Owned(ty),
525            is_inline,
526        })
527    }
528}
529
530pub trait PathTypeTree {
531    /// Resolve default content type based on current [`Type`].
532    fn get_default_content_type(&self) -> &str;
533
534    /// Check whether [`TypeTree`] an option
535    fn is_option(&self) -> bool;
536
537    /// Check whether [`TypeTree`] is a Vec, slice, array or other supported array type
538    fn is_array(&self) -> bool;
539}
540
541impl PathTypeTree for TypeTree<'_> {
542    /// Resolve default content type based on current [`Type`].
543    fn get_default_content_type(&self) -> &'static str {
544        if self.is_array()
545            && self
546                .children
547                .as_ref()
548                .map(|children| {
549                    children
550                        .iter()
551                        .flat_map(|child| &child.path)
552                        .any(|path| SchemaType(path).is_byte())
553                })
554                .unwrap_or(false)
555        {
556            "application/octet-stream"
557        } else if self
558            .path
559            .as_ref()
560            .map(|path| SchemaType(path.deref()))
561            .map(|schema_type| schema_type.is_primitive())
562            .unwrap_or(false)
563        {
564            "text/plain"
565        } else {
566            "application/json"
567        }
568    }
569
570    /// Check whether [`TypeTree`] an option
571    fn is_option(&self) -> bool {
572        matches!(self.generic_type, Some(GenericType::Option))
573    }
574
575    /// Check whether [`TypeTree`] is a Vec, slice, array or other supported array type
576    fn is_array(&self) -> bool {
577        match self.generic_type {
578            Some(GenericType::Vec) => true,
579            Some(_) => self
580                .children
581                .as_ref()
582                .unwrap()
583                .iter()
584                .any(|child| child.is_array()),
585            None => false,
586        }
587    }
588}