Add 'ruma-api/' from commit '2151711f64e99a5da370d48fa92795f2d4799866'

git-subtree-dir: ruma-api
git-subtree-mainline: bb037a5c42c51567a3b9e41c2c131cef9867a4aa
git-subtree-split: 2151711f64e99a5da370d48fa92795f2d4799866
This commit is contained in:
Jonas Platte 2020-06-05 01:56:19 +02:00
commit 51d7875b2f
24 changed files with 3207 additions and 0 deletions

27
ruma-api/.builds/beta.yml Normal file
View File

@ -0,0 +1,27 @@
image: archlinux
packages:
- rustup
sources:
- https://github.com/ruma/ruma-api
tasks:
- rustup: |
# We specify --profile minimal because we'd otherwise download docs
rustup toolchain install beta --profile minimal -c rustfmt -c clippy
rustup default beta
- test: |
cd ruma-api
# We don't want the build to stop on individual failure of independent
# tools, so capture tool exit codes and set the task exit code manually
set +e
cargo fmt -- --check
fmt_exit=$?
cargo clippy --all-targets --all-features -- -D warnings
clippy_exit=$?
cargo test --verbose
test_exit=$?
exit $(( $fmt_exit || $clippy_exit || $test_exit ))

16
ruma-api/.builds/msrv.yml Normal file
View File

@ -0,0 +1,16 @@
image: archlinux
packages:
- rustup
sources:
- https://github.com/ruma/ruma-api
tasks:
- rustup: |
# We specify --profile minimal because we'd otherwise download docs
rustup toolchain install 1.40.0 --profile minimal
rustup default 1.40.0
- test: |
cd ruma-api
# Only make sure the code builds with the MSRV. Tests can require later
# Rust versions, don't compile or run them.
cargo build --verbose

View File

@ -0,0 +1,32 @@
image: archlinux
packages:
- rustup
sources:
- https://github.com/ruma/ruma-api
tasks:
- rustup: |
rustup toolchain install nightly --profile minimal
rustup default nightly
# Try installing rustfmt & clippy for nightly, but don't fail the build
# if they are not available
rustup component add rustfmt || true
rustup component add clippy || true
- test: |
cd ruma-api
# We don't want the build to stop on individual failure of independent
# tools, so capture tool exit codes and set the task exit code manually
set +e
if ( rustup component list | grep -q rustfmt ); then
cargo fmt -- --check
fi
fmt_exit=$?
if ( rustup component list | grep -q clippy ); then
cargo clippy --all-targets --all-features -- -D warnings
fi
clippy_exit=$?
exit $(( $fmt_exit || $clippy_exit ))

View File

@ -0,0 +1,29 @@
image: archlinux
packages:
- rustup
sources:
- https://github.com/ruma/ruma-api
tasks:
- rustup: |
# We specify --profile minimal because we'd otherwise download docs
rustup toolchain install stable --profile minimal -c rustfmt -c clippy
rustup default stable
- test: |
cd ruma-api
# We don't want the build to stop on individual failure of independent
# tools, so capture tool exit codes and set the task exit code manually
set +e
cargo fmt -- --check
fmt_exit=$?
cargo clippy --all-targets --all-features -- -D warnings
clippy_exit=$?
cargo test --all-features --verbose
test_exit=$?
exit $(( $fmt_exit || $clippy_exit || $test_exit ))
# TODO: Add audit task once cargo-audit binary releases are available.
# See https://github.com/RustSec/cargo-audit/issues/66

2
ruma-api/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
Cargo.lock
target

3
ruma-api/.rustfmt.toml Normal file
View File

@ -0,0 +1,3 @@
edition = "2018"
merge_imports = true
use_small_heuristics = "Max"

178
ruma-api/CHANGELOG.md Normal file
View File

@ -0,0 +1,178 @@
# [unreleased]
# 0.16.1
Bug fixes:
* Update ruma-serde to 0.2.0, fixing some issues with query string deserialization (some issues
still remain but will be fixed in a semver-compatible version)
# 0.16.0
Breaking changes:
* Update ruma-identifiers to 0.16.1
* Remove the `Outgoing` trait and update the `Endpoint` trait and code generation accordingly
Improvements:
* Remove dependency on the `url` crate
# 0.15.1
Bug fixes:
* Write `{}` to the body of responses without body fields (fix from ruma-api-macros)
# 0.15.0
Breaking changes:
* Emit an error on non-UTF8 characters in path segments
* Before, they would be replaced by the unknown character codepoint
* `FromHttpResponseError` now has a generic parameter for the expected type of
error the homeserver could return
Improvements:
* Enable deserialization of unsuccessful responses
# 0.14.0
Breaking changes:
* Update ruma-api-macros to 0.11.0
* This includes a fix that uses `TryFrom<&str>` instead of serde_json for path segment
deserialization
# 0.13.1
Improvements:
* Update ruma-api-macros to 0.10.1
* `Incoming` types will now implement `Debug`
# 0.13.0
Breaking changes:
* Instead of one `Error` type, there is now many
* The new types live in their own `error` module
* They provide access to details that were previously hidden
* Our Minimum Supported Rust Version is now 1.40.0
# 0.12.1
Improvements:
* Update ruma-api-macros to 0.9.1 to support `#[ruma_api(raw_body)]`
# 0.12.0
Breaking changes:
* Our Minimum Supported Rust Version is now 1.39.0
* Support for the server-side use case has been restored. For details, see the documentation for
`ruma_api!`, the new `Outgoing` trait and its derive macro
# 0.11.2
Improvements:
* Update ruma-api-macros to 0.8.2
# 0.11.1
Improvements:
* Update ruma-api-macros to 0.8.1
# 0.11.0
Breaking changes:
* To be able to use ruma-event's `EventResult` in ruma-client without large-ish refactorings to ruma-api, we removed support for the server-side use case in ruma-api 0.11.0. It will be added back in a future release.
Improvements:
* Our CI now tests ruma-api on Rust 1.34.2, beta and nightly in addition to stable
* Updated syn and quote to 1.0
# 0.10.0
Breaking changes:
* The `Endpoint` trait is now implemented directly on the relevant request type rather than having both the request and response be associated types.
Improvements:
* ruma-api now re-exports the `ruma_api` macro from ruma-api-macros. Downstream crates no longer need to depend on ruma-api-macros directly.
* The ruma-api and ruma-api-macros repositories have been merged into one Cargo workspace for easier dependency management and development.
# 0.9.0
Breaking changes:
* The `Request` and `Response` associated types on the `Endpoint` trait are now bounded by `std::convert::TryFrom` instead of `futures::future::FutureFrom`. This was done in preparation for futures 0.3 which does not have this trait.
* The conversions required to and from `http::Request` and `http::Response` for the `Request` and `Response` associated types on the `Endpoint` trait now use `Vec<u8>` as the body type. This removes the dependency on hyper. It's possible this will change again in a future release. See https://github.com/rustasync/team/issues/84 for details.
Improvements:
* Internal code quality improvements via clippy and rustfmt.
# 0.8.0
Breaking changes:
* The `Error` type is now an opaque struct that hides implementation details.
* Updates to ruma-identifiers 0.13.
Improvements:
* ruma-api now uses clippy to improve code quality.
# 0.7.0
Improvements:
* ruma-api now runs on stable Rust, requiring version 1.34 or higher.
* Updated all dependencies for upstream improvements.
* Updated all code to use Rust edition 2018.
# 0.6.0
Breaking changes:
* Hyper has been updated to version 0.12.
* A new variant to the `Error` enum for hyper errors has been added.
* Conversions between this crate's request and response types and the http crate's request and response types are now bidirectional.
# 0.5.0
Breaking changes:
* Types from hyper have been replaced with types from the http crate.
* The `Error` enum can no longer be matched exhaustively, to allow for future expansion without breaking the crate's API.
# 0.4.0
Breaking changes:
The crate has been redesign to focus on conversions between an endpoint's request and response types and Hyper request and response types. Implementations are expected to be generated via [ruma-api-macros](https://github.com/ruma/ruma-api-macros).
# 0.3.0
Breaking changes:
* `Endpoint::router_path` now returns a `&'static str`
* Added new required methods to `Endpoint`: `name`, `description`, `requires_authentication`, and `rate_limited`.
# 0.2.0
Breaking changes:
* `Endpoint::Query_params` must now be `Deserialize + Serialize`.
# 0.1.0
Initial release.

34
ruma-api/Cargo.toml Normal file
View File

