Allow ruma_api attributes to contain multiple items, forbid some nonsensical attribute usage

This commit is contained in:
Jonas Platte 2019-11-15 22:24:35 +01:00
parent fa9ec7f145
commit 9699f5c68f
No known key found for this signature in database
GPG Key ID: 7D261D771D915378
3 changed files with 153 additions and 104 deletions

View File

@ -1,10 +1,22 @@
//! Details of the `#[ruma_api(...)]` attributes. //! Details of the `#[ruma_api(...)]` attributes.
use std::vec;
use syn::{ use syn::{
parse::{Parse, ParseStream}, parse::{Parse, ParseStream},
punctuated::{Pair, Punctuated},
Ident, Token, 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 /// Like syn::Meta, but only parses ruma_api attributes
pub enum Meta { pub enum Meta {
/// A single word, like `query` in `#[ruma_api(query)]` /// A single word, like `query` in `#[ruma_api(query)]`
@ -13,7 +25,26 @@ pub enum Meta {
NameValue(MetaNameValue), NameValue(MetaNameValue),
} }
impl Meta { impl Parse for Meta {
fn parse(input: ParseStream) -> syn::Result<Self> {
let ident = input.parse()?;
if input.peek(Token![=]) {
let _ = input.parse::<Token![=]>();
Ok(Meta::NameValue(MetaNameValue {
name: ident,
value: input.parse()?,
}))
} else {
Ok(Meta::Word(ident))
}
}
}
/// List of `Meta`s
pub struct MetaList(Vec<Meta>);
impl MetaList {
/// Check if the given attribute is a ruma_api attribute. If it is, parse it. /// Check if the given attribute is a ruma_api attribute. If it is, parse it.
/// ///
/// # Panics /// # Panics
@ -39,27 +70,22 @@ impl Meta {
} }
} }
/// Like syn::MetaNameValue, but expects an identifier as the value. Also, we don't care about the impl Parse for MetaList {
/// 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 {
fn parse(input: ParseStream) -> syn::Result<Self> { fn parse(input: ParseStream) -> syn::Result<Self> {
let ident = input.parse()?; Ok(MetaList(
Punctuated::<Meta, Token![,]>::parse_terminated(input)?
.into_pairs()
.map(Pair::into_value)
.collect(),
))
}
}
if input.peek(Token![=]) { impl IntoIterator for MetaList {
let _ = input.parse::<Token![=]>(); type Item = Meta;
Ok(Meta::NameValue(MetaNameValue { type IntoIter = vec::IntoIter<Meta>;
name: ident,
value: input.parse()?, fn into_iter(self) -> Self::IntoIter {
})) self.0.into_iter()
} else {
Ok(Meta::Word(ident))
}
} }
} }

View File

