ruma_common/
api.rs

1//! Core types used to define the requests and responses for each endpoint in the various
2//! [Matrix API specifications][apis].
3//!
4//! When implementing a new Matrix API, each endpoint has a request type which implements
5//! [`IncomingRequest`] and [`OutgoingRequest`], and a response type connected via an associated
6//! type.
7//!
8//! An implementation of [`IncomingRequest`] or [`OutgoingRequest`] contains all the information
9//! about the HTTP method, the path and input parameters for requests, and the structure of a
10//! successful response. Such types can then be used by client code to make requests, and by server
11//! code to fulfill those requests.
12//!
13//! [apis]: https://spec.matrix.org/latest/#matrix-apis
14
15use std::{convert::TryInto as _, error::Error as StdError};
16
17use as_variant::as_variant;
18use bytes::BufMut;
19use serde::{Deserialize, Serialize};
20
21use self::error::{FromHttpRequestError, FromHttpResponseError, IntoHttpError};
22use crate::UserId;
23
24/// Convenient constructor for [`Metadata`] constants.
25///
26/// Usage:
27///
28/// ```
29/// # use ruma_common::{metadata, api::Metadata};
30/// const _: Metadata = metadata! {
31///     method: GET, // one of the associated constants of http::Method
32///     rate_limited: true,
33///     authentication: AccessToken, // one of the variants of api::AuthScheme
34///
35///     // history of endpoint paths
36///     // there must be at least one path but otherwise everything is optional
37///     history: {
38///         unstable => "/_matrix/foo/org.bar.msc9000/baz",
39///         unstable => "/_matrix/foo/org.bar.msc9000/qux",
40///         1.0 => "/_matrix/media/r0/qux",
41///         1.1 => "/_matrix/media/v3/qux",
42///         1.2 => deprecated,
43///         1.3 => removed,
44///     }
45/// };
46/// ```
47#[macro_export]
48macro_rules! metadata {
49    ( $( $field:ident: $rhs:tt ),+ $(,)? ) => {
50        $crate::api::Metadata {
51            $( $field: $crate::metadata!(@field $field: $rhs) ),+
52        }
53    };
54
55    ( @field method: $method:ident ) => { $crate::exports::http::Method::$method };
56
57    ( @field authentication: $scheme:ident ) => { $crate::api::AuthScheme::$scheme };
58
59    ( @field history: {
60        $( unstable => $unstable_path:literal, )*
61        $( $( $version:literal => $rhs:tt, )+ )?
62    } ) => {
63        $crate::metadata! {
64            @history_impl
65            [ $($unstable_path),* ]
66            // Flip left and right to avoid macro parsing ambiguities
67            $( $( $rhs = $version ),+ )?
68        }
69    };
70
71    // Simple literal case: used for description, name, rate_limited
72    ( @field $_field:ident: $rhs:expr ) => { $rhs };
73
74    ( @history_impl
75        [ $($unstable_path:literal),* ]
76        $(
77            $( $stable_path:literal = $version:literal ),+
78            $(,
79                deprecated = $deprecated_version:literal
80                $(, removed = $removed_version:literal )?
81            )?
82        )?
83    ) => {
84        $crate::api::VersionHistory::new(
85            &[ $( $unstable_path ),* ],
86            &[ $($(
87                ($crate::api::MatrixVersion::from_lit(stringify!($version)), $stable_path)
88            ),+)? ],
89            $crate::metadata!(@optional_version $($( $deprecated_version )?)?),
90            $crate::metadata!(@optional_version $($($( $removed_version )?)?)?),
91        )
92    };
93
94    ( @optional_version ) => { None };
95    ( @optional_version $version:literal ) => { Some($crate::api::MatrixVersion::from_lit(stringify!($version))) }
96}
97
98/// Generates [`OutgoingRequest`] and [`IncomingRequest`] implementations.
99///
100/// The `OutgoingRequest` impl is on the `Request` type this attribute is used on. It is
101/// feature-gated behind `cfg(feature = "client")`.
102///
103/// The `IncomingRequest` impl is on `IncomingRequest`, which is either a type alias to
104/// `Request` or a fully-owned version of the same, depending of whether `Request` has any
105/// lifetime parameters. It is feature-gated behind `cfg(feature = "server")`.
106///
107/// The generated code expects a `METADATA` constant of type [`Metadata`] to be in scope,
108/// alongside a `Response` type that implements [`OutgoingResponse`] (for
109/// `cfg(feature = "server")`) and / or [`IncomingResponse`] (for `cfg(feature = "client")`).
110///
111/// By default, the type this macro is used on gets a `#[non_exhaustive]` attribute. This
112/// behavior can be controlled by setting the `ruma_unstable_exhaustive_types` compile-time
113/// `cfg` setting as `--cfg=ruma_unstable_exhaustive_types` using `RUSTFLAGS` or
114/// `.cargo/config.toml` (under `[build]` -> `rustflags = ["..."]`). When that setting is
115/// activated, the attribute is not applied so the type is exhaustive.
116///
117/// ## Attributes
118///
119/// To declare which part of the request a field belongs to:
120///
121/// * `#[ruma_api(header = HEADER_NAME)]`: Fields with this attribute will be treated as HTTP
122///   headers on the request. The value must implement `ToString` and `FromStr`. Generally this
123///   is a `String`. The attribute value shown above as `HEADER_NAME` must be a `const`
124///   expression of the type `http::header::HeaderName`, like one of the constants from
125///   `http::header`, e.g. `CONTENT_TYPE`. During deserialization of the request, if the field
126///   is an `Option` and parsing the header fails, the error will be ignored and the value will
127///   be `None`.
128/// * `#[ruma_api(path)]`: Fields with this attribute will be inserted into the matching path
129///   component of the request URL. If there are multiple of these fields, the order in which
130///   they are declared must match the order in which they occur in the request path.
131/// * `#[ruma_api(query)]`: Fields with this attribute will be inserting into the URL's query
132///   string.
133/// * `#[ruma_api(query_all)]`: Instead of individual query fields, one query_all field, of any
134///   type that can be (de)serialized by [serde_html_form], can be used for cases where
135///   multiple endpoints should share a query fields type, the query fields are better
136///   expressed as an `enum` rather than a `struct`, or the endpoint supports arbitrary query
137///   parameters.
138/// * No attribute: Fields without an attribute are part of the body. They can use `#[serde]`
139///   attributes to customize (de)serialization.
140/// * `#[ruma_api(body)]`: Use this if multiple endpoints should share a request body type, or
141///   the request body is better expressed as an `enum` rather than a `struct`. The value of
142///   the field will be used as the JSON body (rather than being a field in the request body
143///   object).
144/// * `#[ruma_api(raw_body)]`: Like `body` in that the field annotated with it represents the
145///   entire request body, but this attribute is for endpoints where the body can be anything,
146///   not just JSON. The field type must be `Vec<u8>`.
147///
148/// ## Examples
149///
150/// ```
151/// pub mod do_a_thing {
152///     use ruma_common::{api::request, OwnedRoomId};
153///     # use ruma_common::{
154///     #     api::{response, Metadata},
155///     #     metadata,
156///     # };
157///
158///     // const METADATA: Metadata = metadata! { ... };
159///     # const METADATA: Metadata = metadata! {
160///     #     method: POST,
161///     #     rate_limited: false,
162///     #     authentication: None,
163///     #     history: {
164///     #         unstable => "/_matrix/some/endpoint/:room_id",
165///     #     },
166///     # };
167///
168///     #[request]
169///     pub struct Request {
170///         #[ruma_api(path)]
171///         pub room_id: OwnedRoomId,
172///
173///         #[ruma_api(query)]
174///         pub bar: String,
175///
176///         #[serde(default)]
177///         pub foo: String,
178///     }
179///
180///     // #[response]
181///     // pub struct Response { ... }
182///     # #[response]
183///     # pub struct Response {}
184/// }
185///
186/// pub mod upload_file {
187///     use http::header::CONTENT_TYPE;
188///     use ruma_common::api::request;
189///     # use ruma_common::{
190///     #     api::{response, Metadata},
191///     #     metadata,
192///     # };
193///
194///     // const METADATA: Metadata = metadata! { ... };
195///     # const METADATA: Metadata = metadata! {
196///     #     method: POST,
197///     #     rate_limited: false,
198///     #     authentication: None,
199///     #     history: {
200///     #         unstable => "/_matrix/some/endpoint/:file_name",
201///     #     },
202///     # };
203///
204///     #[request]
205///     pub struct Request {
206///         #[ruma_api(path)]
207///         pub file_name: String,
208///
209///         #[ruma_api(header = CONTENT_TYPE)]
210///         pub content_type: String,
211///
212///         #[ruma_api(raw_body)]
213///         pub file: Vec<u8>,
214///     }
215///
216///     // #[response]
217///     // pub struct Response { ... }
218///     # #[response]
219///     # pub struct Response {}
220/// }
221/// ```
222///
223/// [serde_html_form]: https://crates.io/crates/serde_html_form
224pub use ruma_macros::request;
225/// Generates [`OutgoingResponse`] and [`IncomingResponse`] implementations.
226///
227/// The `OutgoingResponse` impl is feature-gated behind `cfg(feature = "server")`.
228/// The `IncomingResponse` impl is feature-gated behind `cfg(feature = "client")`.
229///
230/// The generated code expects a `METADATA` constant of type [`Metadata`] to be in scope.
231///
232/// By default, the type this macro is used on gets a `#[non_exhaustive]` attribute. This
233/// behavior can be controlled by setting the `ruma_unstable_exhaustive_types` compile-time
234/// `cfg` setting as `--cfg=ruma_unstable_exhaustive_types` using `RUSTFLAGS` or
235/// `.cargo/config.toml` (under `[build]` -> `rustflags = ["..."]`). When that setting is
236/// activated, the attribute is not applied so the type is exhaustive.
237///
238/// The status code of `OutgoingResponse` can be optionally overridden by adding the `status`
239/// attribute to `response`. The attribute value must be a status code constant from
240/// `http::StatusCode`, e.g. `IM_A_TEAPOT`.
241///
242/// ## Attributes
243///
244/// To declare which part of the response a field belongs to:
245///
246/// * `#[ruma_api(header = HEADER_NAME)]`: Fields with this attribute will be treated as HTTP
247///   headers on the response. The value must implement `ToString` and `FromStr`. Generally
248///   this is a `String`. The attribute value shown above as `HEADER_NAME` must be a header
249///   name constant from `http::header`, e.g. `CONTENT_TYPE`. During deserialization of the
250///   response, if the field is an `Option` and parsing the header fails, the error will be
251///   ignored and the value will be `None`.
252/// * No attribute: Fields without an attribute are part of the body. They can use `#[serde]`
253///   attributes to customize (de)serialization.
254/// * `#[ruma_api(body)]`: Use this if multiple endpoints should share a response body type, or
255///   the response body is better expressed as an `enum` rather than a `struct`. The value of
256///   the field will be used as the JSON body (rather than being a field in the response body
257///   object).
258/// * `#[ruma_api(raw_body)]`: Like `body` in that the field annotated with it represents the
259///   entire response body, but this attribute is for endpoints where the body can be anything,
260///   not just JSON. The field type must be `Vec<u8>`.
261///
262/// ## Examples
263///
264/// ```
265/// pub mod do_a_thing {
266///     use ruma_common::{api::response, OwnedRoomId};
267///     # use ruma_common::{
268///     #     api::{request, Metadata},
269///     #     metadata,
270///     # };
271///
272///     // const METADATA: Metadata = metadata! { ... };
273///     # const METADATA: Metadata = metadata! {
274///     #     method: POST,
275///     #     rate_limited: false,
276///     #     authentication: None,
277///     #     history: {
278///     #         unstable => "/_matrix/some/endpoint",
279///     #     },
280///     # };
281///
282///     // #[request]
283///     // pub struct Request { ... }
284///     # #[request]
285///     # pub struct Request { }
286///
287///     #[response(status = IM_A_TEAPOT)]
288///     pub struct Response {
289///         #[serde(skip_serializing_if = "Option::is_none")]
290///         pub foo: Option<String>,
291///     }
292/// }
293///
294/// pub mod download_file {
295///     use http::header::CONTENT_TYPE;
296///     use ruma_common::api::response;
297///     # use ruma_common::{
298///     #     api::{request, Metadata},
299///     #     metadata,
300///     # };
301///
302///     // const METADATA: Metadata = metadata! { ... };
303///     # const METADATA: Metadata = metadata! {
304///     #     method: POST,
305///     #     rate_limited: false,
306///     #     authentication: None,
307///     #     history: {
308///     #         unstable => "/_matrix/some/endpoint",
309///     #     },
310///     # };
311///
312///     // #[request]
313///     // pub struct Request { ... }
314///     # #[request]
315///     # pub struct Request { }
316///
317///     #[response]
318///     pub struct Response {
319///         #[ruma_api(header = CONTENT_TYPE)]
320///         pub content_type: String,
321///
322///         #[ruma_api(raw_body)]
323///         pub file: Vec<u8>,
324///     }
325/// }
326/// ```
327pub use ruma_macros::response;
328
329pub mod error;
330mod metadata;
331
332pub use self::metadata::{
333    FeatureFlag, MatrixVersion, Metadata, SupportedVersions, VersionHistory, VersioningDecision,
334};
335
336/// An enum to control whether an access token should be added to outgoing requests
337#[derive(Clone, Copy, Debug)]
338#[allow(clippy::exhaustive_enums)]
339pub enum SendAccessToken<'a> {
340    /// Add the given access token to the request only if the `METADATA` on the request requires
341    /// it.
342    IfRequired(&'a str),
343
344    /// Always add the access token.
345    Always(&'a str),
346
347    /// Add the given appservice token to the request only if the `METADATA` on the request
348    /// requires it.
349    Appservice(&'a str),
350
351    /// Don't add an access token.
352    ///
353    /// This will lead to an error if the request endpoint requires authentication
354    None,
355}
356
357impl<'a> SendAccessToken<'a> {
358    /// Get the access token for an endpoint that requires one.
359    ///
360    /// Returns `Some(_)` if `self` contains an access token.
361    pub fn get_required_for_endpoint(self) -> Option<&'a str> {
362        as_variant!(self, Self::IfRequired | Self::Appservice | Self::Always)
363    }
364
365    /// Get the access token for an endpoint that should not require one.
366    ///
367    /// Returns `Some(_)` only if `self` is `SendAccessToken::Always(_)`.
368    pub fn get_not_required_for_endpoint(self) -> Option<&'a str> {
369        as_variant!(self, Self::Always)
370    }
371
372    /// Gets the access token for an endpoint that requires one for appservices.
373    ///
374    /// Returns `Some(_)` if `self` is either `SendAccessToken::Appservice(_)`
375    /// or `SendAccessToken::Always(_)`
376    pub fn get_required_for_appservice(self) -> Option<&'a str> {
377        as_variant!(self, Self::Appservice | Self::Always)
378    }
379}
380
381/// A request type for a Matrix API endpoint, used for sending requests.
382pub trait OutgoingRequest: Sized + Clone {
383    /// A type capturing the expected error conditions the server can return.
384    type EndpointError: EndpointError;
385
386    /// Response type returned when the request is successful.
387    type IncomingResponse: IncomingResponse<EndpointError = Self::EndpointError>;
388
389    /// Metadata about the endpoint.
390    const METADATA: Metadata;
391
392    /// Tries to convert this request into an `http::Request`.
393    ///
394    /// On endpoints with authentication, when adequate information isn't provided through
395    /// access_token, this could result in an error. It may also fail with a serialization error
396    /// in case of bugs in Ruma though.
397    ///
398    /// It may also fail if, for every version in `considering_versions`;
399    /// - The endpoint is too old, and has been removed in all versions.
400    ///   ([`EndpointRemoved`](error::IntoHttpError::EndpointRemoved))
401    /// - The endpoint is too new, and no unstable path is known for this endpoint.
402    ///   ([`NoUnstablePath`](error::IntoHttpError::NoUnstablePath))
403    ///
404    /// Finally, this will emit a warning through `tracing` if it detects if any version in
405    /// `considering_versions` has deprecated this endpoint.
406    ///
407    /// The endpoints path will be appended to the given `base_url`, for example
408    /// `https://matrix.org`. Since all paths begin with a slash, it is not necessary for the
409    /// `base_url` to have a trailing slash. If it has one however, it will be ignored.
410    fn try_into_http_request<T: Default + BufMut>(
411        self,
412        base_url: &str,
413        access_token: SendAccessToken<'_>,
414        considering_versions: &'_ [MatrixVersion],
415    ) -> Result<http::Request<T>, IntoHttpError>;
416}
417
418/// A response type for a Matrix API endpoint, used for receiving responses.
419pub trait IncomingResponse: Sized {
420    /// A type capturing the expected error conditions the server can return.
421    type EndpointError: EndpointError;
422
423    /// Tries to convert the given `http::Response` into this response type.
424    fn try_from_http_response<T: AsRef<[u8]>>(
425        response: http::Response<T>,
426    ) -> Result<Self, FromHttpResponseError<Self::EndpointError>>;
427}
428
429/// An extension to [`OutgoingRequest`] which provides Appservice specific methods.
430pub trait OutgoingRequestAppserviceExt: OutgoingRequest {
431    /// Tries to convert this request into an `http::Request` and appends a virtual `user_id` to
432    /// [assert Appservice identity][id_assert].
433    ///
434    /// [id_assert]: https://spec.matrix.org/latest/application-service-api/#identity-assertion
435    fn try_into_http_request_with_user_id<T: Default + BufMut>(
436        self,
437        base_url: &str,
438        access_token: SendAccessToken<'_>,
439        user_id: &UserId,
440        considering_versions: &'_ [MatrixVersion],
441    ) -> Result<http::Request<T>, IntoHttpError> {
442        let mut http_request =
443            self.try_into_http_request(base_url, access_token, considering_versions)?;
444        let user_id_query = serde_html_form::to_string([("user_id", user_id)])?;
445
446        let uri = http_request.uri().to_owned();
447        let mut parts = uri.into_parts();
448
449        let path_and_query_with_user_id = match &parts.path_and_query {
450            Some(path_and_query) => match path_and_query.query() {
451                Some(_) => format!("{path_and_query}&{user_id_query}"),
452                None => format!("{path_and_query}?{user_id_query}"),
453            },
454            None => format!("/?{user_id_query}"),
455        };
456
457        parts.path_and_query =
458            Some(path_and_query_with_user_id.try_into().map_err(http::Error::from)?);
459
460        *http_request.uri_mut() = parts.try_into().map_err(http::Error::from)?;
461
462        Ok(http_request)
463    }
464}
465
466impl<T: OutgoingRequest> OutgoingRequestAppserviceExt for T {}
467
468/// A request type for a Matrix API endpoint, used for receiving requests.
469pub trait IncomingRequest: Sized {
470    /// A type capturing the error conditions that can be returned in the response.
471    type EndpointError: EndpointError;
472
473    /// Response type to return when the request is successful.
474    type OutgoingResponse: OutgoingResponse;
475
476    /// Metadata about the endpoint.
477    const METADATA: Metadata;
478
479    /// Tries to turn the given `http::Request` into this request type,
480    /// together with the corresponding path arguments.
481    ///
482    /// Note: The strings in path_args need to be percent-decoded.
483    fn try_from_http_request<B, S>(
484        req: http::Request<B>,
485        path_args: &[S],
486    ) -> Result<Self, FromHttpRequestError>
487    where
488        B: AsRef<[u8]>,
489        S: AsRef<str>;
490}
491
492/// A request type for a Matrix API endpoint, used for sending responses.
493pub trait OutgoingResponse {
494    /// Tries to convert this response into an `http::Response`.
495    ///
496    /// This method should only fail when when invalid header values are specified. It may also
497    /// fail with a serialization error in case of bugs in Ruma though.
498    fn try_into_http_response<T: Default + BufMut>(
499        self,
500    ) -> Result<http::Response<T>, IntoHttpError>;
501}
502
503/// Gives users the ability to define their own serializable / deserializable errors.
504pub trait EndpointError: OutgoingResponse + StdError + Sized + Send + 'static {
505    /// Tries to construct `Self` from an `http::Response`.
506    ///
507    /// This will always return `Err` variant when no `error` field is defined in
508    /// the `ruma_api` macro.
509    fn from_http_response<T: AsRef<[u8]>>(response: http::Response<T>) -> Self;
510}
511
512/// Authentication scheme used by the endpoint.
513#[derive(Copy, Clone, Debug, PartialEq, Eq)]
514#[allow(clippy::exhaustive_enums)]
515pub enum AuthScheme {
516    /// No authentication is performed.
517    None,
518
519    /// Authentication is performed by including an access token in the `Authentication` http
520    /// header, or an `access_token` query parameter.
521    ///
522    /// Using the query parameter is deprecated since Matrix 1.11.
523    AccessToken,
524
525    /// Authentication is optional, and it is performed by including an access token in the
526    /// `Authentication` http header, or an `access_token` query parameter.
527    ///
528    /// Using the query parameter is deprecated since Matrix 1.11.
529    AccessTokenOptional,
530
531    /// Authentication is only performed for appservices, by including an access token in the
532    /// `Authentication` http header, or an `access_token` query parameter.
533    ///
534    /// Using the query parameter is deprecated since Matrix 1.11.
535    AppserviceToken,
536
537    /// Authentication is performed by including X-Matrix signatures in the request headers,
538    /// as defined in the federation API.
539    ServerSignatures,
540}
541
542/// The direction to return events from.
543#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
544#[allow(clippy::exhaustive_enums)]
545pub enum Direction {
546    /// Return events backwards in time from the requested `from` token.
547    #[default]
548    #[serde(rename = "b")]
549    Backward,
550
551    /// Return events forwards in time from the requested `from` token.
552    #[serde(rename = "f")]
553    Forward,
554}