ruma_api/
lib.rs

1#![doc(html_favicon_url = "https://www.ruma.io/favicon.ico")]
2#![doc(html_logo_url = "https://www.ruma.io/images/logo.png")]
3//! ⚠ **Deprecated**: this crate has been merged into [ruma-common]. ⚠
4//!
5//! [ruma-common]: https://crates.io/crates/ruma-common
6
7#![warn(missing_docs)]
8
9#[cfg(not(all(feature = "client", feature = "server")))]
10compile_error!("ruma_api's Cargo features only exist as a workaround are not meant to be disabled");
11
12use std::{convert::TryInto as _, error::Error as StdError, fmt};
13
14use bytes::BufMut;
15use ruma_identifiers::UserId;
16
17/// Generates a `ruma_api::Endpoint` from a concise definition.
18///
19/// The macro expects the following structure as input:
20///
21/// ```text
22/// ruma_api! {
23///     metadata: {
24///         description: &'static str,
25///         method: http::Method,
26///         name: &'static str,
27///         path: &'static str,
28///         rate_limited: bool,
29///         authentication: ruma_api::AuthScheme,
30///     }
31///
32///     request: {
33///         // Struct fields for each piece of data required
34///         // to make a request to this API endpoint.
35///     }
36///
37///     response: {
38///         // Struct fields for each piece of data expected
39///         // in the response from this API endpoint.
40///     }
41///
42///     // The error returned when a response fails, defaults to `MatrixError`.
43///     error: path::to::Error
44/// }
45/// ```
46///
47/// This will generate a `ruma_api::Metadata` value to be used for the `ruma_api::Endpoint`'s
48/// associated constant, single `Request` and `Response` structs, and the necessary trait
49/// implementations to convert the request into a `http::Request` and to create a response from
50/// a `http::Response` and vice versa.
51///
52/// The details of each of the three sections of the macros are documented below.
53///
54/// ## Metadata
55///
56/// * `description`: A short description of what the endpoint does.
57/// * `method`: The HTTP method used for requests to the endpoint. It's not necessary to import
58///   `http::Method`'s associated constants. Just write the value as if it was imported, e.g.
59///   `GET`.
60/// * `name`: A unique name for the endpoint. Generally this will be the same as the containing
61///   module.
62/// * `path`: The path component of the URL for the endpoint, e.g. "/foo/bar". Components of
63///   the path that are parameterized can indicate a variable by using a Rust identifier
64///   prefixed with a colon, e.g. `/foo/:some_parameter`. A corresponding query string
65///   parameter will be expected in the request struct (see below for details).
66/// * `rate_limited`: Whether or not the endpoint enforces rate limiting on requests.
67/// * `authentication`: What authentication scheme the endpoint uses.
68///
69/// ## Request
70///
71/// The request block contains normal struct field definitions. Doc comments and attributes are
72/// allowed as normal. There are also a few special attributes available to control how the
73/// struct is converted into an `http::Request`:
74///
75/// * `#[ruma_api(header = HEADER_NAME)]`: Fields with this attribute will be treated as HTTP
76///   headers on the request. The value must implement `AsRef<str>`. Generally this is a
77///   `String`. The attribute value shown above as `HEADER_NAME` must be a header name constant
78///   from `http::header`, e.g. `CONTENT_TYPE`.
79/// * `#[ruma_api(path)]`: Fields with this attribute will be inserted into the matching path
80///   component of the request URL.
81/// * `#[ruma_api(query)]`: Fields with this attribute will be inserting into the URL's query
82///   string.
83/// * `#[ruma_api(query_map)]`: Instead of individual query fields, one query_map field, of any
84///   type that implements `IntoIterator<Item = (String, String)>` (e.g. `HashMap<String,
85///   String>`, can be used for cases where an endpoint supports arbitrary query parameters.
86///
87/// Any field that does not include one of these attributes will be part of the request's JSON
88/// body.
89///
90/// ## Response
91///
92/// Like the request block, the response block consists of normal struct field definitions.
93/// Doc comments and attributes are allowed as normal.
94/// There is also a special attribute available to control how the struct is created from a
95/// `http::Request`:
96///
97/// * `#[ruma_api(header = HEADER_NAME)]`: Fields with this attribute will be treated as HTTP
98///   headers on the response. The value must implement `AsRef<str>`. Generally this is a
99///   `String`. The attribute value shown above as `HEADER_NAME` must be a header name constant
100///   from `http::header`, e.g. `CONTENT_TYPE`.
101///
102/// Any field that does not include the above attribute will be expected in the response's JSON
103/// body.
104///
105/// ## Newtype bodies
106///
107/// Both the request and response block also support "newtype bodies" by using the
108/// `#[ruma_api(body)]` attribute on a field. If present on a field, the entire request or
109/// response body will be treated as the value of the field. This allows you to treat the
110/// entire request or response body as a specific type, rather than a JSON object with named
111/// fields. Only one field in each struct can be marked with this attribute. It is an error to
112/// have a newtype body field and normal body fields within the same struct.
113///
114/// There is another kind of newtype body that is enabled with `#[ruma_api(raw_body)]`. It is
115/// used for endpoints in which the request or response body can be arbitrary bytes instead of
116/// a JSON objects. A field with `#[ruma_api(raw_body)]` needs to have the type `Vec<u8>`.
117///
118/// # Examples
119///
120/// ```
121/// pub mod some_endpoint {
122///     use ruma_api_macros::ruma_api;
123///
124///     ruma_api! {
125///         metadata: {
126///             description: "Does something.",
127///             method: POST,
128///             name: "some_endpoint",
129///             stable_path: "/_matrix/some/endpoint/:baz",
130///             rate_limited: false,
131///             authentication: None,
132///             added: 1.1,
133///         }
134///
135///         request: {
136///             pub foo: String,
137///
138///             #[ruma_api(header = CONTENT_TYPE)]
139///             pub content_type: String,
140///
141///             #[ruma_api(query)]
142///             pub bar: String,
143///
144///             #[ruma_api(path)]
145///             pub baz: String,
146///         }
147///
148///         response: {
149///             #[ruma_api(header = CONTENT_TYPE)]
150///             pub content_type: String,
151///
152///             pub value: String,
153///         }
154///     }
155/// }
156///
157/// pub mod newtype_body_endpoint {
158///     use ruma_api_macros::ruma_api;
159///     use serde::{Deserialize, Serialize};
160///
161///     #[derive(Clone, Debug, Deserialize, Serialize)]
162///     pub struct MyCustomType {
163///         pub foo: String,
164///     }
165///
166///     ruma_api! {
167///         metadata: {
168///             description: "Does something.",
169///             method: PUT,
170///             name: "newtype_body_endpoint",
171///             stable_path: "/_matrix/some/newtype/body/endpoint",
172///             rate_limited: false,
173///             authentication: None,
174///             added: 1.1,
175///         }
176///
177///         request: {
178///             #[ruma_api(raw_body)]
179///             pub file: &'a [u8],
180///         }
181///
182///         response: {
183///             #[ruma_api(body)]
184///             pub my_custom_type: MyCustomType,
185///         }
186///     }
187/// }
188/// ```
189pub use ruma_api_macros::ruma_api;
190
191pub mod error;
192/// This module is used to support the generated code from ruma-api-macros.
193/// It is not considered part of ruma-api's public API.
194#[doc(hidden)]
195pub mod exports {
196    pub use bytes;
197    pub use http;
198    pub use percent_encoding;
199    pub use ruma_api_macros;
200    pub use ruma_serde;
201    pub use serde;
202    pub use serde_json;
203}
204
205mod metadata;
206
207pub use metadata::{MatrixVersion, Metadata};
208
209use error::{FromHttpRequestError, FromHttpResponseError, IntoHttpError};
210
211/// An enum to control whether an access token should be added to outgoing requests
212#[derive(Clone, Copy, Debug)]
213#[allow(clippy::exhaustive_enums)]
214pub enum SendAccessToken<'a> {
215    /// Add the given access token to the request only if the `METADATA` on the request requires
216    /// it.
217    IfRequired(&'a str),
218
219    /// Always add the access token.
220    Always(&'a str),
221
222    /// Don't add an access token.
223    ///
224    /// This will lead to an error if the request endpoint requires authentication
225    None,
226}
227
228impl<'a> SendAccessToken<'a> {
229    /// Get the access token for an endpoint that requires one.
230    ///
231    /// Returns `Some(_)` if `self` contains an access token.
232    pub fn get_required_for_endpoint(self) -> Option<&'a str> {
233        match self {
234            Self::IfRequired(tok) | Self::Always(tok) => Some(tok),
235            Self::None => None,
236        }
237    }
238
239    /// Get the access token for an endpoint that should not require one.
240    ///
241    /// Returns `Some(_)` only if `self` is `SendAccessToken::Always(_)`.
242    pub fn get_not_required_for_endpoint(self) -> Option<&'a str> {
243        match self {
244            Self::Always(tok) => Some(tok),
245            Self::IfRequired(_) | Self::None => None,
246        }
247    }
248}
249
250/// A request type for a Matrix API endpoint, used for sending requests.
251pub trait OutgoingRequest: Sized {
252    /// A type capturing the expected error conditions the server can return.
253    type EndpointError: EndpointError;
254
255    /// Response type returned when the request is successful.
256    type IncomingResponse: IncomingResponse<EndpointError = Self::EndpointError>;
257
258    /// Metadata about the endpoint.
259    const METADATA: Metadata;
260
261    /// Tries to convert this request into an `http::Request`.
262    ///
263    /// On endpoints with authentication, when adequate information isn't provided through
264    /// access_token, this could result in an error. It may also fail with a serialization error
265    /// in case of bugs in Ruma though.
266    ///
267    /// It may also fail if, for every version in `considering_versions`;
268    /// - The endpoint is too old, and has been removed in all versions.
269    ///   ([`EndpointRemoved`](error::IntoHttpError::EndpointRemoved))
270    /// - The endpoint is too new, and no unstable path is known for this endpoint.
271    ///   ([`NoUnstablePath`](error::IntoHttpError::NoUnstablePath))
272    ///
273    /// Finally, this will emit a warning through `tracing` if it detects if any version in
274    /// `considering_versions` has deprecated this endpoint.
275    ///
276    /// The endpoints path will be appended to the given `base_url`, for example
277    /// `https://matrix.org`. Since all paths begin with a slash, it is not necessary for the
278    /// `base_url` to have a trailing slash. If it has one however, it will be ignored.
279    fn try_into_http_request<T: Default + BufMut>(
280        self,
281        base_url: &str,
282        access_token: SendAccessToken<'_>,
283        considering_versions: &'_ [MatrixVersion],
284    ) -> Result<http::Request<T>, IntoHttpError>;
285}
286
287/// A response type for a Matrix API endpoint, used for receiving responses.
288pub trait IncomingResponse: Sized {
289    /// A type capturing the expected error conditions the server can return.
290    type EndpointError: EndpointError;
291
292    /// Tries to convert the given `http::Response` into this response type.
293    fn try_from_http_response<T: AsRef<[u8]>>(
294        response: http::Response<T>,
295    ) -> Result<Self, FromHttpResponseError<Self::EndpointError>>;
296}
297
298/// An extension to `OutgoingRequest` which provides Appservice specific methods.
299pub trait OutgoingRequestAppserviceExt: OutgoingRequest {
300    /// Tries to convert this request into an `http::Request` and appends a virtual `user_id` to
301    /// [assert Appservice identity][id_assert].
302    ///
303    /// [id_assert]: https://spec.matrix.org/v1.2/application-service-api/#identity-assertion
304    fn try_into_http_request_with_user_id<T: Default + BufMut>(
305        self,
306        base_url: &str,
307        access_token: SendAccessToken<'_>,
308        user_id: &UserId,
309        considering_versions: &'_ [MatrixVersion],
310    ) -> Result<http::Request<T>, IntoHttpError> {
311        let mut http_request =
312            self.try_into_http_request(base_url, access_token, considering_versions)?;
313        let user_id_query = ruma_serde::urlencoded::to_string(&[("user_id", user_id)])?;
314
315        let uri = http_request.uri().to_owned();
316        let mut parts = uri.into_parts();
317
318        let path_and_query_with_user_id = match &parts.path_and_query {
319            Some(path_and_query) => match path_and_query.query() {
320                Some(_) => format!("{}&{}", path_and_query, user_id_query),
321                None => format!("{}?{}", path_and_query, user_id_query),
322            },
323            None => format!("/?{}", user_id_query),
324        };
325
326        parts.path_and_query =
327            Some(path_and_query_with_user_id.try_into().map_err(http::Error::from)?);
328
329        *http_request.uri_mut() = parts.try_into().map_err(http::Error::from)?;
330
331        Ok(http_request)
332    }
333}
334
335impl<T: OutgoingRequest> OutgoingRequestAppserviceExt for T {}
336
337/// A request type for a Matrix API endpoint, used for receiving requests.
338pub trait IncomingRequest: Sized {
339    /// A type capturing the error conditions that can be returned in the response.
340    type EndpointError: EndpointError;
341
342    /// Response type to return when the request is successful.
343    type OutgoingResponse: OutgoingResponse;
344
345    /// Metadata about the endpoint.
346    const METADATA: Metadata;
347
348    /// Tries to turn the given `http::Request` into this request type,
349    /// together with the corresponding path arguments.
350    ///
351    /// Note: The strings in path_args need to be percent-decoded.
352    fn try_from_http_request<B, S>(
353        req: http::Request<B>,
354        path_args: &[S],
355    ) -> Result<Self, FromHttpRequestError>
356    where
357        B: AsRef<[u8]>,
358        S: AsRef<str>;
359}
360
361/// A request type for a Matrix API endpoint, used for sending responses.
362pub trait OutgoingResponse {
363    /// Tries to convert this response into an `http::Response`.
364    ///
365    /// This method should only fail when when invalid header values are specified. It may also
366    /// fail with a serialization error in case of bugs in Ruma though.
367    fn try_into_http_response<T: Default + BufMut>(
368        self,
369    ) -> Result<http::Response<T>, IntoHttpError>;
370}
371
372/// Gives users the ability to define their own serializable / deserializable errors.
373pub trait EndpointError: OutgoingResponse + StdError + Sized + Send + 'static {
374    /// Tries to construct `Self` from an `http::Response`.
375    ///
376    /// This will always return `Err` variant when no `error` field is defined in
377    /// the `ruma_api` macro.
378    fn try_from_http_response<T: AsRef<[u8]>>(
379        response: http::Response<T>,
380    ) -> Result<Self, error::DeserializationError>;
381}
382
383/// Marker trait for requests that don't require authentication, for the client side.
384pub trait OutgoingNonAuthRequest: OutgoingRequest {}
385
386/// Marker trait for requests that don't require authentication, for the server side.
387pub trait IncomingNonAuthRequest: IncomingRequest {}
388
389/// Authentication scheme used by the endpoint.
390#[derive(Copy, Clone, Debug, PartialEq, Eq)]
391#[allow(clippy::exhaustive_enums)]
392pub enum AuthScheme {
393    /// No authentication is performed.
394    None,
395
396    /// Authentication is performed by including an access token in the `Authentication` http
397    /// header, or an `access_token` query parameter.
398    ///
399    /// It is recommended to use the header over the query parameter.
400    AccessToken,
401
402    /// Authentication is performed by including X-Matrix signatures in the request headers,
403    /// as defined in the federation API.
404    ServerSignatures,
405
406    /// Authentication is performed by setting the `access_token` query parameter.
407    QueryOnlyAccessToken,
408}
409
410// This function helps picks the right path (or an error) from a set of matrix versions.
411//
412// This function needs to be public, yet hidden, as all `try_into_http_request`s would be using it.
413//
414// Note this assumes that `versions`;
415// - at least has 1 element
416// - have all elements sorted by its `.into_parts` representation.
417#[doc(hidden)]
418pub fn select_path<'a>(
419    versions: &'_ [MatrixVersion],
420    metadata: &'_ Metadata,
421    unstable: Option<fmt::Arguments<'a>>,
422    r0: Option<fmt::Arguments<'a>>,
423    stable: Option<fmt::Arguments<'a>>,
424) -> Result<fmt::Arguments<'a>, IntoHttpError> {
425    let greater_or_equal_any =
426        |version: MatrixVersion| versions.iter().any(|v| v.is_superset_of(version));
427    let greater_or_equal_all =
428        |version: MatrixVersion| versions.iter().all(|v| v.is_superset_of(version));
429
430    let is_stable_any = metadata.added.map(greater_or_equal_any).unwrap_or(false);
431
432    let is_removed_all = metadata.removed.map(greater_or_equal_all).unwrap_or(false);
433
434    // Only when all the versions (e.g. 1.6-9) are compatible with the version that added it (e.g.
435    // 1.1), yet are also "after" the version that removed it (e.g. 1.3), then we return that error.
436    // Otherwise, this all versions may fall into a different major range, such as 2.X, where
437    // "after" and "compatible" do not exist with the 1.X range, so we at least need to make sure
438    // that the versions are part of the same "range" through the `added` check.
439    if is_stable_any && is_removed_all {
440        return Err(IntoHttpError::EndpointRemoved(metadata.removed.unwrap()));
441    }
442
443    if is_stable_any {
444        let is_deprecated_any = metadata.deprecated.map(greater_or_equal_any).unwrap_or(false);
445
446        if is_deprecated_any {
447            let is_removed_any = metadata.removed.map(greater_or_equal_any).unwrap_or(false);
448
449            if is_removed_any {
450                tracing::warn!("endpoint {} is deprecated, but also removed in one of more server-passed versions: {:?}", metadata.name, versions)
451            } else {
452                tracing::warn!(
453                    "endpoint {} is deprecated in one of more server-passed versions: {:?}",
454                    metadata.name,
455                    versions
456                )
457            }
458        }
459
460        if let Some(r0) = r0 {
461            if versions.iter().all(|&v| v == MatrixVersion::V1_0) {
462                // Endpoint was added in 1.0, we return the r0 variant.
463                return Ok(r0);
464            }
465        }
466
467        return Ok(stable.expect("metadata.added enforces the stable path to exist"));
468    }
469
470    unstable.ok_or(IntoHttpError::NoUnstablePath)
471}