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}