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