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}