@ -0,0 +1,34 @@
[package]
authors = [
"Jimmy Cuadra <jimmy@jimmycuadra.com>",
"Jonas Platte <jplatte@posteo.de>",
]
categories = ["api-bindings", "web-programming"]
description = "An abstraction for Matrix API endpoints."
documentation = "https://docs.rs/ruma-api"
homepage = "https://github.com/ruma/ruma-api"
keywords = ["matrix", "chat", "messaging", "ruma"]
license = "MIT"
name = "ruma-api"
readme = "README.md"
repository = "https://github.com/ruma/ruma-api"
version = "0.16.1"
edition = "2018"
[dependencies]
http = "0.2.1"
percent-encoding = "2.1.0"
ruma-api-macros = { version = "=0.16.1", path = "ruma-api-macros" }
ruma-identifiers = "0.16.1"
ruma-serde = "0.2.0"
serde = { version = "1.0.110", features = ["derive"] }
serde_json = "1.0.53"
strum = "0.18.0"
[dev-dependencies]
ruma-events = "0.21.1"
[workspace]
members = [
"ruma-api-macros",
]

19
ruma-api/LICENSE Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2016 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.

16
ruma-api/README.md Normal file
View File

@ -0,0 +1,16 @@
# ruma-api
[![crates.io page](https://img.shields.io/crates/v/ruma-api.svg)](https://crates.io/crates/ruma-api)
[![docs.rs page](https://docs.rs/ruma-api/badge.svg)](https://docs.rs/ruma-api/)
![license: MIT](https://img.shields.io/crates/l/ruma-api.svg)
**ruma-api** contains core types used to define the requests and responses for each endpoint in the various [Matrix](https://matrix.org/) API specifications.
These types can be shared by client and server code for all Matrix APIs.
## Minimum Rust version
ruma-api requires Rust 1.40.0 or later.
## Documentation
ruma-api has [comprehensive documentation](https://docs.rs/ruma-api) available on docs.rs.

View File

@ -0,0 +1,172 @@
Since version 0.15.1 of ruma-api, ruma-api-macros is versioned in lockstep with ruma-api. Since
ruma-api-macros cannot be used independently anyway, it no longer maintains a separate change log or
its own version. Instead, refer to ruma-api's change log for changes in versions 0.15.1 and above.
# 0.12.0
Breaking changes:
* Update code generation to match the changes in ruma-api 0.15.0
# 0.11.0
Breaking changes:
* Use `TryFrom<&str>` instead of serde_json for path segment deserialization
# 0.10.1
Improvements:
* Derive `Debug` for `Incoming` types
# 0.10.0
Breaking changes:
* Update code generation to match the changes in ruma-api 0.13.0
# 0.9.1
Improvements:
* Add `#[ruma_api(raw_body)]` attribute to `ruma_api!`'s grammar
* This attribute is used to bypass (de)serialization for endpoints where the HTTP request /
response is arbitrary data rather than some JSON structure
# 0.9.0
Breaking changes:
* Updated code generation to match the changes in ruma-api 0.12.0
New features:
* Added a derive macro for the new `Outgoing` trait from ruma-api
# 0.8.2
Bug fixes:
* Fix handling of `request` / `response` blocks containing fields with serde attributes ([#31][])
[#31]: https://github.com/ruma/ruma-api/pull/31
# 0.8.1
Improvements:
* Add spans to almost every error that can come up in `ruma_api!`
* Add a new field kind: `#[ruma_api(query_map)]` ([#30][])
* This allows endpoints that have a dynamic set of query parameters to be implemented
* For details see the documentation of `ruma_api!`
* Add more sanity checks
* No multiple `#[ruma_api(body)]` fields in one request / response definition
* No multiple field kind declarations `#[ruma_api(body|query|path)]` on one field
* No (newtype) body fields in GET endpoints
* Lots of refactoring of the internals
[#30]: https://github.com/ruma/ruma-api/pull/30
# 0.7.1
Bug fixes:
* Removed unnecessary dependency on ruma-api 0.9.0.
# 0.7.0
Breaking changes:
* Updated to ruma-api 0.10.0.
Improvements:
* ruma-api now re-exports the `ruma_api` macro from ruma-api-macros. Downstream crates no longer need to depend on ruma-api-macros directly.
* The code generated by the `ruma_api` macro now refers to external dependencies via re-exports in ruma-api, so it is no longer necessary to add them to the dependencies of downstream crates directly.
* The ruma-api and ruma-api-macros repositories have been merged into one Cargo workspace for easier dependency management and development.
# 0.6.0
Breaking changes:
* Updated to ruma-api 0.9.0.
# 0.5.0
Breaking changes:
* Updated to ruma-api 0.8.0.
Improvements:
* Generated documentation now includes the names and descriptions of API endpoints.
* Remove unidiomatic use of `Tokens::append_all` from the `quote` crate.
* ruma-api-macros now uses clippy to improve code quality.
# 0.4.0
Improvements:
* ruma-api-macros now runs on stable Rust, requiring version 1.34 or higher.
* Updated all dependencies for upstream improvements.
# 0.3.1
Improved:
* Code updated to use Rust 2018 idioms.
Bug fixes:
* The crate will now compile when serde's `derive` feature is enabled.
# 0.3.0
Breaking changes:
* The procedural macro now uses hyper's `Body` type instead of `Vec<u8>`. This may prove to be a temporary change as ideally we want ruma-api-macros to be agnostic to HTTP client library.
Improvements:
* Updated to the latest versions of all dependencies.
* Improved error reporting for the procedural macro.
* Conversions between this crate's request and response types and the http crate's request and response types are now bidirectional.
* Request and response types now implement `Clone`.
# 0.2.2
Improvements:
* Updated to proc-macro2 0.4 for compatibility with the latest nightly Rust.
Bug fixes:
* Attributes that don't affect the macro are now ignored instead of causing a panic.
* Added missing commas in request query struct initialization that was causing a syntax error.
* Fixed stripping of serde attributes that was causing them to leak through and trigger a custom attribute error.
* Fixed creation of requests with an empty body that were not correctly using a `Vec<u8>` as the body type.
# 0.2.1
Version 0.2.1 was yanked from crates.io due to a dependency issue. Changes since version 0.2.0 are in the release notes for version 0.2.2.
# 0.2.0
Breaking changes:
* The dependency on the `hyper` crate has been removed. The macro now uses types from the `http` crate. The macro is also compatible with the forthcoming version 0.12 of `hyper`.
* The `method` field in the `metadata` block is now written as the name of an associated constant from `http::Method`, e.g. `GET`.
* HTTP headers are now specified in request and response blocks using `String` as the type, and the name of the constant of the header from `http::header` in the field's attributes. For example:
``` rust
#[ruma_api(header = "CONTENT_TYPE")]
pub content_type: String
```
Improvements:
* The macro is built using version 0.13 of the `syn` crate.
# 0.1.0
Initial release.

View File

@ -0,0 +1,24 @@
[package]
authors = [
"Jimmy Cuadra <jimmy@jimmycuadra.com>",
"Jonas Platte <jplatte@posteo.de>",
]
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"
keywords = ["matrix", "chat", "messaging", "ruma"]
license = "MIT"
name = "ruma-api-macros"
readme = "README.md"
repository = "https://github.com/ruma/ruma-api"
version = "0.16.1"
edition = "2018"
[dependencies]
proc-macro2 = "1.0.12"
quote = "1.0.5"
syn = { version = "1.0.21", features = ["full", "extra-traits"] }
[lib]
proc-macro = true

View File

@ -0,0 +1,64 @@
# 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
This crate is not meant to be used directly; instead, you can use it through the re-exports in ruma-api.
Here is an example that shows most of the macro's functionality:
```rust
pub mod some_endpoint {
use ruma_api::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
Please refer to the documentation of the `ruma_api!` re-export in [ruma-api][].
[ruma-api]: https://docs.rs/ruma-api
## License
[MIT](http://opensource.org/licenses/MIT)

View File

@ -0,0 +1,635 @@
//! Details of the `ruma_api` procedural macro.
use std::convert::{TryFrom, TryInto as _};
use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use syn::{
braced,
parse::{Parse, ParseStream},
Field, FieldValue, Ident, Token, Type,
};
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.retain(|attr| !attr.path.is_ident("serde"));
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,
/// The `error` section of the macro.
error: Type,
}
impl TryFrom<RawApi> for Api {
type Error = syn::Error;
fn try_from(raw_api: RawApi) -> syn::Result<Self> {
let res = Self {
metadata: raw_api.metadata.try_into()?,
request: raw_api.request.try_into()?,
response: raw_api.response.try_into()?,
error: raw_api
.error
.map_or(syn::parse_str::<Type>("ruma_api::error::Void").unwrap(), |err| err.ty),
};
let newtype_body_field = res.request.newtype_body_field();
if res.metadata.method == "GET"
&& (res.request.has_body_fields() || newtype_body_field.is_some())
{
let mut combined_error: Option<syn::Error> = None;
let mut add_error = |field| {
let error = syn::Error::new_spanned(field, "GET endpoints can't have body fields");
if let Some(combined_error_ref) = &mut combined_error {
combined_error_ref.combine(error);
} else {
combined_error = Some(error);
}
};
for field in res.request.body_fields() {
add_error(field);
}
if let Some(field) = newtype_body_field {
add_error(field);
}
Err(combined_error.unwrap())
} else {
Ok(res)
}
}
}
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 non_auth_endpoint_impl = if requires_authentication.value {
quote! {
impl ruma_api::NonAuthEndpoint for Request {}
}
} else {
TokenStream::new()
};
let request_type = &self.request;
let response_type = &self.response;
let request_try_from_type = if self.request.uses_wrap_incoming() {
quote!(IncomingRequest)
} else {
quote!(Request)
};
let response_try_from_type = if self.response.uses_wrap_incoming() {
quote!(IncomingResponse)
} else {
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 (request_path_string, parse_request_path) = if self.request.has_path_fields() {
let path_string = path.value();
assert!(path_string.starts_with('/'), "path needs to start with '/'");
assert!(
path_string.chars().filter(|c| *c == ':').count()
== self.request.path_field_count(),
"number of declared path parameters needs to match amount of placeholders in path"
);
let format_call = {
let mut format_string = path_string.clone();
let mut format_args = Vec::new();
while let Some(start_of_segment) = format_string.find(':') {
// ':' should only ever appear at the start of a segment
assert_eq!(&format_string[start_of_segment - 1..start_of_segment], "/");
let end_of_segment = match format_string[start_of_segment..].find('/') {
Some(rel_pos) => start_of_segment + rel_pos,
None => format_string.len(),
};
let path_var = Ident::new(
&format_string[start_of_segment + 1..end_of_segment],
Span::call_site(),
);
format_args.push(quote! {
ruma_api::exports::percent_encoding::utf8_percent_encode(
&request.#path_var.to_string(),
ruma_api::exports::percent_encoding::NON_ALPHANUMERIC,
)
});
format_string.replace_range(start_of_segment..end_of_segment, "{}");
}
quote! {
format!(#format_string, #(#format_args),*)
}
};
let path_fields = path_string[1..]
.split('/')
.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());
quote! {
#path_var_ident: {
use std::ops::Deref as _;
use ruma_api::error::RequestDeserializationError;
let segment = path_segments.get(#i).unwrap().as_bytes();
let decoded = match ruma_api::exports::percent_encoding::percent_decode(
segment
).decode_utf8() {
Ok(x) => x,
Err(err) => {
return Err(
RequestDeserializationError::new(err, request).into()
);
}
};
match std::convert::TryFrom::try_from(decoded.deref()) {
Ok(val) => val,
Err(err) => {
return Err(
RequestDeserializationError::new(err, request).into()
);
}
}
}
}
});
(format_call, quote! { #(#path_fields,)* })
} else {
(quote! { metadata.path.to_owned() }, TokenStream::new())
};
let request_query_string = if let Some(field) = self.request.query_map_field() {
let field_name = field.ident.as_ref().expect("expected field to have identifier");
let field_type = &field.ty;
quote!({
// This function exists so that the compiler will throw an
// error when the type of the field with the query_map
// attribute doesn't implement IntoIterator<Item = (String, String)>
//
// This is necessary because the ruma_serde::urlencoded::to_string
// call will result in a runtime error when the type cannot be
// encoded as a list key-value pairs (?key1=value1&key2=value2)
//
// By asserting that it implements the iterator trait, we can
// ensure that it won't fail.
fn assert_trait_impl<T>()
where
T: std::iter::IntoIterator<Item = (std::string::String, std::string::String)>,
{}
assert_trait_impl::<#field_type>();
let request_query = RequestQuery(request.#field_name);
format!("?{}", ruma_api::exports::ruma_serde::urlencoded::to_string(request_query)?)
})
} else 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
};
format!("?{}", ruma_api::exports::ruma_serde::urlencoded::to_string(request_query)?)
})
} else {
quote! {
String::new()
}
};
let extract_request_query = if self.request.query_map_field().is_some() {
quote! {
let request_query = match ruma_api::exports::ruma_serde::urlencoded::from_str(
&request.uri().query().unwrap_or("")
) {
Ok(query) => query,
Err(err) => {
return Err(
ruma_api::error::RequestDeserializationError::new(err, request).into()
);
}
};
}
} else if self.request.has_query_fields() {
quote! {
let request_query: RequestQuery =
match ruma_api::exports::ruma_serde::urlencoded::from_str(
&request.uri().query().unwrap_or("")
) {
Ok(query) => query,
Err(err) => {
return Err(
ruma_api::error::RequestDeserializationError::new(err, request)
.into()
);
}
};
}
} else {
TokenStream::new()
};
let parse_request_query = if let Some(field) = self.request.query_map_field() {
let field_name = field.ident.as_ref().expect("expected field to have an identifier");
quote! {
#field_name: request_query
}
} else {
self.request.request_init_query_fields()
};
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 extract_request_body =
if self.request.has_body_fields() || self.request.newtype_body_field().is_some() {
quote! {
let request_body: RequestBody =
match ruma_api::exports::serde_json::from_slice(request.body().as_slice()) {
Ok(body) => body,
Err(err) => {
return Err(
ruma_api::error::RequestDeserializationError::new(err, request)
.into()
);
}
};
}
} else {
TokenStream::new()
};
let parse_request_headers = if self.request.has_header_fields() {
self.request.parse_headers_from_request()
} else {
TokenStream::new()
};
let request_body = if let Some(field) = self.request.newtype_raw_body_field() {
let field_name = field.ident.as_ref().expect("expected field to have an identifier");
quote!(request.#field_name)
} else if self.request.has_body_fields() || self.request.newtype_body_field().is_some() {
let request_body_initializers = if let Some(field) = self.request.newtype_body_field() {
let field_name =
field.ident.as_ref().expect("expected field to have an identifier");
quote! { (request.#field_name) }
} else {
let initializers = self.request.request_body_init_fields();
quote! { { #initializers } }
};
quote! {
{
let request_body = RequestBody #request_body_initializers;
ruma_api::exports::serde_json::to_vec(&request_body)?
}
}
} else {
quote!(Vec::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.0,
}
} else if let Some(field) = self.request.newtype_raw_body_field() {
let field_name = field.ident.as_ref().expect("expected field to have an identifier");
quote! {
#field_name: request.into_body(),
}
} else {
self.request.request_init_body_fields()
};
let extract_response_headers = if self.response.has_header_fields() {
quote! {
let mut headers = response.headers().clone();
}
} else {
TokenStream::new()
};
let typed_response_body_decl = if self.response.has_body_fields()
|| self.response.newtype_body_field().is_some()
{
quote! {
let response_body: ResponseBody =
match ruma_api::exports::serde_json::from_slice(response.body().as_slice()) {
Ok(body) => body,
Err(err) => {
return Err(
ruma_api::error::ResponseDeserializationError::new(err, response)
.into()
);
}
};
}
} else {
TokenStream::new()
};
let response_init_fields = self.response.init_fields();
let serialize_response_headers = self.response.apply_header_fields();
let body = self.response.to_body();
let request_doc = format!(
"Data for a request to the `{}` API endpoint.\n\n{}",
name,
description.value()
);
let response_doc = format!("Data in the response from the `{}` API endpoint.", name);
let error = &self.error;
let api = quote! {
use ruma_api::exports::serde::de::Error as _;
use ruma_api::exports::serde::Deserialize as _;
use ruma_api::Endpoint as _;
use std::convert::TryInto as _;
#[doc = #request_doc]
#request_type
impl std::convert::TryFrom<ruma_api::exports::http::Request<Vec<u8>>> for #request_try_from_type {
type Error = ruma_api::error::FromHttpRequestError;
#[allow(unused_variables)]
fn try_from(request: ruma_api::exports::http::Request<Vec<u8>>) -> Result<Self, Self::Error> {
#extract_request_path
#extract_request_query
#extract_request_headers
#extract_request_body
Ok(Self {
#parse_request_path
#parse_request_query
#parse_request_headers
#parse_request_body
})
}
}
impl std::convert::TryFrom<Request> for ruma_api::exports::http::Request<Vec<u8>> {
type Error = ruma_api::error::IntoHttpError;
#[allow(unused_mut, unused_variables)]
fn try_from(request: Request) -> Result<Self, Self::Error> {
let metadata = Request::METADATA;
let path_and_query = #request_path_string + &#request_query_string;
let mut http_request = ruma_api::exports::http::Request::new(#request_body);
*http_request.method_mut() = ruma_api::exports::http::Method::#method;
*http_request.uri_mut() = ruma_api::exports::http::uri::Builder::new()
.path_and_query(path_and_query.as_str())
.build()
// The only way this can fail is if the path given in the API definition is
// invalid. It is okay to panic in that case.
.unwrap();
{ #add_headers_to_request }
Ok(http_request)
}
}
#[doc = #response_doc]
#response_type
impl std::convert::TryFrom<Response> for ruma_api::exports::http::Response<Vec<u8>> {
type Error = ruma_api::error::IntoHttpError;
#[allow(unused_variables)]
fn try_from(response: Response) -> Result<Self, Self::Error> {
let response = ruma_api::exports::http::Response::builder()
.header(ruma_api::exports::http::header::CONTENT_TYPE, "application/json")
#serialize_response_headers
.body(#body)
// Since we require header names to come from the `http::header` module,
// this cannot fail.
.unwrap();
Ok(response)
}
}
impl std::convert::TryFrom<ruma_api::exports::http::Response<Vec<u8>>> for #response_try_from_type {
type Error = ruma_api::error::FromHttpResponseError<#error>;
#[allow(unused_variables)]
fn try_from(
response: ruma_api::exports::http::Response<Vec<u8>>,
) -> Result<Self, Self::Error> {
if response.status().as_u16() < 400 {
#extract_response_headers
#typed_response_body_decl
Ok(Self {
#response_init_fields
})
} else {
match <#error as ruma_api::EndpointError>::try_from_response(response) {
Ok(err) => Err(ruma_api::error::ServerError::Known(err).into()),
Err(response_err) => Err(ruma_api::error::ServerError::Unknown(response_err).into())
}
}
}
}
impl ruma_api::Endpoint for Request {
type Response = Response;
type ResponseError = #error;
/// Metadata for the `#name` endpoint.
const METADATA: ruma_api::Metadata = ruma_api::Metadata {
description: #description,
method: ruma_api::exports::http::Method::#method,
name: #name,
path: #path,
rate_limited: #rate_limited,
requires_authentication: #requires_authentication,
};
}
#non_auth_endpoint_impl
};
api.to_tokens(tokens);
}
}
/// Custom keyword macros for syn.
mod kw {
use syn::custom_keyword;
custom_keyword!(metadata);
custom_keyword!(request);
custom_keyword!(response);
custom_keyword!(error);
}
/// 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: RawMetadata,
/// The `request` section of the macro.
pub request: RawRequest,
/// The `response` section of the macro.
pub response: RawResponse,
/// The `error` section of the macro.
pub error: Option<RawErrorType>,
}
impl Parse for RawApi {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
Ok(Self {
metadata: input.parse()?,
request: input.parse()?,
response: input.parse()?,
error: input.parse().ok(),
})
}
}
pub struct RawMetadata {
pub metadata_kw: kw::metadata,
pub field_values: Vec<FieldValue>,
}
impl Parse for RawMetadata {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let metadata_kw = input.parse::<kw::metadata>()?;
let field_values;
braced!(field_values in input);
Ok(Self {
metadata_kw,
field_values: field_values
.parse_terminated::<FieldValue, Token![,]>(FieldValue::parse)?
.into_iter()
.collect(),
})
}
}
pub struct RawRequest {
pub request_kw: kw::request,
pub fields: Vec<Field>,
}
impl Parse for RawRequest {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let request_kw = input.parse::<kw::request>()?;
let fields;
braced!(fields in input);
Ok(Self {
request_kw,
fields: fields
.parse_terminated::<Field, Token![,]>(Field::parse_named)?
.into_iter()
.collect(),
})
}
}
pub struct RawResponse {
pub response_kw: kw::response,
pub fields: Vec<Field>,
}
impl Parse for RawResponse {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let response_kw = input.parse::<kw::response>()?;
let fields;
braced!(fields in input);
Ok(Self {
response_kw,
fields: fields
.parse_terminated::<Field, Token![,]>(Field::parse_named)?
.into_iter()
.collect(),
})
}
}
pub struct RawErrorType {
pub error_kw: kw::error,
pub ty: Type,
}
impl Parse for RawErrorType {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let error_kw = input.parse::<kw::error>()?;
input.parse::<Token![:]>()?;
let ty = input.parse()?;
Ok(Self { error_kw, ty })
}
}

View File

@ -0,0 +1,51 @@
//! Details of the `#[ruma_api(...)]` attributes.
use syn::{
parse::{Parse, ParseStream},
Ident, Token,
};
/// 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,
}
/// 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.
///
/// # Panics
///
/// Panics if the given attribute is a ruma_api attribute, but fails to parse.
pub fn from_attribute(attr: &syn::Attribute) -> syn::Result<Option<Self>> {
if attr.path.is_ident("ruma_api") {
attr.parse_args().map(Some)
} else {
Ok(None)
}
}
}
impl Parse for Meta {
fn parse(input: ParseStream) -> syn::Result<Self> {
let ident = input.parse()?;
if input.peek(Token![=]) {
let _ = input.parse::<Token![=]>();
Ok(Meta::NameValue(MetaNameValue { name: ident, value: input.parse()? }))
} else {
Ok(Meta::Word(ident))
}
}
}

View File

@ -0,0 +1,98 @@
//! Details of the `metadata` section of the procedural macro.
use std::convert::TryFrom;
use syn::{Expr, ExprLit, ExprPath, Ident, Lit, LitBool, LitStr, Member};
use crate::api::RawMetadata;
/// 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 TryFrom<RawMetadata> for Metadata {
type Error = syn::Error;
fn try_from(raw: RawMetadata) -> syn::Result<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 raw.field_values {
let identifier = match field_value.member.clone() {
Member::Named(identifier) => identifier,
_ => panic!("expected Member::Named"),
};
let expr = field_value.expr.clone();
match &identifier.to_string()[..] {
"description" => match expr {
Expr::Lit(ExprLit { lit: Lit::Str(literal), .. }) => {
description = Some(literal);
}
_ => return Err(syn::Error::new_spanned(expr, "expected a string literal")),
},
"method" => match expr {
Expr::Path(ExprPath { ref path, .. }) if path.segments.len() == 1 => {
method = Some(path.segments[0].ident.clone());
}
_ => return Err(syn::Error::new_spanned(expr, "expected an identifier")),
},
"name" => match expr {
Expr::Lit(ExprLit { lit: Lit::Str(literal), .. }) => {
name = Some(literal);
}
_ => return Err(syn::Error::new_spanned(expr, "expected a string literal")),
},
"path" => match expr {
Expr::Lit(ExprLit { lit: Lit::Str(literal), .. }) => {
path = Some(literal);
}
_ => return Err(syn::Error::new_spanned(expr, "expected a string literal")),
},
"rate_limited" => match expr {
Expr::Lit(ExprLit { lit: Lit::Bool(literal), .. }) => {
rate_limited = Some(literal);
}
_ => return Err(syn::Error::new_spanned(expr, "expected a bool literal")),
},
"requires_authentication" => match expr {
Expr::Lit(ExprLit { lit: Lit::Bool(literal), .. }) => {
requires_authentication = Some(literal);
}
_ => return Err(syn::Error::new_spanned(expr, "expected a bool literal")),
},
_ => return Err(syn::Error::new_spanned(field_value, "unexpected field")),
}
}
let metadata_kw = raw.metadata_kw;
let missing_field =
|name| syn::Error::new_spanned(metadata_kw, format!("missing field `{}`", name));
Ok(Self {
description: description.ok_or_else(|| missing_field("description"))?,
method: method.ok_or_else(|| missing_field("method"))?,
name: name.ok_or_else(|| missing_field("name"))?,
path: path.ok_or_else(|| missing_field("path"))?,
rate_limited: rate_limited.ok_or_else(|| missing_field("rate_limited"))?,
requires_authentication: requires_authentication
.ok_or_else(|| missing_field("requires_authentication"))?,
})
}
}

