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