Allow ruma_api attributes to contain multiple items, forbid some nonsensical attribute usage
This commit is contained in:
parent
fa9ec7f145
commit
9699f5c68f
@ -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)?
|
||||||
if input.peek(Token![=]) {
|
.into_pairs()
|
||||||
let _ = input.parse::<Token![=]>();
|
.map(Pair::into_value)
|
||||||
Ok(Meta::NameValue(MetaNameValue {
|
.collect(),
|
||||||
name: ident,
|
))
|
||||||
value: input.parse()?,
|
}
|
||||||
}))
|
}
|
||||||
} else {
|
|
||||||
Ok(Meta::Word(ident))
|
impl IntoIterator for MetaList {
|
||||||
}
|
type Item = Meta;
|
||||||
|
type IntoIter = vec::IntoIter<Meta>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.0.into_iter()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
match meta {
|
for meta in meta_list {
|
||||||
Meta::Word(ident) => {
|
|
||||||
match &ident.to_string()[..] {
|
|
||||||
"body" => {
|
|
||||||
assert!(
|
|
||||||
!has_newtype_body,
|
|
||||||
"ruma_api! body attribute can only be used once per request definition"
|
|
||||||
);
|
|
||||||
|
|
||||||
has_newtype_body = true;
|
match meta {
|
||||||
field_kind = RequestFieldKind::NewtypeBody;
|
Meta::Word(ident) => {
|
||||||
}
|
assert!(
|
||||||
"path" => field_kind = RequestFieldKind::Path,
|
field_kind.is_none(),
|
||||||
"query" => field_kind = RequestFieldKind::Query,
|
"ruma_api! field kind can only be set once per field"
|
||||||
_ => panic!("ruma_api! single-word attribute on requests must be: body, path, or query"),
|
);
|
||||||
|
|
||||||
|
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 }) => {
|
||||||
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
|
||||||
|
@ -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,59 +98,67 @@ 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()
|
||||||
|
.map(|mut field| {
|
||||||
|
let mut field_kind = None;
|
||||||
|
let mut header = None;
|
||||||
|
|
||||||
let fields = fields.into_iter().map(|mut field| {
|
field.attrs.retain(|attr| {
|
||||||
let mut field_kind = ResponseFieldKind::Body;
|
let meta_list = match MetaList::from_attribute(attr) {
|
||||||
let mut header = None;
|
Some(list) => list,
|
||||||
|
None => return true,
|
||||||
|
};
|
||||||
|
|
||||||
field.attrs.retain(|attr| {
|
for meta in meta_list {
|
||||||
let meta = match Meta::from_attribute(attr) {
|
match meta {
|
||||||
Some(meta) => meta,
|
Meta::Word(ident) => {
|
||||||
None => return true,
|
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 {
|
field_kind = Some(ResponseFieldKind::NewtypeBody);
|
||||||
Meta::Word(ident) => {
|
}
|
||||||
assert!(
|
Meta::NameValue(MetaNameValue { name, value }) => {
|
||||||
ident == "body",
|
assert!(
|
||||||
"ruma_api! single-word attribute on responses must be: body"
|
name == "header",
|
||||||
);
|
"ruma_api! name/value pair attribute on requests must be: header"
|
||||||
assert!(
|
);
|
||||||
!has_newtype_body,
|
assert!(
|
||||||
"ruma_api! body attribute can only be used once per response definition"
|
field_kind.is_none(),
|
||||||
);
|
"ruma_api! field kind can only be set once per field"
|
||||||
|
);
|
||||||
|
|
||||||
has_newtype_body = true;
|
header = Some(value);
|
||||||
field_kind = ResponseFieldKind::NewtypeBody;
|
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);
|
false
|
||||||
field_kind = ResponseFieldKind::Header;
|
});
|
||||||
|
|
||||||
|
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
|
if fields.len() > 1 {
|
||||||
});
|
assert!(
|
||||||
|
!fields.iter().any(|field| field.is_newtype_body()),
|
||||||
match field_kind {
|
"ruma_api! newtype body has to be the only response field"
|
||||||
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();
|
|
||||||
|
|
||||||
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.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user