utoipa_gen/
ext.rs

1use std::borrow::Cow;
2
3#[cfg(feature = "rocket_extras")]
4use std::cmp::Ordering;
5
6use proc_macro2::TokenStream;
7use quote::{quote, quote_spanned, ToTokens};
8use syn::parse_quote;
9use syn::spanned::Spanned;
10use syn::{punctuated::Punctuated, token::Comma, ItemFn};
11
12use crate::component::{ComponentSchema, ComponentSchemaProps, TypeTree};
13use crate::path::{PathOperation, PathTypeTree};
14
15#[cfg(feature = "auto_into_responses")]
16pub mod auto_types;
17
18#[cfg(feature = "actix_extras")]
19pub mod actix;
20
21#[cfg(feature = "axum_extras")]
22pub mod axum;
23
24#[cfg(feature = "rocket_extras")]
25pub mod rocket;
26
27/// Represents single argument of handler operation.
28#[cfg_attr(
29    not(any(
30        feature = "actix_extras",
31        feature = "rocket_extras",
32        feature = "axum_extras"
33    )),
34    allow(dead_code)
35)]
36#[cfg_attr(feature = "debug", derive(Debug))]
37pub struct ValueArgument<'a> {
38    pub name: Option<Cow<'a, str>>,
39    #[cfg(any(
40        feature = "actix_extras",
41        feature = "rocket_extras",
42        feature = "axum_extras"
43    ))]
44    pub argument_in: ArgumentIn,
45    pub type_tree: Option<TypeTree<'a>>,
46}
47
48#[cfg(feature = "actix_extras")]
49impl<'v> From<(MacroArg, TypeTree<'v>)> for ValueArgument<'v> {
50    fn from((macro_arg, primitive_arg): (MacroArg, TypeTree<'v>)) -> Self {
51        Self {
52            name: match macro_arg {
53                MacroArg::Path(path) => Some(Cow::Owned(path.name)),
54            },
55            type_tree: Some(primitive_arg),
56            argument_in: ArgumentIn::Path,
57        }
58    }
59}
60
61#[cfg_attr(
62    not(any(
63        feature = "actix_extras",
64        feature = "rocket_extras",
65        feature = "axum_extras"
66    )),
67    allow(dead_code)
68)]
69/// Represents Identifier with `parameter_in` provider function which is used to
70/// update the `parameter_in` to [`Parameter::Struct`].
71#[cfg_attr(feature = "debug", derive(Debug))]
72pub struct IntoParamsType<'a> {
73    pub parameter_in_provider: TokenStream,
74    pub type_path: Option<Cow<'a, syn::Path>>,
75}
76
77impl<'i> From<(Option<Cow<'i, syn::Path>>, TokenStream)> for IntoParamsType<'i> {
78    fn from((type_path, parameter_in_provider): (Option<Cow<'i, syn::Path>>, TokenStream)) -> Self {
79        IntoParamsType {
80            parameter_in_provider,
81            type_path,
82        }
83    }
84}
85
86#[cfg(any(
87    feature = "actix_extras",
88    feature = "rocket_extras",
89    feature = "axum_extras"
90))]
91#[cfg_attr(feature = "debug", derive(Debug))]
92#[derive(PartialEq, Eq)]
93pub enum ArgumentIn {
94    Path,
95    #[cfg(feature = "rocket_extras")]
96    Query,
97}
98
99#[cfg_attr(feature = "debug", derive(Debug))]
100pub struct RequestBody<'r> {
101    ty: TypeTree<'r>,
102}
103
104impl<'t> From<TypeTree<'t>> for RequestBody<'t> {
105    fn from(value: TypeTree<'t>) -> RequestBody<'t> {
106        Self { ty: value }
107    }
108}
109
110impl ToTokens for RequestBody<'_> {
111    fn to_tokens(&self, tokens: &mut TokenStream) {
112        let mut actual_body = get_actual_body_type(&self.ty).unwrap().clone();
113
114        if let Some(option) = find_option_type_tree(&self.ty) {
115            let path = option.path.clone();
116            actual_body = TypeTree {
117                children: Some(vec![actual_body]),
118                generic_type: Some(crate::component::GenericType::Option),
119                value_type: crate::component::ValueType::Object,
120                span: Some(path.span()),
121                path,
122            }
123        };
124
125        let required = if actual_body.is_option() {
126            quote!(utoipa::openapi::Required::False)
127        } else {
128            quote!(utoipa::openapi::Required::True)
129        };
130
131        let mut create_body_tokens = |content_type: &str, actual_body: &TypeTree| {
132            let schema = ComponentSchema::new(ComponentSchemaProps {
133                type_tree: actual_body,
134                features: None,
135                description: None,
136                deprecated: None,
137                object_name: "",
138            });
139
140            tokens.extend(quote_spanned! {actual_body.span.unwrap()=>
141                utoipa::openapi::request_body::RequestBodyBuilder::new()
142                    .content(#content_type,
143                        utoipa::openapi::content::Content::new(#schema)
144                    )
145                    .required(Some(#required))
146                    .description(Some(""))
147                    .build()
148            })
149        };
150
151        if self.ty.is("Bytes") {
152            let bytes_as_bytes_vec = parse_quote!(Vec<u8>);
153            let ty = TypeTree::from_type(&bytes_as_bytes_vec);
154            create_body_tokens("application/octet-stream", &ty);
155        } else if self.ty.is("Form") {
156            create_body_tokens("application/x-www-form-urlencoded", &actual_body);
157        } else {
158            create_body_tokens(actual_body.get_default_content_type(), &actual_body);
159        };
160    }
161}
162
163fn get_actual_body_type<'t>(ty: &'t TypeTree<'t>) -> Option<&'t TypeTree<'t>> {
164    ty.path
165        .as_deref()
166        .expect("RequestBody TypeTree must have syn::Path")
167        .segments
168        .iter()
169        .find_map(|segment| match &*segment.ident.to_string() {
170            "Json" => Some(
171                ty.children
172                    .as_deref()
173                    .expect("Json must have children")
174                    .first()
175                    .expect("Json must have one child"),
176            ),
177            "Form" => Some(
178                ty.children
179                    .as_deref()
180                    .expect("Form must have children")
181                    .first()
182                    .expect("Form must have one child"),
183            ),
184            "Option" => get_actual_body_type(
185                ty.children
186                    .as_deref()
187                    .expect("Option must have children")
188                    .first()
189                    .expect("Option must have one child"),
190            ),
191            "Bytes" => Some(ty),
192            _ => None,
193        })
194}
195
196fn find_option_type_tree<'t>(ty: &'t TypeTree) -> Option<&'t TypeTree<'t>> {
197    let eq = ty.generic_type == Some(crate::component::GenericType::Option);
198
199    if !eq {
200        ty.children
201            .as_ref()
202            .and_then(|children| children.iter().find_map(find_option_type_tree))
203    } else {
204        Some(ty)
205    }
206}
207
208#[cfg_attr(feature = "debug", derive(Debug))]
209pub struct MacroPath {
210    pub path: String,
211    pub args: Vec<MacroArg>,
212}
213
214#[cfg_attr(feature = "debug", derive(Debug))]
215pub enum MacroArg {
216    #[cfg_attr(
217        not(any(feature = "actix_extras", feature = "rocket_extras")),
218        allow(dead_code)
219    )]
220    Path(ArgValue),
221    #[cfg(feature = "rocket_extras")]
222    Query(ArgValue),
223}
224
225impl MacroArg {
226    /// Get ordering by name
227    #[cfg(feature = "rocket_extras")]
228    fn by_name(a: &MacroArg, b: &MacroArg) -> Ordering {
229        a.get_value().name.cmp(&b.get_value().name)
230    }
231
232    #[cfg(feature = "rocket_extras")]
233    fn get_value(&self) -> &ArgValue {
234        match self {
235            MacroArg::Path(path) => path,
236            MacroArg::Query(query) => query,
237        }
238    }
239}
240
241#[derive(PartialEq, Eq, PartialOrd, Ord)]
242#[cfg_attr(feature = "debug", derive(Debug))]
243pub struct ArgValue {
244    pub name: String,
245    pub original_name: String,
246}
247
248#[cfg_attr(feature = "debug", derive(Debug))]
249pub struct ResolvedOperation {
250    pub path_operation: PathOperation,
251    pub path: String,
252    pub body: String,
253}
254
255pub trait ArgumentResolver {
256    fn resolve_arguments(
257        _: &'_ Punctuated<syn::FnArg, Comma>,
258        _: Option<Vec<MacroArg>>,
259        _: String,
260    ) -> (
261        Option<Vec<ValueArgument<'_>>>,
262        Option<Vec<IntoParamsType<'_>>>,
263        Option<RequestBody<'_>>,
264    ) {
265        (None, None, None)
266    }
267}
268
269pub trait PathResolver {
270    fn resolve_path(_: &Option<String>) -> Option<MacroPath> {
271        None
272    }
273}
274
275pub trait PathOperationResolver {
276    fn resolve_operation(_: &ItemFn) -> Option<ResolvedOperation> {
277        None
278    }
279}
280
281pub struct PathOperations;
282
283#[cfg(not(any(
284    feature = "actix_extras",
285    feature = "rocket_extras",
286    feature = "axum_extras"
287)))]
288impl ArgumentResolver for PathOperations {}
289
290#[cfg(not(any(
291    feature = "actix_extras",
292    feature = "rocket_extras",
293    feature = "axum_extras"
294)))]
295impl PathResolver for PathOperations {}
296
297#[cfg(not(any(feature = "actix_extras", feature = "rocket_extras")))]
298impl PathOperationResolver for PathOperations {}
299
300#[cfg(any(
301    feature = "actix_extras",
302    feature = "axum_extras",
303    feature = "rocket_extras"
304))]
305pub mod fn_arg {
306
307    use proc_macro2::Ident;
308    use proc_macro_error::abort;
309    #[cfg(any(feature = "actix_extras", feature = "axum_extras"))]
310    use quote::quote;
311    use syn::PatStruct;
312    use syn::{punctuated::Punctuated, token::Comma, Pat, PatType};
313
314    use crate::component::TypeTree;
315    #[cfg(any(feature = "actix_extras", feature = "axum_extras"))]
316    use crate::component::ValueType;
317
318    /// Http operation handler functions fn argument.
319    #[cfg_attr(feature = "debug", derive(Debug))]
320    pub struct FnArg<'a> {
321        pub(super) ty: TypeTree<'a>,
322        pub(super) arg_type: FnArgType<'a>,
323    }
324
325    #[cfg_attr(feature = "debug", derive(Debug))]
326    #[derive(PartialEq, Eq, PartialOrd, Ord)]
327    pub enum FnArgType<'t> {
328        Single(&'t Ident),
329        Destructed(Vec<&'t Ident>),
330    }
331
332    impl FnArgType<'_> {
333        /// Get best effort name `Ident` for the type. For `FnArgType::Tuple` types it will take the first one
334        /// from `Vec`.
335        #[cfg(feature = "rocket_extras")]
336        pub(super) fn get_name(&self) -> &Ident {
337            match self {
338                Self::Single(ident) => ident,
339                // perform best effort name, by just taking the first one from the list
340                Self::Destructed(tuple) => tuple
341                    .first()
342                    .expect("Expected at least one argument in FnArgType::Tuple"),
343            }
344        }
345    }
346
347    impl<'a> From<(TypeTree<'a>, FnArgType<'a>)> for FnArg<'a> {
348        fn from((ty, arg_type): (TypeTree<'a>, FnArgType<'a>)) -> Self {
349            Self { ty, arg_type }
350        }
351    }
352
353    impl<'a> Ord for FnArg<'a> {
354        fn cmp(&self, other: &Self) -> std::cmp::Ordering {
355            self.arg_type.cmp(&other.arg_type)
356        }
357    }
358
359    impl<'a> PartialOrd for FnArg<'a> {
360        fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
361            self.arg_type.partial_cmp(&other.arg_type)
362        }
363    }
364
365    impl<'a> PartialEq for FnArg<'a> {
366        fn eq(&self, other: &Self) -> bool {
367            self.ty == other.ty && self.arg_type == other.arg_type
368        }
369    }
370
371    impl<'a> Eq for FnArg<'a> {}
372
373    pub fn get_fn_args(fn_args: &Punctuated<syn::FnArg, Comma>) -> impl Iterator<Item = FnArg<'_>> {
374        fn_args
375            .iter()
376            .filter_map(|arg| {
377                let pat_type = get_fn_arg_pat_type(arg);
378
379                match pat_type.pat.as_ref() {
380                    syn::Pat::Wild(_) => None,
381                    _ => {
382                        let arg_name = get_pat_fn_arg_type(pat_type.pat.as_ref());
383                        Some((TypeTree::from_type(&pat_type.ty), arg_name))
384                    }
385                }
386            })
387            .map(FnArg::from)
388    }
389
390    #[inline]
391    fn get_pat_fn_arg_type(pat: &Pat) -> FnArgType {
392        let arg_name = match pat {
393            syn::Pat::Ident(ident) => FnArgType::Single(&ident.ident),
394            syn::Pat::Tuple(tuple) => {
395                FnArgType::Destructed(tuple.elems.iter().map(|item| {
396                    match item {
397                        syn::Pat::Ident(ident) => &ident.ident,
398                        _ => abort!(item, "expected syn::Ident in get_pat_fn_arg_type Pat::Tuple")
399                    }
400                }).collect::<Vec<_>>())
401            },
402            syn::Pat::TupleStruct(tuple_struct) => {
403                get_pat_fn_arg_type(tuple_struct.elems.first().as_ref().expect(
404                    "PatTuple expected to have at least one element, cannot get fn argument",
405                ))
406            },
407            syn::Pat::Struct(PatStruct { fields, ..}) => {
408                let idents = fields.iter()
409                    .map(|field| get_pat_fn_arg_type(&field.pat))
410                    .fold(Vec::<&'_ Ident>::new(), |mut idents, field_type| {
411                        if let FnArgType::Single(ident) = field_type {
412                            idents.push(ident)
413                        }
414                        idents
415                    });
416
417                FnArgType::Destructed(idents)
418            }
419            _ => abort!(pat,
420                "unexpected syn::Pat, expected syn::Pat::Ident,in get_fn_args, cannot get fn argument name"
421            ),
422        };
423        arg_name
424    }
425
426    #[inline]
427    fn get_fn_arg_pat_type(fn_arg: &syn::FnArg) -> &PatType {
428        match fn_arg {
429            syn::FnArg::Typed(value) => value,
430            _ => abort!(fn_arg, "unexpected fn argument type, expected FnArg::Typed"),
431        }
432    }
433
434    #[cfg(any(feature = "actix_extras", feature = "axum_extras"))]
435    pub(super) fn with_parameter_in(
436        arg: FnArg<'_>,
437    ) -> Option<(
438        Option<std::borrow::Cow<'_, syn::Path>>,
439        proc_macro2::TokenStream,
440    )> {
441        let parameter_in_provider = if arg.ty.is("Path") {
442            quote! { || Some (utoipa::openapi::path::ParameterIn::Path) }
443        } else if arg.ty.is("Query") {
444            quote! { || Some(utoipa::openapi::path::ParameterIn::Query) }
445        } else {
446            quote! { || None }
447        };
448
449        let type_path = arg
450            .ty
451            .children
452            .expect("FnArg TypeTree generic type Path must have children")
453            .into_iter()
454            .next()
455            .unwrap()
456            .path;
457
458        Some((type_path, parameter_in_provider))
459    }
460
461    // if type is either Path or Query with direct children as Object types without generics
462    #[cfg(any(feature = "actix_extras", feature = "axum_extras"))]
463    pub(super) fn is_into_params(fn_arg: &FnArg) -> bool {
464        use crate::component::GenericType;
465        let mut ty = &fn_arg.ty;
466
467        if fn_arg.ty.generic_type == Some(GenericType::Option) {
468            ty = fn_arg
469                .ty
470                .children
471                .as_ref()
472                .expect("FnArg Option must have children")
473                .first()
474                .expect("FnArg Option must have 1 child");
475        }
476
477        (ty.is("Path") || ty.is("Query"))
478            && ty
479                .children
480                .as_ref()
481                .map(|children| {
482                    children.iter().all(|child| {
483                        matches!(child.value_type, ValueType::Object)
484                            && matches!(child.generic_type, None)
485                    })
486                })
487                .unwrap_or(false)
488    }
489}