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