Replace string literals by identifiers in #[ruma_api] attributes

This commit is contained in:
Jonas Platte 2019-07-25 22:00:24 +02:00
parent 777e9c4c70
commit 8f3b141db5
7 changed files with 143 additions and 117 deletions

View File

@ -39,7 +39,7 @@ pub mod some_endpoint {
pub foo: String,
// This value will be put into the "Content-Type" HTTP header.
#[ruma_api(header = "CONTENT_TYPE")]
#[ruma_api(header = CONTENT_TYPE)]
pub content_type: String
// This value will be put into the query string of the request's URL.
@ -54,7 +54,7 @@ pub mod some_endpoint {
response {
// This value will be extracted from the "Content-Type" HTTP header.
#[ruma_api(header = "CONTENT_TYPE")]
#[ruma_api(header = CONTENT_TYPE)]
pub content_type: String
// With no attribute on the field, it will be extracted from the body of the response.

63
src/api/attribute.rs Normal file
View File

@ -0,0 +1,63 @@
//! Details of the `#[ruma_api(...)]` attributes.
use syn::{
parenthesized,
parse::{Parse, ParseStream},
Ident, Token,
};
/// Like syn::Meta, but only parses ruma_api attributes
pub enum Meta {
/// A single word, like `query` in `#[ruma_api(query)]`
Word(Ident),
/// A name-value pair, like `header = CONTENT_TYPE` in `#[ruma_api(header = CONTENT_TYPE)]`
NameValue(MetaNameValue),
}
impl Meta {
pub fn from_attribute(attr: syn::Attribute) -> Result<Self, syn::Attribute> {
match &attr.path {
syn::Path {
leading_colon: None,
segments,
} => {
if segments.len() == 1 && segments[0].ident == "ruma_api" {
Ok(
syn::parse2(attr.tts)
.expect("ruma_api! could not parse request field attributes"),
)
} else {
Err(attr)
}
}
_ => Err(attr),
}
}
}
/// 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 {
fn parse(input: ParseStream) -> syn::Result<Self> {
let content;
let _ = parenthesized!(content in input);
let ident = content.parse()?;
if content.peek(Token![=]) {
let _ = content.parse::<Token![=]>();
Ok(Meta::NameValue(MetaNameValue {
name: ident,
value: content.parse()?,
}))
} else {
Ok(Meta::Word(ident))
}
}
}

View File

@ -1,4 +1,4 @@
//! Details of the `ruma-api` procedural macro.
//! Details of the `ruma_api` procedural macro.
use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
@ -8,6 +8,7 @@ use syn::{
Field, FieldValue, Ident, Meta, Token,
};
mod attribute;
mod metadata;
mod request;
mod response;

View File

@ -1,10 +1,13 @@
//! Details of the `request` section of the procedural macro.
use proc_macro2::{Span, TokenStream};
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned, ToTokens};
use syn::{spanned::Spanned, Field, Ident, Lit, Meta, NestedMeta};
use syn::{spanned::Spanned, Field, Ident};
use crate::api::strip_serde_attrs;
use crate::api::{
attribute::{Meta, MetaNameValue},
strip_serde_attrs,
};
/// The result of processing the `request` section of the macro.
pub struct Request {
@ -16,13 +19,12 @@ impl Request {
/// Produces code to add necessary HTTP headers to an `http::Request`.
pub fn add_headers_to_request(&self) -> TokenStream {
let append_stmts = self.header_fields().map(|request_field| {
let (field, header_name_string) = match request_field {
RequestField::Header(field, header_name_string) => (field, header_name_string),
let (field, header_name) = match request_field {
RequestField::Header(field, header_name) => (field, header_name),
_ => panic!("expected request field to be header variant"),
};
let field_name = &field.ident;
let header_name = Ident::new(header_name_string.as_ref(), Span::call_site());
quote! {
headers.append(
@ -41,13 +43,13 @@ impl Request {
/// Produces code to extract fields from the HTTP headers in an `http::Request`.
pub fn parse_headers_from_request(&self) -> TokenStream {
let fields = self.header_fields().map(|request_field| {
let (field, header_name_string) = match request_field {
RequestField::Header(field, header_name_string) => (field, header_name_string),
let (field, header_name) = match request_field {
RequestField::Header(field, header_name) => (field, header_name),
_ => panic!("expected request field to be header variant"),
};
let field_name = &field.ident;
let header_name = Ident::new(header_name_string.as_ref(), Span::call_site());
let header_name_string = header_name.to_string();
quote! {
#field_name: headers.get(::http::header::#header_name)
@ -180,57 +182,36 @@ impl From<Vec<Field>> for Request {
let mut field_kind = RequestFieldKind::Body;
let mut header = None;
field.attrs = field.attrs.into_iter().filter(|attr| {
let meta = attr.interpret_meta()
.expect("ruma_api! could not parse request field attributes");
let meta_list = match meta {
Meta::List(meta_list) => meta_list,
_ => return true,
field.attrs = field.attrs.into_iter().filter_map(|attr| {
let meta = match Meta::from_attribute(attr) {
Ok(meta) => meta,
Err(attr) => return Some(attr),
};
if &meta_list.ident.to_string() != "ruma_api" {
return true;
}
for nested_meta_item in meta_list.nested {
match nested_meta_item {
NestedMeta::Meta(meta_item) => {
match meta_item {
Meta::Word(ident) => {
match &ident.to_string()[..] {
"body" => {
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"),
}
}
Meta::NameValue(name_value) => {
match &name_value.ident.to_string()[..] {
"header" => {
match name_value.lit {
Lit::Str(lit_str) => header = Some(lit_str.value()),
_ => panic!("ruma_api! header attribute's value must be a string literal"),
}
field_kind = RequestFieldKind::Header;
}
_ => panic!("ruma_api! name/value pair attribute on requests must be: header"),
}
}
_ => panic!("ruma_api! attributes on requests must be a single word or a name/value pair"),
match meta {
Meta::Word(ident) => {
match &ident.to_string()[..] {
"body" => {
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"),
}
NestedMeta::Literal(_) => panic!(
"ruma_api! attributes on requests must be: body, header, path, or query"
),
}
Meta::NameValue(MetaNameValue { name, value }) => {
assert!(
name == "header",
"ruma_api! name/value pair attribute on requests must be: header"
);
header = Some(value);
field_kind = RequestFieldKind::Header;
}
}
false
None
}).collect();
if field_kind == RequestFieldKind::Body {
@ -370,7 +351,7 @@ pub enum RequestField {
/// JSON data in the body of the request.
Body(Field),
/// Data in an HTTP header.
Header(Field, String),
Header(Field, Ident),
/// A specific data type in the body of the request.
NewtypeBody(Field),
/// Data that appears in the URL path.
@ -381,7 +362,7 @@ pub enum RequestField {
impl RequestField {
/// Creates a new `RequestField`.
fn new(kind: RequestFieldKind, field: Field, header: Option<String>) -> Self {
fn new(kind: RequestFieldKind, field: Field, header: Option<Ident>) -> Self {
match kind {
RequestFieldKind::Body => RequestField::Body(field),
RequestFieldKind::Header => {

View File

@ -1,10 +1,13 @@
//! Details of the `response` section of the procedural macro.
use proc_macro2::{Span, TokenStream};
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned, ToTokens};
use syn::{spanned::Spanned, Field, Ident, Lit, Meta, NestedMeta};
use syn::{spanned::Spanned, Field, Ident};
use crate::api::strip_serde_attrs;
use crate::api::{
attribute::{Meta, MetaNameValue},
strip_serde_attrs,
};
/// The result of processing the `request` section of the macro.
pub struct Response {
@ -50,12 +53,11 @@ impl Response {
#field_name: response_body.#field_name
}
}
ResponseField::Header(ref field, ref header) => {
ResponseField::Header(ref field, ref header_name) => {
let field_name = field
.ident
.clone()
.expect("expected field to have an identifier");
let header_name = Ident::new(header.as_ref(), Span::call_site());
let span = field.span();
quote_spanned! {span=>
@ -87,12 +89,11 @@ impl Response {
/// Produces code to add necessary HTTP headers to an `http::Response`.
pub fn apply_header_fields(&self) -> TokenStream {
let header_calls = self.fields.iter().filter_map(|response_field| {
if let ResponseField::Header(ref field, ref header) = *response_field {
if let ResponseField::Header(ref field, ref header_name) = *response_field {
let field_name = field
.ident
.as_ref()
.expect("expected field to have an identifier");
let header_name = Ident::new(header.as_ref(), Span::call_site());
let span = field.span();
Some(quote_spanned! {span=>
@ -165,64 +166,44 @@ impl From<Vec<Field>> for Response {
let mut field_kind = ResponseFieldKind::Body;
let mut header = None;
field.attrs = field.attrs.into_iter().filter(|attr| {
let meta = attr.interpret_meta()
.expect("ruma_api! could not parse response field attributes");
let meta_list = match meta {
Meta::List(meta_list) => meta_list,
_ => return true,
field.attrs = field.attrs.into_iter().filter_map(|attr| {
let meta = match Meta::from_attribute(attr) {
Ok(meta) => meta,
Err(attr) => return Some(attr),
};
if &meta_list.ident.to_string() != "ruma_api" {
return true;
}
match meta {
Meta::Word(ident) => {
assert!(
ident == "body",
"ruma_api! single-word attribute on responses must be: body"
);
for nested_meta_item in meta_list.nested {
match nested_meta_item {
NestedMeta::Meta(meta_item) => {
match meta_item {
Meta::Word(ident) => {
match &ident.to_string()[..] {
"body" => {
has_newtype_body = true;
field_kind = ResponseFieldKind::NewtypeBody;
}
_ => panic!("ruma_api! single-word attribute on responses must be: body"),
}
}
Meta::NameValue(name_value) => {
match &name_value.ident.to_string()[..] {
"header" => {
match name_value.lit {
Lit::Str(lit_str) => header = Some(lit_str.value()),
_ => panic!("ruma_api! header attribute's value must be a string literal"),
}
has_newtype_body = true;
field_kind = ResponseFieldKind::NewtypeBody;
}
Meta::NameValue(MetaNameValue { name, value }) => {
assert!(
name == "header",
"ruma_api! name/value pair attribute on requests must be: header"
);
field_kind = ResponseFieldKind::Header;
}
_ => panic!("ruma_api! name/value pair attribute on requests must be: header"),
}
}
_ => panic!("ruma_api! attributes on responses must be a single word or a name/value pair"),
}
}
NestedMeta::Literal(_) => panic!(
"ruma_api! attribute meta item on responses must be: header"
),
header = Some(value);
field_kind = ResponseFieldKind::Header;
}
}
false
None
}).collect();
match field_kind {
ResponseFieldKind::Body => {
if has_newtype_body {
panic!("ruma_api! responses cannot have both normal body fields and a newtype body field");
} else {
ResponseField::Body(field)
}
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),
@ -307,7 +288,7 @@ pub enum ResponseField {
/// JSON data in the body of the response.
Body(Field),
/// Data in an HTTP header.
Header(Field, String),
Header(Field, Ident),
/// A specific data type in the body of the response.
NewtypeBody(Field),
}

View File

@ -158,7 +158,7 @@ mod api;
/// request {
/// pub foo: String,
///
/// #[ruma_api(header = "CONTENT_TYPE")]
/// #[ruma_api(header = CONTENT_TYPE)]
/// pub content_type: String,
///
/// #[ruma_api(query)]
@ -169,7 +169,7 @@ mod api;
/// }
///
/// response {
/// #[ruma_api(header = "CONTENT_TYPE")]
/// #[ruma_api(header = CONTENT_TYPE)]
/// pub content_type: String,
///
/// pub value: String,

View File

@ -17,7 +17,7 @@ pub mod some_endpoint {
pub foo: String,
// This value will be put into the "Content-Type" HTTP header.
#[ruma_api(header = "CONTENT_TYPE")]
#[ruma_api(header = CONTENT_TYPE)]
pub content_type: String,
// This value will be put into the query string of the request's URL.
@ -32,7 +32,7 @@ pub mod some_endpoint {
response {
// This value will be extracted from the "Content-Type" HTTP header.
#[ruma_api(header = "CONTENT_TYPE")]
#[ruma_api(header = CONTENT_TYPE)]
pub content_type: String,
// With no attribute on the field, it will be extracted from the body of the response.