Add 'ruma-api-macros/' from commit '02bcb5f0384340d3a722ae93cdb0b74015902c7e'
git-subtree-dir: ruma-api-macros git-subtree-mainline: 954b24f2bbda1617aaa80889ee1dd01f742a4164 git-subtree-split: 02bcb5f0384340d3a722ae93cdb0b74015902c7e
This commit is contained in:
commit
e291730288
2
ruma-api-macros/.gitignore
vendored
Normal file
2
ruma-api-macros/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/target
|
||||
Cargo.lock
|
19
ruma-api-macros/.travis.yml
Normal file
19
ruma-api-macros/.travis.yml
Normal file
@ -0,0 +1,19 @@
|
||||
language: "rust"
|
||||
cache: "cargo"
|
||||
before_script:
|
||||
- "rustup component add rustfmt"
|
||||
- "rustup component add clippy"
|
||||
- "cargo install --force cargo-audit"
|
||||
- "cargo generate-lockfile"
|
||||
script:
|
||||
- "cargo audit"
|
||||
- "cargo fmt --all -- --check"
|
||||
- "cargo clippy --all-targets --all-features -- -D warnings"
|
||||
- "cargo build --verbose"
|
||||
- "cargo test --verbose"
|
||||
notifications:
|
||||
email: false
|
||||
irc:
|
||||
channels:
|
||||
- secure: "FiHwNDkLqlzn+fZnn42uZ+GWm59S9OJreUIz9r7+nXrxUBeBcthQlqamJUiuYryVohzqLydBVv6xmT5wgS/LxRnj4f363eBpKejuSktglnf2rl8JjuSXZVgrPMDmrfgkBdC+aMCPzdw2fIHSWmvQMr/t9kGW9cHl0VlLxPAhnAsry+E1Kxrrz4IuOJmyb43VqPf/GO6VCDzTpHiKHKe5Rp7i2IkbGus2GiSD/UMrgUTWmMOFoejl7fWX7SH9kvSrN/SCYldVOYA4nazeZfaHv7mCX6G8U3GGXTHwjAVAluXyYgUCYpsYKC5KGkUJFcLhjaBu5qpmlI0EZd/rsgscOBzqfJ0D/WkahWiKtlQEKZ7UEAhA3SaAhcrSh2kSQFf2GW1T8kfzqlnBtjpqSvCFuOpY5XQcSYEEX7qxT1aiK2UBi9iAKgMnG1SDEfeFERekw0KJPKbwJDMV7NhCg9kYVBHG1hxvFeYqMmnFrjLlRDQQrbDHrP9Avdtg0FScolsFVmT+uatBuRXDcqunssolfnWguyrQ0Z9KGauv0iqkwFwO7jQSA9f87wgsuzqlzstHRxoGGlPtGt4J/+MhyA3lOEXwBa5eotjILI7iykK+ykJ33cOTGcqyXbkWoYRZ6+fS2guI+f2CxxsYWUOK2UgMyYKEwtraC3duVIGtQR+zuvc="
|
||||
use_notice: true
|
35
ruma-api-macros/Cargo.toml
Normal file
35
ruma-api-macros/Cargo.toml
Normal file
@ -0,0 +1,35 @@
|
||||
[package]
|
||||
authors = ["Jimmy Cuadra <jimmy@jimmycuadra.com>"]
|
||||
categories = ["api-bindings", "web-programming"]
|
||||
description = "A procedural macro for generating ruma-api Endpoints."
|
||||
documentation = "https://docs.rs/ruma-api-macros"
|
||||
homepage = "https://github.com/ruma/ruma-api-macros"
|
||||
keywords = ["matrix", "chat", "messaging", "ruma"]
|
||||
license = "MIT"
|
||||
name = "ruma-api-macros"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/ruma/ruma-api-macros"
|
||||
version = "0.6.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
quote = "0.6.12"
|
||||
ruma-api = "0.9.0"
|
||||
proc-macro2 = "0.4.30"
|
||||
|
||||
[dependencies.syn]
|
||||
version = "0.15.35"
|
||||
features = ["full"]
|
||||
|
||||
[dev-dependencies]
|
||||
http = "0.1.17"
|
||||
serde_json = "1.0.39"
|
||||
serde_urlencoded = "0.5.5"
|
||||
url = "1.7.2"
|
||||
|
||||
[dev-dependencies.serde]
|
||||
version = "1.0.92"
|
||||
features = ["derive"]
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
19
ruma-api-macros/LICENSE
Normal file
19
ruma-api-macros/LICENSE
Normal file
@ -0,0 +1,19 @@
|
||||
Copyright (c) 2017 Jimmy Cuadra
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
73
ruma-api-macros/README.md
Normal file
73
ruma-api-macros/README.md
Normal file
@ -0,0 +1,73 @@
|
||||
# ruma-api-macros
|
||||
|
||||
[](https://travis-ci.org/ruma/ruma-api-macros)
|
||||
|
||||
**ruma-api-macros** provides a procedural macro for easily generating [ruma-api](https://github.com/ruma/ruma-api)-compatible API endpoints.
|
||||
You define the endpoint's metadata, request fields, and response fields, and the macro generates all the necessary types and implements all the necessary traits.
|
||||
|
||||
## Usage
|
||||
|
||||
Here is an example that shows most of the macro's functionality.
|
||||
|
||||
``` rust
|
||||
#![feature(proc_macro, try_from)]
|
||||
|
||||
extern crate http;
|
||||
extern crate ruma_api;
|
||||
extern crate ruma_api_macros;
|
||||
extern crate serde;
|
||||
#[macro_use] extern crate serde_derive;
|
||||
extern crate serde_json;
|
||||
extern crate serde_urlencoded;
|
||||
extern crate url;
|
||||
|
||||
pub mod some_endpoint {
|
||||
use ruma_api_macros::ruma_api;
|
||||
|
||||
ruma_api! {
|
||||
metadata {
|
||||
description: "Does something.",
|
||||
method: GET, // An `http::Method` constant. No imports required.
|
||||
name: "some_endpoint",
|
||||
path: "/_matrix/some/endpoint/:baz", // Variable path components start with a colon.
|
||||
rate_limited: false,
|
||||
requires_authentication: false,
|
||||
}
|
||||
|
||||
request {
|
||||
// With no attribute on the field, it will be put into the body of the request.
|
||||
pub foo: String,
|
||||
|
||||
// This value will be put into the "Content-Type" HTTP header.
|
||||
#[ruma_api(header = CONTENT_TYPE)]
|
||||
pub content_type: String
|
||||
|
||||
// This value will be put into the query string of the request's URL.
|
||||
#[ruma_api(query)]
|
||||
pub bar: String,
|
||||
|
||||
// This value will be inserted into the request's URL in place of the
|
||||
// ":baz" path component.
|
||||
#[ruma_api(path)]
|
||||
pub baz: String,
|
||||
}
|
||||
|
||||
response {
|
||||
// This value will be extracted from the "Content-Type" HTTP header.
|
||||
#[ruma_api(header = CONTENT_TYPE)]
|
||||
pub content_type: String
|
||||
|
||||
// With no attribute on the field, it will be extracted from the body of the response.
|
||||
pub value: String,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
ruma-api-macros has [comprehensive documentation](https://docs.rs/ruma-api-macros) available on docs.rs.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](http://opensource.org/licenses/MIT)
|
63
ruma-api-macros/src/api/attribute.rs
Normal file
63
ruma-api-macros/src/api/attribute.rs
Normal file
@ -0,0 +1,63 @@
|
||||
//! Details of the `#[ruma_api(...)]` attributes.
|
||||
|
||||
use syn::{
|
||||
parenthesized,
|
||||
parse::{Parse, ParseStream},
|
||||
Ident, Token,
|
||||
};
|
||||
|
||||
/// Like syn::Meta, but only parses ruma_api attributes
|
||||
pub enum Meta {
|
||||
/// A single word, like `query` in `#[ruma_api(query)]`
|
||||
Word(Ident),
|
||||
/// A name-value pair, like `header = CONTENT_TYPE` in `#[ruma_api(header = CONTENT_TYPE)]`
|
||||
NameValue(MetaNameValue),
|
||||
}
|
||||
|
||||
impl Meta {
|
||||
/// Check if the given attribute is a ruma_api attribute. If it is, parse it, if not, return
|
||||
/// it unchanged. Panics if the argument is an invalid ruma_api attribute.
|
||||
pub fn from_attribute(attr: syn::Attribute) -> Result<Self, syn::Attribute> {
|
||||
match &attr.path {
|
||||
syn::Path {
|
||||
leading_colon: None,
|
||||
segments,
|
||||
} => {
|
||||
if segments.len() == 1 && segments[0].ident == "ruma_api" {
|
||||
Ok(syn::parse2(attr.tts)
|
||||
.expect("ruma_api! could not parse request field attributes"))
|
||||
} else {
|
||||
Err(attr)
|
||||
}
|
||||
}
|
||||
_ => Err(attr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Like syn::MetaNameValue, but expects an identifier as the value. Also, we don't care about the
|
||||
/// the span of the equals sign, so we don't have the `eq_token` field from syn::MetaNameValue.
|
||||
pub struct MetaNameValue {
|
||||
/// The part left of the equals sign
|
||||
pub name: Ident,
|
||||
/// The part right of the equals sign
|
||||
pub value: Ident,
|
||||
}
|
||||
|
||||
impl Parse for Meta {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let content;
|
||||
let _ = parenthesized!(content in input);
|
||||
let ident = content.parse()?;
|
||||
|
||||
if content.peek(Token![=]) {
|
||||
let _ = content.parse::<Token![=]>();
|
||||
Ok(Meta::NameValue(MetaNameValue {
|
||||
name: ident,
|
||||
value: content.parse()?,
|
||||
}))
|
||||
} else {
|
||||
Ok(Meta::Word(ident))
|
||||
}
|
||||
}
|
||||
}
|
111
ruma-api-macros/src/api/metadata.rs
Normal file
111
ruma-api-macros/src/api/metadata.rs
Normal file
@ -0,0 +1,111 @@
|
||||
//! Details of the `metadata` section of the procedural macro.
|
||||
|
||||
use proc_macro2::Ident;
|
||||
use syn::{Expr, ExprLit, FieldValue, Lit, LitBool, LitStr, Member};
|
||||
|
||||
/// The result of processing the `metadata` section of the macro.
|
||||
pub struct Metadata {
|
||||
/// The description field.
|
||||
pub description: LitStr,
|
||||
/// The method field.
|
||||
pub method: Ident,
|
||||
/// The name field.
|
||||
pub name: LitStr,
|
||||
/// The path field.
|
||||
pub path: LitStr,
|
||||
/// The rate_limited field.
|
||||
pub rate_limited: LitBool,
|
||||
/// The description field.
|
||||
pub requires_authentication: LitBool,
|
||||
}
|
||||
|
||||
impl From<Vec<FieldValue>> for Metadata {
|
||||
fn from(field_values: Vec<FieldValue>) -> Self {
|
||||
let mut description = None;
|
||||
let mut method = None;
|
||||
let mut name = None;
|
||||
let mut path = None;
|
||||
let mut rate_limited = None;
|
||||
let mut requires_authentication = None;
|
||||
|
||||
for field_value in field_values {
|
||||
let identifier = match field_value.member {
|
||||
Member::Named(identifier) => identifier,
|
||||
_ => panic!("expected Member::Named"),
|
||||
};
|
||||
|
||||
match &identifier.to_string()[..] {
|
||||
"description" => {
|
||||
let literal = match field_value.expr {
|
||||
Expr::Lit(ExprLit {
|
||||
lit: Lit::Str(s), ..
|
||||
}) => s,
|
||||
_ => panic!("expected string literal"),
|
||||
};
|
||||
description = Some(literal);
|
||||
}
|
||||
"method" => {
|
||||
let expr_path = match field_value.expr {
|
||||
Expr::Path(expr_path) => expr_path,
|
||||
_ => panic!("expected Expr::Path"),
|
||||
};
|
||||
let path = expr_path.path;
|
||||
let mut segments = path.segments.iter();
|
||||
let method_name = segments.next().expect("expected non-empty path");
|
||||
assert!(
|
||||
segments.next().is_none(),
|
||||
"ruma_api! expects a one-component path for `metadata` `method`"
|
||||
);
|
||||
method = Some(method_name.ident.clone());
|
||||
}
|
||||
"name" => {
|
||||
let literal = match field_value.expr {
|
||||
Expr::Lit(ExprLit {
|
||||
lit: Lit::Str(s), ..
|
||||
}) => s,
|
||||
_ => panic!("expected string literal"),
|
||||
};
|
||||
name = Some(literal);
|
||||
}
|
||||
"path" => {
|
||||
let literal = match field_value.expr {
|
||||
Expr::Lit(ExprLit {
|
||||
lit: Lit::Str(s), ..
|
||||
}) => s,
|
||||
_ => panic!("expected string literal"),
|
||||
};
|
||||
path = Some(literal);
|
||||
}
|
||||
"rate_limited" => {
|
||||
let literal = match field_value.expr {
|
||||
Expr::Lit(ExprLit {
|
||||
lit: Lit::Bool(b), ..
|
||||
}) => b,
|
||||
_ => panic!("expected Expr::Lit"),
|
||||
};
|
||||
rate_limited = Some(literal)
|
||||
}
|
||||
"requires_authentication" => {
|
||||
let literal = match field_value.expr {
|
||||
Expr::Lit(ExprLit {
|
||||
lit: Lit::Bool(b), ..
|
||||
}) => b,
|
||||
_ => panic!("expected Expr::Lit"),
|
||||
};
|
||||
requires_authentication = Some(literal)
|
||||
}
|
||||
_ => panic!("ruma_api! metadata included unexpected field"),
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
description: description.expect("ruma_api! `metadata` is missing `description`"),
|
||||
method: method.expect("ruma_api! `metadata` is missing `method`"),
|
||||
name: name.expect("ruma_api! `metadata` is missing `name`"),
|
||||
path: path.expect("ruma_api! `metadata` is missing `path`"),
|
||||
rate_limited: rate_limited.expect("ruma_api! `metadata` is missing `rate_limited`"),
|
||||
requires_authentication: requires_authentication
|
||||
.expect("ruma_api! `metadata` is missing `requires_authentication`"),
|
||||
}
|
||||
}
|
||||
}
|
486
ruma-api-macros/src/api/mod.rs
Normal file
486
ruma-api-macros/src/api/mod.rs
Normal file
@ -0,0 +1,486 @@
|
||||
//! Details of the `ruma_api` procedural macro.
|
||||
|
||||
use proc_macro2::{Span, TokenStream};
|
||||
use quote::{quote, ToTokens};
|
||||
use syn::{
|
||||
braced,
|
||||
parse::{Parse, ParseStream, Result},
|
||||
Field, FieldValue, Ident, Meta, Token,
|
||||
};
|
||||
|
||||
mod attribute;
|
||||
mod metadata;
|
||||
mod request;
|
||||
mod response;
|
||||
|
||||
use self::{metadata::Metadata, request::Request, response::Response};
|
||||
|
||||
/// Removes `serde` attributes from struct fields.
|
||||
pub fn strip_serde_attrs(field: &Field) -> Field {
|
||||
let mut field = field.clone();
|
||||
|
||||
field.attrs = field
|
||||
.attrs
|
||||
.into_iter()
|
||||
.filter(|attr| {
|
||||
let meta = attr
|
||||
.interpret_meta()
|
||||
.expect("ruma_api! could not parse field attributes");
|
||||
|
||||
let meta_list = match meta {
|
||||
Meta::List(meta_list) => meta_list,
|
||||
_ => return true,
|
||||
};
|
||||
|
||||
if &meta_list.ident.to_string() == "serde" {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
.collect();
|
||||
|
||||
field
|
||||
}
|
||||
|
||||
/// The result of processing the `ruma_api` macro, ready for output back to source code.
|
||||
pub struct Api {
|
||||
/// The `metadata` section of the macro.
|
||||
metadata: Metadata,
|
||||
/// The `request` section of the macro.
|
||||
request: Request,
|
||||
/// The `response` section of the macro.
|
||||
response: Response,
|
||||
}
|
||||
|
||||
impl From<RawApi> for Api {
|
||||
fn from(raw_api: RawApi) -> Self {
|
||||
Self {
|
||||
metadata: raw_api.metadata.into(),
|
||||
request: raw_api.request.into(),
|
||||
response: raw_api.response.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Api {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let description = &self.metadata.description;
|
||||
let method = &self.metadata.method;
|
||||
// We don't (currently) use this literal as a literal in the generated code. Instead we just
|
||||
// put it into doc comments, for which the span information is irrelevant. So we can work
|
||||
// with only the literal's value from here on.
|
||||
let name = &self.metadata.name.value();
|
||||
let path = &self.metadata.path;
|
||||
let rate_limited = &self.metadata.rate_limited;
|
||||
let requires_authentication = &self.metadata.requires_authentication;
|
||||
|
||||
let request = &self.request;
|
||||
let request_types = quote! { #request };
|
||||
let response = &self.response;
|
||||
let response_types = quote! { #response };
|
||||
|
||||
let extract_request_path = if self.request.has_path_fields() {
|
||||
quote! {
|
||||
let path_segments: Vec<&str> = request.uri().path()[1..].split('/').collect();
|
||||
}
|
||||
} else {
|
||||
TokenStream::new()
|
||||
};
|
||||
|
||||
let (set_request_path, parse_request_path) = if self.request.has_path_fields() {
|
||||
let path_str = path.value();
|
||||
|
||||
assert!(path_str.starts_with('/'), "path needs to start with '/'");
|
||||
assert!(
|
||||
path_str.chars().filter(|c| *c == ':').count() == self.request.path_field_count(),
|
||||
"number of declared path parameters needs to match amount of placeholders in path"
|
||||
);
|
||||
|
||||
let request_path_init_fields = self.request.request_path_init_fields();
|
||||
|
||||
let path_segments = path_str[1..].split('/');
|
||||
let path_segment_push = path_segments.clone().map(|segment| {
|
||||
let arg = if segment.starts_with(':') {
|
||||
let path_var = &segment[1..];
|
||||
let path_var_ident = Ident::new(path_var, Span::call_site());
|
||||
quote!(&request_path.#path_var_ident.to_string())
|
||||
} else {
|
||||
quote!(#segment)
|
||||
};
|
||||
|
||||
quote! {
|
||||
path_segments.push(#arg);
|
||||
}
|
||||
});
|
||||
|
||||
let set_tokens = quote! {
|
||||
let request_path = RequestPath {
|
||||
#request_path_init_fields
|
||||
};
|
||||
|
||||
// This `unwrap()` can only fail when the url is a
|
||||
// cannot-be-base url like `mailto:` or `data:`, which is not
|
||||
// the case for our placeholder url.
|
||||
let mut path_segments = url.path_segments_mut().unwrap();
|
||||
#(#path_segment_push)*
|
||||
};
|
||||
|
||||
let path_fields = path_segments
|
||||
.enumerate()
|
||||
.filter(|(_, s)| s.starts_with(':'))
|
||||
.map(|(i, segment)| {
|
||||
let path_var = &segment[1..];
|
||||
let path_var_ident = Ident::new(path_var, Span::call_site());
|
||||
let path_field = self
|
||||
.request
|
||||
.path_field(path_var)
|
||||
.expect("expected request to have path field");
|
||||
let ty = &path_field.ty;
|
||||
|
||||
quote! {
|
||||
#path_var_ident: {
|
||||
let segment = path_segments.get(#i).unwrap().as_bytes();
|
||||
let decoded =
|
||||
::url::percent_encoding::percent_decode(segment)
|
||||
.decode_utf8_lossy();
|
||||
#ty::deserialize(decoded.into_deserializer())
|
||||
.map_err(|e: ::serde_json::error::Error| e)?
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let parse_tokens = quote! {
|
||||
#(#path_fields,)*
|
||||
};
|
||||
|
||||
(set_tokens, parse_tokens)
|
||||
} else {
|
||||
let set_tokens = quote! {
|
||||
url.set_path(metadata.path);
|
||||
};
|
||||
let parse_tokens = TokenStream::new();
|
||||
(set_tokens, parse_tokens)
|
||||
};
|
||||
|
||||
let set_request_query = if self.request.has_query_fields() {
|
||||
let request_query_init_fields = self.request.request_query_init_fields();
|
||||
|
||||
quote! {
|
||||
let request_query = RequestQuery {
|
||||
#request_query_init_fields
|
||||
};
|
||||
|
||||
url.set_query(Some(&::serde_urlencoded::to_string(request_query)?));
|
||||
}
|
||||
} else {
|
||||
TokenStream::new()
|
||||
};
|
||||
|
||||
let extract_request_query = if self.request.has_query_fields() {
|
||||
quote! {
|
||||
let request_query: RequestQuery =
|
||||
::serde_urlencoded::from_str(&request.uri().query().unwrap_or(""))?;
|
||||
}
|
||||
} else {
|
||||
TokenStream::new()
|
||||
};
|
||||
|
||||
let parse_request_query = if self.request.has_query_fields() {
|
||||
self.request.request_init_query_fields()
|
||||
} else {
|
||||
TokenStream::new()
|
||||
};
|
||||
|
||||
let add_headers_to_request = if self.request.has_header_fields() {
|
||||
let add_headers = self.request.add_headers_to_request();
|
||||
quote! {
|
||||
let headers = http_request.headers_mut();
|
||||
#add_headers
|
||||
}
|
||||
} else {
|
||||
TokenStream::new()
|
||||
};
|
||||
|
||||
let extract_request_headers = if self.request.has_header_fields() {
|
||||
quote! {
|
||||
let headers = request.headers();
|
||||
}
|
||||
} else {
|
||||
TokenStream::new()
|
||||
};
|
||||
|
||||
let parse_request_headers = if self.request.has_header_fields() {
|
||||
self.request.parse_headers_from_request()
|
||||
} else {
|
||||
TokenStream::new()
|
||||
};
|
||||
|
||||
let create_http_request = if let Some(field) = self.request.newtype_body_field() {
|
||||
let field_name = field
|
||||
.ident
|
||||
.as_ref()
|
||||
.expect("expected field to have an identifier");
|
||||
|
||||
quote! {
|
||||
let request_body = RequestBody(request.#field_name);
|
||||
|
||||
let mut http_request = ::http::Request::new(::serde_json::to_vec(&request_body)?);
|
||||
}
|
||||
} else if self.request.has_body_fields() {
|
||||
let request_body_init_fields = self.request.request_body_init_fields();
|
||||
|
||||
quote! {
|
||||
let request_body = RequestBody {
|
||||
#request_body_init_fields
|
||||
};
|
||||
|
||||
let mut http_request = ::http::Request::new(::serde_json::to_vec(&request_body)?);
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
let mut http_request = ::http::Request::new(Vec::new());
|
||||
}
|
||||
};
|
||||
|
||||
let extract_request_body = if let Some(field) = self.request.newtype_body_field() {
|
||||
let ty = &field.ty;
|
||||
quote! {
|
||||
let request_body: #ty =
|
||||
::serde_json::from_slice(request.body().as_slice())?;
|
||||
}
|
||||
} else if self.request.has_body_fields() {
|
||||
quote! {
|
||||
let request_body: RequestBody =
|
||||
::serde_json::from_slice(request.body().as_slice())?;
|
||||
}
|
||||
} else {
|
||||
TokenStream::new()
|
||||
};
|
||||
|
||||
let parse_request_body = if let Some(field) = self.request.newtype_body_field() {
|
||||
let field_name = field
|
||||
.ident
|
||||
.as_ref()
|
||||
.expect("expected field to have an identifier");
|
||||
|
||||
quote! {
|
||||
#field_name: request_body,
|
||||
}
|
||||
} else if self.request.has_body_fields() {
|
||||
self.request.request_init_body_fields()
|
||||
} else {
|
||||
TokenStream::new()
|
||||
};
|
||||
|
||||
let try_deserialize_response_body = if let Some(field) = self.response.newtype_body_field()
|
||||
{
|
||||
let field_type = &field.ty;
|
||||
|
||||
quote! {
|
||||
::serde_json::from_slice::<#field_type>(http_response.into_body().as_slice())?
|
||||
}
|
||||
} else if self.response.has_body_fields() {
|
||||
quote! {
|
||||
::serde_json::from_slice::<ResponseBody>(http_response.into_body().as_slice())?
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
()
|
||||
}
|
||||
};
|
||||
|
||||
let extract_response_headers = if self.response.has_header_fields() {
|
||||
quote! {
|
||||
let mut headers = http_response.headers().clone();
|
||||
}
|
||||
} else {
|
||||
TokenStream::new()
|
||||
};
|
||||
|
||||
let response_init_fields = if self.response.has_fields() {
|
||||
self.response.init_fields()
|
||||
} else {
|
||||
TokenStream::new()
|
||||
};
|
||||
|
||||
let serialize_response_headers = self.response.apply_header_fields();
|
||||
|
||||
let try_serialize_response_body = if self.response.has_body() {
|
||||
let body = self.response.to_body();
|
||||
quote! {
|
||||
::serde_json::to_vec(&#body)?
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
"{}".as_bytes().to_vec()
|
||||
}
|
||||
};
|
||||
|
||||
let endpoint_doc = format!("The `{}` API endpoint.\n\n{}", name, description.value());
|
||||
let request_doc = format!("Data for a request to the `{}` API endpoint.", name);
|
||||
let response_doc = format!("Data in the response from the `{}` API endpoint.", name);
|
||||
|
||||
let api = quote! {
|
||||
use ::ruma_api::Endpoint as _;
|
||||
use ::serde::Deserialize as _;
|
||||
use ::serde::de::{Error as _, IntoDeserializer as _};
|
||||
|
||||
use ::std::convert::{TryInto as _};
|
||||
|
||||
#[doc = #endpoint_doc]
|
||||
#[derive(Debug)]
|
||||
pub struct Endpoint;
|
||||
|
||||
#[doc = #request_doc]
|
||||
#request_types
|
||||
|
||||
impl ::std::convert::TryFrom<::http::Request<Vec<u8>>> for Request {
|
||||
type Error = ::ruma_api::Error;
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn try_from(request: ::http::Request<Vec<u8>>) -> Result<Self, Self::Error> {
|
||||
#extract_request_path
|
||||
#extract_request_query
|
||||
#extract_request_headers
|
||||
#extract_request_body
|
||||
|
||||
Ok(Request {
|
||||
#parse_request_path
|
||||
#parse_request_query
|
||||
#parse_request_headers
|
||||
#parse_request_body
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ::std::convert::TryFrom<Request> for ::http::Request<Vec<u8>> {
|
||||
type Error = ::ruma_api::Error;
|
||||
|
||||
#[allow(unused_mut, unused_variables)]
|
||||
fn try_from(request: Request) -> Result<Self, Self::Error> {
|
||||
let metadata = Endpoint::METADATA;
|
||||
|
||||
// Use dummy homeserver url which has to be overwritten in
|
||||
// the calling code. Previously (with http::Uri) this was
|
||||
// not required, but Url::parse only accepts absolute urls.
|
||||
let mut url = ::url::Url::parse("http://invalid-host-please-change/").unwrap();
|
||||
|
||||
{ #set_request_path }
|
||||
{ #set_request_query }
|
||||
|
||||
#create_http_request
|
||||
|
||||
*http_request.method_mut() = ::http::Method::#method;
|
||||
*http_request.uri_mut() = url.into_string().parse().unwrap();
|
||||
|
||||
{ #add_headers_to_request }
|
||||
|
||||
Ok(http_request)
|
||||
}
|
||||
}
|
||||
|
||||
#[doc = #response_doc]
|
||||
#response_types
|
||||
|
||||
impl ::std::convert::TryFrom<Response> for ::http::Response<Vec<u8>> {
|
||||
type Error = ::ruma_api::Error;
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn try_from(response: Response) -> Result<Self, Self::Error> {
|
||||
let response = ::http::Response::builder()
|
||||
.header(::http::header::CONTENT_TYPE, "application/json")
|
||||
#serialize_response_headers
|
||||
.body(#try_serialize_response_body)
|
||||
.unwrap();
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl ::std::convert::TryFrom<::http::Response<Vec<u8>>> for Response {
|
||||
type Error = ::ruma_api::Error;
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn try_from(http_response: ::http::Response<Vec<u8>>) -> Result<Self, Self::Error> {
|
||||
if http_response.status().is_success() {
|
||||
#extract_response_headers
|
||||
|
||||
let response_body = #try_deserialize_response_body;
|
||||
Ok(Response {
|
||||
#response_init_fields
|
||||
})
|
||||
} else {
|
||||
Err(http_response.status().clone().into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ::ruma_api::Endpoint for Endpoint {
|
||||
type Request = Request;
|
||||
type Response = Response;
|
||||
|
||||
/// Metadata for the `#name` endpoint.
|
||||
const METADATA: ::ruma_api::Metadata = ::ruma_api::Metadata {
|
||||
description: #description,
|
||||
method: ::http::Method::#method,
|
||||
name: #name,
|
||||
path: #path,
|
||||
rate_limited: #rate_limited,
|
||||
requires_authentication: #requires_authentication,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
api.to_tokens(tokens);
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom keyword macros for syn.
|
||||
mod kw {
|
||||
use syn::custom_keyword;
|
||||
|
||||
custom_keyword!(metadata);
|
||||
custom_keyword!(request);
|
||||
custom_keyword!(response);
|
||||
}
|
||||
|
||||
/// The entire `ruma_api!` macro structure directly as it appears in the source code..
|
||||
pub struct RawApi {
|
||||
/// The `metadata` section of the macro.
|
||||
pub metadata: Vec<FieldValue>,
|
||||
/// The `request` section of the macro.
|
||||
pub request: Vec<Field>,
|
||||
/// The `response` section of the macro.
|
||||
pub response: Vec<Field>,
|
||||
}
|
||||
|
||||
impl Parse for RawApi {
|
||||
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
||||
input.parse::<kw::metadata>()?;
|
||||
let metadata;
|
||||
braced!(metadata in input);
|
||||
|
||||
input.parse::<kw::request>()?;
|
||||
let request;
|
||||
braced!(request in input);
|
||||
|
||||
input.parse::<kw::response>()?;
|
||||
let response;
|
||||
braced!(response in input);
|
||||
|
||||
Ok(Self {
|
||||
metadata: metadata
|
||||
.parse_terminated::<FieldValue, Token![,]>(FieldValue::parse)?
|
||||
.into_iter()
|
||||
.collect(),
|
||||
request: request
|
||||
.parse_terminated::<Field, Token![,]>(Field::parse_named)?
|
||||
.into_iter()
|
||||
.collect(),
|
||||
response: response
|
||||
.parse_terminated::<Field, Token![,]>(Field::parse_named)?
|
||||
.into_iter()
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
}
|
442
ruma-api-macros/src/api/request.rs
Normal file
442
ruma-api-macros/src/api/request.rs
Normal file
@ -0,0 +1,442 @@
|
||||
//! Details of the `request` section of the procedural macro.
|
||||
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{quote, quote_spanned, ToTokens};
|
||||
use syn::{spanned::Spanned, Field, Ident};
|
||||
|
||||
use crate::api::{
|
||||
attribute::{Meta, MetaNameValue},
|
||||
strip_serde_attrs,
|
||||
};
|
||||
|
||||
/// The result of processing the `request` section of the macro.
|
||||
pub struct Request {
|
||||
/// The fields of the request.
|
||||
fields: Vec<RequestField>,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
/// Produces code to add necessary HTTP headers to an `http::Request`.
|
||||
pub fn add_headers_to_request(&self) -> TokenStream {
|
||||
let append_stmts = self.header_fields().map(|request_field| {
|
||||
let (field, header_name) = match request_field {
|
||||
RequestField::Header(field, header_name) => (field, header_name),
|
||||
_ => panic!("expected request field to be header variant"),
|
||||
};
|
||||
|
||||
let field_name = &field.ident;
|
||||
|
||||
quote! {
|
||||
headers.append(
|
||||
::http::header::#header_name,
|
||||
::http::header::HeaderValue::from_str(request.#field_name.as_ref())
|
||||
.expect("failed to convert value into HeaderValue"),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
#(#append_stmts)*
|
||||
}
|
||||
}
|
||||
|
||||
/// Produces code to extract fields from the HTTP headers in an `http::Request`.
|
||||
pub fn parse_headers_from_request(&self) -> TokenStream {
|
||||
let fields = self.header_fields().map(|request_field| {
|
||||
let (field, header_name) = match request_field {
|
||||
RequestField::Header(field, header_name) => (field, header_name),
|
||||
_ => panic!("expected request field to be header variant"),
|
||||
};
|
||||
|
||||
let field_name = &field.ident;
|
||||
let header_name_string = header_name.to_string();
|
||||
|
||||
quote! {
|
||||
#field_name: headers.get(::http::header::#header_name)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.ok_or(::serde_json::Error::missing_field(#header_name_string))?
|
||||
.to_owned()
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
#(#fields,)*
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether or not this request has any data in the HTTP body.
|
||||
pub 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.
|
||||
pub 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.
|
||||
pub 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.
|
||||
pub fn has_query_fields(&self) -> bool {
|
||||
self.fields.iter().any(|field| field.is_query())
|
||||
}
|
||||
|
||||
/// Produces an iterator over all the header fields.
|
||||
pub fn header_fields(&self) -> impl Iterator<Item = &RequestField> {
|
||||
self.fields.iter().filter(|field| field.is_header())
|
||||
}
|
||||
|
||||
/// Gets the number of path fields.
|
||||
pub fn path_field_count(&self) -> usize {
|
||||
self.fields.iter().filter(|field| field.is_path()).count()
|
||||
}
|
||||
|
||||
/// Gets the path field with the given name.
|
||||
pub fn path_field(&self, name: &str) -> Option<&Field> {
|
||||
self.fields
|
||||
.iter()
|
||||
.flat_map(|f| f.field_of_kind(RequestFieldKind::Path))
|
||||
.find(|field| {
|
||||
field
|
||||
.ident
|
||||
.as_ref()
|
||||
.expect("expected field to have an identifier")
|
||||
== name
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the body field.
|
||||
pub fn newtype_body_field(&self) -> Option<&Field> {
|
||||
for request_field in self.fields.iter() {
|
||||
match *request_field {
|
||||
RequestField::NewtypeBody(ref field) => {
|
||||
return Some(field);
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Produces code for a struct initializer for body fields on a variable named `request`.
|
||||
pub fn request_body_init_fields(&self) -> TokenStream {
|
||||
self.struct_init_fields(RequestFieldKind::Body, quote!(request))
|
||||
}
|
||||
|
||||
/// Produces code for a struct initializer for path fields on a variable named `request`.
|
||||
pub fn request_path_init_fields(&self) -> TokenStream {
|
||||
self.struct_init_fields(RequestFieldKind::Path, quote!(request))
|
||||
}
|
||||
|
||||
/// Produces code for a struct initializer for query string fields on a variable named `request`.
|
||||
pub fn request_query_init_fields(&self) -> TokenStream {
|
||||
self.struct_init_fields(RequestFieldKind::Query, quote!(request))
|
||||
}
|
||||
|
||||
/// Produces code for a struct initializer for body fields on a variable named `request_body`.
|
||||
pub fn request_init_body_fields(&self) -> TokenStream {
|
||||
self.struct_init_fields(RequestFieldKind::Body, quote!(request_body))
|
||||
}
|
||||
|
||||
/// Produces code for a struct initializer for query string fields on a variable named
|
||||
/// `request_query`.
|
||||
pub fn request_init_query_fields(&self) -> TokenStream {
|
||||
self.struct_init_fields(RequestFieldKind::Query, quote!(request_query))
|
||||
}
|
||||
|
||||
/// Produces code for a struct initializer for the given field kind to be accessed through the
|
||||
/// given variable name.
|
||||
fn struct_init_fields(
|
||||
&self,
|
||||
request_field_kind: RequestFieldKind,
|
||||
src: TokenStream,
|
||||
) -> TokenStream {
|
||||
let fields = self.fields.iter().filter_map(|f| {
|
||||
f.field_of_kind(request_field_kind).map(|field| {
|
||||
let field_name = field
|
||||
.ident
|
||||
.as_ref()
|
||||
.expect("expected field to have an identifier");
|
||||
let span = field.span();
|
||||
|
||||
quote_spanned! {span=>
|
||||
#field_name: #src.#field_name
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
quote! {
|
||||
#(#fields,)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Field>> for Request {
|
||||
fn from(fields: Vec<Field>) -> Self {
|
||||
let mut has_newtype_body = false;
|
||||
|
||||
let fields = fields.into_iter().map(|mut field| {
|
||||
let mut field_kind = RequestFieldKind::Body;
|
||||
let mut header = None;
|
||||
|
||||
field.attrs = field.attrs.into_iter().filter_map(|attr| {
|
||||
let meta = match Meta::from_attribute(attr) {
|
||||
Ok(meta) => meta,
|
||||
Err(attr) => return Some(attr),
|
||||
};
|
||||
|
||||
match meta {
|
||||
Meta::Word(ident) => {
|
||||
match &ident.to_string()[..] {
|
||||
"body" => {
|
||||
has_newtype_body = true;
|
||||
field_kind = RequestFieldKind::NewtypeBody;
|
||||
}
|
||||
"path" => field_kind = RequestFieldKind::Path,
|
||||
"query" => field_kind = RequestFieldKind::Query,
|
||||
_ => panic!("ruma_api! single-word attribute on requests must be: body, path, or query"),
|
||||
}
|
||||
}
|
||||
Meta::NameValue(MetaNameValue { name, value }) => {
|
||||
assert!(
|
||||
name == "header",
|
||||
"ruma_api! name/value pair attribute on requests must be: header"
|
||||
);
|
||||
|
||||
header = Some(value);
|
||||
field_kind = RequestFieldKind::Header;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}).collect();
|
||||
|
||||
if field_kind == RequestFieldKind::Body {
|
||||
assert!(
|
||||
!has_newtype_body,
|
||||
"ruma_api! requests cannot have both normal body fields and a newtype body field"
|
||||
);
|
||||
}
|
||||
|
||||
RequestField::new(field_kind, field, header)
|
||||
}).collect();
|
||||
|
||||
Self { fields }
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Request {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let request_struct_header = quote! {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Request
|
||||
};
|
||||
|
||||
let request_struct_body = if self.fields.is_empty() {
|
||||
quote!(;)
|
||||
} else {
|
||||
let fields = self.fields.iter().map(|request_field| {
|
||||
let field = request_field.field();
|
||||
let span = field.span();
|
||||
|
||||
let stripped_field = strip_serde_attrs(field);
|
||||
|
||||
quote_spanned!(span=> #stripped_field)
|
||||
});
|
||||
|
||||
quote! {
|
||||
{
|
||||
#(#fields),*
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let request_body_struct = if let Some(newtype_body_field) = self.newtype_body_field() {
|
||||
let field = newtype_body_field.clone();
|
||||
let ty = &field.ty;
|
||||
let span = field.span();
|
||||
|
||||
quote_spanned! {span=>
|
||||
/// Data in the request body.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct RequestBody(#ty);
|
||||
}
|
||||
} else if self.has_body_fields() {
|
||||
let fields = self
|
||||
.fields
|
||||
.iter()
|
||||
.filter_map(|request_field| match *request_field {
|
||||
RequestField::Body(ref field) => {
|
||||
let span = field.span();
|
||||
Some(quote_spanned!(span=> #field))
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
|
||||
quote! {
|
||||
/// Data in the request body.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct RequestBody {
|
||||
#(#fields),*
|
||||
}
|
||||
}
|
||||
} else {
|
||||
TokenStream::new()
|
||||
};
|
||||
|
||||
let request_path_struct = if self.has_path_fields() {
|
||||
let fields = self
|
||||
.fields
|
||||
.iter()
|
||||
.filter_map(|request_field| match *request_field {
|
||||
RequestField::Path(ref field) => {
|
||||
let span = field.span();
|
||||
|
||||
Some(quote_spanned!(span=> #field))
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
|
||||
quote! {
|
||||
/// Data in the request path.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct RequestPath {
|
||||
#(#fields),*
|
||||
}
|
||||
}
|
||||
} else {
|
||||
TokenStream::new()
|
||||
};
|
||||
|
||||
let request_query_struct = if self.has_query_fields() {
|
||||
let fields = self
|
||||
.fields
|
||||
.iter()
|
||||
.filter_map(|request_field| match *request_field {
|
||||
RequestField::Query(ref field) => {
|
||||
let span = field.span();
|
||||
Some(quote_spanned!(span=> #field))
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
|
||||
quote! {
|
||||
/// Data in the request's query string.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct RequestQuery {
|
||||
#(#fields),*
|
||||
}
|
||||
}
|
||||
} else {
|
||||
TokenStream::new()
|
||||
};
|
||||
|
||||
let request = quote! {
|
||||
#request_struct_header
|
||||
#request_struct_body
|
||||
#request_body_struct
|
||||
#request_path_struct
|
||||
#request_query_struct
|
||||
};
|
||||
|
||||
request.to_tokens(tokens);
|
||||
}
|
||||
}
|
||||
|
||||
/// The types of fields that a request can have.
|
||||
pub 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),
|
||||
/// Data that appears in the URL path.
|
||||
Path(Field),
|
||||
/// Data that appears in the query string.
|
||||
Query(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::Path => RequestField::Path(field),
|
||||
RequestFieldKind::Query => RequestField::Query(field),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the kind of the request field.
|
||||
fn kind(&self) -> RequestFieldKind {
|
||||
match *self {
|
||||
RequestField::Body(..) => RequestFieldKind::Body,
|
||||
RequestField::Header(..) => RequestFieldKind::Header,
|
||||
RequestField::NewtypeBody(..) => RequestFieldKind::NewtypeBody,
|
||||
RequestField::Path(..) => RequestFieldKind::Path,
|
||||
RequestField::Query(..) => RequestFieldKind::Query,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether or not this request field is a body kind.
|
||||
fn is_body(&self) -> bool {
|
||||
self.kind() == RequestFieldKind::Body
|
||||
}
|
||||
|
||||
/// Whether or not this request field is a header kind.
|
||||
fn is_header(&self) -> bool {
|
||||
self.kind() == RequestFieldKind::Header
|
||||
}
|
||||
|
||||
/// Whether or not this request field is a path kind.
|
||||
fn is_path(&self) -> bool {
|
||||
self.kind() == RequestFieldKind::Path
|
||||
}
|
||||
|
||||
/// Whether or not this request field is a query string kind.
|
||||
fn is_query(&self) -> bool {
|
||||
self.kind() == RequestFieldKind::Query
|
||||
}
|
||||
|
||||
/// Gets the inner `Field` value.
|
||||
fn field(&self) -> &Field {
|
||||
match *self {
|
||||
RequestField::Body(ref field)
|
||||
| RequestField::Header(ref field, _)
|
||||
| RequestField::NewtypeBody(ref field)
|
||||
| RequestField::Path(ref field)
|
||||
| RequestField::Query(ref field) => field,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the inner `Field` value if it's of the provided kind.
|
||||
fn field_of_kind(&self, kind: RequestFieldKind) -> Option<&Field> {
|
||||
if self.kind() == kind {
|
||||
Some(self.field())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The types of fields that a request can have, without their values.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum RequestFieldKind {
|
||||
/// See the similarly named variant of `RequestField`.
|
||||
Body,
|
||||
/// See the similarly named variant of `RequestField`.
|
||||
Header,
|
||||
/// See the similarly named variant of `RequestField`.
|
||||
NewtypeBody,
|
||||
/// See the similarly named variant of `RequestField`.
|
||||
Path,
|
||||
/// See the similarly named variant of `RequestField`.
|
||||
Query,
|
||||
}
|
331
ruma-api-macros/src/api/response.rs
Normal file
331
ruma-api-macros/src/api/response.rs
Normal file
@ -0,0 +1,331 @@
|
||||
//! Details of the `response` section of the procedural macro.
|
||||
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{quote, quote_spanned, ToTokens};
|
||||
use syn::{spanned::Spanned, Field, Ident};
|
||||
|
||||
use crate::api::{
|
||||
attribute::{Meta, MetaNameValue},
|
||||
strip_serde_attrs,
|
||||
};
|
||||
|
||||
/// The result of processing the `request` section of the macro.
|
||||
pub struct Response {
|
||||
/// The fields of the response.
|
||||
fields: Vec<ResponseField>,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
/// Whether or not this response has any data in the HTTP body.
|
||||
pub fn has_body_fields(&self) -> bool {
|
||||
self.fields.iter().any(|field| field.is_body())
|
||||
}
|
||||
|
||||
/// Whether or not this response has any fields.
|
||||
pub fn has_fields(&self) -> bool {
|
||||
!self.fields.is_empty()
|
||||
}
|
||||
|
||||
/// Whether or not this response has any data in HTTP headers.
|
||||
pub fn has_header_fields(&self) -> bool {
|
||||
self.fields.iter().any(|field| field.is_header())
|
||||
}
|
||||
|
||||
/// Whether or not this response has any data in the HTTP body.
|
||||
pub fn has_body(&self) -> bool {
|
||||
self.fields.iter().any(|field| !field.is_header())
|
||||
}
|
||||
|
||||
/// Produces code for a request struct initializer.
|
||||
pub fn init_fields(&self) -> TokenStream {
|
||||
let fields = self
|
||||
.fields
|
||||
.iter()
|
||||
.map(|response_field| match *response_field {
|
||||
ResponseField::Body(ref field) => {
|
||||
let field_name = field
|
||||
.ident
|
||||
.clone()
|
||||
.expect("expected field to have an identifier");
|
||||
let span = field.span();
|
||||
|
||||
quote_spanned! {span=>
|
||||
#field_name: response_body.#field_name
|
||||
}
|
||||
}
|
||||
ResponseField::Header(ref field, ref header_name) => {
|
||||
let field_name = field
|
||||
.ident
|
||||
.clone()
|
||||
.expect("expected field to have an identifier");
|
||||
let span = field.span();
|
||||
|
||||
quote_spanned! {span=>
|
||||
#field_name: headers.remove(::http::header::#header_name)
|
||||
.expect("response missing expected header")
|
||||
.to_str()
|
||||
.expect("failed to convert HeaderValue to str")
|
||||
.to_owned()
|
||||
}
|
||||
}
|
||||
ResponseField::NewtypeBody(ref field) => {
|
||||
let field_name = field
|
||||
.ident
|
||||
.clone()
|
||||
.expect("expected field to have an identifier");
|
||||
let span = field.span();
|
||||
|
||||
quote_spanned! {span=>
|
||||
#field_name: response_body
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
#(#fields,)*
|
||||
}
|
||||
}
|
||||
|
||||
/// Produces code to add necessary HTTP headers to an `http::Response`.
|
||||
pub fn apply_header_fields(&self) -> TokenStream {
|
||||
let header_calls = self.fields.iter().filter_map(|response_field| {
|
||||
if let ResponseField::Header(ref field, ref header_name) = *response_field {
|
||||
let field_name = field
|
||||
.ident
|
||||
.as_ref()
|
||||
.expect("expected field to have an identifier");
|
||||
let span = field.span();
|
||||
|
||||
Some(quote_spanned! {span=>
|
||||
.header(::http::header::#header_name, response.#field_name)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
#(#header_calls)*
|
||||
}
|
||||
}
|
||||
|
||||
/// Produces code to initialize the struct that will be used to create the response body.
|
||||
pub fn to_body(&self) -> TokenStream {
|
||||
if let Some(field) = self.newtype_body_field() {
|
||||
let field_name = field
|
||||
.ident
|
||||
.as_ref()
|
||||
.expect("expected field to have an identifier");
|
||||
let span = field.span();
|
||||
quote_spanned!(span=> response.#field_name)
|
||||
} else {
|
||||
let fields = self.fields.iter().filter_map(|response_field| {
|
||||
if let ResponseField::Body(ref field) = *response_field {
|
||||
let field_name = field
|
||||
.ident
|
||||
.as_ref()
|
||||
.expect("expected field to have an identifier");
|
||||
let span = field.span();
|
||||
|
||||
Some(quote_spanned! {span=>
|
||||
#field_name: response.#field_name
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
ResponseBody {
|
||||
#(#fields),*
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the newtype body field, if this request has one.
|
||||
pub fn newtype_body_field(&self) -> Option<&Field> {
|
||||
for response_field in self.fields.iter() {
|
||||
match *response_field {
|
||||
ResponseField::NewtypeBody(ref field) => {
|
||||
return Some(field);
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Field>> for Response {
|
||||
fn from(fields: Vec<Field>) -> Self {
|
||||
let mut has_newtype_body = false;
|
||||
|
||||
let fields = fields.into_iter().map(|mut field| {
|
||||
let mut field_kind = ResponseFieldKind::Body;
|
||||
let mut header = None;
|
||||
|
||||
field.attrs = field.attrs.into_iter().filter_map(|attr| {
|
||||
let meta = match Meta::from_attribute(attr) {
|
||||
Ok(meta) => meta,
|
||||
Err(attr) => return Some(attr),
|
||||
};
|
||||
|
||||
match meta {
|
||||
Meta::Word(ident) => {
|
||||
assert!(
|
||||
ident == "body",
|
||||
"ruma_api! single-word attribute on responses must be: body"
|
||||
);
|
||||
|
||||
has_newtype_body = true;
|
||||
field_kind = ResponseFieldKind::NewtypeBody;
|
||||
}
|
||||
Meta::NameValue(MetaNameValue { name, value }) => {
|
||||
assert!(
|
||||
name == "header",
|
||||
"ruma_api! name/value pair attribute on requests must be: header"
|
||||
);
|
||||
|
||||
header = Some(value);
|
||||
field_kind = ResponseFieldKind::Header;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}).collect();
|
||||
|
||||
match field_kind {
|
||||
ResponseFieldKind::Body => {
|
||||
assert!(
|
||||
!has_newtype_body,
|
||||
"ruma_api! responses cannot have both normal body fields and a newtype body field"
|
||||
);
|
||||
|
||||
ResponseField::Body(field)
|
||||
}
|
||||
ResponseFieldKind::Header => ResponseField::Header(field, header.expect("missing header name")),
|
||||
ResponseFieldKind::NewtypeBody => ResponseField::NewtypeBody(field),
|
||||
}
|
||||
}).collect();
|
||||
|
||||
Self { fields }
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Response {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let response_struct_header = quote! {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Response
|
||||
};
|
||||
|
||||
let response_struct_body = if self.fields.is_empty() {
|
||||
quote!(;)
|
||||
} else {
|
||||
let fields = self.fields.iter().map(|response_field| {
|
||||
let field = response_field.field();
|
||||
let span = field.span();
|
||||
|
||||
let stripped_field = strip_serde_attrs(field);
|
||||
|
||||
quote_spanned!(span=> #stripped_field)
|
||||
});
|
||||
|
||||
quote! {
|
||||
{
|
||||
#(#fields),*
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let response_body_struct = if let Some(newtype_body_field) = self.newtype_body_field() {
|
||||
let field = newtype_body_field.clone();
|
||||
let ty = &field.ty;
|
||||
let span = field.span();
|
||||
|
||||
quote_spanned! {span=>
|
||||
/// Data in the response body.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct ResponseBody(#ty);
|
||||
}
|
||||
} else if self.has_body_fields() {
|
||||
let fields = self
|
||||
.fields
|
||||
.iter()
|
||||
.filter_map(|response_field| match *response_field {
|
||||
ResponseField::Body(ref field) => {
|
||||
let span = field.span();
|
||||
Some(quote_spanned!(span=> #field))
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
|
||||
quote! {
|
||||
/// Data in the response body.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct ResponseBody {
|
||||
#(#fields),*
|
||||
}
|
||||
}
|
||||
} else {
|
||||
TokenStream::new()
|
||||
};
|
||||
|
||||
let response = quote! {
|
||||
#response_struct_header
|
||||
#response_struct_body
|
||||
#response_body_struct
|
||||
};
|
||||
|
||||
response.to_tokens(tokens);
|
||||
}
|
||||
}
|
||||
|
||||
/// The types of fields that a response can have.
|
||||
pub 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),
|
||||
}
|
||||
|
||||
impl ResponseField {
|
||||
/// Gets the inner `Field` value.
|
||||
fn field(&self) -> &Field {
|
||||
match *self {
|
||||
ResponseField::Body(ref field)
|
||||
| ResponseField::Header(ref field, _)
|
||||
| ResponseField::NewtypeBody(ref field) => field,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether or not this response field is a body kind.
|
||||
fn is_body(&self) -> bool {
|
||||
match *self {
|
||||
ResponseField::Body(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether or not this response field is a header kind.
|
||||
fn is_header(&self) -> bool {
|
||||
match *self {
|
||||
ResponseField::Header(..) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The types of fields that a response can have, without their values.
|
||||
enum ResponseFieldKind {
|
||||
/// See the similarly named variant of `ResponseField`.
|
||||
Body,
|
||||
/// See the similarly named variant of `ResponseField`.
|
||||
Header,
|
||||
/// See the similarly named variant of `ResponseField`.
|
||||
NewtypeBody,
|
||||
}
|
219
ruma-api-macros/src/lib.rs
Normal file
219
ruma-api-macros/src/lib.rs
Normal file
@ -0,0 +1,219 @@
|
||||
//! Crate `ruma-api-macros` provides a procedural macro for easily generating
|
||||
//! [ruma-api](https://github.com/ruma/ruma-api)-compatible endpoints.
|
||||
//!
|
||||
//! See the documentation for the `ruma_api!` macro for usage details.
|
||||
|
||||
#![deny(
|
||||
missing_copy_implementations,
|
||||
missing_debug_implementations,
|
||||
// missing_docs, # Uncomment when https://github.com/rust-lang/rust/pull/60562 is released.
|
||||
warnings
|
||||
)]
|
||||
#![warn(
|
||||
clippy::empty_line_after_outer_attr,
|
||||
clippy::expl_impl_clone_on_copy,
|
||||
clippy::if_not_else,
|
||||
clippy::items_after_statements,
|
||||
clippy::match_same_arms,
|
||||
clippy::mem_forget,
|
||||
clippy::missing_docs_in_private_items,
|
||||
clippy::multiple_inherent_impl,
|
||||
clippy::mut_mut,
|
||||
clippy::needless_borrow,
|
||||
clippy::needless_continue,
|
||||
clippy::single_match_else,
|
||||
clippy::unicode_not_nfc,
|
||||
clippy::use_self,
|
||||
clippy::used_underscore_binding,
|
||||
clippy::wrong_pub_self_convention,
|
||||
clippy::wrong_self_convention
|
||||
)]
|
||||
#![recursion_limit = "256"]
|
||||
|
||||
extern crate proc_macro;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::ToTokens;
|
||||
|
||||
use crate::api::{Api, RawApi};
|
||||
|
||||
mod api;
|
||||
|
||||
/// Generates a `ruma_api::Endpoint` from a concise definition.
|
||||
///
|
||||
/// The macro expects the following structure as input:
|
||||
///
|
||||
/// ```text
|
||||
/// ruma_api! {
|
||||
/// metadata {
|
||||
/// description: &'static str
|
||||
/// method: http::Method,
|
||||
/// name: &'static str,
|
||||
/// path: &'static str,
|
||||
/// rate_limited: bool,
|
||||
/// requires_authentication: bool,
|
||||
/// }
|
||||
///
|
||||
/// request {
|
||||
/// // Struct fields for each piece of data required
|
||||
/// // to make a request to this API endpoint.
|
||||
/// }
|
||||
///
|
||||
/// response {
|
||||
/// // Struct fields for each piece of data expected
|
||||
/// // in the response from this API endpoint.
|
||||
/// }
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// This will generate a `ruma_api::Metadata` value to be used for the `ruma_api::Endpoint`'s
|
||||
/// associated constant, single `Request` and `Response` structs, and the necessary trait
|
||||
/// implementations to convert the request into a `http::Request` and to create a response from a
|
||||
/// `http::Response` and vice versa.
|
||||
///
|
||||
/// The details of each of the three sections of the macros are documented below.
|
||||
///
|
||||
/// ## Metadata
|
||||
///
|
||||
/// * `description`: A short description of what the endpoint does.
|
||||
/// * `method`: The HTTP method used for requests to the endpoint.
|
||||
/// It's not necessary to import `http::Method`'s associated constants. Just write
|
||||
/// the value as if it was imported, e.g. `GET`.
|
||||
/// * `name`: A unique name for the endpoint.
|
||||
/// Generally this will be the same as the containing module.
|
||||
/// * `path`: The path component of the URL for the endpoint, e.g. "/foo/bar".
|
||||
/// Components of the path that are parameterized can indicate a varible by using a Rust
|
||||
/// identifier prefixed with a colon, e.g. `/foo/:some_parameter`.
|
||||
/// A corresponding query string parameter will be expected in the request struct (see below
|
||||
/// for details).
|
||||
/// * `rate_limited`: Whether or not the endpoint enforces rate limiting on requests.
|
||||
/// * `requires_authentication`: Whether or not the endpoint requires a valid access token.
|
||||
///
|
||||
/// ## Request
|
||||
///
|
||||
/// The request block contains normal struct field definitions.
|
||||
/// Doc comments and attributes are allowed as normal.
|
||||
/// There are also a few special attributes available to control how the struct is converted into a
|
||||
/// `http::Request`:
|
||||
///
|
||||
/// * `#[ruma_api(header = "HEADER_NAME")]`: Fields with this attribute will be treated as HTTP
|
||||
/// headers on the request.
|
||||
/// The value must implement `AsRef<str>`.
|
||||
/// Generally this is a `String`.
|
||||
/// The attribute value shown above as `HEADER_NAME` must be a header name constant from
|
||||
/// `http::header`, e.g. `CONTENT_TYPE`.
|
||||
/// * `#[ruma_api(path)]`: Fields with this attribute will be inserted into the matching path
|
||||
/// component of the request URL.
|
||||
/// * `#[ruma_api(query)]`: Fields with this attribute will be inserting into the URL's query
|
||||
/// string.
|
||||
///
|
||||
/// Any field that does not include one of these attributes will be part of the request's JSON
|
||||
/// body.
|
||||
///
|
||||
/// ## Response
|
||||
///
|
||||
/// Like the request block, the response block consists of normal struct field definitions.
|
||||
/// Doc comments and attributes are allowed as normal.
|
||||
/// There is also a special attribute available to control how the struct is created from a
|
||||
/// `http::Request`:
|
||||
///
|
||||
/// * `#[ruma_api(header = "HEADER_NAME")]`: Fields with this attribute will be treated as HTTP
|
||||
/// headers on the response.
|
||||
/// The value must implement `AsRef<str>`.
|
||||
/// Generally this is a `String`.
|
||||
/// The attribute value shown above as `HEADER_NAME` must be a header name constant from
|
||||
/// `http::header`, e.g. `CONTENT_TYPE`.
|
||||
///
|
||||
/// Any field that does not include the above attribute will be expected in the response's JSON
|
||||
/// body.
|
||||
///
|
||||
/// ## Newtype bodies
|
||||
///
|
||||
/// Both the request and response block also support "newtype bodies" by using the
|
||||
/// `#[ruma_api(body)]` attribute on a field. If present on a field, the entire request or response
|
||||
/// body will be treated as the value of the field. This allows you to treat the entire request or
|
||||
/// response body as a specific type, rather than a JSON object with named fields. Only one field in
|
||||
/// each struct can be marked with this attribute. It is an error to have a newtype body field and
|
||||
/// normal body fields within the same struct.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # fn main() {
|
||||
/// pub mod some_endpoint {
|
||||
/// use ruma_api_macros::ruma_api;
|
||||
/// use serde::{Deserialize, Serialize};
|
||||
///
|
||||
/// ruma_api! {
|
||||
/// metadata {
|
||||
/// description: "Does something.",
|
||||
/// method: GET,
|
||||
/// name: "some_endpoint",
|
||||
/// path: "/_matrix/some/endpoint/:baz",
|
||||
/// rate_limited: false,
|
||||
/// requires_authentication: false,
|
||||
/// }
|
||||
///
|
||||
/// request {
|
||||
/// pub foo: String,
|
||||
///
|
||||
/// #[ruma_api(header = CONTENT_TYPE)]
|
||||
/// pub content_type: String,
|
||||
///
|
||||
/// #[ruma_api(query)]
|
||||
/// pub bar: String,
|
||||
///
|
||||
/// #[ruma_api(path)]
|
||||
/// pub baz: String,
|
||||
/// }
|
||||
///
|
||||
/// response {
|
||||
/// #[ruma_api(header = CONTENT_TYPE)]
|
||||
/// pub content_type: String,
|
||||
///
|
||||
/// pub value: String,
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// pub mod newtype_body_endpoint {
|
||||
/// use ruma_api_macros::ruma_api;
|
||||
/// use serde::{Deserialize, Serialize};
|
||||
///
|
||||
/// #[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
/// pub struct MyCustomType {
|
||||
/// pub foo: String,
|
||||
/// }
|
||||
///
|
||||
/// ruma_api! {
|
||||
/// metadata {
|
||||
/// description: "Does something.",
|
||||
/// method: GET,
|
||||
/// name: "newtype_body_endpoint",
|
||||
/// path: "/_matrix/some/newtype/body/endpoint",
|
||||
/// rate_limited: false,
|
||||
/// requires_authentication: false,
|
||||
/// }
|
||||
///
|
||||
/// request {
|
||||
/// #[ruma_api(body)]
|
||||
/// pub file: Vec<u8>,
|
||||
/// }
|
||||
///
|
||||
/// response {
|
||||
/// #[ruma_api(body)]
|
||||
/// pub my_custom_type: MyCustomType,
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
#[proc_macro]
|
||||
pub fn ruma_api(input: TokenStream) -> TokenStream {
|
||||
let raw_api = syn::parse_macro_input!(input as RawApi);
|
||||
|
||||
let api = Api::from(raw_api);
|
||||
|
||||
api.into_token_stream().into()
|
||||
}
|
42
ruma-api-macros/tests/ruma_api_macros.rs
Normal file
42
ruma-api-macros/tests/ruma_api_macros.rs
Normal file
@ -0,0 +1,42 @@
|
||||
pub mod some_endpoint {
|
||||
use ruma_api_macros::ruma_api;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
ruma_api! {
|
||||
metadata {
|
||||
description: "Does something.",
|
||||
method: GET, // An `http::Method` constant. No imports required.
|
||||
name: "some_endpoint",
|
||||
path: "/_matrix/some/endpoint/:baz",
|
||||
rate_limited: false,
|
||||
requires_authentication: false,
|
||||
}
|
||||
|
||||
request {
|
||||
// With no attribute on the field, it will be put into the body of the request.
|
||||
pub foo: String,
|
||||
|
||||
// This value will be put into the "Content-Type" HTTP header.
|
||||
#[ruma_api(header = CONTENT_TYPE)]
|
||||
pub content_type: String,
|
||||
|
||||
// This value will be put into the query string of the request's URL.
|
||||
#[ruma_api(query)]
|
||||
pub bar: String,
|
||||
|
||||
// This value will be inserted into the request's URL in place of the
|
||||
// ":baz" path component.
|
||||
#[ruma_api(path)]
|
||||
pub baz: String,
|
||||
}
|
||||
|
||||
response {
|
||||
// This value will be extracted from the "Content-Type" HTTP header.
|
||||
#[ruma_api(header = CONTENT_TYPE)]
|
||||
pub content_type: String,
|
||||
|
||||
// With no attribute on the field, it will be extracted from the body of the response.
|
||||
pub value: String,
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user