Add 'ruma-api/' from commit '2151711f64e99a5da370d48fa92795f2d4799866'
git-subtree-dir: ruma-api git-subtree-mainline: bb037a5c42c51567a3b9e41c2c131cef9867a4aa git-subtree-split: 2151711f64e99a5da370d48fa92795f2d4799866
This commit is contained in:
commit
51d7875b2f
27
ruma-api/.builds/beta.yml
Normal file
27
ruma-api/.builds/beta.yml
Normal 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
16
ruma-api/.builds/msrv.yml
Normal 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
|
32
ruma-api/.builds/nightly.yml
Normal file
32
ruma-api/.builds/nightly.yml
Normal 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 ))
|
29
ruma-api/.builds/stable.yml
Normal file
29
ruma-api/.builds/stable.yml
Normal 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
2
ruma-api/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
Cargo.lock
|
||||
target
|
3
ruma-api/.rustfmt.toml
Normal file
3
ruma-api/.rustfmt.toml
Normal file
@ -0,0 +1,3 @@
|
||||
edition = "2018"
|
||||
merge_imports = true
|
||||
use_small_heuristics = "Max"
|
178
ruma-api/CHANGELOG.md
Normal file
178
ruma-api/CHANGELOG.md
Normal 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
34
ruma-api/Cargo.toml
Normal 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
19
ruma-api/LICENSE
Normal 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
16
ruma-api/README.md
Normal file
@ -0,0 +1,16 @@
|
||||
# ruma-api
|
||||
|
||||
[](https://crates.io/crates/ruma-api)
|
||||
[](https://docs.rs/ruma-api/)
|
||||

|
||||
|
||||
**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.
|
172
ruma-api/ruma-api-macros/CHANGELOG.md
Normal file
172
ruma-api/ruma-api-macros/CHANGELOG.md
Normal 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.
|
24
ruma-api/ruma-api-macros/Cargo.toml
Normal file
24
ruma-api/ruma-api-macros/Cargo.toml
Normal 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
|
64
ruma-api/ruma-api-macros/README.md
Normal file
64
ruma-api/ruma-api-macros/README.md
Normal 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)
|
635
ruma-api/ruma-api-macros/src/api.rs
Normal file
635
ruma-api/ruma-api-macros/src/api.rs
Normal 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 })
|
||||
}
|
||||
}
|
51
ruma-api/ruma-api-macros/src/api/attribute.rs
Normal file
51
ruma-api/ruma-api-macros/src/api/attribute.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
98
ruma-api/ruma-api-macros/src/api/metadata.rs
Normal file
98
ruma-api/ruma-api-macros/src/api/metadata.rs
Normal 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"))?,
|
||||
})
|
||||
}
|
||||
}
|
519
ruma-api/ruma-api-macros/src/api/request.rs
Normal file
519
ruma-api/ruma-api-macros/src/api/request.rs
Normal 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,
|
||||
}
|
361
ruma-api/ruma-api-macros/src/api/response.rs
Normal file
361
ruma-api/ruma-api-macros/src/api/response.rs
Normal 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,
|
||||
}
|
30
ruma-api/ruma-api-macros/src/lib.rs
Normal file
30
ruma-api/ruma-api-macros/src/lib.rs
Normal 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
267
ruma-api/src/error.rs
Normal 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
401
ruma-api/src/lib.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
61
ruma-api/tests/conversions.rs
Normal file
61
ruma-api/tests/conversions.rs
Normal 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(())
|
||||
}
|
33
ruma-api/tests/no_fields.rs
Normal file
33
ruma-api/tests/no_fields.rs
Normal 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"{}");
|
||||
}
|
135
ruma-api/tests/ruma_api_macros.rs
Normal file
135
ruma-api/tests/ruma_api_macros.rs
Normal 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 {
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user