utoipa_gen/
ext.rs

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