api-macros: Move ruma_api! trait impl generation into derive macros

This commit is contained in:
Jonas Platte 2021-08-05 20:32:32 +02:00
parent fae75410a9
commit 696c9fba4e
No known key found for this signature in database
GPG Key ID: CC154DE0E30B7C67
17 changed files with 1244 additions and 1018 deletions

View File

@ -2,17 +2,27 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::Type;
use syn::{
braced,
parse::{Parse, ParseStream},
Attribute, Field, Token, Type,
};
pub(crate) mod attribute;
pub(crate) mod metadata;
pub(crate) mod parse;
pub(crate) mod request;
pub(crate) mod response;
mod metadata;
mod request;
mod response;
use self::{metadata::Metadata, request::Request, response::Response};
use crate::util;
mod kw {
use syn::custom_keyword;
custom_keyword!(error);
custom_keyword!(request);
custom_keyword!(response);
}
/// The result of processing the `ruma_api` macro, ready for output back to source code.
pub struct Api {
/// The `metadata` section of the macro.
@ -28,65 +38,129 @@ pub struct Api {
error_ty: Option<Type>,
}
pub fn expand_all(api: Api) -> syn::Result<TokenStream> {
let ruma_api = util::import_ruma_api();
let http = quote! { #ruma_api::exports::http };
impl Api {
pub fn expand_all(self) -> TokenStream {
let ruma_api = util::import_ruma_api();
let http = quote! { #ruma_api::exports::http };
let metadata = &api.metadata;
let description = &metadata.description;
let method = &metadata.method;
let name = &metadata.name;
let path = &metadata.path;
let rate_limited: TokenStream = metadata
.rate_limited
.iter()
.map(|r| {
let attrs = &r.attrs;
let value = &r.value;
quote! {
#( #attrs )*
rate_limited: #value,
}
})
.collect();
let authentication: TokenStream = api
.metadata
.authentication
.iter()
.map(|r| {
let attrs = &r.attrs;
let value = &r.value;
quote! {
#( #attrs )*
authentication: #ruma_api::AuthScheme::#value,
}
})
.collect();
let metadata = &self.metadata;
let description = &metadata.description;
let method = &metadata.method;
let name = &metadata.name;
let path = &metadata.path;
let rate_limited: TokenStream = metadata
.rate_limited
.iter()
.map(|r| {
let attrs = &r.attrs;
let value = &r.value;
quote! {
#( #attrs )*
rate_limited: #value,
}
})
.collect();
let authentication: TokenStream = self
.metadata
.authentication
.iter()
.map(|r| {
let attrs = &r.attrs;
let value = &r.value;
quote! {
#( #attrs )*
authentication: #ruma_api::AuthScheme::#value,
}
})
.collect();
let error_ty = api
.error_ty
.map_or_else(|| quote! { #ruma_api::error::MatrixError }, |err_ty| quote! { #err_ty });
let error_ty = self
.error_ty
.map_or_else(|| quote! { #ruma_api::error::MatrixError }, |err_ty| quote! { #err_ty });
let request = api.request.map(|req| req.expand(metadata, &error_ty, &ruma_api));
let response = api.response.map(|res| res.expand(metadata, &error_ty, &ruma_api));
let request = self.request.map(|req| req.expand(metadata, &error_ty, &ruma_api));
let response = self.response.map(|res| res.expand(metadata, &error_ty, &ruma_api));
let metadata_doc = format!("Metadata for the `{}` API endpoint.", name.value());
let metadata_doc = format!("Metadata for the `{}` API endpoint.", name.value());
Ok(quote! {
#[doc = #metadata_doc]
pub const METADATA: #ruma_api::Metadata = #ruma_api::Metadata {
description: #description,
method: #http::Method::#method,
name: #name,
path: #path,
#rate_limited
#authentication
quote! {
#[doc = #metadata_doc]
pub const METADATA: #ruma_api::Metadata = #ruma_api::Metadata {
description: #description,
method: #http::Method::#method,
name: #name,
path: #path,
#rate_limited
#authentication
};
#request
#response
#[cfg(not(any(feature = "client", feature = "server")))]
type _SilenceUnusedError = #error_ty;
}
}
}
impl Parse for Api {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let metadata: Metadata = input.parse()?;
let req_attrs = input.call(Attribute::parse_outer)?;
let (request, attributes) = if input.peek(kw::request) {
let request = parse_request(input, req_attrs)?;
let after_req_attrs = input.call(Attribute::parse_outer)?;
(Some(request), after_req_attrs)
} else {
// There was no `request` field so the attributes are for `response`
(None, req_attrs)
};
#request
#response
let response = if input.peek(kw::response) {
Some(parse_response(input, attributes)?)
} else if !attributes.is_empty() {
return Err(syn::Error::new_spanned(
&attributes[0],
"attributes are not supported on the error type",
));
} else {
None
};
#[cfg(not(any(feature = "client", feature = "server")))]
type _SilenceUnusedError = #error_ty;
})
let error_ty = input
.peek(kw::error)
.then(|| {
let _: kw::error = input.parse()?;
let _: Token![:] = input.parse()?;
input.parse()
})
.transpose()?;
Ok(Self { metadata, request, response, error_ty })
}
}
fn parse_request(input: ParseStream<'_>, attributes: Vec<Attribute>) -> syn::Result<Request> {
let request_kw: kw::request = input.parse()?;
let _: Token![:] = input.parse()?;
let fields;
braced!(fields in input);
let fields = fields.parse_terminated::<_, Token![,]>(Field::parse_named)?;
Ok(Request { request_kw, attributes, fields })
}
fn parse_response(input: ParseStream<'_>, attributes: Vec<Attribute>) -> syn::Result<Response> {
let response_kw: kw::response = input.parse()?;
let _: Token![:] = input.parse()?;
let fields;
braced!(fields in input);
let fields = fields.parse_terminated::<_, Token![,]>(Field::parse_named)?;
Ok(Response { attributes, fields, response_kw })
}

View File

@ -1,6 +1,5 @@
//! Details of the `metadata` section of the procedural macro.
use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::{
braced,
@ -8,7 +7,7 @@ use syn::{
Attribute, Ident, LitBool, LitStr, Token,
};
use crate::util;
use crate::{auth_scheme::AuthScheme, util};
mod kw {
syn::custom_keyword!(metadata);
@ -18,11 +17,6 @@ mod kw {
syn::custom_keyword!(path);
syn::custom_keyword!(rate_limited);
syn::custom_keyword!(authentication);
syn::custom_keyword!(None);
syn::custom_keyword!(AccessToken);
syn::custom_keyword!(ServerSignatures);
syn::custom_keyword!(QueryOnlyAccessToken);
}
/// A field of Metadata that contains attribute macros
@ -124,42 +118,6 @@ impl Parse for Metadata {
}
}
pub enum AuthScheme {
None(kw::None),
AccessToken(kw::AccessToken),
ServerSignatures(kw::ServerSignatures),
QueryOnlyAccessToken(kw::QueryOnlyAccessToken),
}
impl Parse for AuthScheme {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let lookahead = input.lookahead1();
if lookahead.peek(kw::None) {
input.parse().map(Self::None)
} else if lookahead.peek(kw::AccessToken) {
input.parse().map(Self::AccessToken)
} else if lookahead.peek(kw::ServerSignatures) {
input.parse().map(Self::ServerSignatures)
} else if lookahead.peek(kw::QueryOnlyAccessToken) {
input.parse().map(Self::QueryOnlyAccessToken)
} else {
Err(lookahead.error())
}
}
}
impl ToTokens for AuthScheme {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
AuthScheme::None(kw) => kw.to_tokens(tokens),
AuthScheme::AccessToken(kw) => kw.to_tokens(tokens),
AuthScheme::ServerSignatures(kw) => kw.to_tokens(tokens),
AuthScheme::QueryOnlyAccessToken(kw) => kw.to_tokens(tokens),
}
}
}
enum Field {
Description,
Method,

View File

@ -1,350 +0,0 @@
use std::{collections::BTreeSet, mem};
use syn::{
braced,
parse::{Parse, ParseStream},
spanned::Spanned,
visit::Visit,
Attribute, Field, Ident, Lifetime, Token, Type,
};
use super::{
attribute::{Meta, MetaNameValue},
request::{RequestField, RequestFieldKind, RequestLifetimes},
response::{ResponseField, ResponseFieldKind},
Api, Metadata, Request, Response,
};
mod kw {
use syn::custom_keyword;
custom_keyword!(error);
custom_keyword!(request);
custom_keyword!(response);
}
impl Parse for Api {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let metadata: Metadata = input.parse()?;
let req_attrs = input.call(Attribute::parse_outer)?;
let (request, attributes) = if input.peek(kw::request) {
let request = parse_request(input, req_attrs)?;
let after_req_attrs = input.call(Attribute::parse_outer)?;
(Some(request), after_req_attrs)
} else {
// There was no `request` field so the attributes are for `response`
(None, req_attrs)
};
let response = if input.peek(kw::response) {
Some(parse_response(input, attributes)?)
} else if !attributes.is_empty() {
return Err(syn::Error::new_spanned(
&attributes[0],
"attributes are not supported on the error type",
));
} else {
None
};
let error_ty = input
.peek(kw::error)
.then(|| {
let _: kw::error = input.parse()?;
let _: Token![:] = input.parse()?;
input.parse()
})
.transpose()?;
if let Some(req) = &request {
let newtype_body_field = req.newtype_body_field();
if metadata.method == "GET" && (req.has_body_fields() || newtype_body_field.is_some()) {
let mut combined_error: Option<syn::Error> = None;
let mut add_error = |field| {
let error =
syn::Error::new_spanned(field, "GET endpoints can't have body fields");
if let Some(combined_error_ref) = &mut combined_error {
combined_error_ref.combine(error);
} else {
combined_error = Some(error);
}
};
for field in req.body_fields() {
add_error(field);
}
if let Some(field) = newtype_body_field {
add_error(field);
}
return Err(combined_error.unwrap());
}
}
Ok(Self { metadata, request, response, error_ty })
}
}
fn parse_request(input: ParseStream<'_>, attributes: Vec<Attribute>) -> syn::Result<Request> {
let request_kw: kw::request = input.parse()?;
let _: Token![:] = input.parse()?;
let fields;
braced!(fields in input);
let mut newtype_body_field = None;
let mut query_map_field = None;
let mut lifetimes = RequestLifetimes::default();
let fields: Vec<_> = fields
.parse_terminated::<Field, Token![,]>(Field::parse_named)?
.into_iter()
.map(|mut field| {
let mut field_kind = None;
let mut header = None;
for attr in mem::take(&mut field.attrs) {
let meta = match Meta::from_attribute(&attr)? {
Some(m) => m,
None => {
field.attrs.push(attr);
continue;
}
};
if field_kind.is_some() {
return Err(syn::Error::new_spanned(
attr,
"There can only be one field kind attribute",
));
}
field_kind = Some(match meta {
Meta::Word(ident) => match &ident.to_string()[..] {
attr @ "body" | attr @ "raw_body" => req_res_meta_word(
attr,
&field,
&mut newtype_body_field,
RequestFieldKind::NewtypeBody,
RequestFieldKind::NewtypeRawBody,
)?,
"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`, `query_map`",
));
}
},
Meta::NameValue(MetaNameValue { name, value }) => {
req_res_name_value(name, value, &mut header, RequestFieldKind::Header)?
}
});
}
match field_kind.unwrap_or(RequestFieldKind::Body) {
RequestFieldKind::Header => {
collect_lifetime_idents(&mut lifetimes.header, &field.ty)
}
RequestFieldKind::Body => collect_lifetime_idents(&mut lifetimes.body, &field.ty),
RequestFieldKind::NewtypeBody => {
collect_lifetime_idents(&mut lifetimes.body, &field.ty)
}
RequestFieldKind::NewtypeRawBody => {
collect_lifetime_idents(&mut lifetimes.body, &field.ty)
}
RequestFieldKind::Path => collect_lifetime_idents(&mut lifetimes.path, &field.ty),
RequestFieldKind::Query => collect_lifetime_idents(&mut lifetimes.query, &field.ty),
RequestFieldKind::QueryMap => {
collect_lifetime_idents(&mut lifetimes.query, &field.ty)
}
}
Ok(RequestField::new(field_kind.unwrap_or(RequestFieldKind::Body), field, header))
})
.collect::<syn::Result<_>>()?;
if newtype_body_field.is_some() && fields.iter().any(|f| f.is_body()) {
// TODO: highlight conflicting fields,
return Err(syn::Error::new_spanned(
request_kw,
"Can't have both a newtype body field and regular body fields",
));
}
if query_map_field.is_some() && fields.iter().any(|f| f.is_query()) {
return Err(syn::Error::new_spanned(
// TODO: raw,
request_kw,
"Can't have both a query map field and regular query fields",
));
}
// TODO when/if `&[(&str, &str)]` is supported remove this
if query_map_field.is_some() && !lifetimes.query.is_empty() {
return Err(syn::Error::new_spanned(
request_kw,
"Lifetimes are not allowed for query_map fields",
));
}
Ok(Request { attributes, fields, lifetimes })
}
fn parse_response(input: ParseStream<'_>, attributes: Vec<Attribute>) -> syn::Result<Response> {
let response_kw: kw::response = input.parse()?;
let _: Token![:] = input.parse()?;
let fields;
braced!(fields in input);
let mut newtype_body_field = None;
let fields: Vec<_> = fields
.parse_terminated::<Field, Token![,]>(Field::parse_named)?
.into_iter()
.map(|mut field| {
if has_lifetime(&field.ty) {
return Err(syn::Error::new(
field.ident.span(),
"Lifetimes on Response fields cannot be supported until GAT are stable",
));
}
let mut field_kind = None;
let mut header = None;
for attr in mem::take(&mut field.attrs) {
let meta = match Meta::from_attribute(&attr)? {
Some(m) => m,
None => {
field.attrs.push(attr);
continue;
}
};
if field_kind.is_some() {
return Err(syn::Error::new_spanned(
attr,
"There can only be one field kind attribute",
));
}
field_kind = Some(match meta {
Meta::Word(ident) => match &ident.to_string()[..] {
s @ "body" | s @ "raw_body" => req_res_meta_word(
s,
&field,
&mut newtype_body_field,
ResponseFieldKind::NewtypeBody,
ResponseFieldKind::NewtypeRawBody,
)?,
_ => {
return Err(syn::Error::new_spanned(
ident,
"Invalid #[ruma_api] argument with value, expected `body`",
));
}
},
Meta::NameValue(MetaNameValue { name, value }) => {
req_res_name_value(name, value, &mut header, ResponseFieldKind::Header)?
}
});
}
Ok(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),
ResponseFieldKind::NewtypeRawBody => ResponseField::NewtypeRawBody(field),
})
})
.collect::<syn::Result<_>>()?;
if newtype_body_field.is_some() && fields.iter().any(|f| f.is_body()) {
// TODO: highlight conflicting fields,
return Err(syn::Error::new_spanned(
response_kw,
"Can't have both a newtype body field and regular body fields",
));
}
Ok(Response { attributes, fields })
}
fn has_lifetime(ty: &Type) -> bool {
let mut lifetimes = BTreeSet::new();
collect_lifetime_idents(&mut lifetimes, ty);
!lifetimes.is_empty()
}
fn collect_lifetime_idents(lifetimes: &mut BTreeSet<Lifetime>, ty: &Type) {
struct Visitor<'lt>(&'lt mut BTreeSet<Lifetime>);
impl<'ast> Visit<'ast> for Visitor<'_> {
fn visit_lifetime(&mut self, lt: &'ast Lifetime) {
self.0.insert(lt.clone());
}
}
Visitor(lifetimes).visit_type(ty)
}
fn req_res_meta_word<T>(
attr_kind: &str,
field: &Field,
newtype_body_field: &mut Option<Field>,
body_field_kind: T,
raw_field_kind: T,
) -> syn::Result<T> {
if let Some(f) = &newtype_body_field {
let mut error = syn::Error::new_spanned(field, "There can only be one newtype body field");
error.combine(syn::Error::new_spanned(f, "Previous newtype body field"));
return Err(error);
}
*newtype_body_field = Some(field.clone());
Ok(match attr_kind {
"body" => body_field_kind,
"raw_body" => raw_field_kind,
_ => unreachable!(),
})
}
fn req_res_name_value<T>(
name: Ident,
value: Ident,
header: &mut Option<Ident>,
field_kind: T,
) -> syn::Result<T> {
if name != "header" {
return Err(syn::Error::new_spanned(
name,
"Invalid #[ruma_api] argument with value, expected `header`",
));
}
*header = Some(value);
Ok(field_kind)
}