@ -5,7 +5,7 @@ use quote::{quote, quote_spanned, ToTokens};
use syn::{spanned::Spanned, Field, Ident}; use syn::{spanned::Spanned, Field, Ident};
use crate::api::{ use crate::api::{
attribute::{Meta, MetaNameValue}, attribute::{Meta, MetaList, MetaNameValue},
strip_serde_attrs, strip_serde_attrs,
}; };
@ -128,59 +128,61 @@ impl Request {
impl From<Vec<Field>> for Request { impl From<Vec<Field>> for Request {
fn from(fields: Vec<Field>) -> Self { fn from(fields: Vec<Field>) -> Self {
let mut has_newtype_body = false; let fields: Vec<_> = fields.into_iter().map(|mut field| {
let mut field_kind = None;
let fields = fields.into_iter().map(|mut field| {
let mut field_kind = RequestFieldKind::Body;
let mut header = None; let mut header = None;
field.attrs.retain(|attr| { field.attrs.retain(|attr| {
let meta = match Meta::from_attribute(attr) { let meta_list = match MetaList::from_attribute(attr) {
Some(meta) => meta, Some(list) => list,
None => return true, None => return true,
}; };
for meta in meta_list {
match meta { match meta {
Meta::Word(ident) => { Meta::Word(ident) => {
match &ident.to_string()[..] {
"body" => {
assert!( assert!(
!has_newtype_body, field_kind.is_none(),
"ruma_api! body attribute can only be used once per request definition" "ruma_api! field kind can only be set once per field"
); );
has_newtype_body = true; field_kind = Some(match &ident.to_string()[..] {
field_kind = RequestFieldKind::NewtypeBody; "body" => RequestFieldKind::NewtypeBody,
} "path" => RequestFieldKind::Path,
"path" => field_kind = RequestFieldKind::Path, "query" => RequestFieldKind::Query,
"query" => field_kind = RequestFieldKind::Query,
_ => panic!("ruma_api! single-word attribute on requests must be: body, path, or query"), _ => panic!("ruma_api! single-word attribute on requests must be: body, path, or query"),
} });
} }
Meta::NameValue(MetaNameValue { name, value }) => { Meta::NameValue(MetaNameValue { name, value }) => {
assert!( assert!(
name == "header", name == "header",
"ruma_api! name/value pair attribute on requests must be: 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); header = Some(value);
field_kind = RequestFieldKind::Header; field_kind = Some(RequestFieldKind::Header);
}
} }
} }
false false
}); });
if field_kind == RequestFieldKind::Body { RequestField::new(field_kind.unwrap_or(RequestFieldKind::Body), field, header)
assert!(
!has_newtype_body,
"ruma_api! requests cannot have both normal body fields and a newtype body field"
);
}
RequestField::new(field_kind, field, header)
}).collect(); }).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 } Self { fields }
} }
} }
@ -360,6 +362,11 @@ impl RequestField {
self.kind() == RequestFieldKind::Header 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. /// Whether or not this request field is a path kind.
fn is_path(&self) -> bool { fn is_path(&self) -> bool {
self.kind() == RequestFieldKind::Path self.kind() == RequestFieldKind::Path

View File

@ -5,7 +5,7 @@ use quote::{quote, quote_spanned, ToTokens};
use syn::{spanned::Spanned, Field, Ident}; use syn::{spanned::Spanned, Field, Ident};
use crate::api::{ use crate::api::{
attribute::{Meta, MetaNameValue}, attribute::{Meta, MetaList, MetaNameValue},
strip_serde_attrs, strip_serde_attrs,
}; };
@ -98,18 +98,19 @@ impl Response {
impl From<Vec<Field>> for Response { impl From<Vec<Field>> for Response {
fn from(fields: Vec<Field>) -> Self { fn from(fields: Vec<Field>) -> Self {
let mut has_newtype_body = false; let fields: Vec<_> = fields
.into_iter()
let fields = fields.into_iter().map(|mut field| { .map(|mut field| {
let mut field_kind = ResponseFieldKind::Body; let mut field_kind = None;
let mut header = None; let mut header = None;
field.attrs.retain(|attr| { field.attrs.retain(|attr| {
let meta = match Meta::from_attribute(attr) { let meta_list = match MetaList::from_attribute(attr) {
Some(meta) => meta, Some(list) => list,
None => return true, None => return true,
}; };
for meta in meta_list {
match meta { match meta {
Meta::Word(ident) => { Meta::Word(ident) => {
assert!( assert!(
@ -117,40 +118,47 @@ impl From<Vec<Field>> for Response {
"ruma_api! single-word attribute on responses must be: body" "ruma_api! single-word attribute on responses must be: body"
); );
assert!( assert!(
!has_newtype_body, field_kind.is_none(),
"ruma_api! body attribute can only be used once per response definition" "ruma_api! field kind can only be set once per field"
); );
has_newtype_body = true; field_kind = Some(ResponseFieldKind::NewtypeBody);
field_kind = ResponseFieldKind::NewtypeBody;
} }
Meta::NameValue(MetaNameValue { name, value }) => { Meta::NameValue(MetaNameValue { name, value }) => {
assert!( assert!(
name == "header", name == "header",
"ruma_api! name/value pair attribute on requests must be: 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); header = Some(value);
field_kind = ResponseFieldKind::Header; field_kind = Some(ResponseFieldKind::Header);
}
} }
} }
false false
}); });
match field_kind { match field_kind.unwrap_or(ResponseFieldKind::Body) {
ResponseFieldKind::Body => { ResponseFieldKind::Body => ResponseField::Body(field),
assert!( ResponseFieldKind::Header => {
!has_newtype_body, ResponseField::Header(field, header.expect("missing header name"))
"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), ResponseFieldKind::NewtypeBody => ResponseField::NewtypeBody(field),
} }
}).collect(); })
.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 } Self { fields }
} }
@ -260,6 +268,14 @@ impl ResponseField {
_ => false, _ => 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. /// The types of fields that a response can have, without their values.