View File

@ -0,0 +1,519 @@
//! Details of the `request` section of the procedural macro.
use std::{convert::TryFrom, mem};
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, RawRequest,
};
/// 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),
_ => unreachable!("expected request field to be header variant"),
};
let field_name = &field.ident;
quote! {
headers.append(
ruma_api::exports::http::header::#header_name,
ruma_api::exports::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: match headers.get(ruma_api::exports::http::header::#header_name)
.and_then(|v| v.to_str().ok()) {
Some(header) => header.to_owned(),
None => {
return Err(
ruma_api::error::RequestDeserializationError::new(
ruma_api::exports::serde_json::Error::missing_field(
#header_name_string
),
request,
)
.into()
);
}
}
}
});
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 body fields.
pub fn body_fields(&self) -> impl Iterator<Item = &Field> {
self.fields.iter().filter_map(|field| field.as_body_field())
}
/// Whether any field has a #[wrap_incoming] attribute.
pub fn uses_wrap_incoming(&self) -> bool {
self.fields.iter().any(|f| f.has_wrap_incoming_attr())
}
/// 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()
}
/// Returns the body field.
pub fn newtype_body_field(&self) -> Option<&Field> {
self.fields.iter().find_map(RequestField::as_newtype_body_field)
}
/// Returns the body field.
pub fn newtype_raw_body_field(&self) -> Option<&Field> {
self.fields.iter().find_map(RequestField::as_newtype_raw_body_field)
}
/// Returns the query map field.
pub fn query_map_field(&self) -> Option<&Field> {
self.fields.iter().find_map(RequestField::as_query_map_field)
}
/// 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 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 TryFrom<RawRequest> for Request {
type Error = syn::Error;
fn try_from(raw: RawRequest) -> syn::Result<Self> {
let mut newtype_body_field = None;
let mut query_map_field = None;
let fields = raw
.fields
.into_iter()
.map(|mut field| {
let mut field_kind = None;
let mut header = None;
for attr in mem::replace(&mut field.attrs, Vec::new()) {
let meta = match Meta::from_attribute(&attr)? {
Some(m) => m,
None => {
field.attrs.push(attr);
continue;
}
};
if field_kind.is_some() {
return Err(syn::Error::new_spanned(
attr,
"There can only be one field kind attribute",
));
}
field_kind = Some(match meta {
Meta::Word(ident) => {
match &ident.to_string()[..] {
s @ "body" | s @ "raw_body" => {
if let Some(f) = &newtype_body_field {
let mut error = syn::Error::new_spanned(
field,
"There can only be one newtype body field",
);
error.combine(syn::Error::new_spanned(
f,
"Previous newtype body field",
));
return Err(error);
}
newtype_body_field = Some(field.clone());
match s {
"body" => RequestFieldKind::NewtypeBody,
"raw_body" => RequestFieldKind::NewtypeRawBody,
_ => unreachable!(),
}
}
"path" => RequestFieldKind::Path,
"query" => RequestFieldKind::Query,
"query_map" => {
if let Some(f) = &query_map_field {
let mut error = syn::Error::new_spanned(
field,
"There can only be one query map field",
);
error.combine(syn::Error::new_spanned(
f,
"Previous query map field",
));
return Err(error);
}
query_map_field = Some(field.clone());
RequestFieldKind::QueryMap
},
_ => {
return Err(syn::Error::new_spanned(
ident,
"Invalid #[ruma_api] argument, expected one of `body`, `path`, `query`, `query_map`",
));
}
}
}
Meta::NameValue(MetaNameValue { name, value }) => {
if name != "header" {
return Err(syn::Error::new_spanned(
name,
"Invalid #[ruma_api] argument with value, expected `header`"
));
}
header = Some(value);
RequestFieldKind::Header
}
});
}
Ok(RequestField::new(
field_kind.unwrap_or(RequestFieldKind::Body),
field,
header,
))
})
.collect::<syn::Result<Vec<_>>>()?;
if newtype_body_field.is_some() && fields.iter().any(|f| f.is_body()) {
// TODO: highlight conflicting fields,
return Err(syn::Error::new_spanned(
raw.request_kw,
"Can't have both a newtype body field and regular body fields",
));
}
if query_map_field.is_some() && fields.iter().any(|f| f.is_query()) {
return Err(syn::Error::new_spanned(
// TODO: raw,
raw.request_kw,
"Can't have both a query map field and regular query fields",
));
}
Ok(Self { fields })
}
}
impl ToTokens for Request {
fn to_tokens(&self, tokens: &mut TokenStream) {
let request_def = if self.fields.is_empty() {
quote!(;)
} else {
let fields =
self.fields.iter().map(|request_field| strip_serde_attrs(request_field.field()));
quote! { { #(#fields),* } }
};
let request_body_struct =
if let Some(body_field) = self.fields.iter().find(|f| f.is_newtype_body()) {
let field = Field { ident: None, colon_token: None, ..body_field.field().clone() };
Some(quote! { (#field); })
} else if self.has_body_fields() {
let fields = self.fields.iter().filter(|f| f.is_body());
let fields = fields.map(RequestField::field);
Some(quote! { { #(#fields),* } })
} else {
None
}
.map(|def| {
quote! {
/// Data in the request body.
#[derive(
Debug,
ruma_api::exports::serde::Deserialize,
ruma_api::exports::serde::Serialize,
)]
struct RequestBody #def
}
});
let request_query_struct = if let Some(f) = self.query_map_field() {
let field = Field { ident: None, colon_token: None, ..f.clone() };
quote! {
/// Data in the request's query string.
#[derive(
Debug,
ruma_api::exports::serde::Deserialize,
ruma_api::exports::serde::Serialize,
)]
struct RequestQuery(#field);
}
} else if self.has_query_fields() {
let fields = self.fields.iter().filter_map(RequestField::as_query_field);
quote! {
/// Data in the request's query string.
#[derive(
Debug,
ruma_api::exports::serde::Deserialize,
ruma_api::exports::serde::Serialize,
)]
struct RequestQuery {
#(#fields),*
}
}
} else {
TokenStream::new()
};
let request = quote! {
#[derive(Debug, Clone)]
pub struct Request #request_def
#request_body_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),
/// Arbitrary bytes in the body of the request.
NewtypeRawBody(Field),
/// Data that appears in the URL path.
Path(Field),
/// Data that appears in the query string.
Query(Field),
/// Data that appears in the query string as dynamic key-value pairs.
QueryMap(Field),
}
impl RequestField {
/// Creates a new `RequestField`.
fn new(kind: RequestFieldKind, field: Field, header: Option<Ident>) -> Self {
match kind {
RequestFieldKind::Body => RequestField::Body(field),
RequestFieldKind::Header => {
RequestField::Header(field, header.expect("missing header name"))
}
RequestFieldKind::NewtypeBody => RequestField::NewtypeBody(field),
RequestFieldKind::NewtypeRawBody => RequestField::NewtypeRawBody(field),
RequestFieldKind::Path => RequestField::Path(field),
RequestFieldKind::Query => RequestField::Query(field),
RequestFieldKind::QueryMap => RequestField::QueryMap(field),
}
}
/// 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::NewtypeRawBody(..) => RequestFieldKind::NewtypeRawBody,
RequestField::Path(..) => RequestFieldKind::Path,
RequestField::Query(..) => RequestFieldKind::Query,
RequestField::QueryMap(..) => RequestFieldKind::QueryMap,
}
}
/// 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 newtype body kind.
fn is_newtype_body(&self) -> bool {
self.kind() == RequestFieldKind::NewtypeBody
}
/// 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
}
/// Return the contained field if this request field is a body kind.
fn as_body_field(&self) -> Option<&Field> {
self.field_of_kind(RequestFieldKind::Body)
}
/// Return the contained field if this request field is a body kind.
fn as_newtype_body_field(&self) -> Option<&Field> {
self.field_of_kind(RequestFieldKind::NewtypeBody)
}
/// Return the contained field if this request field is a raw body kind.
fn as_newtype_raw_body_field(&self) -> Option<&Field> {
self.field_of_kind(RequestFieldKind::NewtypeRawBody)
}
/// Return the contained field if this request field is a query kind.
fn as_query_field(&self) -> Option<&Field> {
self.field_of_kind(RequestFieldKind::Query)
}
/// Return the contained field if this request field is a query map kind.
fn as_query_map_field(&self) -> Option<&Field> {
self.field_of_kind(RequestFieldKind::QueryMap)
}
/// Gets the inner `Field` value.
fn field(&self) -> &Field {
match self {
RequestField::Body(field)
| RequestField::Header(field, _)
| RequestField::NewtypeBody(field)
| RequestField::NewtypeRawBody(field)
| RequestField::Path(field)
| RequestField::Query(field)
| RequestField::QueryMap(field) => field,
}
}
/// Gets the inner `Field` value if it's of the provided kind.
fn field_of_kind(&self, kind: RequestFieldKind) -> Option<&Field> {
if self.kind() == kind {
Some(self.field())
} else {
None
}
}
/// Whether or not the request field has a #[wrap_incoming] attribute.
fn has_wrap_incoming_attr(&self) -> bool {
self.field().attrs.iter().any(|attr| {
attr.path.segments.len() == 1 && attr.path.segments[0].ident == "wrap_incoming"
})
}
}
/// 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`.
NewtypeRawBody,
/// See the similarly named variant of `RequestField`.
Path,
/// See the similarly named variant of `RequestField`.
Query,
/// See the similarly named variant of `RequestField`.
QueryMap,
}

View File

@ -0,0 +1,361 @@
//! Details of the `response` section of the procedural macro.
use std::{convert::TryFrom, mem};
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, RawResponse,
};
/// The result of processing the `response` 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 data in HTTP headers.
pub fn has_header_fields(&self) -> bool {
self.fields.iter().any(|field| field.is_header())
}
/// Whether any field has a #[wrap_incoming] attribute.
pub fn uses_wrap_incoming(&self) -> bool {
self.fields.iter().any(|f| f.has_wrap_incoming_attr())
}
/// Produces code for a response struct initializer.
pub fn init_fields(&self) -> TokenStream {
let fields = self.fields.iter().map(|response_field| {
let field = response_field.field();
let field_name = field.ident.as_ref().expect("expected field to have an identifier");
let span = field.span();
match response_field {
ResponseField::Body(_) => {
quote_spanned! {span=>
#field_name: response_body.#field_name
}
}
ResponseField::Header(_, header_name) => {
quote_spanned! {span=>
#field_name: headers.remove(ruma_api::exports::http::header::#header_name)
.expect("response missing expected header")
.to_str()
.expect("failed to convert HeaderValue to str")
.to_owned()
}
}
ResponseField::NewtypeBody(_) => {
quote_spanned! {span=>
#field_name: response_body.0
}
}
ResponseField::NewtypeRawBody(_) => {
quote_spanned! {span=>
#field_name: response.into_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(ruma_api::exports::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_raw_body_field() {
let field_name = field.ident.as_ref().expect("expected field to have an identifier");
let span = field.span();
return quote_spanned!(span=> response.#field_name);
}
let body = 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),* }
}
};
quote!(ruma_api::exports::serde_json::to_vec(&#body)?)
}
/// Gets the newtype body field, if this response has one.
pub fn newtype_body_field(&self) -> Option<&Field> {
self.fields.iter().find_map(ResponseField::as_newtype_body_field)
}
/// Gets the newtype raw body field, if this response has one.
pub fn newtype_raw_body_field(&self) -> Option<&Field> {
self.fields.iter().find_map(ResponseField::as_newtype_raw_body_field)
}
}
impl TryFrom<RawResponse> for Response {
type Error = syn::Error;
fn try_from(raw: RawResponse) -> syn::Result<Self> {
let mut newtype_body_field = None;
let fields = raw
.fields
.into_iter()
.map(|mut field| {
let mut field_kind = None;
let mut header = None;
for attr in mem::replace(&mut field.attrs, Vec::new()) {
let meta = match Meta::from_attribute(&attr)? {
Some(m) => m,
None => {
field.attrs.push(attr);
continue;
}
};
if field_kind.is_some() {
return Err(syn::Error::new_spanned(
attr,
"There can only be one field kind attribute",
));
}
field_kind = Some(match meta {
Meta::Word(ident) => match &ident.to_string()[..] {
s @ "body" | s @ "raw_body" => {
if let Some(f) = &newtype_body_field {
let mut error = syn::Error::new_spanned(
field,
"There can only be one newtype body field",
);
error.combine(syn::Error::new_spanned(
f,
"Previous newtype body field",
));
return Err(error);
}
newtype_body_field = Some(field.clone());
match s {
"body" => ResponseFieldKind::NewtypeBody,
"raw_body" => ResponseFieldKind::NewtypeRawBody,
_ => unreachable!(),
}
}
_ => {
return Err(syn::Error::new_spanned(
ident,
"Invalid #[ruma_api] argument with value, expected `body`",
));
}
},
Meta::NameValue(MetaNameValue { name, value }) => {
if name != "header" {
return Err(syn::Error::new_spanned(
name,
"Invalid #[ruma_api] argument with value, expected `header`",
));
}
header = Some(value);
ResponseFieldKind::Header
}
});
}
Ok(match field_kind.unwrap_or(ResponseFieldKind::Body) {
ResponseFieldKind::Body => ResponseField::Body(field),
ResponseFieldKind::Header => {
ResponseField::Header(field, header.expect("missing header name"))
}
ResponseFieldKind::NewtypeBody => ResponseField::NewtypeBody(field),
ResponseFieldKind::NewtypeRawBody => ResponseField::NewtypeRawBody(field),
})
})
.collect::<syn::Result<Vec<_>>>()?;
if newtype_body_field.is_some() && fields.iter().any(|f| f.is_body()) {
// TODO: highlight conflicting fields,
return Err(syn::Error::new_spanned(
raw.response_kw,
"Can't have both a newtype body field and regular body fields",
));
}
Ok(Self { fields })
}
}
impl ToTokens for Response {
fn to_tokens(&self, tokens: &mut TokenStream) {
let response_def = if self.fields.is_empty() {
quote!(;)
} else {
let fields =
self.fields.iter().map(|response_field| strip_serde_attrs(response_field.field()));
quote! { { #(#fields),* } }
};
let def = if let Some(body_field) = self.fields.iter().find(|f| f.is_newtype_body()) {
let field = Field { ident: None, colon_token: None, ..body_field.field().clone() };
quote! { (#field); }
} else if self.has_body_fields() {
let fields = self.fields.iter().filter_map(|f| f.as_body_field());
quote!({ #(#fields),* })
} else {
quote!({})
};
let response_body_struct = quote! {
/// Data in the response body.
#[derive(
Debug,
ruma_api::exports::serde::Deserialize,
ruma_api::exports::serde::Serialize,
)]
struct ResponseBody #def
};
let response = quote! {
#[derive(Debug, Clone)]
pub struct Response #response_def
#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),
/// Arbitrary bytes in the body of the response.
NewtypeRawBody(Field),
}
impl ResponseField {
/// Gets the inner `Field` value.
fn field(&self) -> &Field {
match self {
ResponseField::Body(field)
| ResponseField::Header(field, _)
| ResponseField::NewtypeBody(field)
| ResponseField::NewtypeRawBody(field) => field,
}
}
/// Whether or not this response field is a body kind.
fn is_body(&self) -> bool {
self.as_body_field().is_some()
}
/// Whether or not this response field is a header kind.
fn is_header(&self) -> bool {
match self {
ResponseField::Header(..) => true,
_ => false,
}
}
/// Whether or not this response field is a newtype body kind.
fn is_newtype_body(&self) -> bool {
self.as_newtype_body_field().is_some()
}
/// Return the contained field if this response field is a body kind.
fn as_body_field(&self) -> Option<&Field> {
match self {
ResponseField::Body(field) => Some(field),
_ => None,
}
}
/// Return the contained field if this response field is a newtype body kind.
fn as_newtype_body_field(&self) -> Option<&Field> {
match self {
ResponseField::NewtypeBody(field) => Some(field),
_ => None,
}
}
/// Return the contained field if this response field is a newtype raw body kind.
fn as_newtype_raw_body_field(&self) -> Option<&Field> {
match self {
ResponseField::NewtypeRawBody(field) => Some(field),
_ => None,
}
}
/// Whether or not the reponse field has a #[wrap_incoming] attribute.
fn has_wrap_incoming_attr(&self) -> bool {
self.field().attrs.iter().any(|attr| {
attr.path.segments.len() == 1 && attr.path.segments[0].ident == "wrap_incoming"
})
}
}
/// 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,
/// See the similarly named variant of `ResponseField`.
NewtypeRawBody,
}

View File

@ -0,0 +1,30 @@
//! Crate ruma-api-macros provides a procedural macro for easily generating
//! [ruma-api](https://github.com/ruma/ruma-api)-compatible endpoints.
//!
//! This crate should never be used directly; instead, use it through the
//! re-exports in ruma-api. Also note that for technical reasons, the
//! `ruma_api!` macro is only documented in ruma-api, not here.
#![allow(clippy::cognitive_complexity)]
#![recursion_limit = "256"]
extern crate proc_macro;
use std::convert::TryFrom as _;
use proc_macro::TokenStream;
use quote::ToTokens;
use syn::parse_macro_input;
use self::api::{Api, RawApi};
mod api;
#[proc_macro]
pub fn ruma_api(input: TokenStream) -> TokenStream {
let raw_api = parse_macro_input!(input as RawApi);
match Api::try_from(raw_api) {
Ok(api) => api.into_token_stream().into(),
Err(err) => err.to_compile_error().into(),
}
}

267
ruma-api/src/error.rs Normal file
View File

@ -0,0 +1,267 @@
//! This module contains types for all kinds of errors that can occur when
//! converting between http requests / responses and ruma's representation of
//! matrix API requests / responses.
use std::fmt::{self, Display, Formatter};
// FIXME when `!` becomes stable use it
/// Default `ResponseError` for `ruma_api!` macro
#[derive(Clone, Copy, Debug)]
pub struct Void;
impl crate::EndpointError for Void {
fn try_from_response(
response: http::Response<Vec<u8>>,
) -> Result<Self, ResponseDeserializationError> {
Err(ResponseDeserializationError::from_response(response))
}
}
/// An error when converting one of ruma's endpoint-specific request or response
/// types to the corresponding http type.
#[derive(Debug)]
pub struct IntoHttpError(SerializationError);
#[doc(hidden)]
impl From<serde_json::Error> for IntoHttpError {
fn from(err: serde_json::Error) -> Self {
Self(SerializationError::Json(err))
}
}
#[doc(hidden)]
impl From<ruma_serde::urlencoded::ser::Error> for IntoHttpError {
fn from(err: ruma_serde::urlencoded::ser::Error) -> Self {
Self(SerializationError::Query(err))
}
}
impl Display for IntoHttpError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match &self.0 {
SerializationError::Json(err) => write!(f, "JSON serialization failed: {}", err),
SerializationError::Query(err) => {
write!(f, "Query parameter serialization failed: {}", err)
}
}
}
}
impl std::error::Error for IntoHttpError {}
/// An error when converting a http request to one of ruma's endpoint-specific
/// request types.
#[derive(Debug)]
#[non_exhaustive]
pub enum FromHttpRequestError {
/// Deserialization failed
Deserialization(RequestDeserializationError),
}
impl Display for FromHttpRequestError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Deserialization(err) => write!(f, "deserialization failed: {}", err),
}
}
}
impl From<RequestDeserializationError> for FromHttpRequestError {
fn from(err: RequestDeserializationError) -> Self {
Self::Deserialization(err)
}
}
impl std::error::Error for FromHttpRequestError {}
/// An error that occurred when trying to deserialize a request.
#[derive(Debug)]
pub struct RequestDeserializationError {
inner: DeserializationError,
http_request: http::Request<Vec<u8>>,
}
impl RequestDeserializationError {
/// This method is public so it is accessible from `ruma_api!` generated
/// code. It is not considered part of ruma-api's public API.
#[doc(hidden)]
pub fn new(
inner: impl Into<DeserializationError>,
http_request: http::Request<Vec<u8>>,
) -> Self {
Self { inner: inner.into(), http_request }
}
}
impl Display for RequestDeserializationError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(&self.inner, f)
}
}
impl std::error::Error for RequestDeserializationError {}
/// An error when converting a http response to one of ruma's endpoint-specific
/// response types.
#[derive(Debug)]
#[non_exhaustive]
pub enum FromHttpResponseError<E> {
/// Deserialization failed
Deserialization(ResponseDeserializationError),
/// The server returned a non-success status
Http(ServerError<E>),
}
impl<E: Display> Display for FromHttpResponseError<E> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Deserialization(err) => write!(f, "deserialization failed: {}", err),
Self::Http(err) => write!(f, "the server returned an error: {}", err),
}
}
}
impl<E> From<ServerError<E>> for FromHttpResponseError<E> {
fn from(err: ServerError<E>) -> Self {
Self::Http(err)
}
}
impl<E> From<ResponseDeserializationError> for FromHttpResponseError<E> {
fn from(err: ResponseDeserializationError) -> Self {
Self::Deserialization(err)
}
}
/// An error that occurred when trying to deserialize a response.
#[derive(Debug)]
pub struct ResponseDeserializationError {
inner: Option<DeserializationError>,
http_response: http::Response<Vec<u8>>,
}
impl ResponseDeserializationError {
/// This method is public so it is accessible from `ruma_api!` generated
/// code. It is not considered part of ruma-api's public API.
#[doc(hidden)]
pub fn new(
inner: impl Into<DeserializationError>,
http_response: http::Response<Vec<u8>>,
) -> Self {
Self { inner: Some(inner.into()), http_response }
}
/// This method is public so it is accessible from `ruma_api!` generated
/// code. It is not considered part of ruma-api's public API.
/// Creates an Error from a `http::Response`.
#[doc(hidden)]
pub fn from_response(http_response: http::Response<Vec<u8>>) -> Self {
Self { http_response, inner: None }
}
}
impl Display for ResponseDeserializationError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if let Some(ref inner) = self.inner {
Display::fmt(inner, f)
} else {
Display::fmt("deserialization error, no error specified", f)
}
}
}
impl std::error::Error for ResponseDeserializationError {}
/// An error was reported by the server (HTTP status code 4xx or 5xx)
#[derive(Debug)]
pub enum ServerError<E> {
/// An error that is expected to happen under certain circumstances and
/// that has a well-defined structure
Known(E),
/// An error of unexpected type of structure
Unknown(ResponseDeserializationError),
}
impl<E: Display> Display for ServerError<E> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
ServerError::Known(e) => Display::fmt(e, f),
ServerError::Unknown(res_err) => Display::fmt(res_err, f),
}
}
}
impl<E: std::error::Error> std::error::Error for ServerError<E> {}
#[derive(Debug)]
enum SerializationError {
Json(serde_json::Error),
Query(ruma_serde::urlencoded::ser::Error),
}
/// This type is public so it is accessible from `ruma_api!` generated code.
/// It is not considered part of ruma-api's public API.
#[doc(hidden)]
#[derive(Debug)]
pub enum DeserializationError {
Utf8(std::str::Utf8Error),
Json(serde_json::Error),
Query(ruma_serde::urlencoded::de::Error),
Ident(ruma_identifiers::Error),
// String <> Enum conversion failed. This can currently only happen in path
// segment deserialization
Strum(strum::ParseError),
}
impl Display for DeserializationError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
DeserializationError::Utf8(err) => Display::fmt(err, f),
DeserializationError::Json(err) => Display::fmt(err, f),
DeserializationError::Query(err) => Display::fmt(err, f),
DeserializationError::Ident(err) => Display::fmt(err, f),
DeserializationError::Strum(err) => Display::fmt(err, f),
}
}
}
#[doc(hidden)]
impl From<std::str::Utf8Error> for DeserializationError {
fn from(err: std::str::Utf8Error) -> Self {
Self::Utf8(err)
}
}
#[doc(hidden)]
impl From<serde_json::Error> for DeserializationError {
fn from(err: serde_json::Error) -> Self {
Self::Json(err)
}
}
#[doc(hidden)]
impl From<ruma_serde::urlencoded::de::Error> for DeserializationError {
fn from(err: ruma_serde::urlencoded::de::Error) -> Self {
Self::Query(err)
}
}
#[doc(hidden)]
impl From<ruma_identifiers::Error> for DeserializationError {
fn from(err: ruma_identifiers::Error) -> Self {
Self::Ident(err)
}
}
#[doc(hidden)]
impl From<strum::ParseError> for DeserializationError {
fn from(err: strum::ParseError) -> Self {
Self::Strum(err)
}
}
#[doc(hidden)]
impl From<std::convert::Infallible> for DeserializationError {
fn from(err: std::convert::Infallible) -> Self {
match err {}
}
}

