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;
19/// Generates [`OutgoingRequest`] and [`IncomingRequest`] implementations.
20///
21/// The `OutgoingRequest` impl is on the `Request` type this attribute is used on. It is
22/// feature-gated behind `cfg(feature = "client")`.
23///
24/// The `IncomingRequest` impl is on `IncomingRequest`, which is either a type alias to
25/// `Request` or a fully-owned version of the same, depending of whether `Request` has any
26/// lifetime parameters. It is feature-gated behind `cfg(feature = "server")`.
27///
28/// The generated code expects a `METADATA` constant of type [`Metadata`] to be in scope,
29/// alongside a `Response` type that implements [`OutgoingResponse`] (for
30/// `cfg(feature = "server")`) and / or [`IncomingResponse`] (for `cfg(feature = "client")`).
31///
32/// By default, the type this macro is used on gets a `#[non_exhaustive]` attribute. This
33/// behavior can be controlled by setting the `ruma_unstable_exhaustive_types` compile-time
34/// `cfg` setting as `--cfg=ruma_unstable_exhaustive_types` using `RUSTFLAGS` or
35/// `.cargo/config.toml` (under `[build]` -> `rustflags = ["..."]`). When that setting is
36/// activated, the attribute is not applied so the type is exhaustive.
37///
38/// ## Attributes
39///
40/// To declare which part of the request a field belongs to:
41///
42/// * `#[ruma_api(header = HEADER_NAME)]`: Fields with this attribute will be treated as HTTP
43///   headers on the request. The value must implement `ToString` and `FromStr`. Generally this
44///   is a `String`. The attribute value shown above as `HEADER_NAME` must be a `const`
45///   expression of the type `http::header::HeaderName`, like one of the constants from
46///   `http::header`, e.g. `CONTENT_TYPE`. During deserialization of the request, if the field
47///   is an `Option` and parsing the header fails, the error will be ignored and the value will
48///   be `None`.
49/// * `#[ruma_api(path)]`: Fields with this attribute will be inserted into the matching path
50///   component of the request URL. If there are multiple of these fields, the order in which
51///   they are declared must match the order in which they occur in the request path.
52/// * `#[ruma_api(query)]`: Fields with this attribute will be inserting into the URL's query
53///   string.
54/// * `#[ruma_api(query_all)]`: Instead of individual query fields, one query_all field, of any
55///   type that can be (de)serialized by [serde_html_form], can be used for cases where
56///   multiple endpoints should share a query fields type, the query fields are better
57///   expressed as an `enum` rather than a `struct`, or the endpoint supports arbitrary query
58///   parameters.
59/// * No attribute: Fields without an attribute are part of the body. They can use `#[serde]`
60///   attributes to customize (de)serialization.
61/// * `#[ruma_api(body)]`: Use this if multiple endpoints should share a request body type, or
62///   the request body is better expressed as an `enum` rather than a `struct`. The value of
63///   the field will be used as the JSON body (rather than being a field in the request body
64///   object).
65/// * `#[ruma_api(raw_body)]`: Like `body` in that the field annotated with it represents the
66///   entire request body, but this attribute is for endpoints where the body can be anything,
67///   not just JSON. The field type must be `Vec<u8>`.
68///
69/// ## Examples
70///
71/// ```
72/// pub mod do_a_thing {
73///     use ruma_common::{api::request, OwnedRoomId};
74///     # use ruma_common::{api::response, metadata};
75///
76///     // metadata! { ... };
77///     # metadata! {
78///     #     method: POST,
79///     #     rate_limited: false,
80///     #     authentication: NoAuthentication,
81///     #     history: {
82///     #         unstable => "/_matrix/some/endpoint/{room_id}",
83///     #     },
84///     # }
85///
86///     #[request]
87///     pub struct Request {
88///         #[ruma_api(path)]
89///         pub room_id: OwnedRoomId,
90///
91///         #[ruma_api(query)]
92///         pub bar: String,
93///
94///         #[serde(default)]
95///         pub foo: String,
96///     }
97///
98///     // #[response]
99///     // pub struct Response { ... }
100///     # #[response]
101///     # pub struct Response {}
102/// }
103///
104/// pub mod upload_file {
105///     use http::header::CONTENT_TYPE;
106///     use ruma_common::api::request;
107///     # use ruma_common::{api::response, metadata};
108///
109///     // metadata! { ... };
110///     # metadata! {
111///     #     method: POST,
112///     #     rate_limited: false,
113///     #     authentication: NoAuthentication,
114///     #     history: {
115///     #         unstable => "/_matrix/some/endpoint/{file_name}",
116///     #     },
117///     # }
118///
119///     #[request]
120///     pub struct Request {
121///         #[ruma_api(path)]
122///         pub file_name: String,
123///
124///         #[ruma_api(header = CONTENT_TYPE)]
125///         pub content_type: String,
126///
127///         #[ruma_api(raw_body)]
128///         pub file: Vec<u8>,
129///     }
130///
131///     // #[response]
132///     // pub struct Response { ... }
133///     # #[response]
134///     # pub struct Response {}
135/// }
136/// ```
137///
138/// [serde_html_form]: https://crates.io/crates/serde_html_form
139pub use ruma_macros::request;
140/// Generates [`OutgoingResponse`] and [`IncomingResponse`] implementations.
141///
142/// The `OutgoingResponse` impl is feature-gated behind `cfg(feature = "server")`.
143/// The `IncomingResponse` impl is feature-gated behind `cfg(feature = "client")`.
144///
145/// The generated code expects a `METADATA` constant of type [`Metadata`] to be in scope.
146///
147/// By default, the type this macro is used on gets a `#[non_exhaustive]` attribute. This
148/// behavior can be controlled by setting the `ruma_unstable_exhaustive_types` compile-time
149/// `cfg` setting as `--cfg=ruma_unstable_exhaustive_types` using `RUSTFLAGS` or
150/// `.cargo/config.toml` (under `[build]` -> `rustflags = ["..."]`). When that setting is
151/// activated, the attribute is not applied so the type is exhaustive.
152///
153/// The status code of `OutgoingResponse` can be optionally overridden by adding the `status`
154/// attribute to `response`. The attribute value must be a status code constant from
155/// `http::StatusCode`, e.g. `IM_A_TEAPOT`.
156///
157/// ## Attributes
158///
159/// To declare which part of the response a field belongs to:
160///
161/// * `#[ruma_api(header = HEADER_NAME)]`: Fields with this attribute will be treated as HTTP
162///   headers on the response. The value must implement `ToString` and `FromStr`. Generally
163///   this is a `String`. The attribute value shown above as `HEADER_NAME` must be a header
164///   name constant from `http::header`, e.g. `CONTENT_TYPE`. During deserialization of the
165///   response, if the field is an `Option` and parsing the header fails, the error will be
166///   ignored and the value will be `None`.
167/// * No attribute: Fields without an attribute are part of the body. They can use `#[serde]`
168///   attributes to customize (de)serialization.
169/// * `#[ruma_api(body)]`: Use this if multiple endpoints should share a response body type, or
170///   the response body is better expressed as an `enum` rather than a `struct`. The value of
171///   the field will be used as the JSON body (rather than being a field in the response body
172///   object).
173/// * `#[ruma_api(raw_body)]`: Like `body` in that the field annotated with it represents the
174///   entire response body, but this attribute is for endpoints where the body can be anything,
175///   not just JSON. The field type must be `Vec<u8>`.
176///
177/// ## Examples
178///
179/// ```
180/// pub mod do_a_thing {
181///     use ruma_common::{api::response, OwnedRoomId};
182///     # use ruma_common::{api::request, metadata};
183///
184///     // metadata! { ... };
185///     # metadata! {
186///     #     method: POST,
187///     #     rate_limited: false,
188///     #     authentication: NoAuthentication,
189///     #     history: {
190///     #         unstable => "/_matrix/some/endpoint",
191///     #     },
192///     # }
193///
194///     // #[request]
195///     // pub struct Request { ... }
196///     # #[request]
197///     # pub struct Request { }
198///
199///     #[response(status = IM_A_TEAPOT)]
200///     pub struct Response {
201///         #[serde(skip_serializing_if = "Option::is_none")]
202///         pub foo: Option<String>,
203///     }
204/// }
205///
206/// pub mod download_file {
207///     use http::header::CONTENT_TYPE;
208///     use ruma_common::api::response;
209///     # use ruma_common::{api::request, metadata};
210///
211///     // metadata! { ... };
212///     # metadata! {
213///     #     method: POST,
214///     #     rate_limited: false,
215///     #     authentication: NoAuthentication,
216///     #     history: {
217///     #         unstable => "/_matrix/some/endpoint",
218///     #     },
219///     # }
220///
221///     // #[request]
222///     // pub struct Request { ... }
223///     # #[request]
224///     # pub struct Request { }
225///
226///     #[response]
227///     pub struct Response {
228///         #[ruma_api(header = CONTENT_TYPE)]
229///         pub content_type: String,
230///
231///         #[ruma_api(raw_body)]
232///         pub file: Vec<u8>,
233///     }
234/// }
235/// ```
236pub use ruma_macros::response;
237use serde::{Deserialize, Serialize};
238
239use self::error::{FromHttpRequestError, FromHttpResponseError, IntoHttpError};
240#[doc(inline)]
241pub use crate::metadata;
242use crate::UserId;
243
244pub mod auth_scheme;
245pub mod error;
246mod metadata;
247
248pub use self::metadata::{
249    FeatureFlag, MatrixVersion, Metadata, StablePathSelector, SupportedVersions, VersionHistory,
250    VersioningDecision,
251};
252
253/// An enum to control whether an access token should be added to outgoing requests
254#[derive(Clone, Copy, Debug)]
255#[allow(clippy::exhaustive_enums)]
256pub enum SendAccessToken<'a> {
257    /// Add the given access token to the request only if the `METADATA` on the request requires
258    /// it.
259    IfRequired(&'a str),
260
261    /// Always add the access token.
262    Always(&'a str),
263
264    /// Add the given appservice token to the request only if the `METADATA` on the request
265    /// requires it.
266    Appservice(&'a str),
267
268    /// Don't add an access token.
269    ///
270    /// This will lead to an error if the request endpoint requires authentication
271    None,
272}
273
274impl<'a> SendAccessToken<'a> {
275    /// Get the access token for an endpoint that requires one.
276    ///
277    /// Returns `Some(_)` if `self` contains an access token.
278    pub fn get_required_for_endpoint(self) -> Option<&'a str> {
279        as_variant!(self, Self::IfRequired | Self::Appservice | Self::Always)
280    }
281
282    /// Get the access token for an endpoint that should not require one.
283    ///
284    /// Returns `Some(_)` only if `self` is `SendAccessToken::Always(_)`.
285    pub fn get_not_required_for_endpoint(self) -> Option<&'a str> {
286        as_variant!(self, Self::Always)
287    }
288
289    /// Gets the access token for an endpoint that requires one for appservices.
290    ///
291    /// Returns `Some(_)` if `self` is either `SendAccessToken::Appservice(_)`
292    /// or `SendAccessToken::Always(_)`
293    pub fn get_required_for_appservice(self) -> Option<&'a str> {
294        as_variant!(self, Self::Appservice | Self::Always)
295    }
296}
297
298/// A request type for a Matrix API endpoint, used for sending requests.
299pub trait OutgoingRequest: Metadata + Clone {
300    /// A type capturing the expected error conditions the server can return.
301    type EndpointError: EndpointError;
302
303    /// Response type returned when the request is successful.
304    type IncomingResponse: IncomingResponse<EndpointError = Self::EndpointError>;
305
306    /// Tries to convert this request into an `http::Request`.
307    ///
308    /// On endpoints with authentication, when adequate information isn't provided through
309    /// access_token, this could result in an error. It may also fail with a serialization error
310    /// in case of bugs in Ruma though.
311    ///
312    /// It may also fail if, for every version in `considering`;
313    /// - The endpoint is too old, and has been removed in all versions.
314    ///   ([`EndpointRemoved`](error::IntoHttpError::EndpointRemoved))
315    /// - The endpoint is too new, and no unstable path is known for this endpoint.
316    ///   ([`NoUnstablePath`](error::IntoHttpError::NoUnstablePath))
317    ///
318    /// Finally, this will emit a warning through [`tracing`] if it detects that any version in
319    /// `considering` has deprecated this endpoint.
320    ///
321    /// The endpoints path will be appended to the given `base_url`, for example
322    /// `https://matrix.org`. Since all paths begin with a slash, it is not necessary for the
323    /// `base_url` to have a trailing slash. If it has one however, it will be ignored.
324    fn try_into_http_request<T: Default + BufMut>(
325        self,
326        base_url: &str,
327        access_token: SendAccessToken<'_>,
328        considering: &'_ SupportedVersions,
329    ) -> Result<http::Request<T>, IntoHttpError>;
330
331    /// Whether the homeserver advertises support for this endpoint.
332    ///
333    /// Returns `true` if any version or feature in the given [`SupportedVersions`] matches a path
334    /// in the history of this endpoint, unless the endpoint was removed.
335    ///
336    /// Note that this is likely to return false negatives, since some endpoints don't specify a
337    /// stable or unstable feature, and homeservers should not advertise support for a Matrix
338    /// version unless they support all of its features.
339    fn is_supported(considering_versions: &SupportedVersions) -> bool {
340        Self::HISTORY.is_supported(considering_versions)
341    }
342}
343
344/// A response type for a Matrix API endpoint, used for receiving responses.
345pub trait IncomingResponse: Sized {
346    /// A type capturing the expected error conditions the server can return.
347    type EndpointError: EndpointError;
348
349    /// Tries to convert the given `http::Response` into this response type.
350    fn try_from_http_response<T: AsRef<[u8]>>(
351        response: http::Response<T>,
352    ) -> Result<Self, FromHttpResponseError<Self::EndpointError>>;
353}
354
355/// An extension to [`OutgoingRequest`] which provides Appservice specific methods.
356pub trait OutgoingRequestAppserviceExt: OutgoingRequest {
357    /// Tries to convert this request into an `http::Request` and appends a virtual `user_id` to
358    /// [assert Appservice identity][id_assert].
359    ///
360    /// [id_assert]: https://spec.matrix.org/latest/application-service-api/#identity-assertion
361    fn try_into_http_request_with_user_id<T: Default + BufMut>(
362        self,
363        base_url: &str,
364        access_token: SendAccessToken<'_>,
365        user_id: &UserId,
366        considering: &'_ SupportedVersions,
367    ) -> Result<http::Request<T>, IntoHttpError> {
368        let mut http_request = self.try_into_http_request(base_url, access_token, considering)?;
369        let user_id_query = serde_html_form::to_string([("user_id", user_id)])?;
370
371        let uri = http_request.uri().to_owned();
372        let mut parts = uri.into_parts();
373
374        let path_and_query_with_user_id = match &parts.path_and_query {
375            Some(path_and_query) => match path_and_query.query() {
376                Some(_) => format!("{path_and_query}&{user_id_query}"),
377                None => format!("{path_and_query}?{user_id_query}"),
378            },
379            None => format!("/?{user_id_query}"),
380        };
381
382        parts.path_and_query =
383            Some(path_and_query_with_user_id.try_into().map_err(http::Error::from)?);
384
385        *http_request.uri_mut() = parts.try_into().map_err(http::Error::from)?;
386
387        Ok(http_request)
388    }
389}
390
391impl<T: OutgoingRequest> OutgoingRequestAppserviceExt for T {}
392
393/// A request type for a Matrix API endpoint, used for receiving requests.
394pub trait IncomingRequest: Metadata {
395    /// A type capturing the error conditions that can be returned in the response.
396    type EndpointError: EndpointError;
397
398    /// Response type to return when the request is successful.
399    type OutgoingResponse: OutgoingResponse;
400
401    /// Tries to turn the given `http::Request` into this request type,
402    /// together with the corresponding path arguments.
403    ///
404    /// Note: The strings in path_args need to be percent-decoded.
405    fn try_from_http_request<B, S>(
406        req: http::Request<B>,
407        path_args: &[S],
408    ) -> Result<Self, FromHttpRequestError>
409    where
410        B: AsRef<[u8]>,
411        S: AsRef<str>;
412}
413
414/// A request type for a Matrix API endpoint, used for sending responses.
415pub trait OutgoingResponse {
416    /// Tries to convert this response into an `http::Response`.
417    ///
418    /// This method should only fail when when invalid header values are specified. It may also
419    /// fail with a serialization error in case of bugs in Ruma though.
420    fn try_into_http_response<T: Default + BufMut>(
421        self,
422    ) -> Result<http::Response<T>, IntoHttpError>;
423}
424
425/// Gives users the ability to define their own serializable / deserializable errors.
426pub trait EndpointError: OutgoingResponse + StdError + Sized + Send + 'static {
427    /// Tries to construct `Self` from an `http::Response`.
428    ///
429    /// This will always return `Err` variant when no `error` field is defined in
430    /// the `ruma_api` macro.
431    fn from_http_response<T: AsRef<[u8]>>(response: http::Response<T>) -> Self;
432}
433
434/// The direction to return events from.
435#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
436#[allow(clippy::exhaustive_enums)]
437pub enum Direction {
438    /// Return events backwards in time from the requested `from` token.
439    #[default]
440    #[serde(rename = "b")]
441    Backward,
442
443    /// Return events forwards in time from the requested `from` token.
444    #[serde(rename = "f")]
445    Forward,
446}