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.19/#matrix-apis
14
15use std::{convert::TryInto as _, error::Error as StdError};
16
17use bytes::BufMut;
18pub use ruma_macros::OutgoingBodyJson;
19/// Generates [`OutgoingRequest`] and [`IncomingRequest`] implementations.
20///
21/// The `OutgoingRequest` impl is feature-gated behind `cfg(feature = "client")`.
22/// The `IncomingRequest` impl is feature-gated behind `cfg(feature = "server")`.
23///
24/// The generated code expects the `Request` type to implement [`Metadata`], alongside a
25/// `Response` type that implements [`OutgoingResponse`] (for `cfg(feature = "server")`) and /
26/// or [`IncomingResponse`] (for `cfg(feature = "client")`).
27///
28/// The `Content-Type` header of the `OutgoingRequest` is unset for endpoints using the `GET`
29/// method, and defaults to `application/json` for all other methods, except if the `raw_body`
30/// attribute is set on a field, in which case it defaults to `application/octet-stream`.
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/// ## Container Attributes
39///
40/// * `#[request(error = ERROR_TYPE)]`: Override the `EndpointError` associated type of the
41/// `OutgoingRequest` and `IncomingRequest` implementations. The default error type is
42/// [`Error`](error::Error).
43///
44/// ## Field Attributes
45///
46/// To declare which part of the request a field belongs to:
47///
48/// * `#[ruma_api(header = HEADER_NAME)]`: Fields with this attribute will be treated as HTTP
49/// headers on the request. The value must implement `ToString` and `FromStr`. Generally this
50/// is a `String`. The attribute value shown above as `HEADER_NAME` must be a `const`
51/// expression of the type `http::header::HeaderName`, like one of the constants from
52/// `http::header`, e.g. `CONTENT_TYPE`. During deserialization of the request, if the field
53/// is an `Option` and parsing the header fails, the error will be ignored and the value will
54/// be `None`.
55/// * `#[ruma_api(path)]`: Fields with this attribute will be inserted into the matching path
56/// component of the request URL. If there are multiple of these fields, the order in which
57/// they are declared must match the order in which they occur in the request path.
58/// * `#[ruma_api(query)]`: Fields with this attribute will be inserting into the URL's query
59/// string.
60/// * `#[ruma_api(query_all)]`: Instead of individual query fields, one query_all field, of any
61/// type that can be (de)serialized by [serde_html_form], can be used for cases where
62/// multiple endpoints should share a query fields type, the query fields are better
63/// expressed as an `enum` rather than a `struct`, or the endpoint supports arbitrary query
64/// parameters.
65/// * No attribute: Fields without an attribute are part of the body. They can use `#[serde]`
66/// attributes to customize (de)serialization.
67/// * `#[ruma_api(body)]`: Use this if multiple endpoints should share a request body type, or
68/// the request body is better expressed as an `enum` rather than a `struct`. The value of
69/// the field will be used as the JSON body (rather than being a field in the request body
70/// object).
71/// * `#[ruma_api(raw_body)]`: Like `body` in that the field annotated with it represents the
72/// entire request body, but this attribute is for endpoints where the body can be anything,
73/// not just JSON. The field type must be `Vec<u8>`.
74///
75/// ## Examples
76///
77/// ```
78/// pub mod do_a_thing {
79/// use ruma_common::{OwnedRoomId, api::request};
80/// # use ruma_common::{api::{auth_scheme::NoAuthentication, response}, metadata};
81///
82/// // metadata! { ... };
83/// # metadata! {
84/// # method: POST,
85/// # rate_limited: false,
86/// # authentication: NoAuthentication,
87/// # history: {
88/// # unstable => "/_matrix/some/endpoint/{room_id}",
89/// # },
90/// # }
91///
92/// #[request]
93/// pub struct Request {
94/// #[ruma_api(path)]
95/// pub room_id: OwnedRoomId,
96///
97/// #[ruma_api(query)]
98/// pub bar: String,
99///
100/// #[serde(default)]
101/// pub foo: String,
102/// }
103///
104/// // #[response]
105/// // pub struct Response { ... }
106/// # #[response]
107/// # pub struct Response {}
108/// }
109///
110/// pub mod upload_file {
111/// use http::header::CONTENT_TYPE;
112/// use ruma_common::api::request;
113/// # use ruma_common::{api::{auth_scheme::NoAuthentication, response}, metadata};
114///
115/// // metadata! { ... };
116/// # metadata! {
117/// # method: POST,
118/// # rate_limited: false,
119/// # authentication: NoAuthentication,
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 `Content-Type` header of the `OutgoingResponse` defaults to `application/json`, except
152/// if the `raw_body` attribute is set on a field, in which case it defaults to
153/// `application/octet-stream`.
154///
155/// By default, the type this macro is used on gets a `#[non_exhaustive]` attribute. This
156/// behavior can be controlled by setting the `ruma_unstable_exhaustive_types` compile-time
157/// `cfg` setting as `--cfg=ruma_unstable_exhaustive_types` using `RUSTFLAGS` or
158/// `.cargo/config.toml` (under `[build]` -> `rustflags = ["..."]`). When that setting is
159/// activated, the attribute is not applied so the type is exhaustive.
160///
161/// ## Container Attributes
162///
163/// * `#[response(error = ERROR_TYPE)]`: Override the `EndpointError` associated type of the
164/// `IncomingResponse` implementation. The default error type is [`Error`](error::Error).
165/// * `#[response(status = HTTP_STATUS)]`: Override the status code of `OutgoingResponse`.
166/// `HTTP_STATUS` must be a status code constant from [`http::StatusCode`], e.g.
167/// `IM_A_TEAPOT`. The default status code is [`200 OK`](http::StatusCode::OK);
168///
169/// ## Field Attributes
170///
171/// To declare which part of the response a field belongs to:
172///
173/// * `#[ruma_api(header = HEADER_NAME)]`: Fields with this attribute will be treated as HTTP
174/// headers on the response. `HEADER_NAME` must implement
175/// `TryInto<http::header::HeaderName>`, this is usually a constant from [`http::header`].
176/// The value of the field must implement `ToString` and `FromStr`, this is usually a
177/// `String`. During deserialization of the response, if the field is an `Option` and parsing
178/// the header fails, the error will be ignored and the value will be `None`.
179/// * No attribute: Fields without an attribute are part of the body. They can use `#[serde]`
180/// attributes to customize (de)serialization.
181/// * `#[ruma_api(body)]`: Use this if multiple endpoints should share a response body type, or
182/// the response body is better expressed as an `enum` rather than a `struct`. The value of
183/// the field will be used as the JSON body (rather than being a field in the response body
184/// object).
185/// * `#[ruma_api(raw_body)]`: Like `body` in that the field annotated with it represents the
186/// entire response body, but this attribute is for endpoints where the body can be anything,
187/// not just JSON. The field type must be `Vec<u8>`.
188///
189/// ## Examples
190///
191/// ```
192/// pub mod do_a_thing {
193/// use ruma_common::{OwnedRoomId, api::response};
194/// # use ruma_common::{api::{auth_scheme::NoAuthentication, request}, metadata};
195///
196/// // metadata! { ... };
197/// # metadata! {
198/// # method: POST,
199/// # rate_limited: false,
200/// # authentication: NoAuthentication,
201/// # history: {
202/// # unstable => "/_matrix/some/endpoint",
203/// # },
204/// # }
205///
206/// // #[request]
207/// // pub struct Request { ... }
208/// # #[request]
209/// # pub struct Request { }
210///
211/// #[response(status = IM_A_TEAPOT)]
212/// pub struct Response {
213/// #[serde(skip_serializing_if = "Option::is_none")]
214/// pub foo: Option<String>,
215/// }
216/// }
217///
218/// pub mod download_file {
219/// use http::header::CONTENT_TYPE;
220/// use ruma_common::api::response;
221/// # use ruma_common::{api::{auth_scheme::NoAuthentication, request}, metadata};
222///
223/// // metadata! { ... };
224/// # metadata! {
225/// # method: POST,
226/// # rate_limited: false,
227/// # authentication: NoAuthentication,
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::{DeviceId, UserId};
255
256pub mod auth_scheme;
257mod body;
258pub mod error;
259mod metadata;
260pub mod path_builder;
261
262use self::error::DeserializationError;
263pub use self::{
264 body::{BytesBody, EmptyBody, OutgoingBody},
265 metadata::{FeatureFlag, MatrixVersion, Metadata, OAuthClientScope, SupportedVersions},
266};
267
268/// A request type for a Matrix API endpoint, used for sending requests.
269pub trait OutgoingRequest: Metadata + Clone {
270 /// HTTP body type pre-serialization.
271 type Body: OutgoingBody;
272
273 /// A type capturing the expected error conditions the server can return.
274 type EndpointError: EndpointError;
275
276 /// Response type returned when the request is successful.
277 type IncomingResponse: IncomingResponse<EndpointError = Self::EndpointError>;
278
279 /// Tries to convert this request into an `http::Request`.
280 ///
281 /// The endpoints path will be appended to the given `base_url`, for example
282 /// `https://matrix.org`. Since all paths begin with a slash, it is not necessary for the
283 /// `base_url` to have a trailing slash. If it has one however, it will be ignored.
284 ///
285 /// ## Errors
286 ///
287 /// This method can return an error in the following cases:
288 ///
289 /// * On endpoints that have several versions for the path, when there are no supported versions
290 /// for the endpoint, i.e. when [`PathBuilder::make_endpoint_url()`] returns an error.
291 /// * If the request serialization fails, which should only happen in case of bugs in Ruma.
292 ///
293 /// [`AuthScheme::add_authentication()`]: auth_scheme::AuthScheme::add_authentication
294 /// [`PathBuilder::make_endpoint_url()`]: path_builder::PathBuilder::make_endpoint_url
295 fn try_into_http_request_inner(
296 self,
297 base_url: &str,
298 path_builder_input: <Self::PathBuilder as path_builder::PathBuilder>::Input<'_>,
299 ) -> Result<http::Request<Self::Body>, IntoHttpError>;
300}
301
302/// Convenience functionality on top of [`OutgoingRequest`].
303pub trait OutgoingRequestExt: OutgoingRequest {
304 /// Tries to convert this request into an `http::Request`.
305 ///
306 /// The endpoints path will be appended to the given `base_url`, for example
307 /// `https://matrix.org`. Since all paths begin with a slash, it is not necessary for the
308 /// `base_url` to have a trailing slash. If it has one however, it will be ignored.
309 ///
310 /// ## Errors
311 ///
312 /// This method can return an error in the following cases:
313 ///
314 /// * On endpoints that require authentication, when adequate information isn't provided through
315 /// `authentication_input`, i.e. when [`AuthScheme::add_authentication()`] returns an error.
316 /// * On endpoints that have several versions for the path, when there are no supported versions
317 /// for the endpoint, i.e. when [`PathBuilder::make_endpoint_url()`] returns an error.
318 /// * If the request serialization fails, which should only happen in case of bugs in Ruma.
319 ///
320 /// [`AuthScheme::add_authentication()`]: auth_scheme::AuthScheme::add_authentication
321 /// [`PathBuilder::make_endpoint_url()`]: path_builder::PathBuilder::make_endpoint_url
322 fn try_into_http_request<T: Default + BufMut + AsRef<[u8]>>(
323 self,
324 base_url: &str,
325 authentication_input: <Self::Authentication as auth_scheme::AuthScheme>::Input<'_>,
326 path_builder_input: <Self::PathBuilder as path_builder::PathBuilder>::Input<'_>,
327 ) -> Result<http::Request<T>, IntoHttpError> {
328 let (mut parts, body) =
329 self.try_into_http_request_inner(base_url, path_builder_input)?.into_parts();
330
331 if let Some(content_type) = body.content_type()
332 && !parts.headers.contains_key(&http::header::CONTENT_TYPE)
333 {
334 parts.headers.insert(http::header::CONTENT_TYPE, content_type);
335 }
336
337 let mut request =
338 http::Request::from_parts(parts, body.try_into_buf().map_err(Into::into)?);
339
340 <Self::Authentication as auth_scheme::AuthScheme>::add_authentication(
341 &mut request,
342 authentication_input,
343 )
344 .map_err(IntoHttpError::authentication)?;
345
346 Ok(request)
347 }
348}
349
350impl<T: OutgoingRequest> OutgoingRequestExt for T {}
351
352/// A response type for a Matrix API endpoint, used for receiving responses.
353pub trait IncomingResponse: Sized {
354 /// A type capturing the expected error conditions the server can return.
355 type EndpointError: EndpointError;
356
357 /// Tries to convert the given `http::Response` into this response type.
358 ///
359 /// Only called for successful responses (HTTP status code < 400).
360 fn try_from_http_response_inner(
361 response: http::Response<&[u8]>,
362 ) -> Result<Self, DeserializationError>;
363}
364
365/// Convenience functionality on top of [`IncomingResponse`].
366pub trait IncomingResponseExt: IncomingResponse {
367 /// Tries to convert the given `http::Response` into this response type.
368 fn try_from_http_response(
369 response: http::Response<&[u8]>,
370 ) -> Result<Self, FromHttpResponseError<Self::EndpointError>> {
371 if response.status().as_u16() >= 400 {
372 return Err(FromHttpResponseError::Server(Self::EndpointError::from_http_response(
373 response,
374 )));
375 }
376
377 Self::try_from_http_response_inner(response).map_err(Into::into)
378 }
379}
380
381impl<T: IncomingResponse> IncomingResponseExt for T {}
382
383/// An extension to [`OutgoingRequest`] which provides Appservice specific methods.
384///
385/// This is only implemented for implementors of [`AuthScheme`](auth_scheme::AuthScheme) that use a
386/// [`SendAccessToken`](auth_scheme::SendAccessToken), because application services should only use
387/// these methods with the Client-Server API.
388pub trait OutgoingRequestAppserviceExt: OutgoingRequest
389where
390 for<'a> Self::Authentication:
391 auth_scheme::AuthScheme<Input<'a> = auth_scheme::SendAccessToken<'a>>,
392{
393 /// Tries to convert this request into an `http::Request` and adds the given
394 /// [`AppserviceUserIdentity`] to it, if the identity is not empty.
395 fn try_into_http_request_with_identity<T: Default + BufMut + AsRef<[u8]>>(
396 self,
397 base_url: &str,
398 access_token: auth_scheme::SendAccessToken<'_>,
399 identity: AppserviceUserIdentity<'_>,
400 path_builder_input: <Self::PathBuilder as path_builder::PathBuilder>::Input<'_>,
401 ) -> Result<http::Request<T>, IntoHttpError> {
402 let mut http_request =
403 self.try_into_http_request(base_url, access_token, path_builder_input)?;
404
405 identity.maybe_add_to_uri(http_request.uri_mut())?;
406
407 Ok(http_request)
408 }
409}
410
411impl<T: OutgoingRequest> OutgoingRequestAppserviceExt for T where
412 for<'a> Self::Authentication:
413 auth_scheme::AuthScheme<Input<'a> = auth_scheme::SendAccessToken<'a>>
414{
415}
416
417/// A request type for a Matrix API endpoint, used for receiving requests.
418pub trait IncomingRequest: Metadata {
419 /// A type capturing the error conditions that can be returned in the response.
420 type EndpointError: EndpointError;
421
422 /// Response type to return when the request is successful.
423 type OutgoingResponse: OutgoingResponse;
424
425 /// Tries to turn the given `http::Request` into this request type,
426 /// together with the corresponding path arguments.
427 ///
428 /// Note: The strings in `path_args` need to be percent-decoded.
429 ///
430 /// This function should not check the method of the HTTP request, this check is performed
431 /// in [`IncomingRequestExt::try_from_http_request()`].
432 fn try_from_http_request_inner(
433 request: http::Request<&[u8]>,
434 path_args: &[&str],
435 ) -> Result<Self, DeserializationError>;
436}
437
438/// Convenience functionality on top of [`IncomingRequest`].
439pub trait IncomingRequestExt: IncomingRequest {
440 /// Tries to convert the given `http::Request` into this request type.
441 fn try_from_http_request(
442 request: http::Request<&[u8]>,
443 path_args: &[&str],
444 ) -> Result<Self, FromHttpRequestError> {
445 let method = request.method();
446
447 if !(method == Self::METHOD
448 || (Self::METHOD == http::Method::GET && method == http::Method::HEAD))
449 {
450 return Err(FromHttpRequestError::MethodMismatch {
451 expected: Self::METHOD,
452 received: method.clone(),
453 });
454 }
455
456 Ok(Self::try_from_http_request_inner(request, path_args)?)
457 }
458}
459
460impl<T: IncomingRequest> IncomingRequestExt for T {}
461
462/// A request type for a Matrix API endpoint, used for sending responses.
463pub trait OutgoingResponse: Sized {
464 /// HTTP body type pre-serialization.
465 type Body: OutgoingBody;
466
467 /// Tries to convert this response into an `http::Response`.
468 ///
469 /// This method should only fail when invalid header values are specified. It may also fail with
470 /// a serialization error in case of bugs in Ruma though.
471 fn try_into_http_response_inner(self) -> Result<http::Response<Self::Body>, IntoHttpError>;
472}
473
474/// A request type for a Matrix API endpoint, used for sending responses.
475pub trait OutgoingResponseExt: OutgoingResponse {
476 /// Tries to convert this response into an `http::Response`.
477 ///
478 /// This method should only fail when invalid header values are specified. It may also fail with
479 /// a serialization error in case of bugs in Ruma though.
480 fn try_into_http_response<T: Default + BufMut + AsRef<[u8]>>(
481 self,
482 ) -> Result<http::Response<T>, IntoHttpError> {
483 let (mut parts, body) = self.try_into_http_response_inner()?.into_parts();
484
485 if let Some(content_type) = body.content_type()
486 && !parts.headers.contains_key(&http::header::CONTENT_TYPE)
487 {
488 parts.headers.insert(http::header::CONTENT_TYPE, content_type);
489 }
490
491 Ok(http::Response::from_parts(parts, body.try_into_buf().map_err(Into::into)?))
492 }
493}
494
495impl<T: OutgoingResponse> OutgoingResponseExt for T {}
496
497/// Gives users the ability to define their own serializable / deserializable errors.
498pub trait EndpointError: OutgoingResponse + StdError + Sized + Send + 'static {
499 /// Tries to construct `Self` from an `http::Response`.
500 ///
501 /// This will always return `Err` variant when no `error` field is defined in
502 /// the `ruma_api` macro.
503 fn from_http_response(response: http::Response<&[u8]>) -> Self;
504}
505
506/// The direction to return events from.
507#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
508#[allow(clippy::exhaustive_enums)]
509pub enum Direction {
510 /// Return events backwards in time from the requested `from` token.
511 #[default]
512 #[serde(rename = "b")]
513 Backward,
514
515 /// Return events forwards in time from the requested `from` token.
516 #[serde(rename = "f")]
517 Forward,
518}
519
520impl Direction {
521 /// method for providing forward as the default direction to serde instead
522 pub fn forward() -> Self {
523 Self::Forward
524 }
525}
526
527/// Data to [assert the identity] of an appservice virtual user.
528///
529/// [assert the identity]: https://spec.matrix.org/v1.19/application-service-api/#identity-assertion
530#[derive(Debug, Clone, Copy, Default, Serialize)]
531#[non_exhaustive]
532pub struct AppserviceUserIdentity<'a> {
533 /// The ID of the virtual user.
534 ///
535 /// If this is not set, the user implied by the `sender_localpart` property of the registration
536 /// will be used by the server.
537 #[serde(skip_serializing_if = "Option::is_none")]
538 pub user_id: Option<&'a UserId>,
539
540 /// The ID of a specific device belonging to the virtual user.
541 #[serde(skip_serializing_if = "Option::is_none")]
542 pub device_id: Option<&'a DeviceId>,
543}
544
545impl<'a> AppserviceUserIdentity<'a> {
546 /// Construct a new `AppserviceUserIdentity` with the given user ID.
547 pub fn new(user_id: &'a UserId) -> Self {
548 Self { user_id: Some(user_id), device_id: None }
549 }
550
551 /// Whether this identity is empty.
552 fn is_empty(&self) -> bool {
553 self.user_id.is_none() && self.device_id.is_none()
554 }
555
556 /// Add this identity to the given URI, if the identity is not empty.
557 pub fn maybe_add_to_uri(&self, uri: &mut http::Uri) -> Result<(), IntoHttpError> {
558 if self.is_empty() {
559 // There will be no change to the URI.
560 return Ok(());
561 }
562
563 // Serialize the query arguments of the identity.
564 let identity_query = serde_html_form::to_string(self)?;
565
566 // Add the query arguments to the URI.
567 let mut parts = uri.clone().into_parts();
568
569 let path_and_query_with_user_id = match &parts.path_and_query {
570 Some(path_and_query) => match path_and_query.query() {
571 Some(_) => format!("{path_and_query}&{identity_query}"),
572 None => format!("{path_and_query}?{identity_query}"),
573 },
574 None => format!("/?{identity_query}"),
575 };
576
577 parts.path_and_query =
578 Some(path_and_query_with_user_id.try_into().map_err(http::Error::from)?);
579
580 *uri = parts.try_into().map_err(http::Error::from)?;
581
582 Ok(())
583 }
584}