utoipa_gen/ext/
actix.rs

1use std::borrow::Cow;
2
3use proc_macro2::Ident;
4use proc_macro_error::abort;
5use regex::{Captures, Regex};
6use syn::{parse::Parse, punctuated::Punctuated, token::Comma, ItemFn, LitStr};
7
8use crate::{
9    component::{TypeTree, ValueType},
10    ext::ArgValue,
11    path::PathOperation,
12};
13
14use super::{
15    fn_arg::{self, FnArg},
16    ArgumentIn, ArgumentResolver, MacroArg, MacroPath, PathOperationResolver, PathOperations,
17    PathResolver, ResolvedOperation, ValueArgument,
18};
19
20impl ArgumentResolver for PathOperations {
21    fn resolve_arguments(
22        fn_args: &Punctuated<syn::FnArg, Comma>,
23        macro_args: Option<Vec<MacroArg>>,
24        _: String,
25    ) -> (
26        Option<Vec<super::ValueArgument<'_>>>,
27        Option<Vec<super::IntoParamsType<'_>>>,
28        Option<super::RequestBody<'_>>,
29    ) {
30        let (into_params_args, value_args): (Vec<FnArg>, Vec<FnArg>) =
31            fn_arg::get_fn_args(fn_args).partition(fn_arg::is_into_params);
32
33        if let Some(macro_args) = macro_args {
34            let (primitive_args, body) = split_path_args_and_request(value_args);
35
36            (
37                Some(
38                    macro_args
39                        .into_iter()
40                        .zip(primitive_args)
41                        .map(into_value_argument)
42                        .collect(),
43                ),
44                Some(
45                    into_params_args
46                        .into_iter()
47                        .flat_map(fn_arg::with_parameter_in)
48                        .map(Into::into)
49                        .collect(),
50                ),
51                body.into_iter().next().map(Into::into),
52            )
53        } else {
54            let (_, body) = split_path_args_and_request(value_args);
55            (
56                None,
57                Some(
58                    into_params_args
59                        .into_iter()
60                        .flat_map(fn_arg::with_parameter_in)
61                        .map(Into::into)
62                        .collect(),
63                ),
64                body.into_iter().next().map(Into::into),
65            )
66        }
67    }
68}
69
70fn split_path_args_and_request(
71    value_args: Vec<FnArg>,
72) -> (
73    impl Iterator<Item = TypeTree>,
74    impl Iterator<Item = TypeTree>,
75) {
76    let (path_args, body_types): (Vec<FnArg>, Vec<FnArg>) = value_args
77        .into_iter()
78        .filter(|arg| {
79            arg.ty.is("Path") || arg.ty.is("Json") || arg.ty.is("Form") || arg.ty.is("Bytes")
80        })
81        .partition(|arg| arg.ty.is("Path"));
82
83    (
84        path_args
85            .into_iter()
86            .flat_map(|path_arg| {
87                path_arg
88                    .ty
89                    .children
90                    .expect("Path argument must have children")
91            })
92            .flat_map(|path_arg| match path_arg.value_type {
93                ValueType::Primitive => vec![path_arg],
94                ValueType::Tuple => path_arg
95                    .children
96                    .expect("ValueType::Tuple will always have children"),
97                ValueType::Object | ValueType::Value => {
98                    unreachable!("Value arguments does not have ValueType::Object arguments")
99                }
100            }),
101        body_types.into_iter().map(|json| json.ty),
102    )
103}
104
105fn into_value_argument((macro_arg, primitive_arg): (MacroArg, TypeTree)) -> ValueArgument {
106    ValueArgument {
107        name: match macro_arg {
108            MacroArg::Path(path) => Some(Cow::Owned(path.name)),
109        },
110        type_tree: Some(primitive_arg),
111        argument_in: ArgumentIn::Path,
112    }
113}
114
115impl PathOperationResolver for PathOperations {
116    fn resolve_operation(item_fn: &ItemFn) -> Option<ResolvedOperation> {
117        item_fn.attrs.iter().find_map(|attribute| {
118            if is_valid_request_type(attribute.path().get_ident()) {
119                match attribute.parse_args::<Path>() {
120                    Ok(path) => Some(ResolvedOperation {
121                        path: path.0,
122                        path_operation: PathOperation::from_ident(
123                            attribute.path().get_ident().unwrap(),
124                        ),
125                        body: String::new(),
126                    }),
127                    Err(error) => abort!(
128                        error.span(),
129                        "parse path of path operation attribute: {}",
130                        error
131                    ),
132                }
133            } else {
134                None
135            }
136        })
137    }
138}
139
140struct Path(String);
141
142impl Parse for Path {
143    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
144        let path = input.parse::<LitStr>()?.value();
145
146        // ignore rest of the tokens from actix-web path attribute macro
147        input.step(|cursor| {
148            let mut rest = *cursor;
149            while let Some((_, next)) = rest.token_tree() {
150                rest = next;
151            }
152            Ok(((), rest))
153        })?;
154
155        Ok(Self(path))
156    }
157}
158
159impl PathResolver for PathOperations {
160    fn resolve_path(path: &Option<String>) -> Option<MacroPath> {
161        path.as_ref().map(|path| {
162            let regex = Regex::new(r"\{[a-zA-Z0-9_][^{}]*}").unwrap();
163
164            let mut args = Vec::<MacroArg>::with_capacity(regex.find_iter(path).count());
165            MacroPath {
166                path: regex
167                    .replace_all(path, |captures: &Captures| {
168                        let mut capture = &captures[0];
169                        let original_name = String::from(capture);
170
171                        if capture.contains("_:") {
172                            // replace unnamed capture with generic 'arg0' name
173                            args.push(MacroArg::Path(ArgValue {
174                                name: String::from("arg0"),
175                                original_name,
176                            }));
177                            "{arg0}".to_string()
178                        } else if let Some(colon) = capture.find(':') {
179                            //  replace colon (:) separated regexp with empty string
180                            capture = &capture[1..colon];
181
182                            args.push(MacroArg::Path(ArgValue {
183                                name: String::from(capture),
184                                original_name,
185                            }));
186
187                            format!("{{{capture}}}")
188                        } else {
189                            args.push(MacroArg::Path(ArgValue {
190                                name: String::from(&capture[1..capture.len() - 1]),
191                                original_name,
192                            }));
193                            // otherwise return the capture itself
194                            capture.to_string()
195                        }
196                    })
197                    .to_string(),
198                args,
199            }
200        })
201    }
202}
203
204#[inline]
205fn is_valid_request_type(ident: Option<&Ident>) -> bool {
206    matches!(ident, Some(operation) if ["get", "post", "put", "delete", "head", "connect", "options", "trace", "patch"]
207        .iter().any(|expected_operation| operation == expected_operation))
208}