aboutsummaryrefslogtreecommitdiff
path: root/src/api/common/router_macros.rs
diff options
context:
space:
mode:
authorAlex Auvolat <lx@deuxfleurs.fr>2025-02-01 19:07:17 +0100
committerAlex Auvolat <lx@deuxfleurs.fr>2025-02-01 19:07:17 +0100
commitfe937c290195e5451f9e533e02664548817e6e13 (patch)
tree318ba12ff76dc3e435a01a246188c6a964e572f2 /src/api/common/router_macros.rs
parent3192088aac0e1795401304a8dec715cd343538cf (diff)
parentd601f311865b8159a7bf1801dd8f43021d0b443b (diff)
downloadgarage-fe937c290195e5451f9e533e02664548817e6e13.tar.gz
garage-fe937c290195e5451f9e533e02664548817e6e13.zip
Merge branch 'main' into next-v2
Diffstat (limited to 'src/api/common/router_macros.rs')
-rw-r--r--src/api/common/router_macros.rs304
1 files changed, 304 insertions, 0 deletions
diff --git a/src/api/common/router_macros.rs b/src/api/common/router_macros.rs
new file mode 100644
index 00000000..299420f7
--- /dev/null
+++ b/src/api/common/router_macros.rs
@@ -0,0 +1,304 @@
+/// This macro is used to generate very repetitive match {} blocks in this module
+/// It is _not_ made to be used anywhere else
+#[macro_export]
+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),*))?,)*
+ ]) => {{
+ {
+ #[allow(unused_parens)]
+ 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_path_parser_v2 ($method:expr, $reqpath:expr, $pathprefix:literal, $query:expr, $req:expr)
+ [
+ $(@special $spec_meth:ident $spec_path:pat => $spec_api:ident $spec_params:tt,)*
+ $($meth:ident $api:ident $params:tt,)*
+ ]) => {{
+ {
+ #[allow(unused_parens)]
+ match ($method, $reqpath) {
+ $(
+ (&Method::$spec_meth, $spec_path) => AdminApiRequest::$spec_api (
+ router_match!(@@gen_parse_request $spec_api, $spec_params, $query, $req)
+ ),
+ )*
+ $(
+ (&Method::$meth, concat!($pathprefix, stringify!($api)))
+ => AdminApiRequest::$api (
+ router_match!(@@gen_parse_request $api, $params, $query, $req)
+ ),
+ )*
+ (m, p) => {
+ return Err(Error::bad_request(format!(
+ "Unknown API endpoint: {} {}",
+ m, p
+ )))
+ }
+ }
+ }
+ }};
+ (@@gen_parse_request $api:ident, (), $query: expr, $req:expr) => {{
+ paste!(
+ [< $api Request >]
+ )
+ }};
+ (@@gen_parse_request $api:ident, (body), $query: expr, $req:expr) => {{
+ paste!({
+ parse_json_body::< [<$api Request>], _, Error>($req).await?
+ })
+ }};
+ (@@gen_parse_request $api:ident, (body_field, $($conv:ident $(($conv_arg:expr))? :: $param:ident),*), $query: expr, $req:expr)
+ =>
+ {{
+ paste!({
+ let body = parse_json_body::< [<$api RequestBody>], _, Error>($req).await?;
+ [< $api Request >] {
+ body,
+ $(
+ $param: router_match!(@@parse_param $query, $conv $(($conv_arg))?, $param),
+ )+
+ }
+ })
+ }};
+ (@@gen_parse_request $api:ident, ($($conv:ident $(($conv_arg:expr))? :: $param:ident),*), $query: expr, $req:expr)
+ =>
+ {{
+ paste!({
+ [< $api Request >] {
+ $(
+ $param: router_match!(@@parse_param $query, $conv $(($conv_arg))?, $param),
+ )+
+ }
+ })
+ }};
+ (@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(
+ format!("Missing argument `{}` for endpoint", stringify!($param))
+ )?.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(
+ format!("Missing argument `{}` for endpoint", stringify!($param))
+ )?
+ .parse()
+ .map_err(|_| Error::bad_request("Failed to parse query parameter"))?
+ }};
+ (@@parse_param $query:expr, parse_default($default:expr), $param:ident) => {{
+ // extract and parse optional query parameter
+ // using provided value as default if paramter is missing
+ $query.$param.take().map(|x| x
+ .parse()
+ .map_err(|_| Error::bad_request("Failed to parse query parameter")))
+ .transpose()?
+ .unwrap_or($default)
+ }};
+ (@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),)*
+ }
+ }
+ }
+ };
+}
+
+/// 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_export]
+macro_rules! generateQueryParameters {
+ (
+ keywords: [ $($kw_param:expr => $kw_name: ident),* ],
+ fields: [ $($f_param:expr => $f_name:ident),* ]
+ ) => {
+ #[derive(Debug)]
+ #[allow(non_camel_case_types)]
+ #[allow(clippy::upper_case_acronyms)]
+ 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
+ )));
+ },
+ )*
+ $(
+ // FIXME: remove if !v.is_empty() ?
+ $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 Endpoint 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 use generateQueryParameters;
+pub use router_match;