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}