View File

@ -1,129 +1,67 @@
//! Details of the `request` section of the procedural macro.
use std::collections::BTreeSet;
use std::collections::btree_map::{BTreeMap, Entry};
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Attribute, Field, Ident, Lifetime};
use syn::{
parse_quote, punctuated::Punctuated, spanned::Spanned, visit::Visit, Attribute, Field, Ident,
Lifetime, Token,
};
use crate::util;
use super::metadata::Metadata;
mod incoming;
mod outgoing;
#[derive(Default)]
pub(super) struct RequestLifetimes {
pub body: BTreeSet<Lifetime>,
pub path: BTreeSet<Lifetime>,
pub query: BTreeSet<Lifetime>,
pub header: BTreeSet<Lifetime>,
}
use super::{kw, metadata::Metadata};
use crate::util::{all_cfgs, all_cfgs_expr, extract_cfg};
/// The result of processing the `request` section of the macro.
pub(crate) struct Request {
/// The `request` keyword
pub(super) request_kw: kw::request,
/// The attributes that will be applied to the struct definition.
pub(super) attributes: Vec<Attribute>,
/// The fields of the request.
pub(super) fields: Vec<RequestField>,
/// The collected lifetime identifiers from the declared fields.
pub(super) lifetimes: RequestLifetimes,
pub(super) fields: Punctuated<Field, Token![,]>,
}
impl Request {
/// Whether or not this request has any data in the HTTP body.
pub(super) fn has_body_fields(&self) -> bool {
self.fields.iter().any(|field| field.is_body())
}
/// Whether or not this request has any data in HTTP headers.
fn has_header_fields(&self) -> bool {
self.fields.iter().any(|field| field.is_header())
}
/// Whether or not this request has any data in the URL path.
fn has_path_fields(&self) -> bool {
self.fields.iter().any(|field| field.is_path())
}
/// Whether or not this request has any data in the query string.
fn has_query_fields(&self) -> bool {
self.fields.iter().any(|field| field.is_query())
}
/// Produces an iterator over all the body fields.
pub(super) fn body_fields(&self) -> impl Iterator<Item = &Field> {
self.fields.iter().filter_map(|field| field.as_body_field())
}
/// Whether any `body` field has a lifetime annotation.
fn has_body_lifetimes(&self) -> bool {
!self.lifetimes.body.is_empty()
}
/// Whether any `query` field has a lifetime annotation.
fn has_query_lifetimes(&self) -> bool {
!self.lifetimes.query.is_empty()
}
/// Whether any field has a lifetime.
fn contains_lifetimes(&self) -> bool {
!(self.lifetimes.body.is_empty()
&& self.lifetimes.path.is_empty()
&& self.lifetimes.query.is_empty()
&& self.lifetimes.header.is_empty())
}
/// The combination of every fields unique lifetime annotation.
fn combine_lifetimes(&self) -> TokenStream {
util::unique_lifetimes_to_tokens(
[
&self.lifetimes.body,
&self.lifetimes.path,
&self.lifetimes.query,
&self.lifetimes.header,
]
.iter()
.flat_map(|set| set.iter()),
)
}
fn all_lifetimes(&self) -> BTreeMap<Lifetime, Option<Attribute>> {
let mut lifetimes = BTreeMap::new();
/// The lifetimes on fields with the `query` attribute.
fn query_lifetimes(&self) -> TokenStream {
util::unique_lifetimes_to_tokens(&self.lifetimes.query)
}
struct Visitor<'lt> {
field_cfg: Option<Attribute>,
lifetimes: &'lt mut BTreeMap<Lifetime, Option<Attribute>>,
}
/// The lifetimes on fields with the `body` attribute.
fn body_lifetimes(&self) -> TokenStream {
util::unique_lifetimes_to_tokens(&self.lifetimes.body)
}
impl<'ast> Visit<'ast> for Visitor<'_> {
fn visit_lifetime(&mut self, lt: &'ast Lifetime) {
match self.lifetimes.entry(lt.clone()) {
Entry::Vacant(v) => {
v.insert(self.field_cfg.clone());
}
Entry::Occupied(mut o) => {
let lifetime_cfg = o.get_mut();
/// Produces an iterator over all the header fields.
fn header_fields(&self) -> impl Iterator<Item = &RequestField> {
self.fields.iter().filter(|field| field.is_header())
}
// If at least one field uses this lifetime and has no cfg attribute, we
// don't need a cfg attribute for the lifetime either.
*lifetime_cfg = Option::zip(lifetime_cfg.as_ref(), self.field_cfg.as_ref())
.map(|(a, b)| {
let expr_a = extract_cfg(a);
let expr_b = extract_cfg(b);
parse_quote! { #[cfg( any( #expr_a, #expr_b ) )] }
});
}
}
}
}
/// Gets the number of path fields.
fn path_field_count(&self) -> usize {
self.fields.iter().filter(|field| field.is_path()).count()
}
for field in &self.fields {
let field_cfg = if field.attrs.is_empty() { None } else { all_cfgs(&field.attrs) };
Visitor { lifetimes: &mut lifetimes, field_cfg }.visit_type(&field.ty);
}
/// Returns the body field.
pub fn newtype_body_field(&self) -> Option<&Field> {
self.fields.iter().find_map(RequestField::as_newtype_body_field)
}
/// Returns the body field.
fn newtype_raw_body_field(&self) -> Option<&Field> {
self.fields.iter().find_map(RequestField::as_newtype_raw_body_field)
}
/// Returns the query map field.
fn query_map_field(&self) -> Option<&Field> {
self.fields.iter().find_map(RequestField::as_query_map_field)
lifetimes
}
pub(super) fn expand(
@ -132,8 +70,8 @@ impl Request {
error_ty: &TokenStream,
ruma_api: &TokenStream,
) -> TokenStream {
let ruma_api_macros = quote! { #ruma_api::exports::ruma_api_macros };
let ruma_serde = quote! { #ruma_api::exports::ruma_serde };
let serde = quote! { #ruma_api::exports::serde };
let docs = format!(
"Data for a request to the `{}` API endpoint.\n\n{}",
@ -142,239 +80,44 @@ impl Request {
);
let struct_attributes = &self.attributes;
let request_body_struct =
if let Some(body_field) = self.fields.iter().find(|f| f.is_newtype_body()) {
let field = Field { ident: None, colon_token: None, ..body_field.field().clone() };
// Though we don't track the difference between new type body and body
// for lifetimes, the outer check and the macro failing if it encounters
// an illegal combination of field attributes, is enough to guarantee
// `body_lifetimes` correctness.
let (derive_deserialize, lifetimes) = if self.has_body_lifetimes() {
(TokenStream::new(), self.body_lifetimes())
} else {
(quote! { #serde::Deserialize }, TokenStream::new())
};
let method = &metadata.method;
let path = &metadata.path;
let auth_attributes = metadata.authentication.iter().map(|field| {
let cfg_expr = all_cfgs_expr(&field.attrs);
let value = &field.value;
Some((derive_deserialize, quote! { #lifetimes (#field); }))
} else if self.has_body_fields() {
let fields = self.fields.iter().filter(|f| f.is_body());
let (derive_deserialize, lifetimes) = if self.has_body_lifetimes() {
(TokenStream::new(), self.body_lifetimes())
} else {
(quote! { #serde::Deserialize }, TokenStream::new())
};
let fields = fields.map(RequestField::field);
Some((derive_deserialize, quote! { #lifetimes { #(#fields),* } }))
} else {
None
match cfg_expr {
Some(expr) => quote! { #[cfg_attr(#expr, ruma_api(authentication = #value))] },
None => quote! { #[ruma_api(authentication = #value)] },
}
.map(|(derive_deserialize, def)| {
quote! {
/// Data in the request body.
#[derive(
Debug,
#ruma_serde::Outgoing,
#serde::Serialize,
#derive_deserialize
)]
struct RequestBody #def
}
});
});
let request_query_struct = if let Some(f) = self.query_map_field() {
let field = Field { ident: None, colon_token: None, ..f.clone() };
let (derive_deserialize, lifetime) = if self.has_query_lifetimes() {
(TokenStream::new(), self.query_lifetimes())
} else {
(quote! { #serde::Deserialize }, TokenStream::new())
};
quote! {
/// Data in the request's query string.
#[derive(
Debug,
#ruma_serde::Outgoing,
#serde::Serialize,
#derive_deserialize
)]
struct RequestQuery #lifetime (#field);
}
} else if self.has_query_fields() {
let fields = self.fields.iter().filter_map(RequestField::as_query_field);
let (derive_deserialize, lifetime) = if self.has_query_lifetimes() {
(TokenStream::new(), self.query_lifetimes())
} else {
(quote! { #serde::Deserialize }, TokenStream::new())
};
quote! {
/// Data in the request's query string.
#[derive(
Debug,
#ruma_serde::Outgoing,
#serde::Serialize,
#derive_deserialize
)]
struct RequestQuery #lifetime {
#(#fields),*
}
}
} else {
TokenStream::new()
};
let lifetimes = self.combine_lifetimes();
let fields = self.fields.iter().map(|request_field| request_field.field());
let outgoing_request_impl = self.expand_outgoing(metadata, error_ty, &lifetimes, ruma_api);
let incoming_request_impl = self.expand_incoming(metadata, error_ty, ruma_api);
let request_ident = Ident::new("Request", self.request_kw.span());
let lifetimes = self.all_lifetimes();
let lifetimes = lifetimes.iter().map(|(lt, attr)| quote! { #attr #lt });
let fields = &self.fields;
quote! {
#[doc = #docs]
#[derive(Debug, Clone, #ruma_serde::Outgoing, #ruma_serde::_FakeDeriveSerde)]
#[derive(
Clone,
Debug,
#ruma_api_macros::Request,
#ruma_serde::Outgoing,
#ruma_serde::_FakeDeriveSerde,
)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[incoming_derive(!Deserialize)]
#[incoming_derive(!Deserialize, #ruma_api_macros::_FakeDeriveRumaApi)]
#[ruma_api(
method = #method,
path = #path,
error_ty = #error_ty,
)]
#( #auth_attributes )*
#( #struct_attributes )*
pub struct Request #lifetimes {
#(#fields),*
pub struct #request_ident < #(#lifetimes),* > {
#fields
}
#request_body_struct
#request_query_struct
#outgoing_request_impl
#incoming_request_impl
}
}
}
/// The types of fields that a request can have.
pub(crate) enum RequestField {
/// JSON data in the body of the request.
Body(Field),
/// Data in an HTTP header.
Header(Field, Ident),
/// A specific data type in the body of the request.
NewtypeBody(Field),
/// Arbitrary bytes in the body of the request.
NewtypeRawBody(Field),
/// Data that appears in the URL path.
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 {
/// Creates a new `RequestField`.
pub(super) fn new(kind: RequestFieldKind, field: Field, header: Option<Ident>) -> Self {
match kind {
RequestFieldKind::Body => RequestField::Body(field),
RequestFieldKind::Header => {
RequestField::Header(field, header.expect("missing header name"))
}
RequestFieldKind::NewtypeBody => RequestField::NewtypeBody(field),
RequestFieldKind::NewtypeRawBody => RequestField::NewtypeRawBody(field),
RequestFieldKind::Path => RequestField::Path(field),
RequestFieldKind::Query => RequestField::Query(field),
RequestFieldKind::QueryMap => RequestField::QueryMap(field),
}
}
/// Whether or not this request field is a body kind.
pub(super) fn is_body(&self) -> bool {
matches!(self, RequestField::Body(..))
}
/// Whether or not this request field is a header kind.
fn is_header(&self) -> bool {
matches!(self, RequestField::Header(..))
}
/// Whether or not this request field is a newtype body kind.
fn is_newtype_body(&self) -> bool {
matches!(self, RequestField::NewtypeBody(..))
}
/// Whether or not this request field is a path kind.
fn is_path(&self) -> bool {
matches!(self, RequestField::Path(..))
}
/// Whether or not this request field is a query string kind.
pub(super) fn is_query(&self) -> bool {
matches!(self, RequestField::Query(..))
}
/// Return the contained field if this request field is a body kind.
fn as_body_field(&self) -> Option<&Field> {
self.field_of_kind(RequestFieldKind::Body)
}
/// Return the contained field if this request field is a body kind.
fn as_newtype_body_field(&self) -> Option<&Field> {
self.field_of_kind(RequestFieldKind::NewtypeBody)
}
/// Return the contained field if this request field is a raw body kind.
fn as_newtype_raw_body_field(&self) -> Option<&Field> {
self.field_of_kind(RequestFieldKind::NewtypeRawBody)
}
/// Return the contained field if this request field is a query kind.
fn as_query_field(&self) -> Option<&Field> {
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 {
RequestField::Body(field)
| RequestField::Header(field, _)
| RequestField::NewtypeBody(field)
| RequestField::NewtypeRawBody(field)
| RequestField::Path(field)
| RequestField::Query(field)
| RequestField::QueryMap(field) => field,
}
}
/// Gets the inner `Field` value if it's of the provided kind.
fn field_of_kind(&self, kind: RequestFieldKind) -> Option<&Field> {
match (self, kind) {
(RequestField::Body(field), RequestFieldKind::Body)
| (RequestField::Header(field, _), RequestFieldKind::Header)
| (RequestField::NewtypeBody(field), RequestFieldKind::NewtypeBody)
| (RequestField::NewtypeRawBody(field), RequestFieldKind::NewtypeRawBody)
| (RequestField::Path(field), RequestFieldKind::Path)
| (RequestField::Query(field), RequestFieldKind::Query)
| (RequestField::QueryMap(field), RequestFieldKind::QueryMap) => Some(field),
_ => None,
}
}
}
/// The types of fields that a request can have, without their values.
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum RequestFieldKind {
Body,
Header,
NewtypeBody,
NewtypeRawBody,
Path,
Query,
QueryMap,
}

View File

@ -2,76 +2,40 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Attribute, Field, Ident};
use syn::{punctuated::Punctuated, spanned::Spanned, Attribute, Field, Ident, Token};
use super::metadata::Metadata;
mod incoming;
mod outgoing;
use super::{kw, metadata::Metadata};
/// The result of processing the `response` section of the macro.
pub(crate) struct Response {
/// The `response` keyword
pub(super) response_kw: kw::response,
/// The attributes that will be applied to the struct definition.
pub attributes: Vec<Attribute>,
/// The fields of the response.
pub fields: Vec<ResponseField>,
pub fields: Punctuated<Field, Token![,]>,
}
impl Response {
/// Whether or not this response has any data in the HTTP body.
fn has_body_fields(&self) -> bool {
self.fields.iter().any(|field| field.is_body())
}
/// Whether or not this response has any data in HTTP headers.
fn has_header_fields(&self) -> bool {
self.fields.iter().any(|field| field.is_header())
}
/// Gets the newtype body field, if this response has one.
fn newtype_body_field(&self) -> Option<&Field> {
self.fields.iter().find_map(ResponseField::as_newtype_body_field)
}
/// Gets the newtype raw body field, if this response has one.
fn newtype_raw_body_field(&self) -> Option<&Field> {
self.fields.iter().find_map(ResponseField::as_newtype_raw_body_field)
}
pub(super) fn expand(
&self,
metadata: &Metadata,
error_ty: &TokenStream,
ruma_api: &TokenStream,
) -> TokenStream {
let ruma_api_macros = quote! { #ruma_api::exports::ruma_api_macros };
let ruma_serde = quote! { #ruma_api::exports::ruma_serde };
let serde = quote! { #ruma_api::exports::serde };
let docs =
format!("Data in the response from the `{}` API endpoint.", metadata.name.value());
let struct_attributes = &self.attributes;
let def = if let Some(body_field) = self.fields.iter().find(|f| f.is_newtype_body()) {
let field = Field { ident: None, colon_token: None, ..body_field.field().clone() };
quote! { (#field); }
} else if self.has_body_fields() {
let fields = self.fields.iter().filter(|f| f.is_body()).map(ResponseField::field);
quote! { { #(#fields),* } }
} else {
quote! { {} }
};
let response_body_struct = quote! {
/// Data in the response body.
#[derive(Debug, #ruma_serde::Outgoing, #serde::Deserialize, #serde::Serialize)]
struct ResponseBody #def
};
let has_test_exhaustive_field = self
.fields
.iter()
.filter_map(|f| f.field().ident.as_ref())
.filter_map(|f| f.ident.as_ref())
.any(|ident| ident == "__test_exhaustive");
let non_exhaustive_attr = if has_test_exhaustive_field {
@ -80,99 +44,24 @@ impl Response {
quote! { #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] }
};
let fields = self.fields.iter().map(|response_field| response_field.field());
let outgoing_response_impl = self.expand_outgoing(ruma_api);
let incoming_response_impl = self.expand_incoming(error_ty, ruma_api);
let response_ident = Ident::new("Response", self.response_kw.span());
let fields = &self.fields;
quote! {
#[doc = #docs]
#[derive(Debug, Clone, #ruma_serde::Outgoing, #ruma_serde::_FakeDeriveSerde)]
#[derive(
Clone,
Debug,
#ruma_api_macros::Response,
#ruma_serde::Outgoing,
#ruma_serde::_FakeDeriveSerde,
)]
#non_exhaustive_attr
#[incoming_derive(!Deserialize)]
#[incoming_derive(!Deserialize, #ruma_api_macros::_FakeDeriveRumaApi)]
#[ruma_api(error_ty = #error_ty)]
#( #struct_attributes )*
pub struct Response {
#(#fields),*
pub struct #response_ident {
#fields
}
#response_body_struct
#outgoing_response_impl
#incoming_response_impl
}
}
}
/// The types of fields that a response can have.
pub(crate) enum ResponseField {
/// JSON data in the body of the response.
Body(Field),
/// Data in an HTTP header.
Header(Field, Ident),
/// A specific data type in the body of the response.
NewtypeBody(Field),
/// Arbitrary bytes in the body of the response.
NewtypeRawBody(Field),
}
impl ResponseField {
/// Gets the inner `Field` value.
fn field(&self) -> &Field {
match self {
ResponseField::Body(field)
| ResponseField::Header(field, _)
| ResponseField::NewtypeBody(field)
| ResponseField::NewtypeRawBody(field) => field,
}
}
/// Whether or not this response field is a body kind.
pub(super) fn is_body(&self) -> bool {
self.as_body_field().is_some()
}
/// Whether or not this response field is a header kind.
fn is_header(&self) -> bool {
matches!(self, ResponseField::Header(..))
}
/// Whether or not this response field is a newtype body kind.
fn is_newtype_body(&self) -> bool {
self.as_newtype_body_field().is_some()
}
/// Return the contained field if this response field is a body kind.
fn as_body_field(&self) -> Option<&Field> {
match self {
ResponseField::Body(field) => Some(field),
_ => None,
}
}
/// Return the contained field if this response field is a newtype body kind.
fn as_newtype_body_field(&self) -> Option<&Field> {
match self {
ResponseField::NewtypeBody(field) => Some(field),
_ => None,
}
}
/// Return the contained field if this response field is a newtype raw body kind.
fn as_newtype_raw_body_field(&self) -> Option<&Field> {
match self {
ResponseField::NewtypeRawBody(field) => Some(field),
_ => None,
}
}
}
/// The types of fields that a response can have, without their values.
pub(crate) enum ResponseFieldKind {
Body,
Header,
NewtypeBody,
NewtypeRawBody,
}

View File

@ -2,17 +2,42 @@
use syn::{
parse::{Parse, ParseStream},
Ident, Token,
Ident, Lit, Token, Type,
};
/// Value type used for request and response struct attributes
#[allow(clippy::large_enum_variant)]
pub enum MetaValue {
Lit(Lit),
Type(Type),
}
impl Parse for MetaValue {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
if input.peek(Lit) {
input.parse().map(Self::Lit)
} else {
input.parse().map(Self::Type)
}
}
}
/// 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 {
pub struct MetaNameValue<V> {
/// The part left of the equals sign
pub name: Ident,
/// The part right of the equals sign
pub value: Ident,
pub value: V,
}
impl<V: Parse> Parse for MetaNameValue<V> {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let ident = input.parse()?;
let _: Token![=] = input.parse()?;
Ok(MetaNameValue { name: ident, value: input.parse()? })
}
}
/// Like syn::Meta, but only parses ruma_api attributes
@ -21,7 +46,7 @@ pub enum Meta {
Word(Ident),
/// A name-value pair, like `header = CONTENT_TYPE` in `#[ruma_api(header = CONTENT_TYPE)]`
NameValue(MetaNameValue),
NameValue(MetaNameValue<Ident>),
}
impl Meta {

View File

@ -0,0 +1,46 @@
use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::parse::{Parse, ParseStream};
mod kw {
syn::custom_keyword!(None);
syn::custom_keyword!(AccessToken);
syn::custom_keyword!(ServerSignatures);
syn::custom_keyword!(QueryOnlyAccessToken);
}
pub enum AuthScheme {
None(kw::None),
AccessToken(kw::AccessToken),
ServerSignatures(kw::ServerSignatures),
QueryOnlyAccessToken(kw::QueryOnlyAccessToken),
}
impl Parse for AuthScheme {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let lookahead = input.lookahead1();
if lookahead.peek(kw::None) {
input.parse().map(Self::None)
} else if lookahead.peek(kw::AccessToken) {
input.parse().map(Self::AccessToken)
} else if lookahead.peek(kw::ServerSignatures) {
input.parse().map(Self::ServerSignatures)
} else if lookahead.peek(kw::QueryOnlyAccessToken) {
input.parse().map(Self::QueryOnlyAccessToken)
} else {
Err(lookahead.error())
}
}
}
impl ToTokens for AuthScheme {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
AuthScheme::None(kw) => kw.to_tokens(tokens),
AuthScheme::AccessToken(kw) => kw.to_tokens(tokens),
AuthScheme::ServerSignatures(kw) => kw.to_tokens(tokens),
AuthScheme::QueryOnlyAccessToken(kw) => kw.to_tokens(tokens),
}
}
}

View File

@ -11,15 +11,44 @@
#![recursion_limit = "256"]
use proc_macro::TokenStream;
use syn::parse_macro_input;
use self::api::Api;
use syn::{parse_macro_input, DeriveInput};
mod api;
mod attribute;
mod auth_scheme;
mod request;
mod response;
mod util;
use api::Api;
use request::expand_derive_request;
use response::expand_derive_response;
#[proc_macro]
pub fn ruma_api(input: TokenStream) -> TokenStream {
let api = parse_macro_input!(input as Api);
api::expand_all(api).unwrap_or_else(syn::Error::into_compile_error).into()
api.expand_all().into()
}
/// Internal helper taking care of the request-specific parts of `ruma_api!`.
#[proc_macro_derive(Request, attributes(ruma_api))]
pub fn derive_request(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
expand_derive_request(input).unwrap_or_else(syn::Error::into_compile_error).into()
}
/// Internal helper taking care of the response-specific parts of `ruma_api!`.
#[proc_macro_derive(Response, attributes(ruma_api))]
pub fn derive_response(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
expand_derive_response(input).unwrap_or_else(syn::Error::into_compile_error).into()
}
/// A derive macro that generates no code, but registers the ruma_api attribute so both
/// `#[ruma_api(...)]` and `#[cfg_attr(..., ruma_api(...))]` are accepted on the type, its fields
/// and (in case the input is an enum) variants fields.
#[doc(hidden)]
#[proc_macro_derive(_FakeDeriveRumaApi, attributes(ruma_api))]
pub fn fake_derive_ruma_api(_input: TokenStream) -> TokenStream {
TokenStream::new()
}

View File

@ -0,0 +1,491 @@
use std::{
collections::BTreeSet,
convert::{TryFrom, TryInto},
mem,
};
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::{
parse::{Parse, ParseStream},
parse_quote,
punctuated::Punctuated,
DeriveInput, Field, Generics, Ident, Lifetime, Lit, LitStr, Token, Type,
};
use crate::{
attribute::{Meta, MetaNameValue, MetaValue},
auth_scheme::AuthScheme,
util::{collect_lifetime_idents, import_ruma_api},
};
mod incoming;
mod outgoing;
pub fn expand_derive_request(input: DeriveInput) -> syn::Result<TokenStream> {
let fields = match input.data {
syn::Data::Struct(s) => s.fields,
_ => panic!("This derive macro only works on structs"),
};
let mut lifetimes = RequestLifetimes::default();
let fields = fields
.into_iter()
.map(|f| {
let f = RequestField::try_from(f)?;
let ty = &f.field().ty;
match &f {
RequestField::Header(..) => collect_lifetime_idents(&mut lifetimes.header, ty),
RequestField::Body(_) => collect_lifetime_idents(&mut lifetimes.body, ty),
RequestField::NewtypeBody(_) => collect_lifetime_idents(&mut lifetimes.body, ty),
RequestField::NewtypeRawBody(_) => collect_lifetime_idents(&mut lifetimes.body, ty),
RequestField::Path(_) => collect_lifetime_idents(&mut lifetimes.path, ty),
RequestField::Query(_) => collect_lifetime_idents(&mut lifetimes.query, ty),
RequestField::QueryMap(_) => collect_lifetime_idents(&mut lifetimes.query, ty),
}
Ok(f)
})
.collect::<syn::Result<_>>()?;
let mut authentication = None;
let mut error_ty = None;
let mut method = None;
let mut path = None;
for attr in input.attrs {
if !attr.path.is_ident("ruma_api") {
continue;
}
let meta = attr.parse_args_with(Punctuated::<_, Token![,]>::parse_terminated)?;
for MetaNameValue { name, value } in meta {
match value {
MetaValue::Type(t) if name == "authentication" => {
authentication = Some(parse_quote!(#t));
}
MetaValue::Type(t) if name == "method" => {
method = Some(parse_quote!(#t));
}
MetaValue::Type(t) if name == "error_ty" => {
error_ty = Some(t);
}
MetaValue::Lit(Lit::Str(s)) if name == "path" => {
path = Some(s);
}
_ => unreachable!("invalid ruma_api({}) attribute", name),
}
}
}
let request = Request {
ident: input.ident,
generics: input.generics,
fields,
lifetimes,
authentication: authentication.expect("missing authentication attribute"),
method: method.expect("missing method attribute"),
path: path.expect("missing path attribute"),
error_ty: error_ty.expect("missing error_ty attribute"),
};
request.check()?;
Ok(request.expand_all())
}
#[derive(Default)]
struct RequestLifetimes {
pub body: BTreeSet<Lifetime>,
pub path: BTreeSet<Lifetime>,
pub query: BTreeSet<Lifetime>,
pub header: BTreeSet<Lifetime>,
}
struct Request {
ident: Ident,
generics: Generics,
lifetimes: RequestLifetimes,
fields: Vec<RequestField>,
authentication: AuthScheme,
method: Ident,
path: LitStr,
error_ty: Type,
}
impl Request {
fn body_fields(&self) -> impl Iterator<Item = &Field> {
self.fields.iter().filter_map(RequestField::as_body_field)
}
fn query_fields(&self) -> impl Iterator<Item = &Field> {
self.fields.iter().filter_map(RequestField::as_query_field)
}
fn has_body_fields(&self) -> bool {
self.fields.iter().any(|f| matches!(f, RequestField::Body(..)))
}
fn has_header_fields(&self) -> bool {
self.fields.iter().any(|f| matches!(f, RequestField::Header(..)))
}
fn has_path_fields(&self) -> bool {
self.fields.iter().any(|f| matches!(f, RequestField::Path(..)))
}
fn has_query_fields(&self) -> bool {
self.fields.iter().any(|f| matches!(f, RequestField::Query(..)))
}
fn has_lifetimes(&self) -> bool {
!(self.lifetimes.body.is_empty()
&& self.lifetimes.path.is_empty()
&& self.lifetimes.query.is_empty()
&& self.lifetimes.header.is_empty())
}
fn header_fields(&self) -> impl Iterator<Item = &RequestField> {
self.fields.iter().filter(|f| matches!(f, RequestField::Header(..)))
}
fn path_field_count(&self) -> usize {
self.fields.iter().filter(|f| matches!(f, RequestField::Path(..))).count()
}
fn newtype_body_field(&self) -> Option<&Field> {
self.fields.iter().find_map(RequestField::as_newtype_body_field)
}
fn newtype_raw_body_field(&self) -> Option<&Field> {
self.fields.iter().find_map(RequestField::as_newtype_raw_body_field)
}
fn query_map_field(&self) -> Option<&Field> {
self.fields.iter().find_map(RequestField::as_query_map_field)
}
fn expand_all(&self) -> TokenStream {
let ruma_api = import_ruma_api();
let ruma_api_macros = quote! { #ruma_api::exports::ruma_api_macros };
let ruma_serde = quote! { #ruma_api::exports::ruma_serde };
let serde = quote! { #ruma_api::exports::serde };
let request_body_def = if let Some(body_field) = self.newtype_body_field() {
let field = Field { ident: None, colon_token: None, ..body_field.clone() };
Some(quote! { (#field); })
} else if self.has_body_fields() {
let fields = self.fields.iter().filter_map(RequestField::as_body_field);
Some(quote! { { #(#fields),* } })
} else {
None
};
let request_body_struct = request_body_def.map(|def| {
// Though we don't track the difference between newtype body and body
// for lifetimes, the outer check and the macro failing if it encounters
// an illegal combination of field attributes, is enough to guarantee
// `body_lifetimes` correctness.
let (derive_deserialize, generics) = if self.lifetimes.body.is_empty() {
(quote! { #serde::Deserialize }, TokenStream::new())
} else {
let lifetimes = &self.lifetimes.body;
(TokenStream::new(), quote! { < #(#lifetimes),* > })
};
quote! {
/// Data in the request body.
#[derive(
Debug,
#ruma_api_macros::_FakeDeriveRumaApi,
#ruma_serde::Outgoing,
#serde::Serialize,
#derive_deserialize
)]
struct RequestBody #generics #def
}
});
let request_query_def = if let Some(f) = self.query_map_field() {
let field = Field { ident: None, colon_token: None, ..f.clone() };
Some(quote! { (#field); })
} else if self.has_query_fields() {
let fields = self.fields.iter().filter_map(RequestField::as_query_field);
Some(quote! { { #(#fields),* } })
} else {
None
};
let request_query_struct = request_query_def.map(|def| {
let (derive_deserialize, generics) = if self.lifetimes.query.is_empty() {
(quote! { #serde::Deserialize }, TokenStream::new())
} else {
let lifetimes = &self.lifetimes.query;
(TokenStream::new(), quote! { < #(#lifetimes),* > })
};
quote! {
/// Data in the request's query string.
#[derive(
Debug,
#ruma_api_macros::_FakeDeriveRumaApi,
#ruma_serde::Outgoing,
#serde::Serialize,
#derive_deserialize
)]
struct RequestQuery #generics #def
}
});
let outgoing_request_impl = self.expand_outgoing(&ruma_api);
let incoming_request_impl = self.expand_incoming(&ruma_api);
quote! {
#request_body_struct
#request_query_struct
#outgoing_request_impl
#incoming_request_impl
}
}
pub(super) fn check(&self) -> syn::Result<()> {
// TODO: highlight problematic fields
let newtype_body_fields = self.fields.iter().filter(|field| {
matches!(field, RequestField::NewtypeBody(_) | RequestField::NewtypeRawBody(_))
});
let has_newtype_body_field = match newtype_body_fields.count() {
0 => false,
1 => true,
_ => {
return Err(syn::Error::new_spanned(
&self.ident,
"Can't have more than one newtype body field",
))
}
};
let query_map_fields =
self.fields.iter().filter(|f| matches!(f, RequestField::QueryMap(_)));
let has_query_map_field = match query_map_fields.count() {
0 => false,
1 => true,
_ => {
return Err(syn::Error::new_spanned(
&self.ident,
"Can't have more than one query_map field",
))
}
};
let has_body_fields = self.body_fields().count() > 0;
let has_query_fields = self.query_fields().count() > 0;
if has_newtype_body_field && has_body_fields {
return Err(syn::Error::new_spanned(
&self.ident,
"Can't have both a newtype body field and regular body fields",
));
}
if has_query_map_field && has_query_fields {
return Err(syn::Error::new_spanned(
&self.ident,
"Can't have both a query map field and regular query fields",
));
}
// TODO when/if `&[(&str, &str)]` is supported remove this
if has_query_map_field && !self.lifetimes.query.is_empty() {
return Err(syn::Error::new_spanned(
&self.ident,
"Lifetimes are not allowed for query_map fields",
));
}
if self.method == "GET" && (has_body_fields || has_newtype_body_field) {
return Err(syn::Error::new_spanned(
&self.ident,
"GET endpoints can't have body fields",
));
}
Ok(())
}
}
/// The types of fields that a request can have.
enum RequestField {
/// JSON data in the body of the request.
Body(Field),
/// Data in an HTTP header.
Header(Field, Ident),
/// A specific data type in the body of the request.
NewtypeBody(Field),
/// Arbitrary bytes in the body of the request.
NewtypeRawBody(Field),
/// Data that appears in the URL path.
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 {
/// Creates a new `RequestField`.
fn new(kind: RequestFieldKind, field: Field, header: Option<Ident>) -> Self {
match kind {
RequestFieldKind::Body => RequestField::Body(field),
RequestFieldKind::Header => {
RequestField::Header(field, header.expect("missing header name"))
}
RequestFieldKind::NewtypeBody => RequestField::NewtypeBody(field),
RequestFieldKind::NewtypeRawBody => RequestField::NewtypeRawBody(field),
RequestFieldKind::Path => RequestField::Path(field),
RequestFieldKind::Query => RequestField::Query(field),
RequestFieldKind::QueryMap => RequestField::QueryMap(field),
}
}
/// Return the contained field if this request field is a body kind.
pub fn as_body_field(&self) -> Option<&Field> {
self.field_of_kind(RequestFieldKind::Body)
}
/// Return the contained field if this request field is a body kind.
pub fn as_newtype_body_field(&self) -> Option<&Field> {
self.field_of_kind(RequestFieldKind::NewtypeBody)
}
/// Return the contained field if this request field is a raw body kind.
pub fn as_newtype_raw_body_field(&self) -> Option<&Field> {
self.field_of_kind(RequestFieldKind::NewtypeRawBody)
}
/// Return the contained field if this request field is a query kind.
pub fn as_query_field(&self) -> Option<&Field> {
self.field_of_kind(RequestFieldKind::Query)
}
/// Return the contained field if this request field is a query map kind.
pub fn as_query_map_field(&self) -> Option<&Field> {
self.field_of_kind(RequestFieldKind::QueryMap)
}
/// Gets the inner `Field` value.
pub fn field(&self) -> &Field {
match self {
RequestField::Body(field)
| RequestField::Header(field, _)
| RequestField::NewtypeBody(field)
| RequestField::NewtypeRawBody(field)
| RequestField::Path(field)
| RequestField::Query(field)
| RequestField::QueryMap(field) => field,
}
}
/// Gets the inner `Field` value if it's of the provided kind.
fn field_of_kind(&self, kind: RequestFieldKind) -> Option<&Field> {
match (self, kind) {
(RequestField::Body(field), RequestFieldKind::Body)
| (RequestField::Header(field, _), RequestFieldKind::Header)
| (RequestField::NewtypeBody(field), RequestFieldKind::NewtypeBody)
| (RequestField::NewtypeRawBody(field), RequestFieldKind::NewtypeRawBody)
| (RequestField::Path(field), RequestFieldKind::Path)
| (RequestField::Query(field), RequestFieldKind::Query)
| (RequestField::QueryMap(field), RequestFieldKind::QueryMap) => Some(field),
_ => None,
}
}
}
impl TryFrom<Field> for RequestField {
type Error = syn::Error;
fn try_from(mut field: Field) -> syn::Result<Self> {
let mut field_kind = None;
let mut header = None;
for attr in mem::take(&mut field.attrs) {
let meta = match Meta::from_attribute(&attr)? {
Some(m) => m,
None => {
field.attrs.push(attr);
continue;
}
};
if field_kind.is_some() {
return Err(syn::Error::new_spanned(
attr,
"There can only be one field kind attribute",
));
}
field_kind = Some(match meta {
Meta::Word(ident) => match &ident.to_string()[..] {
"body" => RequestFieldKind::Body,
"raw_body" => RequestFieldKind::NewtypeRawBody,
"path" => RequestFieldKind::Path,
"query" => RequestFieldKind::Query,
"query_map" => RequestFieldKind::QueryMap,
_ => {
return Err(syn::Error::new_spanned(
ident,
"Invalid #[ruma_api] argument, expected one of \
`body`, `raw_body`, `path`, `query`, `query_map`",
));
}
},
Meta::NameValue(MetaNameValue { name, value }) => {
if name != "header" {
return Err(syn::Error::new_spanned(
name,
"Invalid #[ruma_api] argument with value, expected `header`",
));
}
header = Some(value);
RequestFieldKind::Header
}
});
}
Ok(RequestField::new(field_kind.unwrap_or(RequestFieldKind::Body), field, header))
}
}
impl Parse for RequestField {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
input.call(Field::parse_named)?.try_into()
}
}
impl ToTokens for RequestField {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.field().to_tokens(tokens)
}
}
/// The types of fields that a request can have, without their values.
#[derive(Clone, Copy, PartialEq, Eq)]
enum RequestFieldKind {
Body,
Header,
NewtypeBody,
NewtypeRawBody,
Path,
Query,
QueryMap,
}

View File

@ -2,23 +2,19 @@ use proc_macro2::{Ident, Span, TokenStream};
use quote::quote;
use super::{Request, RequestField, RequestFieldKind};
use crate::api::metadata::{AuthScheme, Metadata};
use crate::auth_scheme::AuthScheme;
impl Request {
pub fn expand_incoming(
&self,
metadata: &Metadata,
error_ty: &TokenStream,
ruma_api: &TokenStream,
) -> TokenStream {
pub fn expand_incoming(&self, ruma_api: &TokenStream) -> TokenStream {
let http = quote! { #ruma_api::exports::http };
let percent_encoding = quote! { #ruma_api::exports::percent_encoding };
let ruma_serde = quote! { #ruma_api::exports::ruma_serde };
let serde_json = quote! { #ruma_api::exports::serde_json };
let method = &metadata.method;
let method = &self.method;
let error_ty = &self.error_ty;
let incoming_request_type = if self.contains_lifetimes() {
let incoming_request_type = if self.has_lifetimes() {
quote! { IncomingRequest }
} else {
quote! { Request }
@ -28,7 +24,7 @@ impl Request {
// except this one. If we get errors about missing fields in IncomingRequest for
// a path field look here.
let (parse_request_path, path_vars) = if self.has_path_fields() {
let path_string = metadata.path.value();
let path_string = self.path.value();
assert!(path_string.starts_with('/'), "path needs to start with '/'");
assert!(
@ -172,7 +168,7 @@ impl Request {
let extract_body =
(self.has_body_fields() || self.newtype_body_field().is_some()).then(|| {
let body_lifetimes = self.has_body_lifetimes().then(|| {
let body_lifetimes = (!self.lifetimes.body.is_empty()).then(|| {
// duplicate the anonymous lifetime as many times as needed
let lifetimes =
std::iter::repeat(quote! { '_ }).take(self.lifetimes.body.len());
@ -218,16 +214,12 @@ impl Request {
self.vars(RequestFieldKind::Body, quote! { request_body })
};
let non_auth_impls = metadata.authentication.iter().filter_map(|auth| {
matches!(auth.value, AuthScheme::None(_)).then(|| {
let attrs = &auth.attrs;
quote! {
#( #attrs )*
#[automatically_derived]
#[cfg(feature = "server")]
impl #ruma_api::IncomingNonAuthRequest for #incoming_request_type {}
}
})
let non_auth_impl = matches!(self.authentication, AuthScheme::None(_)).then(|| {
quote! {
#[automatically_derived]
#[cfg(feature = "server")]
impl #ruma_api::IncomingNonAuthRequest for #incoming_request_type {}
}
});
quote! {
@ -265,7 +257,7 @@ impl Request {
}
}
#(#non_auth_impls)*
#non_auth_impl
}
}

View File

@ -1,26 +1,21 @@
use proc_macro2::{Ident, Span, TokenStream};
use quote::quote;
use crate::api::metadata::{AuthScheme, Metadata};
use crate::auth_scheme::AuthScheme;
use super::{Request, RequestField, RequestFieldKind};
impl Request {
pub fn expand_outgoing(
&self,
metadata: &Metadata,
error_ty: &TokenStream,
lifetimes: &TokenStream,
ruma_api: &TokenStream,
) -> TokenStream {
pub fn expand_outgoing(&self, ruma_api: &TokenStream) -> TokenStream {
let bytes = quote! { #ruma_api::exports::bytes };
let http = quote! { #ruma_api::exports::http };
let percent_encoding = quote! { #ruma_api::exports::percent_encoding };
let ruma_serde = quote! { #ruma_api::exports::ruma_serde };
let method = &metadata.method;
let method = &self.method;
let error_ty = &self.error_ty;
let request_path_string = if self.has_path_fields() {
let mut format_string = metadata.path.value();
let mut format_string = self.path.value();
let mut format_args = Vec::new();
while let Some(start_of_segment) = format_string.find(':') {
@ -132,38 +127,31 @@ impl Request {
})
.collect();
for auth in &metadata.authentication {
let attrs = &auth.attrs;
let hdr_kv = match auth.value {
AuthScheme::AccessToken(_) => quote! {
#( #attrs )*
let hdr_kv = match self.authentication {
AuthScheme::AccessToken(_) => quote! {
req_headers.insert(
#http::header::AUTHORIZATION,
::std::convert::TryFrom::<_>::try_from(::std::format!(
"Bearer {}",
access_token
.get_required_for_endpoint()
.ok_or(#ruma_api::error::IntoHttpError::NeedsAuthentication)?,
))?,
);
},
AuthScheme::None(_) => quote! {
if let Some(access_token) = access_token.get_not_required_for_endpoint() {
req_headers.insert(
#http::header::AUTHORIZATION,
::std::convert::TryFrom::<_>::try_from(::std::format!(
"Bearer {}",
access_token
.get_required_for_endpoint()
.ok_or(#ruma_api::error::IntoHttpError::NeedsAuthentication)?,
))?,
::std::convert::TryFrom::<_>::try_from(
::std::format!("Bearer {}", access_token),
)?
);
},
AuthScheme::None(_) => quote! {
if let Some(access_token) = access_token.get_not_required_for_endpoint() {
#( #attrs )*
req_headers.insert(
#http::header::AUTHORIZATION,
::std::convert::TryFrom::<_>::try_from(
::std::format!("Bearer {}", access_token),
)?
);
}
},
AuthScheme::QueryOnlyAccessToken(_) | AuthScheme::ServerSignatures(_) => quote! {},
};
header_kvs.extend(hdr_kv);
}
}
},
AuthScheme::QueryOnlyAccessToken(_) | AuthScheme::ServerSignatures(_) => quote! {},
};
header_kvs.extend(hdr_kv);
let request_body = if let Some(field) = self.newtype_raw_body_field() {
let field_name = field.ident.as_ref().expect("expected field to have an identifier");
@ -185,22 +173,21 @@ impl Request {
quote! { <T as ::std::default::Default>::default() }
};
let non_auth_impls = metadata.authentication.iter().filter_map(|auth| {
matches!(auth.value, AuthScheme::None(_)).then(|| {
let attrs = &auth.attrs;
quote! {
#( #attrs )*
#[automatically_derived]
#[cfg(feature = "client")]
impl #lifetimes #ruma_api::OutgoingNonAuthRequest for Request #lifetimes {}
}
})
let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();
let non_auth_impl = matches!(self.authentication, AuthScheme::None(_)).then(|| {
quote! {
#[automatically_derived]
#[cfg(feature = "client")]
impl #impl_generics #ruma_api::OutgoingNonAuthRequest
for Request #ty_generics #where_clause {}
}
});
quote! {
#[automatically_derived]
#[cfg(feature = "client")]
impl #lifetimes #ruma_api::OutgoingRequest for Request #lifetimes {
impl #impl_generics #ruma_api::OutgoingRequest for Request #ty_generics #where_clause {
type EndpointError = #error_ty;
type IncomingResponse = <Response as #ruma_serde::Outgoing>::Incoming;
@ -236,7 +223,7 @@ impl Request {
}
}
#(#non_auth_impls)*
#non_auth_impl
}
}

View File

@ -0,0 +1,314 @@
use std::{
convert::{TryFrom, TryInto},
mem,
};
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::{
parse::{Parse, ParseStream},
punctuated::Punctuated,
visit::Visit,
DeriveInput, Field, Generics, Ident, Lifetime, Token, Type,
};
use crate::{
attribute::{Meta, MetaNameValue, MetaValue},
util,
};
mod incoming;
mod outgoing;
pub fn expand_derive_response(input: DeriveInput) -> syn::Result<TokenStream> {
let fields = match input.data {
syn::Data::Struct(s) => s.fields,
_ => panic!("This derive macro only works on structs"),
};
let fields = fields.into_iter().map(ResponseField::try_from).collect::<syn::Result<_>>()?;
let mut error_ty = None;
for attr in input.attrs {
if !attr.path.is_ident("ruma_api") {
continue;
}
let meta = attr.parse_args_with(Punctuated::<_, Token![,]>::parse_terminated)?;
for MetaNameValue { name, value } in meta {
match value {
MetaValue::Type(t) if name == "error_ty" => {
error_ty = Some(t);
}
_ => unreachable!("invalid ruma_api({}) attribute", name),
}
}
}
let response = Response {
ident: input.ident,
generics: input.generics,
fields,
error_ty: error_ty.unwrap(),
};
response.check()?;
Ok(response.expand_all())
}
struct Response {
ident: Ident,
generics: Generics,
fields: Vec<ResponseField>,
error_ty: Type,
}
impl Response {
/// Whether or not this request has any data in the HTTP body.
fn has_body_fields(&self) -> bool {
self.fields.iter().any(|f| matches!(f, ResponseField::Body(_)))
}
/// Returns the body field.
fn newtype_body_field(&self) -> Option<&Field> {
self.fields.iter().find_map(ResponseField::as_newtype_body_field)
}
/// Returns the body field.
fn newtype_raw_body_field(&self) -> Option<&Field> {
self.fields.iter().find_map(ResponseField::as_newtype_raw_body_field)
}
/// Whether or not this request has any data in the URL path.
fn has_header_fields(&self) -> bool {
self.fields.iter().any(|f| matches!(f, &ResponseField::Header(..)))
}
fn expand_all(&self) -> TokenStream {
let ruma_api = util::import_ruma_api();
let ruma_api_macros = quote! { #ruma_api::exports::ruma_api_macros };
let ruma_serde = quote! { #ruma_api::exports::ruma_serde };
let serde = quote! { #ruma_api::exports::serde };
let response_body_struct =
self.fields.iter().all(|f| !matches!(f, ResponseField::NewtypeRawBody(_))).then(|| {
let newtype_body_field =
self.fields.iter().find(|f| matches!(f, ResponseField::NewtypeBody(_)));
let def = if let Some(body_field) = newtype_body_field {
let field =
Field { ident: None, colon_token: None, ..body_field.field().clone() };
quote! { (#field); }
} else {
let fields = self.fields.iter().filter_map(|f| f.as_body_field());
quote! { { #(#fields),* } }
};
quote! {
/// Data in the response body.
#[derive(
Debug,
#ruma_api_macros::_FakeDeriveRumaApi,
#ruma_serde::Outgoing,
#serde::Deserialize,
#serde::Serialize,
)]
struct ResponseBody #def
}
});
let outgoing_response_impl = self.expand_outgoing(&ruma_api);
let incoming_response_impl = self.expand_incoming(&self.error_ty, &ruma_api);
quote! {
#response_body_struct
#outgoing_response_impl
#incoming_response_impl
}
}
pub fn check(&self) -> syn::Result<()> {
// TODO: highlight problematic fields
if !self.generics.params.is_empty() || self.generics.where_clause.is_some() {
panic!("This macro doesn't support generic types");
}
let newtype_body_fields = self.fields.iter().filter(|f| {
matches!(f, ResponseField::NewtypeBody(_) | ResponseField::NewtypeRawBody(_))
});
let has_newtype_body_field = match newtype_body_fields.count() {
0 => false,
1 => true,
_ => {
return Err(syn::Error::new_spanned(
&self.ident,
"Can't have more than one newtype body field",
))
}
};
let has_body_fields = self.fields.iter().any(|f| matches!(f, ResponseField::Body(_)));
if has_newtype_body_field && has_body_fields {
return Err(syn::Error::new_spanned(
&self.ident,
"Can't have both a newtype body field and regular body fields",
));
}
Ok(())
}
}
/// The types of fields that a response can have.
enum ResponseField {
/// JSON data in the body of the response.
Body(Field),
/// Data in an HTTP header.
Header(Field, Ident),
/// A specific data type in the body of the response.
NewtypeBody(Field),
/// Arbitrary bytes in the body of the response.
NewtypeRawBody(Field),
}
impl ResponseField {
/// Gets the inner `Field` value.
fn field(&self) -> &Field {
match self {
ResponseField::Body(field)
| ResponseField::Header(field, _)
| ResponseField::NewtypeBody(field)
| ResponseField::NewtypeRawBody(field) => field,
}
}
/// Return the contained field if this response field is a body kind.
fn as_body_field(&self) -> Option<&Field> {
match self {
ResponseField::Body(field) => Some(field),
_ => None,
}
}
/// Return the contained field if this response field is a newtype body kind.
fn as_newtype_body_field(&self) -> Option<&Field> {
match self {
ResponseField::NewtypeBody(field) => Some(field),
_ => None,
}
}
/// Return the contained field if this response field is a newtype raw body kind.
fn as_newtype_raw_body_field(&self) -> Option<&Field> {
match self {
ResponseField::NewtypeRawBody(field) => Some(field),
_ => None,
}
}
}
impl TryFrom<Field> for ResponseField {
type Error = syn::Error;
fn try_from(mut field: Field) -> syn::Result<Self> {
if has_lifetime(&field.ty) {
return Err(syn::Error::new_spanned(
field.ident,
"Lifetimes on Response fields cannot be supported until GAT are stable",
));
}
let mut field_kind = None;
let mut header = None;
for attr in mem::take(&mut field.attrs) {
let meta = match Meta::from_attribute(&attr)? {
Some(m) => m,
None => {
field.attrs.push(attr);
continue;
}
};
if field_kind.is_some() {
return Err(syn::Error::new_spanned(
attr,
"There can only be one field kind attribute",
));
}
field_kind = Some(match meta {
Meta::Word(ident) => match &ident.to_string()[..] {
"body" => ResponseFieldKind::NewtypeBody,
"raw_body" => ResponseFieldKind::NewtypeRawBody,
_ => {
return Err(syn::Error::new_spanned(
ident,
"Invalid #[ruma_api] argument with value, expected `body`",
));
}
},
Meta::NameValue(MetaNameValue { name, value }) => {
if name != "header" {
return Err(syn::Error::new_spanned(
name,
"Invalid #[ruma_api] argument with value, expected `header`",
));
}
header = Some(value);
ResponseFieldKind::Header
}
});
}
Ok(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),
ResponseFieldKind::NewtypeRawBody => ResponseField::NewtypeRawBody(field),
})
}
}
impl Parse for ResponseField {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
input.call(Field::parse_named)?.try_into()
}
}
impl ToTokens for ResponseField {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.field().to_tokens(tokens)
}
}
/// The types of fields that a response can have, without their values.
enum ResponseFieldKind {
Body,
Header,
NewtypeBody,
NewtypeRawBody,
}
fn has_lifetime(ty: &Type) -> bool {
struct Visitor {
found_lifetime: bool,
}
impl<'ast> Visit<'ast> for Visitor {
fn visit_lifetime(&mut self, _lt: &'ast Lifetime) {
self.found_lifetime = true;
}
}
let mut vis = Visitor { found_lifetime: false };
vis.visit_type(ty);
vis.found_lifetime
}

View File

@ -1,10 +1,11 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::Type;
use super::{Response, ResponseField};
impl Response {
pub fn expand_incoming(&self, error_ty: &TokenStream, ruma_api: &TokenStream) -> TokenStream {
pub fn expand_incoming(&self, error_ty: &Type, ruma_api: &TokenStream) -> TokenStream {
let http = quote! { #ruma_api::exports::http };
let ruma_serde = quote! { #ruma_api::exports::ruma_serde };
let serde_json = quote! { #ruma_api::exports::serde_json };

View File

@ -5,25 +5,9 @@ use std::collections::BTreeSet;
use proc_macro2::TokenStream;
use proc_macro_crate::{crate_name, FoundCrate};
use quote::{format_ident, quote};
use syn::{AttrStyle, Attribute, Lifetime};
use syn::{parse_quote, visit::Visit, AttrStyle, Attribute, Lifetime, NestedMeta, Type};
/// Generates a `TokenStream` of lifetime identifiers `<'lifetime>`.
pub(crate) fn unique_lifetimes_to_tokens<'a, I: IntoIterator<Item = &'a Lifetime>>(
lifetimes: I,
) -> TokenStream {
let lifetimes = lifetimes.into_iter().collect::<BTreeSet<_>>();
if lifetimes.is_empty() {
TokenStream::new()
} else {
quote! { < #( #lifetimes ),* > }
}
}
pub(crate) fn is_valid_endpoint_path(string: &str) -> bool {
string.as_bytes().iter().all(|b| (0x21..=0x7E).contains(b))
}
pub(crate) fn import_ruma_api() -> TokenStream {
pub fn import_ruma_api() -> TokenStream {
if let Ok(FoundCrate::Name(name)) = crate_name("ruma-api") {
let import = format_ident!("{}", name);
quote! { ::#import }
@ -41,6 +25,48 @@ pub(crate) fn import_ruma_api() -> TokenStream {
}
}
pub(crate) fn is_cfg_attribute(attr: &Attribute) -> bool {
pub fn is_valid_endpoint_path(string: &str) -> bool {
string.as_bytes().iter().all(|b| (0x21..=0x7E).contains(b))
}
pub fn collect_lifetime_idents(lifetimes: &mut BTreeSet<Lifetime>, ty: &Type) {
struct Visitor<'lt>(&'lt mut BTreeSet<Lifetime>);
impl<'ast> Visit<'ast> for Visitor<'_> {
fn visit_lifetime(&mut self, lt: &'ast Lifetime) {
self.0.insert(lt.clone());
}
}
Visitor(lifetimes).visit_type(ty)
}
pub fn is_cfg_attribute(attr: &Attribute) -> bool {
matches!(attr.style, AttrStyle::Outer) && attr.path.is_ident("cfg")
}
pub fn all_cfgs_expr(cfgs: &[Attribute]) -> Option<TokenStream> {
let sub_cfgs: Vec<_> = cfgs.iter().filter_map(extract_cfg).collect();
(!sub_cfgs.is_empty()).then(|| quote! { all( #(#sub_cfgs),* ) })
}
pub fn all_cfgs(cfgs: &[Attribute]) -> Option<Attribute> {
let cfg_expr = all_cfgs_expr(cfgs)?;
Some(parse_quote! { #[cfg( #cfg_expr )] })
}
pub fn extract_cfg(attr: &Attribute) -> Option<NestedMeta> {
if !attr.path.is_ident("cfg") {
return None;
}
let meta = attr.parse_meta().expect("cfg attribute can be parsed to syn::Meta");
let mut list = match meta {
syn::Meta::List(l) => l,
_ => panic!("unexpected cfg syntax"),
};
assert!(list.path.is_ident("cfg"), "expected cfg attributes only");
assert_eq!(list.nested.len(), 1, "expected one item inside cfg()");
Some(list.nested.pop().unwrap().into_value())
}

View File

@ -205,6 +205,7 @@ pub mod exports {
pub use bytes;
pub use http;
pub use percent_encoding;
pub use ruma_api_macros;
pub use ruma_serde;
pub use serde;
pub use serde_json;

View File

@ -1,6 +1,6 @@
#![allow(clippy::exhaustive_structs)]
#[derive(Copy, Clone, Debug, ruma_serde::Outgoing, serde::Serialize)]
/*#[derive(Copy, Clone, Debug, ruma_serde::Outgoing, serde::Serialize)]
pub struct OtherThing<'t> {
pub some: &'t str,
pub t: &'t [u8],
@ -31,7 +31,7 @@ mod empty_response {
response: {}
}
}
}*/
mod nested_types {
use ruma_api::ruma_api;
@ -59,7 +59,7 @@ mod nested_types {
}
}
mod full_request_response {
/*mod full_request_response {
use ruma_api::ruma_api;
use super::{IncomingOtherThing, OtherThing};
@ -159,4 +159,4 @@ mod query_fields {
response: {}
}
}
}*/