From f0e724d39156db4cf48ae82b75cf7664a7f6c900 Mon Sep 17 00:00:00 2001 From: Wim de With Date: Wed, 20 Nov 2019 14:31:38 +0100 Subject: [PATCH] Add query_map attribute to ruma_api --- ruma-api-macros/src/api.rs | 36 +++++++++++++++++- ruma-api-macros/src/api/request.rs | 61 ++++++++++++++++++++++++++++-- tests/ruma_api_macros.rs | 23 +++++++++++ 3 files changed, 116 insertions(+), 4 deletions(-) diff --git a/ruma-api-macros/src/api.rs b/ruma-api-macros/src/api.rs index b9a82a92..9ddb4b12 100644 --- a/ruma-api-macros/src/api.rs +++ b/ruma-api-macros/src/api.rs @@ -135,7 +135,41 @@ impl ToTokens for Api { } }; - let set_request_query = if self.request.has_query_fields() { + let set_request_query = if let Some(field) = self.request.query_map_field() { + let field_name = field.ident.as_ref().expect("expected field to have identifier"); + let field_type = &field.ty; + + quote! { + // This function exists so that the compiler will throw an + // error when the type of the field with the query_map + // attribute doesn't implement IntoIterator + // + // This is necessary because the serde_urlencoded::to_string + // call will result in a runtime error when the type cannot be + // encoded as a list key-value pairs (?key1=value1&key2=value2) + // + // By asserting that it implements the iterator trait, we can + // ensure that it won't fail. + fn assert_trait_impl() + where + T: std::iter::IntoIterator, + {} + assert_trait_impl::<#field_type>(); + + let request_query = RequestQuery(request.#field_name); + let query_str = ruma_api::exports::serde_urlencoded::to_string( + request_query, + )?; + + let query_opt: Option<&str> = if query_str.is_empty() { + None + } else { + Some(&query_str) + }; + + url.set_query(query_opt); + } + } else if self.request.has_query_fields() { let request_query_init_fields = self.request.request_query_init_fields(); quote! { diff --git a/ruma-api-macros/src/api/request.rs b/ruma-api-macros/src/api/request.rs index ef297bed..3a741289 100644 --- a/ruma-api-macros/src/api/request.rs +++ b/ruma-api-macros/src/api/request.rs @@ -82,6 +82,11 @@ impl Request { self.fields.iter().find_map(RequestField::as_newtype_body_field) } + /// Returns the query map field. + pub fn query_map_field(&self) -> Option<&Field> { + self.fields.iter().find_map(RequestField::as_query_map_field) + } + /// Produces code for a struct initializer for body fields on a variable named `request`. pub fn request_body_init_fields(&self) -> TokenStream { self.struct_init_fields(RequestFieldKind::Body, quote!(request)) @@ -127,6 +132,7 @@ impl TryFrom for Request { fn try_from(raw: RawRequest) -> syn::Result { let mut newtype_body_field = None; + let mut query_map_field = None; let fields = raw .fields @@ -172,10 +178,26 @@ impl TryFrom for Request { } "path" => RequestFieldKind::Path, "query" => RequestFieldKind::Query, + "query_map" => { + if let Some(f) = &query_map_field { + let mut error = syn::Error::new_spanned( + field, + "There can only be one query map field", + ); + error.combine(syn::Error::new_spanned( + f, + "Previous query map field", + )); + return Err(error); + } + + query_map_field = Some(field.clone()); + RequestFieldKind::QueryMap + }, _ => { return Err(syn::Error::new_spanned( ident, - "Invalid #[ruma_api] argument, expected one of `body`, `path`, `query`", + "Invalid #[ruma_api] argument, expected one of `body`, `path`, `query`, `query_map`", )); } } @@ -210,6 +232,14 @@ impl TryFrom for Request { )); } + if query_map_field.is_some() && fields.iter().any(|f| f.is_query()) { + return Err(syn::Error::new_spanned( + // TODO: raw, + raw.request_kw, + "Can't have both a query map field and regular query fields", + )); + } + Ok(Self { fields }) } } @@ -275,7 +305,20 @@ impl ToTokens for Request { TokenStream::new() }; - let request_query_struct = if self.has_query_fields() { + let request_query_struct = if let Some(field) = self.query_map_field() { + let ty = &field.ty; + let span = field.span(); + + quote_spanned! {span=> + /// Data in the request's query string. + #[derive( + Debug, + ruma_api::exports::serde::Deserialize, + ruma_api::exports::serde::Serialize, + )] + struct RequestQuery(#ty); + } + } else if self.has_query_fields() { let fields = self.fields.iter().filter_map(RequestField::as_query_field); quote! { @@ -317,6 +360,8 @@ pub enum RequestField { Path(Field), /// Data that appears in the query string. Query(Field), + /// Data that appears in the query string as dynamic key-value pairs. + QueryMap(Field), } impl RequestField { @@ -330,6 +375,7 @@ impl RequestField { RequestFieldKind::NewtypeBody => RequestField::NewtypeBody(field), RequestFieldKind::Path => RequestField::Path(field), RequestFieldKind::Query => RequestField::Query(field), + RequestFieldKind::QueryMap => RequestField::QueryMap(field), } } @@ -341,6 +387,7 @@ impl RequestField { RequestField::NewtypeBody(..) => RequestFieldKind::NewtypeBody, RequestField::Path(..) => RequestFieldKind::Path, RequestField::Query(..) => RequestFieldKind::Query, + RequestField::QueryMap(..) => RequestFieldKind::QueryMap, } } @@ -384,6 +431,11 @@ impl RequestField { self.field_of_kind(RequestFieldKind::Query) } + /// Return the contained field if this request field is a query map kind. + fn as_query_map_field(&self) -> Option<&Field> { + self.field_of_kind(RequestFieldKind::QueryMap) + } + /// Gets the inner `Field` value. fn field(&self) -> &Field { match self { @@ -391,7 +443,8 @@ impl RequestField { | RequestField::Header(field, _) | RequestField::NewtypeBody(field) | RequestField::Path(field) - | RequestField::Query(field) => field, + | RequestField::Query(field) + | RequestField::QueryMap(field) => field, } } @@ -418,4 +471,6 @@ enum RequestFieldKind { Path, /// See the similarly named variant of `RequestField`. Query, + /// See the similarly named variant of `RequestField`. + QueryMap, } diff --git a/tests/ruma_api_macros.rs b/tests/ruma_api_macros.rs index ddc7b014..fc033277 100644 --- a/tests/ruma_api_macros.rs +++ b/tests/ruma_api_macros.rs @@ -69,3 +69,26 @@ pub mod newtype_body_endpoint { } } } + +pub mod query_map_endpoint { + use ruma_api_macros::ruma_api; + + ruma_api! { + metadata { + description: "Does something.", + method: GET, + name: "newtype_body_endpoint", + path: "/_matrix/some/query/map/endpoint", + rate_limited: false, + requires_authentication: false, + } + + request { + #[ruma_api(query_map)] + pub fields: Vec<(String, String)>, + } + + response { + } + } +}