diff --git a/ruma-api-macros/src/api/attribute.rs b/ruma-api-macros/src/api/attribute.rs index b587fe65..add646a8 100644 --- a/ruma-api-macros/src/api/attribute.rs +++ b/ruma-api-macros/src/api/attribute.rs @@ -1,10 +1,22 @@ //! Details of the `#[ruma_api(...)]` attributes. +use std::vec; + use syn::{ parse::{Parse, ParseStream}, + punctuated::{Pair, Punctuated}, Ident, Token, }; +/// Like syn::MetaNameValue, but expects an identifier as the value. Also, we don't care about the +/// the span of the equals sign, so we don't have the `eq_token` field from syn::MetaNameValue. +pub struct MetaNameValue { + /// The part left of the equals sign + pub name: Ident, + /// The part right of the equals sign + pub value: Ident, +} + /// Like syn::Meta, but only parses ruma_api attributes pub enum Meta { /// A single word, like `query` in `#[ruma_api(query)]` @@ -13,7 +25,26 @@ pub enum Meta { NameValue(MetaNameValue), } -impl Meta { +impl Parse for Meta { + fn parse(input: ParseStream) -> syn::Result { + let ident = input.parse()?; + + if input.peek(Token![=]) { + let _ = input.parse::(); + Ok(Meta::NameValue(MetaNameValue { + name: ident, + value: input.parse()?, + })) + } else { + Ok(Meta::Word(ident)) + } + } +} + +/// List of `Meta`s +pub struct MetaList(Vec); + +impl MetaList { /// Check if the given attribute is a ruma_api attribute. If it is, parse it. /// /// # Panics @@ -39,27 +70,22 @@ impl Meta { } } -/// Like syn::MetaNameValue, but expects an identifier as the value. Also, we don't care about the -/// the span of the equals sign, so we don't have the `eq_token` field from syn::MetaNameValue. -pub struct MetaNameValue { - /// The part left of the equals sign - pub name: Ident, - /// The part right of the equals sign - pub value: Ident, -} - -impl Parse for Meta { +impl Parse for MetaList { fn parse(input: ParseStream) -> syn::Result { - let ident = input.parse()?; - - if input.peek(Token![=]) { - let _ = input.parse::(); - Ok(Meta::NameValue(MetaNameValue { - name: ident, - value: input.parse()?, - })) - } else { - Ok(Meta::Word(ident)) - } + Ok(MetaList( + Punctuated::::parse_terminated(input)? + .into_pairs() + .map(Pair::into_value) + .collect(), + )) + } +} + +impl IntoIterator for MetaList { + type Item = Meta; + type IntoIter = vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() } } diff --git a/ruma-api-macros/src/api/request.rs b/ruma-api-macros/src/api/request.rs index c5c41964..2d425872 100644 --- a/ruma-api-macros/src/api/request.rs +++ b/ruma-api-macros/src/api/request.rs @@ -5,7 +5,7 @@ use quote::{quote, quote_spanned, ToTokens}; use syn::{spanned::Spanned, Field, Ident}; use crate::api::{ - attribute::{Meta, MetaNameValue}, + attribute::{Meta, MetaList, MetaNameValue}, strip_serde_attrs, }; @@ -128,59 +128,61 @@ impl Request { impl From> for Request { fn from(fields: Vec) -> Self { - let mut has_newtype_body = false; - - let fields = fields.into_iter().map(|mut field| { - let mut field_kind = RequestFieldKind::Body; + let fields: Vec<_> = fields.into_iter().map(|mut field| { + let mut field_kind = None; let mut header = None; field.attrs.retain(|attr| { - let meta = match Meta::from_attribute(attr) { - Some(meta) => meta, + let meta_list = match MetaList::from_attribute(attr) { + Some(list) => list, None => return true, }; - match meta { - Meta::Word(ident) => { - match &ident.to_string()[..] { - "body" => { - assert!( - !has_newtype_body, - "ruma_api! body attribute can only be used once per request definition" - ); + for meta in meta_list { - has_newtype_body = true; - field_kind = RequestFieldKind::NewtypeBody; - } - "path" => field_kind = RequestFieldKind::Path, - "query" => field_kind = RequestFieldKind::Query, - _ => panic!("ruma_api! single-word attribute on requests must be: body, path, or query"), + match meta { + Meta::Word(ident) => { + assert!( + field_kind.is_none(), + "ruma_api! field kind can only be set once per field" + ); + + field_kind = Some(match &ident.to_string()[..] { + "body" => RequestFieldKind::NewtypeBody, + "path" => RequestFieldKind::Path, + "query" => RequestFieldKind::Query, + _ => panic!("ruma_api! single-word attribute on requests must be: body, path, or query"), + }); } - } - Meta::NameValue(MetaNameValue { name, value }) => { - assert!( - name == "header", - "ruma_api! name/value pair attribute on requests must be: header" - ); + Meta::NameValue(MetaNameValue { name, value }) => { + assert!( + name == "header", + "ruma_api! name/value pair attribute on requests must be: header" + ); + assert!( + field_kind.is_none(), + "ruma_api! field kind can only be set once per field" + ); - header = Some(value); - field_kind = RequestFieldKind::Header; + header = Some(value); + field_kind = Some(RequestFieldKind::Header); + } } } false }); - if field_kind == RequestFieldKind::Body { - assert!( - !has_newtype_body, - "ruma_api! requests cannot have both normal body fields and a newtype body field" - ); - } - - RequestField::new(field_kind, field, header) + RequestField::new(field_kind.unwrap_or(RequestFieldKind::Body), field, header) }).collect(); + if fields.len() > 1 { + assert!( + !fields.iter().any(|field| field.is_newtype_body()), + "ruma_api! newtype body has to be the only response field" + ) + } + Self { fields } } } @@ -360,6 +362,11 @@ impl RequestField { self.kind() == RequestFieldKind::Header } + /// Whether or not this request field is a newtype body kind. + fn is_newtype_body(&self) -> bool { + self.kind() == RequestFieldKind::NewtypeBody + } + /// Whether or not this request field is a path kind. fn is_path(&self) -> bool { self.kind() == RequestFieldKind::Path diff --git a/ruma-api-macros/src/api/response.rs b/ruma-api-macros/src/api/response.rs index 8bf74528..3daf613b 100644 --- a/ruma-api-macros/src/api/response.rs +++ b/ruma-api-macros/src/api/response.rs @@ -5,7 +5,7 @@ use quote::{quote, quote_spanned, ToTokens}; use syn::{spanned::Spanned, Field, Ident}; use crate::api::{ - attribute::{Meta, MetaNameValue}, + attribute::{Meta, MetaList, MetaNameValue}, strip_serde_attrs, }; @@ -98,59 +98,67 @@ impl Response { impl From> for Response { fn from(fields: Vec) -> Self { - let mut has_newtype_body = false; + let fields: Vec<_> = fields + .into_iter() + .map(|mut field| { + let mut field_kind = None; + let mut header = None; - let fields = fields.into_iter().map(|mut field| { - let mut field_kind = ResponseFieldKind::Body; - let mut header = None; + field.attrs.retain(|attr| { + let meta_list = match MetaList::from_attribute(attr) { + Some(list) => list, + None => return true, + }; - field.attrs.retain(|attr| { - let meta = match Meta::from_attribute(attr) { - Some(meta) => meta, - None => return true, - }; + for meta in meta_list { + match meta { + Meta::Word(ident) => { + assert!( + ident == "body", + "ruma_api! single-word attribute on responses must be: body" + ); + assert!( + field_kind.is_none(), + "ruma_api! field kind can only be set once per field" + ); - match meta { - Meta::Word(ident) => { - assert!( - ident == "body", - "ruma_api! single-word attribute on responses must be: body" - ); - assert!( - !has_newtype_body, - "ruma_api! body attribute can only be used once per response definition" - ); + field_kind = Some(ResponseFieldKind::NewtypeBody); + } + Meta::NameValue(MetaNameValue { name, value }) => { + assert!( + name == "header", + "ruma_api! name/value pair attribute on requests must be: header" + ); + assert!( + field_kind.is_none(), + "ruma_api! field kind can only be set once per field" + ); - has_newtype_body = true; - field_kind = ResponseFieldKind::NewtypeBody; + header = Some(value); + field_kind = Some(ResponseFieldKind::Header); + } + } } - Meta::NameValue(MetaNameValue { name, value }) => { - assert!( - name == "header", - "ruma_api! name/value pair attribute on requests must be: header" - ); - header = Some(value); - field_kind = ResponseFieldKind::Header; + false + }); + + match field_kind.unwrap_or(ResponseFieldKind::Body) { + ResponseFieldKind::Body => ResponseField::Body(field), + ResponseFieldKind::Header => { + ResponseField::Header(field, header.expect("missing header name")) } + ResponseFieldKind::NewtypeBody => ResponseField::NewtypeBody(field), } + }) + .collect(); - false - }); - - match field_kind { - ResponseFieldKind::Body => { - assert!( - !has_newtype_body, - "ruma_api! responses cannot have both normal body fields and a newtype body field" - ); - - ResponseField::Body(field) - } - ResponseFieldKind::Header => ResponseField::Header(field, header.expect("missing header name")), - ResponseFieldKind::NewtypeBody => ResponseField::NewtypeBody(field), - } - }).collect(); + if fields.len() > 1 { + assert!( + !fields.iter().any(|field| field.is_newtype_body()), + "ruma_api! newtype body has to be the only response field" + ) + } Self { fields } } @@ -260,6 +268,14 @@ impl ResponseField { _ => false, } } + + /// Whether or not this response field is a newtype body kind. + fn is_newtype_body(&self) -> bool { + match *self { + ResponseField::NewtypeBody(..) => true, + _ => false, + } + } } /// The types of fields that a response can have, without their values.