aboutsummaryrefslogtreecommitdiff
path: root/src/api/router_macros.rs
blob: 959e69a3f7526f314f660e786112beeace0aa7c1 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
/// This macro is used to generate very repetitive match {} blocks in this module
/// It is _not_ made to be used anywhere else
macro_rules! router_match {
    (@match $enum:expr , [ $($endpoint:ident,)* ]) => {{
        // usage: router_match {@match my_enum, [ VariantWithField1, VariantWithField2 ..] }
        // returns true if the variant was one of the listed variants, false otherwise.
        match $enum {
            $(
            Endpoint::$endpoint { .. } => true,
            )*
            _ => false
        }
    }};
    (@extract $enum:expr , $param:ident, [ $($endpoint:ident,)* ]) => {{
        // usage: router_match {@extract my_enum, field_name, [ VariantWithField1, VariantWithField2 ..] }
        // returns Some(field_value), or None if the variant was not one of the listed variants.
        match $enum {
            $(
            Endpoint::$endpoint {$param, ..} => Some($param),
            )*
            _ => None
        }
    }};
    (@gen_path_parser ($method:expr, $reqpath:expr, $query:expr)
     [
     $($meth:ident $path:pat $(if $required:ident)? => $api:ident $(($($conv:ident :: $param:ident),*))?,)*
     ]) => {{
        {
            match ($method, $reqpath) {
                $(
                    (&Method::$meth, $path) if true $(&& $query.$required.is_some())? => Endpoint::$api {
                        $($(
                            $param: router_match!(@@parse_param $query, $conv, $param),
                        )*)?
                    },
                )*
                (m, p) => {
                    return Err(Error::bad_request(format!(
                        "Unknown API endpoint: {} {}",
                        m, p
                    )))
                }
            }
        }
    }};
    (@gen_parser ($keyword:expr, $key:ident, $query:expr, $header:expr),
        key: [$($kw_k:ident $(if $required_k:ident)? $(header $header_k:expr)? => $api_k:ident $(($($conv_k:ident :: $param_k:ident),*))?,)*],
        no_key: [$($kw_nk:ident $(if $required_nk:ident)? $(if_header $header_nk:expr)? => $api_nk:ident $(($($conv_nk:ident :: $param_nk:ident),*))?,)*]) => {{
        // usage: router_match {@gen_parser (keyword, key, query, header),
        //   key: [
        //      SOME_KEYWORD => VariantWithKey,
        //      ...
        //   ],
        //   no_key: [
        //      SOME_KEYWORD => VariantWithoutKey,
        //      ...
        //   ]
        // }
        // See in from_{method} for more detailed usage.
        match ($keyword, !$key.is_empty()){
            $(
            (Keyword::$kw_k, true) if true $(&& $query.$required_k.is_some())? $(&& $header.contains_key($header_k))? => Ok(Endpoint::$api_k {
                $key,
                $($(
                    $param_k: router_match!(@@parse_param $query, $conv_k, $param_k),
                )*)?
            }),
            )*
            $(
            (Keyword::$kw_nk, false) $(if $query.$required_nk.is_some())? $(if $header.contains($header_nk))? => Ok(Endpoint::$api_nk {
                $($(
                    $param_nk: router_match!(@@parse_param $query, $conv_nk, $param_nk),
                )*)?
            }),
            )*
            (kw, _) => Err(Error::bad_request(format!("Invalid endpoint: {}", kw)))
        }
    }};

    (@@parse_param $query:expr, query_opt, $param:ident) => {{
        // extract optional query parameter
        $query.$param.take().map(|param| param.into_owned())
    }};
    (@@parse_param $query:expr, query, $param:ident) => {{
        // extract mendatory query parameter
        $query.$param.take().ok_or_bad_request("Missing argument for endpoint")?.into_owned()
    }};
    (@@parse_param $query:expr, opt_parse, $param:ident) => {{
        // extract and parse optional query parameter
        // missing parameter is file, however parse error is reported as an error
        $query.$param
            .take()
            .map(|param| param.parse())
            .transpose()
            .map_err(|_| Error::bad_request("Failed to parse query parameter"))?
    }};
    (@@parse_param $query:expr, parse, $param:ident) => {{
        // extract and parse mandatory query parameter
        // both missing and un-parseable parameters are reported as errors
        $query.$param.take().ok_or_bad_request("Missing argument for endpoint")?
            .parse()
            .map_err(|_| Error::bad_request("Failed to parse query parameter"))?
    }};
    (@func
    $(#[$doc:meta])*
     pub enum Endpoint {
        $(
            $(#[$outer:meta])*
            $variant:ident $({
                $($name:ident: $ty:ty,)*
            })?,
        )*
    }) => {
    $(#[$doc])*
        pub enum Endpoint {
            $(
                $(#[$outer])*
                $variant $({
                    $($name: $ty, )*
                })?,
            )*
        }
        impl Endpoint {
            pub fn name(&self) -> &'static str {
                match self {
                    $(Endpoint::$variant $({ $($name: _,)* .. })? => stringify!($variant),)*
                }
            }
        }
    };
    (@if ($($cond:tt)+) then ($($then:tt)*) else ($($else:tt)*)) => {
        $($then)*
    };
    (@if () then ($($then:tt)*) else ($($else:tt)*)) => {
        $($else)*
    };
}

/// This macro is used to generate part of the code in this module. It must be called only one, and
/// is useless outside of this module.
macro_rules! generateQueryParameters {
    (
        keywords: [ $($kw_param:expr => $kw_name: ident),* ],
        fields: [ $($f_param:expr => $f_name:ident),* ]
    ) => {
        #[derive(Debug)]
        #[allow(non_camel_case_types)]
        enum Keyword {
            EMPTY,
            $( $kw_name, )*
        }

        impl std::fmt::Display for Keyword {
            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                match self {
                    Keyword::EMPTY => write!(f, "``"),
                    $( Keyword::$kw_name => write!(f, "`{}`", $kw_param), )*
                }
            }
        }

        impl Default for Keyword {
            fn default() -> Self {
                Keyword::EMPTY
            }
        }

        /// Struct containing all query parameters used in endpoints. Think of it as an HashMap,
        /// but with keys statically known.
        #[derive(Debug, Default)]
        struct QueryParameters<'a> {
            keyword: Option<Keyword>,
            $(
            $f_name: Option<Cow<'a, str>>,
            )*
        }

        impl<'a> QueryParameters<'a> {
            /// Build this struct from the query part of an URI.
            fn from_query(query: &'a str) -> Result<Self, Error> {
                let mut res: Self = Default::default();
                for (k, v) in url::form_urlencoded::parse(query.as_bytes()) {
                    match k.as_ref() {
                        $(
                            $kw_param => if let Some(prev_kw) = res.keyword.replace(Keyword::$kw_name) {
                                return Err(Error::bad_request(format!(
                                    "Multiple keywords: '{}' and '{}'", prev_kw, $kw_param
                                )));
                            },
                        )*
                        $(
                            $f_param => if !v.is_empty() {
                                if res.$f_name.replace(v).is_some() {
                                    return Err(Error::bad_request(format!(
                                        "Query parameter repeated: '{}'", k
                                    )));
                                }
                            },
                        )*
                        _ => {
                            if !(k.starts_with("response-") || k.starts_with("X-Amz-")) {
                                debug!("Received an unknown query parameter: '{}'", k);
                            }
                        }
                    };
                }
                Ok(res)
            }

            /// Get an error message in case not all parameters where used when extracting them to
            /// build an Enpoint variant
            fn nonempty_message(&self) -> Option<&str> {
                if self.keyword.is_some() {
                    Some("Keyword not used")
                } $(
                    else if self.$f_name.is_some() {
                        Some(concat!("'", $f_param, "'"))
                    }
                )* else {
                    None
                }
            }
        }
    }
}

pub(crate) use generateQueryParameters;
pub(crate) use router_match;