401
ruma-api/src/lib.rs Normal file
View File

@ -0,0 +1,401 @@
//! Crate `ruma_api` contains core types used to define the requests and responses for each endpoint
//! in the various [Matrix](https://matrix.org) API specifications.
//! These types can be shared by client and server code for all Matrix APIs.
//!
//! When implementing a new Matrix API, each endpoint has a request type which implements
//! `Endpoint`, and a response type connected via an associated type.
//!
//! An implementation of `Endpoint` contains all the information about the HTTP method, the path and
//! input parameters for requests, and the structure of a successful response.
//! Such types can then be used by client code to make requests, and by server code to fulfill
//! those requests.
#![doc(html_favicon_url = "https://www.ruma.io/favicon.ico")]
#![warn(rust_2018_idioms)]
#![deny(missing_copy_implementations, missing_debug_implementations, missing_docs)]
use std::convert::{TryFrom, TryInto};
use http::Method;
/// 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.
/// * `#[ruma_api(query_map)]`: Instead of individual query fields, one query_map field, of any
/// type that implements `IntoIterator<Item = (String, String)>` (e.g.
/// `HashMap<String, String>`, can be used for cases where an endpoint supports arbitrary query
/// parameters.
///
/// 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.
///
/// There is another kind of newtype body that is enabled with `#[ruma_api(raw_body)]`. It is used
/// for endpoints in which the request or response body can be arbitrary bytes instead of a JSON
/// objects. A field with `#[ruma_api(raw_body)]` needs to have the type `Vec<u8>`.
///
/// # Examples
///
/// ```
/// pub mod some_endpoint {
/// use ruma_api_macros::ruma_api;
///
/// ruma_api! {
/// metadata {
/// description: "Does something.",
/// method: POST,
/// 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: PUT,
/// name: "newtype_body_endpoint",
/// path: "/_matrix/some/newtype/body/endpoint",
/// rate_limited: false,
/// requires_authentication: false,
/// }
///
/// request {
/// #[ruma_api(raw_body)]
/// pub file: Vec<u8>,
/// }
///
/// response {
/// #[ruma_api(body)]
/// pub my_custom_type: MyCustomType,
/// }
/// }
/// }
/// ```
pub use ruma_api_macros::ruma_api;
pub mod error;
/// This module is used to support the generated code from ruma-api-macros.
/// It is not considered part of ruma-api's public API.
#[doc(hidden)]
pub mod exports {
pub use http;
pub use percent_encoding;
pub use ruma_serde;
pub use serde;
pub use serde_json;
}
use error::{FromHttpRequestError, FromHttpResponseError, IntoHttpError};
/// Gives users the ability to define their own serializable/deserializable errors.
pub trait EndpointError: Sized {
/// Tries to construct `Self` from an `http::Response`.
///
/// This will always return `Err` variant when no `error` field is defined in
/// the `ruma_api` macro.
fn try_from_response(
response: http::Response<Vec<u8>>,
) -> Result<Self, error::ResponseDeserializationError>;
}
/// A Matrix API endpoint.
///
/// The type implementing this trait contains any data needed to make a request to the endpoint.
pub trait Endpoint:
TryInto<http::Request<Vec<u8>>, Error = IntoHttpError>
+ TryFrom<http::Request<Vec<u8>>, Error = FromHttpRequestError>
{
/// Data returned in a successful response from the endpoint.
type Response: TryInto<http::Response<Vec<u8>>, Error = IntoHttpError>
+ TryFrom<http::Response<Vec<u8>>, Error = FromHttpResponseError<Self::ResponseError>>;
/// Error type returned when response from endpoint fails.
type ResponseError: EndpointError;
/// Metadata about the endpoint.
const METADATA: Metadata;
}
/// A Matrix API endpoint that doesn't require authentication.
///
/// This marker trait is to indicate that a type implementing `Endpoint` doesn't require any authentication.
pub trait NonAuthEndpoint: Endpoint {}
/// Metadata about an API endpoint.
#[derive(Clone, Debug)]
pub struct Metadata {
/// A human-readable description of the endpoint.
pub description: &'static str,
/// The HTTP method used by this endpoint.
pub method: Method,
/// A unique identifier for this endpoint.
pub name: &'static str,
/// The path of this endpoint's URL, with variable names where path parameters should be filled
/// in during a request.
pub path: &'static str,
/// Whether or not this endpoint is rate limited by the server.
pub rate_limited: bool,
/// Whether or not the server requires an authenticated user for this endpoint.
pub requires_authentication: bool,
}
#[cfg(test)]
mod tests {
/// PUT /_matrix/client/r0/directory/room/:room_alias
pub mod create {
use std::{convert::TryFrom, ops::Deref};
use http::{header::CONTENT_TYPE, method::Method};
use ruma_identifiers::{RoomAliasId, RoomId};
use serde::{Deserialize, Serialize};
use crate::{
error::{
FromHttpRequestError, FromHttpResponseError, IntoHttpError,
RequestDeserializationError, ServerError, Void,
},
Endpoint, Metadata,
};
/// A request to create a new room alias.
#[derive(Debug)]
pub struct Request {
pub room_id: RoomId, // body
pub room_alias: RoomAliasId, // path
}
impl Endpoint for Request {
type Response = Response;
type ResponseError = Void;
const METADATA: Metadata = Metadata {
description: "Add an alias to a room.",
method: Method::PUT,
name: "create_alias",
path: "/_matrix/client/r0/directory/room/:room_alias",
rate_limited: false,
requires_authentication: true,
};
}
impl TryFrom<Request> for http::Request<Vec<u8>> {
type Error = IntoHttpError;
fn try_from(request: Request) -> Result<http::Request<Vec<u8>>, Self::Error> {
let metadata = Request::METADATA;
let path = metadata
.path
.to_string()
.replace(":room_alias", &request.room_alias.to_string());
let request_body = RequestBody { room_id: request.room_id };
let http_request = http::Request::builder()
.method(metadata.method)
.uri(path)
.body(serde_json::to_vec(&request_body)?)
// this cannot fail because we don't give user-supplied data to any of the
// builder methods
.unwrap();
Ok(http_request)
}
}
impl TryFrom<http::Request<Vec<u8>>> for Request {
type Error = FromHttpRequestError;
fn try_from(request: http::Request<Vec<u8>>) -> Result<Self, Self::Error> {
let request_body: RequestBody =
match serde_json::from_slice(request.body().as_slice()) {
Ok(body) => body,
Err(err) => {
return Err(RequestDeserializationError::new(err, request).into());
}
};
let path_segments: Vec<&str> = request.uri().path()[1..].split('/').collect();
Ok(Request {
room_id: request_body.room_id,
room_alias: {
let segment = path_segments.get(5).unwrap().as_bytes();
let decoded = match percent_encoding::percent_decode(segment).decode_utf8()
{
Ok(x) => x,
Err(err) => {
return Err(RequestDeserializationError::new(err, request).into())
}
};
match serde_json::from_str(decoded.deref()) {
Ok(id) => id,
Err(err) => {
return Err(RequestDeserializationError::new(err, request).into())
}
}
},
})
}
}
#[derive(Debug, Serialize, Deserialize)]
struct RequestBody {
room_id: RoomId,
}
/// The response to a request to create a new room alias.
#[derive(Clone, Copy, Debug)]
pub struct Response;
impl TryFrom<http::Response<Vec<u8>>> for Response {
type Error = FromHttpResponseError<Void>;
fn try_from(http_response: http::Response<Vec<u8>>) -> Result<Response, Self::Error> {
if http_response.status().as_u16() < 400 {
Ok(Response)
} else {
Err(FromHttpResponseError::Http(ServerError::Unknown(
crate::error::ResponseDeserializationError::from_response(http_response),
)))
}
}
}
impl TryFrom<Response> for http::Response<Vec<u8>> {
type Error = IntoHttpError;
fn try_from(_: Response) -> Result<http::Response<Vec<u8>>, Self::Error> {
let response = http::Response::builder()
.header(CONTENT_TYPE, "application/json")
.body(b"{}".to_vec())
.unwrap();
Ok(response)
}
}
}
}

