api-macros: Move ruma_api! trait impl generation into derive macros
This commit is contained in:
parent
fae75410a9
commit
696c9fba4e
@ -2,17 +2,27 @@
|
|||||||
|
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::TokenStream;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn::Type;
|
use syn::{
|
||||||
|
braced,
|
||||||
|
parse::{Parse, ParseStream},
|
||||||
|
Attribute, Field, Token, Type,
|
||||||
|
};
|
||||||
|
|
||||||
pub(crate) mod attribute;
|
mod metadata;
|
||||||
pub(crate) mod metadata;
|
mod request;
|
||||||
pub(crate) mod parse;
|
mod response;
|
||||||
pub(crate) mod request;
|
|
||||||
pub(crate) mod response;
|
|
||||||
|
|
||||||
use self::{metadata::Metadata, request::Request, response::Response};
|
use self::{metadata::Metadata, request::Request, response::Response};
|
||||||
use crate::util;
|
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.
|
/// The result of processing the `ruma_api` macro, ready for output back to source code.
|
||||||
pub struct Api {
|
pub struct Api {
|
||||||
/// The `metadata` section of the macro.
|
/// The `metadata` section of the macro.
|
||||||
@ -28,65 +38,129 @@ pub struct Api {
|
|||||||
error_ty: Option<Type>,
|
error_ty: Option<Type>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn expand_all(api: Api) -> syn::Result<TokenStream> {
|
impl Api {
|
||||||
let ruma_api = util::import_ruma_api();
|
pub fn expand_all(self) -> TokenStream {
|
||||||
let http = quote! { #ruma_api::exports::http };
|
let ruma_api = util::import_ruma_api();
|
||||||
|
let http = quote! { #ruma_api::exports::http };
|
||||||
|
|
||||||
let metadata = &api.metadata;
|
let metadata = &self.metadata;
|
||||||
let description = &metadata.description;
|
let description = &metadata.description;
|
||||||
let method = &metadata.method;
|
let method = &metadata.method;
|
||||||
let name = &metadata.name;
|
let name = &metadata.name;
|
||||||
let path = &metadata.path;
|
let path = &metadata.path;
|
||||||
let rate_limited: TokenStream = metadata
|
let rate_limited: TokenStream = metadata
|
||||||
.rate_limited
|
.rate_limited
|
||||||
.iter()
|
.iter()
|
||||||
.map(|r| {
|
.map(|r| {
|
||||||
let attrs = &r.attrs;
|
let attrs = &r.attrs;
|
||||||
let value = &r.value;
|
let value = &r.value;
|
||||||
quote! {
|
quote! {
|
||||||
#( #attrs )*
|
#( #attrs )*
|
||||||
rate_limited: #value,
|
rate_limited: #value,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let authentication: TokenStream = api
|
let authentication: TokenStream = self
|
||||||
.metadata
|
.metadata
|
||||||
.authentication
|
.authentication
|
||||||
.iter()
|
.iter()
|
||||||
.map(|r| {
|
.map(|r| {
|
||||||
let attrs = &r.attrs;
|
let attrs = &r.attrs;
|
||||||
let value = &r.value;
|
let value = &r.value;
|
||||||
quote! {
|
quote! {
|
||||||
#( #attrs )*
|
#( #attrs )*
|
||||||
authentication: #ruma_api::AuthScheme::#value,
|
authentication: #ruma_api::AuthScheme::#value,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let error_ty = api
|
let error_ty = self
|
||||||
.error_ty
|
.error_ty
|
||||||
.map_or_else(|| quote! { #ruma_api::error::MatrixError }, |err_ty| quote! { #err_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 request = self.request.map(|req| req.expand(metadata, &error_ty, &ruma_api));
|
||||||
let response = api.response.map(|res| res.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! {
|
quote! {
|
||||||
#[doc = #metadata_doc]
|
#[doc = #metadata_doc]
|
||||||
pub const METADATA: #ruma_api::Metadata = #ruma_api::Metadata {
|
pub const METADATA: #ruma_api::Metadata = #ruma_api::Metadata {
|
||||||
description: #description,
|
description: #description,
|
||||||
method: #http::Method::#method,
|
method: #http::Method::#method,
|
||||||
name: #name,
|
name: #name,
|
||||||
path: #path,
|
path: #path,
|
||||||
#rate_limited
|
#rate_limited
|
||||||
#authentication
|
#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
|
let response = if input.peek(kw::response) {
|
||||||
#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")))]
|
let error_ty = input
|
||||||
type _SilenceUnusedError = #error_ty;
|
.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 })
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
//! Details of the `metadata` section of the procedural macro.
|
//! Details of the `metadata` section of the procedural macro.
|
||||||
|
|
||||||
use proc_macro2::TokenStream;
|
|
||||||
use quote::ToTokens;
|
use quote::ToTokens;
|
||||||
use syn::{
|
use syn::{
|
||||||
braced,
|
braced,
|
||||||
@ -8,7 +7,7 @@ use syn::{
|
|||||||
Attribute, Ident, LitBool, LitStr, Token,
|
Attribute, Ident, LitBool, LitStr, Token,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::util;
|
use crate::{auth_scheme::AuthScheme, util};
|
||||||
|
|
||||||
mod kw {
|
mod kw {
|
||||||
syn::custom_keyword!(metadata);
|
syn::custom_keyword!(metadata);
|
||||||
@ -18,11 +17,6 @@ mod kw {
|
|||||||
syn::custom_keyword!(path);
|
syn::custom_keyword!(path);
|
||||||
syn::custom_keyword!(rate_limited);
|
syn::custom_keyword!(rate_limited);
|
||||||
syn::custom_keyword!(authentication);
|
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
|
/// 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 {
|
enum Field {
|
||||||
Description,
|
Description,
|
||||||
Method,
|
Method,
|
||||||
|
@ -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)
|
|
||||||
}
|
|
@ -1,129 +1,67 @@
|
|||||||
//! Details of the `request` section of the procedural macro.
|
//! 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 proc_macro2::TokenStream;
|
||||||
use quote::quote;
|
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::{kw, metadata::Metadata};
|
||||||
|
use crate::util::{all_cfgs, all_cfgs_expr, extract_cfg};
|
||||||
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>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The result of processing the `request` section of the macro.
|
/// The result of processing the `request` section of the macro.
|
||||||
pub(crate) struct Request {
|
pub(crate) struct Request {
|
||||||
|
/// The `request` keyword
|
||||||
|
pub(super) request_kw: kw::request,
|
||||||
|
|
||||||
/// The attributes that will be applied to the struct definition.
|
/// The attributes that will be applied to the struct definition.
|
||||||
pub(super) attributes: Vec<Attribute>,
|
pub(super) attributes: Vec<Attribute>,
|
||||||
|
|
||||||
/// The fields of the request.
|
/// The fields of the request.
|
||||||
pub(super) fields: Vec<RequestField>,
|
pub(super) fields: Punctuated<Field, Token![,]>,
|
||||||
|
|
||||||
/// The collected lifetime identifiers from the declared fields.
|
|
||||||
pub(super) lifetimes: RequestLifetimes,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Request {
|
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.
|
/// The combination of every fields unique lifetime annotation.
|
||||||
fn combine_lifetimes(&self) -> TokenStream {
|
fn all_lifetimes(&self) -> BTreeMap<Lifetime, Option<Attribute>> {
|
||||||
util::unique_lifetimes_to_tokens(
|
let mut lifetimes = BTreeMap::new();
|
||||||
[
|
|
||||||
&self.lifetimes.body,
|
|
||||||
&self.lifetimes.path,
|
|
||||||
&self.lifetimes.query,
|
|
||||||
&self.lifetimes.header,
|
|
||||||
]
|
|
||||||
.iter()
|
|
||||||
.flat_map(|set| set.iter()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The lifetimes on fields with the `query` attribute.
|
struct Visitor<'lt> {
|
||||||
fn query_lifetimes(&self) -> TokenStream {
|
field_cfg: Option<Attribute>,
|
||||||
util::unique_lifetimes_to_tokens(&self.lifetimes.query)
|
lifetimes: &'lt mut BTreeMap<Lifetime, Option<Attribute>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The lifetimes on fields with the `body` attribute.
|
impl<'ast> Visit<'ast> for Visitor<'_> {
|
||||||
fn body_lifetimes(&self) -> TokenStream {
|
fn visit_lifetime(&mut self, lt: &'ast Lifetime) {
|
||||||
util::unique_lifetimes_to_tokens(&self.lifetimes.body)
|
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.
|
// If at least one field uses this lifetime and has no cfg attribute, we
|
||||||
fn header_fields(&self) -> impl Iterator<Item = &RequestField> {
|
// don't need a cfg attribute for the lifetime either.
|
||||||
self.fields.iter().filter(|field| field.is_header())
|
*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.
|
for field in &self.fields {
|
||||||
fn path_field_count(&self) -> usize {
|
let field_cfg = if field.attrs.is_empty() { None } else { all_cfgs(&field.attrs) };
|
||||||
self.fields.iter().filter(|field| field.is_path()).count()
|
Visitor { lifetimes: &mut lifetimes, field_cfg }.visit_type(&field.ty);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the body field.
|
lifetimes
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn expand(
|
pub(super) fn expand(
|
||||||
@ -132,8 +70,8 @@ impl Request {
|
|||||||
error_ty: &TokenStream,
|
error_ty: &TokenStream,
|
||||||
ruma_api: &TokenStream,
|
ruma_api: &TokenStream,
|
||||||
) -> TokenStream {
|
) -> TokenStream {
|
||||||
|
let ruma_api_macros = quote! { #ruma_api::exports::ruma_api_macros };
|
||||||
let ruma_serde = quote! { #ruma_api::exports::ruma_serde };
|
let ruma_serde = quote! { #ruma_api::exports::ruma_serde };
|
||||||
let serde = quote! { #ruma_api::exports::serde };
|
|
||||||
|
|
||||||
let docs = format!(
|
let docs = format!(
|
||||||
"Data for a request to the `{}` API endpoint.\n\n{}",
|
"Data for a request to the `{}` API endpoint.\n\n{}",
|
||||||
@ -142,239 +80,44 @@ impl Request {
|
|||||||
);
|
);
|
||||||
let struct_attributes = &self.attributes;
|
let struct_attributes = &self.attributes;
|
||||||
|
|
||||||
let request_body_struct =
|
let method = &metadata.method;
|
||||||
if let Some(body_field) = self.fields.iter().find(|f| f.is_newtype_body()) {
|
let path = &metadata.path;
|
||||||
let field = Field { ident: None, colon_token: None, ..body_field.field().clone() };
|
let auth_attributes = metadata.authentication.iter().map(|field| {
|
||||||
// Though we don't track the difference between new type body and body
|
let cfg_expr = all_cfgs_expr(&field.attrs);
|
||||||
// for lifetimes, the outer check and the macro failing if it encounters
|
let value = &field.value;
|
||||||
// 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())
|
|
||||||
};
|
|
||||||
|
|
||||||
Some((derive_deserialize, quote! { #lifetimes (#field); }))
|
match cfg_expr {
|
||||||
} else if self.has_body_fields() {
|
Some(expr) => quote! { #[cfg_attr(#expr, ruma_api(authentication = #value))] },
|
||||||
let fields = self.fields.iter().filter(|f| f.is_body());
|
None => quote! { #[ruma_api(authentication = #value)] },
|
||||||
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
|
|
||||||
}
|
}
|
||||||
.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 request_ident = Ident::new("Request", self.request_kw.span());
|
||||||
let field = Field { ident: None, colon_token: None, ..f.clone() };
|
let lifetimes = self.all_lifetimes();
|
||||||
let (derive_deserialize, lifetime) = if self.has_query_lifetimes() {
|
let lifetimes = lifetimes.iter().map(|(lt, attr)| quote! { #attr #lt });
|
||||||
(TokenStream::new(), self.query_lifetimes())
|
let fields = &self.fields;
|
||||||
} 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);
|
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
#[doc = #docs]
|
#[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)]
|
#[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 )*
|
#( #struct_attributes )*
|
||||||
pub struct Request #lifetimes {
|
pub struct #request_ident < #(#lifetimes),* > {
|
||||||
#(#fields),*
|
#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,
|
|
||||||
}
|
|
||||||
|
@ -2,76 +2,40 @@
|
|||||||
|
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::TokenStream;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn::{Attribute, Field, Ident};
|
use syn::{punctuated::Punctuated, spanned::Spanned, Attribute, Field, Ident, Token};
|
||||||
|
|
||||||
use super::metadata::Metadata;
|
use super::{kw, metadata::Metadata};
|
||||||
|
|
||||||
mod incoming;
|
|
||||||
mod outgoing;
|
|
||||||
|
|
||||||
/// The result of processing the `response` section of the macro.
|
/// The result of processing the `response` section of the macro.
|
||||||
pub(crate) struct Response {
|
pub(crate) struct Response {
|
||||||
|
/// The `response` keyword
|
||||||
|
pub(super) response_kw: kw::response,
|
||||||
|
|
||||||
/// The attributes that will be applied to the struct definition.
|
/// The attributes that will be applied to the struct definition.
|
||||||
pub attributes: Vec<Attribute>,
|
pub attributes: Vec<Attribute>,
|
||||||
|
|
||||||
/// The fields of the response.
|
/// The fields of the response.
|
||||||
pub fields: Vec<ResponseField>,
|
pub fields: Punctuated<Field, Token![,]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Response {
|
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(
|
pub(super) fn expand(
|
||||||
&self,
|
&self,
|
||||||
metadata: &Metadata,
|
metadata: &Metadata,
|
||||||
error_ty: &TokenStream,
|
error_ty: &TokenStream,
|
||||||
ruma_api: &TokenStream,
|
ruma_api: &TokenStream,
|
||||||
) -> TokenStream {
|
) -> TokenStream {
|
||||||
|
let ruma_api_macros = quote! { #ruma_api::exports::ruma_api_macros };
|
||||||
let ruma_serde = quote! { #ruma_api::exports::ruma_serde };
|
let ruma_serde = quote! { #ruma_api::exports::ruma_serde };
|
||||||
let serde = quote! { #ruma_api::exports::serde };
|
|
||||||
|
|
||||||
let docs =
|
let docs =
|
||||||
format!("Data in the response from the `{}` API endpoint.", metadata.name.value());
|
format!("Data in the response from the `{}` API endpoint.", metadata.name.value());
|
||||||
let struct_attributes = &self.attributes;
|
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
|
let has_test_exhaustive_field = self
|
||||||
.fields
|
.fields
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|f| f.field().ident.as_ref())
|
.filter_map(|f| f.ident.as_ref())
|
||||||
.any(|ident| ident == "__test_exhaustive");
|
.any(|ident| ident == "__test_exhaustive");
|
||||||
|
|
||||||
let non_exhaustive_attr = if has_test_exhaustive_field {
|
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)] }
|
quote! { #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] }
|
||||||
};
|
};
|
||||||
|
|
||||||
let fields = self.fields.iter().map(|response_field| response_field.field());
|
let response_ident = Ident::new("Response", self.response_kw.span());
|
||||||
|
let fields = &self.fields;
|
||||||
let outgoing_response_impl = self.expand_outgoing(ruma_api);
|
|
||||||
let incoming_response_impl = self.expand_incoming(error_ty, ruma_api);
|
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
#[doc = #docs]
|
#[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
|
#non_exhaustive_attr
|
||||||
#[incoming_derive(!Deserialize)]
|
#[incoming_derive(!Deserialize, #ruma_api_macros::_FakeDeriveRumaApi)]
|
||||||
|
#[ruma_api(error_ty = #error_ty)]
|
||||||
#( #struct_attributes )*
|
#( #struct_attributes )*
|
||||||
pub struct Response {
|
pub struct #response_ident {
|
||||||
#(#fields),*
|
#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,
|
|
||||||
}
|
|
||||||
|
@ -2,17 +2,42 @@
|
|||||||
|
|
||||||
use syn::{
|
use syn::{
|
||||||
parse::{Parse, ParseStream},
|
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
|
/// 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.
|
/// 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
|
/// The part left of the equals sign
|
||||||
pub name: Ident,
|
pub name: Ident,
|
||||||
|
|
||||||
/// The part right of the equals sign
|
/// 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
|
/// Like syn::Meta, but only parses ruma_api attributes
|
||||||
@ -21,7 +46,7 @@ pub enum Meta {
|
|||||||
Word(Ident),
|
Word(Ident),
|
||||||
|
|
||||||
/// A name-value pair, like `header = CONTENT_TYPE` in `#[ruma_api(header = CONTENT_TYPE)]`
|
/// A name-value pair, like `header = CONTENT_TYPE` in `#[ruma_api(header = CONTENT_TYPE)]`
|
||||||
NameValue(MetaNameValue),
|
NameValue(MetaNameValue<Ident>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Meta {
|
impl Meta {
|
46
crates/ruma-api-macros/src/auth_scheme.rs
Normal file
46
crates/ruma-api-macros/src/auth_scheme.rs
Normal 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,15 +11,44 @@
|
|||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "256"]
|
||||||
|
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use syn::parse_macro_input;
|
use syn::{parse_macro_input, DeriveInput};
|
||||||
|
|
||||||
use self::api::Api;
|
|
||||||
|
|
||||||
mod api;
|
mod api;
|
||||||
|
mod attribute;
|
||||||
|
mod auth_scheme;
|
||||||
|
mod request;
|
||||||
|
mod response;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
|
use api::Api;
|
||||||
|
use request::expand_derive_request;
|
||||||
|
use response::expand_derive_response;
|
||||||
|
|
||||||
#[proc_macro]
|
#[proc_macro]
|
||||||
pub fn ruma_api(input: TokenStream) -> TokenStream {
|
pub fn ruma_api(input: TokenStream) -> TokenStream {
|
||||||
let api = parse_macro_input!(input as Api);
|
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()
|
||||||
}
|
}
|
||||||
|
491
crates/ruma-api-macros/src/request.rs
Normal file
491
crates/ruma-api-macros/src/request.rs
Normal 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,
|
||||||
|
}
|
@ -2,23 +2,19 @@ use proc_macro2::{Ident, Span, TokenStream};
|
|||||||
use quote::quote;
|
use quote::quote;
|
||||||
|
|
||||||
use super::{Request, RequestField, RequestFieldKind};
|
use super::{Request, RequestField, RequestFieldKind};
|
||||||
use crate::api::metadata::{AuthScheme, Metadata};
|
use crate::auth_scheme::AuthScheme;
|
||||||
|
|
||||||
impl Request {
|
impl Request {
|
||||||
pub fn expand_incoming(
|
pub fn expand_incoming(&self, ruma_api: &TokenStream) -> TokenStream {
|
||||||
&self,
|
|
||||||
metadata: &Metadata,
|
|
||||||
error_ty: &TokenStream,
|
|
||||||
ruma_api: &TokenStream,
|
|
||||||
) -> TokenStream {
|
|
||||||
let http = quote! { #ruma_api::exports::http };
|
let http = quote! { #ruma_api::exports::http };
|
||||||
let percent_encoding = quote! { #ruma_api::exports::percent_encoding };
|
let percent_encoding = quote! { #ruma_api::exports::percent_encoding };
|
||||||
let ruma_serde = quote! { #ruma_api::exports::ruma_serde };
|
let ruma_serde = quote! { #ruma_api::exports::ruma_serde };
|
||||||
let serde_json = quote! { #ruma_api::exports::serde_json };
|
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 }
|
quote! { IncomingRequest }
|
||||||
} else {
|
} else {
|
||||||
quote! { Request }
|
quote! { Request }
|
||||||
@ -28,7 +24,7 @@ impl Request {
|
|||||||
// except this one. If we get errors about missing fields in IncomingRequest for
|
// except this one. If we get errors about missing fields in IncomingRequest for
|
||||||
// a path field look here.
|
// a path field look here.
|
||||||
let (parse_request_path, path_vars) = if self.has_path_fields() {
|
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!(path_string.starts_with('/'), "path needs to start with '/'");
|
||||||
assert!(
|
assert!(
|
||||||
@ -172,7 +168,7 @@ impl Request {
|
|||||||
|
|
||||||
let extract_body =
|
let extract_body =
|
||||||
(self.has_body_fields() || self.newtype_body_field().is_some()).then(|| {
|
(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
|
// duplicate the anonymous lifetime as many times as needed
|
||||||
let lifetimes =
|
let lifetimes =
|
||||||
std::iter::repeat(quote! { '_ }).take(self.lifetimes.body.len());
|
std::iter::repeat(quote! { '_ }).take(self.lifetimes.body.len());
|
||||||
@ -218,16 +214,12 @@ impl Request {
|
|||||||
self.vars(RequestFieldKind::Body, quote! { request_body })
|
self.vars(RequestFieldKind::Body, quote! { request_body })
|
||||||
};
|
};
|
||||||
|
|
||||||
let non_auth_impls = metadata.authentication.iter().filter_map(|auth| {
|
let non_auth_impl = matches!(self.authentication, AuthScheme::None(_)).then(|| {
|
||||||
matches!(auth.value, AuthScheme::None(_)).then(|| {
|
quote! {
|
||||||
let attrs = &auth.attrs;
|
#[automatically_derived]
|
||||||
quote! {
|
#[cfg(feature = "server")]
|
||||||
#( #attrs )*
|
impl #ruma_api::IncomingNonAuthRequest for #incoming_request_type {}
|
||||||
#[automatically_derived]
|
}
|
||||||
#[cfg(feature = "server")]
|
|
||||||
impl #ruma_api::IncomingNonAuthRequest for #incoming_request_type {}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
@ -265,7 +257,7 @@ impl Request {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#(#non_auth_impls)*
|
#non_auth_impl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,26 +1,21 @@
|
|||||||
use proc_macro2::{Ident, Span, TokenStream};
|
use proc_macro2::{Ident, Span, TokenStream};
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
|
|
||||||
use crate::api::metadata::{AuthScheme, Metadata};
|
use crate::auth_scheme::AuthScheme;
|
||||||
|
|
||||||
use super::{Request, RequestField, RequestFieldKind};
|
use super::{Request, RequestField, RequestFieldKind};
|
||||||
|
|
||||||
impl Request {
|
impl Request {
|
||||||
pub fn expand_outgoing(
|
pub fn expand_outgoing(&self, ruma_api: &TokenStream) -> TokenStream {
|
||||||
&self,
|
|
||||||
metadata: &Metadata,
|
|
||||||
error_ty: &TokenStream,
|
|
||||||
lifetimes: &TokenStream,
|
|
||||||
ruma_api: &TokenStream,
|
|
||||||
) -> TokenStream {
|
|
||||||
let bytes = quote! { #ruma_api::exports::bytes };
|
let bytes = quote! { #ruma_api::exports::bytes };
|
||||||
let http = quote! { #ruma_api::exports::http };
|
let http = quote! { #ruma_api::exports::http };
|
||||||
let percent_encoding = quote! { #ruma_api::exports::percent_encoding };
|
let percent_encoding = quote! { #ruma_api::exports::percent_encoding };
|
||||||
let ruma_serde = quote! { #ruma_api::exports::ruma_serde };
|
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 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();
|
let mut format_args = Vec::new();
|
||||||
|
|
||||||
while let Some(start_of_segment) = format_string.find(':') {
|
while let Some(start_of_segment) = format_string.find(':') {
|
||||||
@ -132,38 +127,31 @@ impl Request {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
for auth in &metadata.authentication {
|
let hdr_kv = match self.authentication {
|
||||||
let attrs = &auth.attrs;
|
AuthScheme::AccessToken(_) => quote! {
|
||||||
|
req_headers.insert(
|
||||||
let hdr_kv = match auth.value {
|
#http::header::AUTHORIZATION,
|
||||||
AuthScheme::AccessToken(_) => quote! {
|
::std::convert::TryFrom::<_>::try_from(::std::format!(
|
||||||
#( #attrs )*
|
"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(
|
req_headers.insert(
|
||||||
#http::header::AUTHORIZATION,
|
#http::header::AUTHORIZATION,
|
||||||
::std::convert::TryFrom::<_>::try_from(::std::format!(
|
::std::convert::TryFrom::<_>::try_from(
|
||||||
"Bearer {}",
|
::std::format!("Bearer {}", access_token),
|
||||||
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() {
|
AuthScheme::QueryOnlyAccessToken(_) | AuthScheme::ServerSignatures(_) => quote! {},
|
||||||
#( #attrs )*
|
};
|
||||||
req_headers.insert(
|
header_kvs.extend(hdr_kv);
|
||||||
#http::header::AUTHORIZATION,
|
|
||||||
::std::convert::TryFrom::<_>::try_from(
|
|
||||||
::std::format!("Bearer {}", access_token),
|
|
||||||
)?
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
AuthScheme::QueryOnlyAccessToken(_) | AuthScheme::ServerSignatures(_) => quote! {},
|
|
||||||
};
|
|
||||||
|
|
||||||
header_kvs.extend(hdr_kv);
|
|
||||||
}
|
|
||||||
|
|
||||||
let request_body = if let Some(field) = self.newtype_raw_body_field() {
|
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");
|
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() }
|
quote! { <T as ::std::default::Default>::default() }
|
||||||
};
|
};
|
||||||
|
|
||||||
let non_auth_impls = metadata.authentication.iter().filter_map(|auth| {
|
let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();
|
||||||
matches!(auth.value, AuthScheme::None(_)).then(|| {
|
|
||||||
let attrs = &auth.attrs;
|
let non_auth_impl = matches!(self.authentication, AuthScheme::None(_)).then(|| {
|
||||||
quote! {
|
quote! {
|
||||||
#( #attrs )*
|
#[automatically_derived]
|
||||||
#[automatically_derived]
|
#[cfg(feature = "client")]
|
||||||
#[cfg(feature = "client")]
|
impl #impl_generics #ruma_api::OutgoingNonAuthRequest
|
||||||
impl #lifetimes #ruma_api::OutgoingNonAuthRequest for Request #lifetimes {}
|
for Request #ty_generics #where_clause {}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
#[automatically_derived]
|
#[automatically_derived]
|
||||||
#[cfg(feature = "client")]
|
#[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 EndpointError = #error_ty;
|
||||||
type IncomingResponse = <Response as #ruma_serde::Outgoing>::Incoming;
|
type IncomingResponse = <Response as #ruma_serde::Outgoing>::Incoming;
|
||||||
|
|
||||||
@ -236,7 +223,7 @@ impl Request {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#(#non_auth_impls)*
|
#non_auth_impl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
314
crates/ruma-api-macros/src/response.rs
Normal file
314
crates/ruma-api-macros/src/response.rs
Normal 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
|
||||||
|
}
|
@ -1,10 +1,11 @@
|
|||||||
use proc_macro2::TokenStream;
|
use proc_macro2::TokenStream;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
|
use syn::Type;
|
||||||
|
|
||||||
use super::{Response, ResponseField};
|
use super::{Response, ResponseField};
|
||||||
|
|
||||||
impl Response {
|
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 http = quote! { #ruma_api::exports::http };
|
||||||
let ruma_serde = quote! { #ruma_api::exports::ruma_serde };
|
let ruma_serde = quote! { #ruma_api::exports::ruma_serde };
|
||||||
let serde_json = quote! { #ruma_api::exports::serde_json };
|
let serde_json = quote! { #ruma_api::exports::serde_json };
|
@ -5,25 +5,9 @@ use std::collections::BTreeSet;
|
|||||||
use proc_macro2::TokenStream;
|
use proc_macro2::TokenStream;
|
||||||
use proc_macro_crate::{crate_name, FoundCrate};
|
use proc_macro_crate::{crate_name, FoundCrate};
|
||||||
use quote::{format_ident, quote};
|
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 fn import_ruma_api() -> TokenStream {
|
||||||
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 {
|
|
||||||
if let Ok(FoundCrate::Name(name)) = crate_name("ruma-api") {
|
if let Ok(FoundCrate::Name(name)) = crate_name("ruma-api") {
|
||||||
let import = format_ident!("{}", name);
|
let import = format_ident!("{}", name);
|
||||||
quote! { ::#import }
|
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")
|
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())
|
||||||
|
}
|
||||||
|
@ -205,6 +205,7 @@ pub mod exports {
|
|||||||
pub use bytes;
|
pub use bytes;
|
||||||
pub use http;
|
pub use http;
|
||||||
pub use percent_encoding;
|
pub use percent_encoding;
|
||||||
|
pub use ruma_api_macros;
|
||||||
pub use ruma_serde;
|
pub use ruma_serde;
|
||||||
pub use serde;
|
pub use serde;
|
||||||
pub use serde_json;
|
pub use serde_json;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
#![allow(clippy::exhaustive_structs)]
|
#![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 struct OtherThing<'t> {
|
||||||
pub some: &'t str,
|
pub some: &'t str,
|
||||||
pub t: &'t [u8],
|
pub t: &'t [u8],
|
||||||
@ -31,7 +31,7 @@ mod empty_response {
|
|||||||
|
|
||||||
response: {}
|
response: {}
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
mod nested_types {
|
mod nested_types {
|
||||||
use ruma_api::ruma_api;
|
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 ruma_api::ruma_api;
|
||||||
|
|
||||||
use super::{IncomingOtherThing, OtherThing};
|
use super::{IncomingOtherThing, OtherThing};
|
||||||
@ -159,4 +159,4 @@ mod query_fields {
|
|||||||
|
|
||||||
response: {}
|
response: {}
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
Loading…
x
Reference in New Issue
Block a user