//! 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. /// } /// /// // The error returned when a response fails, defaults to `Void`. /// error: path::to::Error /// } /// ``` /// /// 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`. /// 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` (e.g. /// `HashMap`, 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`. /// 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`. /// /// # 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, /// } /// /// response: { /// #[ruma_api(body)] /// pub my_custom_type: MyCustomType, /// } /// } /// } /// ``` /// /// ## Fallible deserialization /// /// All request and response types also derive [`Outgoing`][Outgoing]. As such, to allow fallible /// deserialization, you can use the `#[wrap_incoming]` attribute. For details, see the /// documentation for [the derive macro](derive.Outgoing.html). // TODO: Explain the concept of fallible deserialization before jumping to `ruma_api::Outgoing` pub use ruma_api_macros::ruma_api; pub use ruma_api_macros::Outgoing; 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}; /// A type that can be sent to another party that understands the matrix protocol. If any of the /// fields of `Self` don't implement serde's `Deserialize`, you can derive this trait to generate a /// corresponding 'Incoming' type that supports deserialization. This is useful for things like /// ruma_events' `EventResult` type. For more details, see the [derive macro's documentation][doc]. /// /// [doc]: derive.Outgoing.html // TODO: Better explain how this trait relates to serde's traits pub trait Outgoing { /// The 'Incoming' variant of `Self`. type Incoming; } /// 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>, ) -> Result; } /// A Matrix API endpoint. /// /// The type implementing this trait contains any data needed to make a request to the endpoint. pub trait Endpoint: Outgoing where ::Incoming: TryFrom>, Error = FromHttpRequestError>, ::Incoming: TryFrom< http::Response>, Error = FromHttpResponseError<::ResponseError>, >, { /// Data returned in a successful response from the endpoint. type Response: Outgoing + TryInto>, Error = IntoHttpError>; /// Error type returned when response from endpoint fails. type ResponseError: EndpointError; /// Metadata about the endpoint. const METADATA: Metadata; /// Tries to convert this request into an `http::Request`. /// /// This method should only fail when called on endpoints that require authentication. It may /// also fail with a serialization error in case of bugs in Ruma though. /// /// The endpoints path will be appended to the given `base_url`, for example /// `https://matrix.org`. Since all paths begin with a slash, it is not necessary for the /// `base_url` to have a trailing slash. If it has one however, it will be ignored. fn try_into_http_request( self, base_url: &str, access_token: Option<&str>, ) -> Result>, IntoHttpError>; } /// 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 where ::Incoming: TryFrom>, Error = FromHttpRequestError>, ::Incoming: TryFrom< http::Response>, Error = FromHttpResponseError<::ResponseError>, >, { } /// 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, } #[doc(hidden)] #[macro_export] macro_rules! try_deserialize { ($kind:ident, $call:expr $(,)?) => { ::ruma_api::try_deserialize!(@$kind, $kind, $call) }; (@request, $kind:ident, $call:expr) => { match $call { Ok(val) => val, Err(err) => return Err(::ruma_api::error::RequestDeserializationError::new(err, $kind).into()), } }; (@response, $kind:ident, $call:expr) => { match $call { Ok(val) => val, Err(err) => return Err(::ruma_api::error::ResponseDeserializationError::new(err, $kind).into()), } }; } #[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, Outgoing, }; /// A request to create a new room alias. #[derive(Debug)] pub struct Request { pub room_id: RoomId, // body pub room_alias: RoomAliasId, // path } impl Outgoing for Request { type Incoming = Self; } 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: false, }; fn try_into_http_request( self, base_url: &str, _access_token: Option<&str>, ) -> Result>, IntoHttpError> { let metadata = Request::METADATA; let url = (base_url.to_owned() + metadata.path) .replace(":room_alias", &self.room_alias.to_string()); let request_body = RequestBody { room_id: self.room_id }; let http_request = http::Request::builder() .method(metadata.method) .uri(url) .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>> for Request { type Error = FromHttpRequestError; fn try_from(request: http::Request>) -> Result { 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 Outgoing for Response { type Incoming = Self; } impl TryFrom>> for Response { type Error = FromHttpResponseError; fn try_from(http_response: http::Response>) -> Result { if http_response.status().as_u16() < 400 { Ok(Response) } else { Err(FromHttpResponseError::Http(ServerError::Unknown( crate::error::ResponseDeserializationError::from_response(http_response), ))) } } } impl TryFrom for http::Response> { type Error = IntoHttpError; fn try_from(_: Response) -> Result>, Self::Error> { let response = http::Response::builder() .header(CONTENT_TYPE, "application/json") .body(b"{}".to_vec()) .unwrap(); Ok(response) } } } }