View File

@ -0,0 +1,61 @@
use ruma_api::ruma_api;
use ruma_identifiers::UserId;
ruma_api! {
metadata {
description: "Does something.",
method: POST,
name: "my_endpoint",
path: "/_matrix/foo/:bar/:baz",
rate_limited: false,
requires_authentication: false,
}
request {
pub hello: String,
#[ruma_api(header = CONTENT_TYPE)]
pub world: String,
#[ruma_api(query)]
pub q1: String,
#[ruma_api(query)]
pub q2: u32,
#[ruma_api(path)]
pub bar: String,
#[ruma_api(path)]
pub baz: UserId,
}
response {
pub hello: String,
#[ruma_api(header = CONTENT_TYPE)]
pub world: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub optional_flag: Option<bool>,
}
}
#[test]
fn request_serde() -> Result<(), Box<dyn std::error::Error + 'static>> {
use std::convert::TryFrom;
let req = Request {
hello: "hi".to_owned(),
world: "test".to_owned(),
q1: "query_param_special_chars %/&@!".to_owned(),
q2: 55,
bar: "barVal".to_owned(),
baz: UserId::try_from("@bazme:ruma.io")?,
};
let http_req = http::Request::<Vec<u8>>::try_from(req.clone())?;
let req2 = Request::try_from(http_req)?;
assert_eq!(req.hello, req2.hello);
assert_eq!(req.world, req2.world);
assert_eq!(req.q1, req2.q1);
assert_eq!(req.q2, req2.q2);
assert_eq!(req.bar, req2.bar);
assert_eq!(req.baz, req2.baz);
Ok(())
}

View File

@ -0,0 +1,33 @@
use std::convert::TryFrom;
use ruma_api::ruma_api;
ruma_api! {
metadata {
description: "Does something.",
method: GET,
name: "no_fields",
path: "/_matrix/my/endpoint",
rate_limited: false,
requires_authentication: false,
}
request {}
response {}
}
#[test]
fn empty_request_http_repr() {
let req = Request {};
let http_req = http::Request::<Vec<u8>>::try_from(req).unwrap();
assert!(http_req.body().is_empty());
}
#[test]
fn empty_response_http_repr() {
let res = Response {};
let http_res = http::Response::<Vec<u8>>::try_from(res).unwrap();
assert_eq!(http_res.body(), b"{}");
}

View File

@ -0,0 +1,135 @@
pub mod some_endpoint {
use ruma_api::ruma_api;
use ruma_events::{collections::all, tag::TagEvent, EventJson};
ruma_api! {
metadata {
description: "Does something.",
method: POST, // 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,
// You can use serde attributes on any kind of field
#[serde(skip_serializing_if = "Option::is_none")]
pub optional_flag: Option<bool>,
// Use `EventJson` instead of the actual event to allow additional fields to be sent...
pub event: EventJson<TagEvent>,
// ... and to allow unknown events when the endpoint deals with event collections.
pub list_of_events: Vec<EventJson<all::RoomEvent>>,
}
}
}
pub mod newtype_body_endpoint {
use ruma_api::ruma_api;
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct MyCustomType {
pub foo: String,
}
ruma_api! {
metadata {
description: "Does something.",
method: PUT,
name: "newtype_body_endpoint",
path: "/_matrix/some/newtype/body/endpoint",
rate_limited: false,
requires_authentication: false,
}
request {
#[ruma_api(body)]
pub list_of_custom_things: Vec<MyCustomType>,
}
response {
#[ruma_api(body)]
pub my_custom_thing: MyCustomType,
}
}
}
pub mod newtype_raw_body_endpoint {
use ruma_api::ruma_api;
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct MyCustomType {
pub foo: String,
}
ruma_api! {
metadata {
description: "Does something.",
method: PUT,
name: "newtype_body_endpoint",
path: "/_matrix/some/newtype/body/endpoint",
rate_limited: false,
requires_authentication: false,
}
request {
#[ruma_api(raw_body)]
pub file: Vec<u8>,
}
response {
#[ruma_api(raw_body)]
pub file: Vec<u8>,
}
}
}
pub mod query_map_endpoint {
use ruma_api::ruma_api;
ruma_api! {
metadata {
description: "Does something.",
method: GET,
name: "newtype_body_endpoint",
path: "/_matrix/some/query/map/endpoint",
rate_limited: false,
requires_authentication: false,
}
request {
#[ruma_api(query_map)]
pub fields: Vec<(String, String)>,
}
response {
}
}
}