Skip to main content

ruma_common/api/
metadata.rs

1use std::{
2    borrow::Cow,
3    cmp::Ordering,
4    collections::{BTreeMap, BTreeSet},
5    fmt::Display,
6    str::FromStr,
7};
8
9use http::Method;
10use ruma_macros::{
11    AsRefStr, AsStrAsRefStr, DebugAsRefStr, DisplayAsRefStr, EqAsRefStr, OrdAsRefStr,
12    SerializeAsRefStr, StringEnum,
13};
14use serde::{Deserialize, de};
15
16use super::{auth_scheme::AuthScheme, error::UnknownVersionError, path_builder::PathBuilder};
17use crate::{
18    PrivOwnedStr, RoomVersionId,
19    api::{auth_scheme::ClientScopedAuthScheme, error::IntoHttpError},
20    serde::deserialize_cow_str,
21};
22
23/// Convenient constructor for [`Metadata`] implementation.
24///
25/// ## Definition
26///
27/// By default, `Metadata` is implemented on a type named `Request` that is in scope. This can be
28/// overridden by adding `@for MyType` at the beginning of the declaration.
29///
30/// The rest of the definition of the macro is made to look like a struct, with the following
31/// fields:
32///
33/// * `method` - The HTTP method to use for the endpoint. Its value must be one of the associated
34///   constants of [`http::Method`]. In most cases it should be one of `GET`, `POST`, `PUT` or
35///   `DELETE`.
36/// * `rate_limited` - Whether the endpoint should be rate-limited, according to the specification.
37///   Its value must be a `bool`.
38/// * `authentication` - The type of authentication that is required for the endpoint, according to
39///   the specification. The type must be in scope and implement [`AuthScheme`].
40/// * `required_client_scopes` - The OAuth scopes which are required to access this endpoint, for clients
41///   authenticated using the [OAuth 2.0 API](https://spec.matrix.org/v1.19/client-server-api/#oauth-20-api).
42///   Its value must be an array of [`OAuthClientScope`]s. It defaults to `[OAuthClientScope::ApiFullAccess]`.
43///
44/// And either of the following fields to define the path(s) of the endpoint.
45///
46/// * `history` - The history of the paths of the endpoint. This should be used for endpoints from
47///   Matrix APIs that have a `/versions` endpoint that returns a list a [`MatrixVersion`]s and
48///   possibly features, like the Client-Server API or the Identity Service API. However, a few
49///   endpoints from those APIs shouldn't use this field because they cannot be versioned, like the
50///   `/versions` or the `/.well-known` endpoints.
51///
52///   Its definition is made to look like match arms and must include at least one arm. The match
53///   arms accept the following syntax:
54///
55///   * `unstable => "unstable/endpoint/path/{variable}"` - An unstable version of the endpoint as
56///     defined in the MSC that adds it, if the MSC does **NOT** define an unstable feature in the
57///     `unstable_features` field of the client-server API's `/versions` endpoint.
58///   * `unstable("org.bar.unstable_feature") => "unstable/endpoint/path/{variable}"` - An unstable
59///     version of the endpoint as defined in the MSC that adds it, if the MSC defines an unstable
60///     feature in the `unstable_features` field of the client-server API's `/versions` endpoint.
61///   * `1.0 | stable("org.bar.feature.stable") => "stable/endpoint/path/{variable}"` - A stable
62///     version of the endpoint as defined in an MSC or the Matrix specification. The match arm can
63///     be a Matrix version, a stable feature, or both separated by `|`.
64///
65///     A stable feature can be defined in an MSC alongside an unstable feature, and can be found in
66///     the `unstable_features` field of the client-server API's `/versions` endpoint. It is meant
67///     to be used by homeservers if they want to declare stable support for a feature before they
68///     can declare support for a whole Matrix version that supports it.
69///
70///   * `1.2 => deprecated` - The Matrix version that deprecated the endpoint, if any. It must be
71///     preceded by a match arm with a stable path and a different Matrix version.
72///   * `1.3 => removed` - The Matrix version that removed the endpoint, if any. It must be preceded
73///     by a match arm with a deprecation and a different Matrix version.
74///
75///   A Matrix version is a `float` representation of the version that looks like `major.minor`.
76///   It must match one of the variants of [`MatrixVersion`]. For example `1.0` matches
77///   [`MatrixVersion::V1_0`], `1.1` matches [`MatrixVersion::V1_1`], etc.
78///
79///   It is expected that the match arms are ordered by descending age. Usually the older unstable
80///   paths would be before the newer unstable paths, then we would find the stable paths, and
81///   finally the deprecation and removal.
82///
83///   The following checks occur at compile time:
84///
85///   * All unstable and stable paths contain the same variables (or lack thereof).
86///   * Matrix versions in match arms are all different and in ascending order.
87///
88///   This field is represented as the [`VersionHistory`](super::path_builder::VersionHistory) type
89///   in the generated implementation.
90/// * `path` - The only path of the endpoint. This should be used for endpoints from Matrix APIs
91///   that do NOT have a `/versions` endpoint that returns a list a [`MatrixVersion`]s, like the
92///   Server-Server API or the Appservice API. It should also be used for endpoints that cannot be
93///   versioned, like the `/versions` or the `/.well-known` endpoints.
94///
95///   Its value must be a static string representing the path, like `"endpoint/path/{variable}"`.
96///
97///   This field is represented as the [`SinglePath`](super::path_builder::SinglePath) type in the
98///   generated implementation.
99///
100/// ## Example
101///
102/// ```
103/// use ruma_common::{
104///     api::{
105///         OAuthClientScope,
106///         auth_scheme::{AccessToken, NoAuthentication},
107///     },
108///     metadata,
109/// };
110///
111/// /// A Request with a path version history.
112/// pub struct Request {
113///     body: Vec<u8>,
114/// }
115///
116/// metadata! {
117///     method: GET,
118///     rate_limited: true,
119///     authentication: AccessToken,
120///     // unnecessary here because that's the default
121///     required_client_scopes: [OAuthClientScope::ApiFullAccess],
122///
123///     history: {
124///         unstable => "/_matrix/unstable/org.bar.msc9000/baz",
125///         unstable("org.bar.msc9000.v1") => "/_matrix/unstable/org.bar.msc9000.v1/qux",
126///         1.0 | stable("org.bar.msc9000.stable") => "/_matrix/media/r0/qux",
127///         1.1 => "/_matrix/media/v3/qux",
128///         1.2 => deprecated,
129///         1.3 => removed,
130///     }
131/// };
132///
133/// /// A request with a single path.
134/// pub struct MySinglePathRequest {
135///     body: Vec<u8>,
136/// }
137///
138/// metadata! {
139///     @for MySinglePathRequest,
140///
141///     method: GET,
142///     rate_limited: false,
143///     authentication: NoAuthentication,
144///     path: "/_matrix/key/query",
145/// };
146/// ```
147#[doc(hidden)]
148#[macro_export]
149macro_rules! metadata {
150    ( @for $request_type:ty, $( $field:ident: $rhs:tt ),+ $(,)? ) => {
151        #[allow(deprecated)]
152        impl $crate::api::Metadata for $request_type {
153            $( $crate::metadata!(@field $field: $rhs); )+
154        }
155    };
156
157    ( $( $field:ident: $rhs:tt ),+ $(,)? ) => {
158        $crate::metadata!{ @for Request, $( $field: $rhs),+ }
159    };
160
161    ( @field method: $method:ident ) => {
162        const METHOD: $crate::exports::http::Method = $crate::exports::http::Method::$method;
163    };
164
165    ( @field rate_limited: $rate_limited:literal ) => { const RATE_LIMITED: bool = $rate_limited; };
166
167    ( @field required_client_scopes: [$( $scope:expr ),+ $(,)?] ) => {
168        fn required_client_scopes() -> &'static [$crate::api::OAuthClientScope]
169        where
170            Self::Authentication: $crate::api::auth_scheme::ClientScopedAuthScheme
171        {
172            &[$($scope,)+]
173        }
174    };
175
176    ( @field authentication: $scheme:path ) => {
177        type Authentication = $scheme;
178    };
179
180    ( @field path: $path:literal ) => {
181        type PathBuilder = $crate::api::path_builder::SinglePath;
182        const PATH_BUILDER: $crate::api::path_builder::SinglePath = $crate::api::path_builder::SinglePath::new($path);
183    };
184
185    ( @field history: {
186        $( unstable $(($unstable_feature:literal))? => $unstable_path:literal, )*
187        $( stable ($stable_feature_only:literal) => $stable_feature_path:literal, )*
188        $( $version:literal $(| stable ($stable_feature:literal))? => $stable_rhs:tt, )*
189    } ) => {
190        $crate::metadata! {
191            @history_impl
192            $( unstable $( ($unstable_feature) )? => $unstable_path, )*
193            $( stable ($stable_feature_only) => $stable_feature_path, )*
194            // Flip left and right to avoid macro parsing ambiguities
195            $( $stable_rhs = $version $( | stable ($stable_feature) )?, )*
196        }
197    };
198
199    ( @history_impl
200        $( unstable $(($unstable_feature:literal))? => $unstable_path:literal, )*
201        $( stable ($stable_feature_only:literal) => $stable_feature_path:literal, )*
202        $( $stable_path:literal = $version:literal $(| stable ($stable_feature:literal))?, )*
203        $( deprecated = $deprecated_version:literal, )?
204        $( removed = $removed_version:literal, )?
205    ) => {
206        type PathBuilder = $crate::api::path_builder::VersionHistory;
207        const PATH_BUILDER: $crate::api::path_builder::VersionHistory = $crate::api::path_builder::VersionHistory::new(
208            &[ $(($crate::metadata!(@optional_feature $($unstable_feature)?), $unstable_path)),* ],
209            &[
210                $((
211                    $crate::metadata!(@stable_path_selector stable($stable_feature_only)),
212                    $stable_feature_path
213                ),)*
214                $((
215                    $crate::metadata!(@stable_path_selector $version $( | stable($stable_feature) )?),
216                    $stable_path
217                ),)*
218            ],
219            $crate::metadata!(@optional_version $( $deprecated_version )?),
220            $crate::metadata!(@optional_version $( $removed_version )?),
221        );
222    };
223
224    ( @optional_feature ) => { None };
225    ( @optional_feature $feature:literal ) => { Some($feature) };
226    ( @stable_path_selector stable($feature:literal)) => {
227        $crate::api::path_builder::StablePathSelector::Feature($feature)
228    };
229    ( @stable_path_selector $version:literal | stable($feature:literal)) => {
230        $crate::api::path_builder::StablePathSelector::FeatureAndVersion {
231            feature: $feature,
232            version: $crate::api::MatrixVersion::from_lit(stringify!($version)),
233        }
234    };
235    ( @stable_path_selector $version:literal) => {
236        $crate::api::path_builder::StablePathSelector::Version(
237            $crate::api::MatrixVersion::from_lit(stringify!($version))
238        )
239    };
240    ( @optional_version ) => { None };
241    ( @optional_version $version:literal ) => { Some($crate::api::MatrixVersion::from_lit(stringify!($version))) }
242}
243
244/// Metadata about an API endpoint.
245pub trait Metadata: Sized {
246    /// The HTTP method used by this endpoint.
247    const METHOD: Method;
248
249    /// Whether or not this endpoint is rate limited by the server.
250    const RATE_LIMITED: bool;
251
252    /// What authentication scheme the server uses for this endpoint.
253    type Authentication: AuthScheme;
254
255    /// The type used to build an endpoint's path.
256    type PathBuilder: PathBuilder;
257
258    /// All info pertaining to an endpoint's path.
259    const PATH_BUILDER: Self::PathBuilder;
260
261    /// Generate the endpoint URL for this endpoint.
262    fn make_endpoint_url(
263        path_builder_input: <Self::PathBuilder as PathBuilder>::Input<'_>,
264        base_url: &str,
265        path_args: &[&dyn Display],
266        query_string: &str,
267    ) -> Result<String, IntoHttpError> {
268        Self::PATH_BUILDER.make_endpoint_url(path_builder_input, base_url, path_args, query_string)
269    }
270
271    /// The OAuth scopes which grant access to this endpoint.
272    ///
273    /// Clients which authenticated using the [OAuth 2.0 API] may only use this endpoint
274    /// if they requested _any one_ of the scopes in the returned slice. For most endpoints,
275    /// this is only [`OAuthClientScope::ApiFullAccess`].
276    ///
277    /// This function is only defined for request structs with an authentication scheme
278    /// that implements the [`ClientScopedAuthScheme`] marker trait.
279    ///
280    /// [OAuth 2.0 API]: https://spec.matrix.org/v1.19/client-server-api/#oauth-20-api
281    fn required_client_scopes() -> &'static [OAuthClientScope]
282    where
283        Self::Authentication: ClientScopedAuthScheme,
284    {
285        // TODO: In the future this could possibly be converted into a generic associated constant
286        // once those are stabilized. See https://github.com/rust-lang/rust/issues/113521.
287        &[OAuthClientScope::ApiFullAccess]
288    }
289
290    /// The list of path parameters in the metadata.
291    ///
292    /// Used for `#[test]`s generated by the API macros.
293    #[doc(hidden)]
294    fn _path_parameters() -> Vec<&'static str> {
295        Self::PATH_BUILDER._path_parameters()
296    }
297}
298
299/// The Matrix versions Ruma currently understands to exist.
300///
301/// Matrix, since fall 2021, has a quarterly release schedule, using a global `vX.Y` versioning
302/// scheme. Usually `Y` is bumped for new backwards compatible changes, but `X` can be bumped
303/// instead when a large number of `Y` changes feel deserving of a major version increase.
304///
305/// Every new version denotes stable support for endpoints in a *relatively* backwards-compatible
306/// manner.
307///
308/// Matrix has a deprecation policy, read more about it here: <https://spec.matrix.org/v1.19/#deprecation-policy>.
309///
310/// Ruma keeps track of when endpoints are added, deprecated, and removed. It'll automatically
311/// select the right endpoint stability variation to use depending on which Matrix versions you
312/// pass to [`try_into_http_request`](super::OutgoingRequestExt::try_into_http_request), see its
313/// respective documentation for more information.
314///
315/// The `PartialOrd` and `Ord` implementations of this type sort the variants by release date. A
316/// newer release is greater than an older release.
317///
318/// `MatrixVersion::is_superset_of()` is used to keep track of compatibility between versions.
319#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
320#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
321pub enum MatrixVersion {
322    /// Matrix 1.0 was a release prior to the global versioning system and does not correspond to a
323    /// version of the Matrix specification.
324    ///
325    /// It matches the following per-API versions:
326    ///
327    /// * Client-Server API: r0.5.0 to r0.6.1
328    /// * Identity Service API: r0.2.0 to r0.3.0
329    ///
330    /// The other APIs are not supported because they do not have a `GET /versions` endpoint.
331    ///
332    /// See <https://spec.matrix.org/v1.19/#legacy-versioning>.
333    V1_0,
334
335    /// Version 1.1 of the Matrix specification, released in Q4 2021.
336    ///
337    /// See <https://spec.matrix.org/v1.1/>.
338    V1_1,
339
340    /// Version 1.2 of the Matrix specification, released in Q1 2022.
341    ///
342    /// See <https://spec.matrix.org/v1.2/>.
343    V1_2,
344
345    /// Version 1.3 of the Matrix specification, released in Q2 2022.
346    ///
347    /// See <https://spec.matrix.org/v1.3/>.
348    V1_3,
349
350    /// Version 1.4 of the Matrix specification, released in Q3 2022.
351    ///
352    /// See <https://spec.matrix.org/v1.4/>.
353    V1_4,
354
355    /// Version 1.5 of the Matrix specification, released in Q4 2022.
356    ///
357    /// See <https://spec.matrix.org/v1.5/>.
358    V1_5,
359
360    /// Version 1.6 of the Matrix specification, released in Q1 2023.
361    ///
362    /// See <https://spec.matrix.org/v1.6/>.
363    V1_6,
364
365    /// Version 1.7 of the Matrix specification, released in Q2 2023.
366    ///
367    /// See <https://spec.matrix.org/v1.7/>.
368    V1_7,
369
370    /// Version 1.8 of the Matrix specification, released in Q3 2023.
371    ///
372    /// See <https://spec.matrix.org/v1.8/>.
373    V1_8,
374
375    /// Version 1.9 of the Matrix specification, released in Q4 2023.
376    ///
377    /// See <https://spec.matrix.org/v1.9/>.
378    V1_9,
379
380    /// Version 1.10 of the Matrix specification, released in Q1 2024.
381    ///
382    /// See <https://spec.matrix.org/v1.10/>.
383    V1_10,
384
385    /// Version 1.11 of the Matrix specification, released in Q2 2024.
386    ///
387    /// See <https://spec.matrix.org/v1.11/>.
388    V1_11,
389
390    /// Version 1.12 of the Matrix specification, released in Q3 2024.
391    ///
392    /// See <https://spec.matrix.org/v1.12/>.
393    V1_12,
394
395    /// Version 1.13 of the Matrix specification, released in Q4 2024.
396    ///
397    /// See <https://spec.matrix.org/v1.13/>.
398    V1_13,
399
400    /// Version 1.14 of the Matrix specification, released in Q1 2025.
401    ///
402    /// See <https://spec.matrix.org/v1.14/>.
403    V1_14,
404
405    /// Version 1.15 of the Matrix specification, released in Q2 2025.
406    ///
407    /// See <https://spec.matrix.org/v1.15/>.
408    V1_15,
409
410    /// Version 1.16 of the Matrix specification, released in Q3 2025.
411    ///
412    /// See <https://spec.matrix.org/v1.16/>.
413    V1_16,
414
415    /// Version 1.17 of the Matrix specification, released in Q4 2025.
416    ///
417    /// See <https://spec.matrix.org/v1.17/>.
418    V1_17,
419
420    /// Version 1.18 of the Matrix specification, released in Q1 2026.
421    ///
422    /// See <https://spec.matrix.org/v1.18/>.
423    V1_18,
424
425    /// Version 1.19 of the Matrix specification, released in Q2 2026.
426    ///
427    /// See <https://spec.matrix.org/v1.19/>.
428    V1_19,
429}
430
431impl TryFrom<&str> for MatrixVersion {
432    type Error = UnknownVersionError;
433
434    fn try_from(value: &str) -> Result<MatrixVersion, Self::Error> {
435        use MatrixVersion::*;
436
437        Ok(match value {
438            // Identity service API versions between Matrix 1.0 and 1.1.
439            // They might match older client-server API versions but that should not be a problem in practice.
440            "r0.2.0" | "r0.2.1" | "r0.3.0" |
441            // Client-server API versions between Matrix 1.0 and 1.1.
442            "r0.5.0" | "r0.6.0" | "r0.6.1" => V1_0,
443            "v1.1" => V1_1,
444            "v1.2" => V1_2,
445            "v1.3" => V1_3,
446            "v1.4" => V1_4,
447            "v1.5" => V1_5,
448            "v1.6" => V1_6,
449            "v1.7" => V1_7,
450            "v1.8" => V1_8,
451            "v1.9" => V1_9,
452            "v1.10" => V1_10,
453            "v1.11" => V1_11,
454            "v1.12" => V1_12,
455            "v1.13" => V1_13,
456            "v1.14" => V1_14,
457            "v1.15" => V1_15,
458            "v1.16" => V1_16,
459            "v1.17" => V1_17,
460            "v1.18" => V1_18,
461            "v1.19" => V1_19,
462            _ => return Err(UnknownVersionError),
463        })
464    }
465}
466
467impl FromStr for MatrixVersion {
468    type Err = UnknownVersionError;
469
470    fn from_str(s: &str) -> Result<Self, Self::Err> {
471        Self::try_from(s)
472    }
473}
474
475impl MatrixVersion {
476    /// Checks whether a version is compatible with another.
477    ///
478    /// Currently, all versions of Matrix are considered backwards compatible with all the previous
479    /// versions, so this is equivalent to `self >= other`. This behaviour may change in the future,
480    /// if a new release is considered to be breaking compatibility with the previous ones.
481    ///
482    /// > ⚠ Matrix has a deprecation policy, and Matrix versioning is not as straightforward as this
483    /// > function makes it out to be. This function only exists to prune breaking changes between
484    /// > versions, and versions too new for `self`.
485    pub fn is_superset_of(self, other: Self) -> bool {
486        self >= other
487    }
488
489    /// Get a string representation of this Matrix version.
490    ///
491    /// This is the string that can be found in the response to one of the `GET /versions`
492    /// endpoints. Parsing this string will give the same variant.
493    ///
494    /// Returns `None` for [`MatrixVersion::V1_0`] because it can match several per-API versions.
495    pub const fn as_str(self) -> Option<&'static str> {
496        let string = match self {
497            MatrixVersion::V1_0 => return None,
498            MatrixVersion::V1_1 => "v1.1",
499            MatrixVersion::V1_2 => "v1.2",
500            MatrixVersion::V1_3 => "v1.3",
501            MatrixVersion::V1_4 => "v1.4",
502            MatrixVersion::V1_5 => "v1.5",
503            MatrixVersion::V1_6 => "v1.6",
504            MatrixVersion::V1_7 => "v1.7",
505            MatrixVersion::V1_8 => "v1.8",
506            MatrixVersion::V1_9 => "v1.9",
507            MatrixVersion::V1_10 => "v1.10",
508            MatrixVersion::V1_11 => "v1.11",
509            MatrixVersion::V1_12 => "v1.12",
510            MatrixVersion::V1_13 => "v1.13",
511            MatrixVersion::V1_14 => "v1.14",
512            MatrixVersion::V1_15 => "v1.15",
513            MatrixVersion::V1_16 => "v1.16",
514            MatrixVersion::V1_17 => "v1.17",
515            MatrixVersion::V1_18 => "v1.18",
516            MatrixVersion::V1_19 => "v1.19",
517        };
518
519        Some(string)
520    }
521
522    /// Decompose the Matrix version into its major and minor number.
523    const fn into_parts(self) -> (u8, u8) {
524        match self {
525            MatrixVersion::V1_0 => (1, 0),
526            MatrixVersion::V1_1 => (1, 1),
527            MatrixVersion::V1_2 => (1, 2),
528            MatrixVersion::V1_3 => (1, 3),
529            MatrixVersion::V1_4 => (1, 4),
530            MatrixVersion::V1_5 => (1, 5),
531            MatrixVersion::V1_6 => (1, 6),
532            MatrixVersion::V1_7 => (1, 7),
533            MatrixVersion::V1_8 => (1, 8),
534            MatrixVersion::V1_9 => (1, 9),
535            MatrixVersion::V1_10 => (1, 10),
536            MatrixVersion::V1_11 => (1, 11),
537            MatrixVersion::V1_12 => (1, 12),
538            MatrixVersion::V1_13 => (1, 13),
539            MatrixVersion::V1_14 => (1, 14),
540            MatrixVersion::V1_15 => (1, 15),
541            MatrixVersion::V1_16 => (1, 16),
542            MatrixVersion::V1_17 => (1, 17),
543            MatrixVersion::V1_18 => (1, 18),
544            MatrixVersion::V1_19 => (1, 19),
545        }
546    }
547
548    /// Try to turn a pair of (major, minor) version components back into a `MatrixVersion`.
549    const fn from_parts(major: u8, minor: u8) -> Result<Self, UnknownVersionError> {
550        match (major, minor) {
551            (1, 0) => Ok(MatrixVersion::V1_0),
552            (1, 1) => Ok(MatrixVersion::V1_1),
553            (1, 2) => Ok(MatrixVersion::V1_2),
554            (1, 3) => Ok(MatrixVersion::V1_3),
555            (1, 4) => Ok(MatrixVersion::V1_4),
556            (1, 5) => Ok(MatrixVersion::V1_5),
557            (1, 6) => Ok(MatrixVersion::V1_6),
558            (1, 7) => Ok(MatrixVersion::V1_7),
559            (1, 8) => Ok(MatrixVersion::V1_8),
560            (1, 9) => Ok(MatrixVersion::V1_9),
561            (1, 10) => Ok(MatrixVersion::V1_10),
562            (1, 11) => Ok(MatrixVersion::V1_11),
563            (1, 12) => Ok(MatrixVersion::V1_12),
564            (1, 13) => Ok(MatrixVersion::V1_13),
565            (1, 14) => Ok(MatrixVersion::V1_14),
566            (1, 15) => Ok(MatrixVersion::V1_15),
567            (1, 16) => Ok(MatrixVersion::V1_16),
568            (1, 17) => Ok(MatrixVersion::V1_17),
569            (1, 18) => Ok(MatrixVersion::V1_18),
570            (1, 19) => Ok(MatrixVersion::V1_19),
571            _ => Err(UnknownVersionError),
572        }
573    }
574
575    /// Constructor for use by the `metadata!` macro.
576    ///
577    /// Accepts string literals and parses them.
578    #[doc(hidden)]
579    pub const fn from_lit(lit: &'static str) -> Self {
580        use konst::{result, string};
581
582        let mut lit_parts = string::split(lit, ".");
583
584        let checked_first = lit_parts.next().unwrap(); // First iteration always succeeds
585        let major = result::unwrap_or_else!(u8::from_str_radix(checked_first, 10), |_| panic!(
586            "major version is not a valid number"
587        ));
588
589        let Some(checked_second) = lit_parts.next() else {
590            panic!("could not find dot to denote second number");
591        };
592        let minor = result::unwrap_or_else!(u8::from_str_radix(checked_second, 10), |_| panic!(
593            "minor version is not a valid number"
594        ));
595
596        if lit_parts.next().is_some() {
597            panic!("version literal contains more than one dot")
598        }
599
600        result::unwrap_or_else!(Self::from_parts(major, minor), |_| panic!(
601            "not a valid version literal"
602        ))
603    }
604
605    // Internal function to do ordering in const-fn contexts
606    pub(super) const fn const_ord(&self, other: &Self) -> Ordering {
607        let self_parts = self.into_parts();
608        let other_parts = other.into_parts();
609
610        use konst::primitive::cmp::cmp_u8;
611
612        let major_ord = cmp_u8(self_parts.0, other_parts.0);
613        if major_ord.is_ne() { major_ord } else { cmp_u8(self_parts.1, other_parts.1) }
614    }
615
616    // Internal function to check if this version is the legacy (v1.0) version in const-fn contexts
617    pub(super) const fn is_legacy(&self) -> bool {
618        let self_parts = self.into_parts();
619
620        use konst::primitive::cmp::cmp_u8;
621
622        cmp_u8(self_parts.0, 1).is_eq() && cmp_u8(self_parts.1, 0).is_eq()
623    }
624
625    /// Get the default [`RoomVersionId`] for this `MatrixVersion`.
626    pub fn default_room_version(&self) -> RoomVersionId {
627        match self {
628            // <https://spec.matrix.org/historical/index.html#complete-list-of-room-versions>
629            MatrixVersion::V1_0
630            // <https://spec.matrix.org/v1.1/rooms/#complete-list-of-room-versions>
631            | MatrixVersion::V1_1
632            // <https://spec.matrix.org/v1.2/rooms/#complete-list-of-room-versions>
633            | MatrixVersion::V1_2 => RoomVersionId::V6,
634            // <https://spec.matrix.org/v1.3/rooms/#complete-list-of-room-versions>
635            MatrixVersion::V1_3
636            // <https://spec.matrix.org/v1.4/rooms/#complete-list-of-room-versions>
637            | MatrixVersion::V1_4
638            // <https://spec.matrix.org/v1.5/rooms/#complete-list-of-room-versions>
639            | MatrixVersion::V1_5 => RoomVersionId::V9,
640            // <https://spec.matrix.org/v1.6/rooms/#complete-list-of-room-versions>
641            MatrixVersion::V1_6
642            // <https://spec.matrix.org/v1.7/rooms/#complete-list-of-room-versions>
643            | MatrixVersion::V1_7
644            // <https://spec.matrix.org/v1.8/rooms/#complete-list-of-room-versions>
645            | MatrixVersion::V1_8
646            // <https://spec.matrix.org/v1.9/rooms/#complete-list-of-room-versions>
647            | MatrixVersion::V1_9
648            // <https://spec.matrix.org/v1.10/rooms/#complete-list-of-room-versions>
649            | MatrixVersion::V1_10
650            // <https://spec.matrix.org/v1.11/rooms/#complete-list-of-room-versions>
651            | MatrixVersion::V1_11
652            // <https://spec.matrix.org/v1.12/rooms/#complete-list-of-room-versions>
653            | MatrixVersion::V1_12
654            // <https://spec.matrix.org/v1.13/rooms/#complete-list-of-room-versions>
655            | MatrixVersion::V1_13 => RoomVersionId::V10,
656            // <https://spec.matrix.org/v1.14/rooms/#complete-list-of-room-versions>
657            | MatrixVersion::V1_14
658            // <https://spec.matrix.org/v1.15/rooms/#complete-list-of-room-versions>
659            | MatrixVersion::V1_15 => RoomVersionId::V11,
660            // <https://spec.matrix.org/v1.16/rooms/#complete-list-of-room-versions>
661            MatrixVersion::V1_16
662            // <https://spec.matrix.org/v1.17/rooms/#complete-list-of-room-versions>
663            | MatrixVersion::V1_17
664            // <https://spec.matrix.org/v1.18/rooms/#complete-list-of-room-versions>
665            | MatrixVersion::V1_18
666            // <https://spec.matrix.org/v1.19/rooms/#complete-list-of-room-versions>
667            | MatrixVersion::V1_19 => RoomVersionId::V12,
668        }
669    }
670}
671
672/// The list of Matrix versions and features supported by a homeserver.
673#[derive(Debug, Clone)]
674#[allow(clippy::exhaustive_structs)]
675pub struct SupportedVersions {
676    /// The Matrix versions that are supported by the homeserver.
677    ///
678    /// This set contains only known versions.
679    pub versions: BTreeSet<MatrixVersion>,
680
681    /// The features that are supported by the homeserver.
682    ///
683    /// This matches the `unstable_features` field of the `/versions` endpoint, without the boolean
684    /// value.
685    pub features: BTreeSet<FeatureFlag>,
686}
687
688impl SupportedVersions {
689    /// Construct a `SupportedVersions` from the parts of a `/versions` response.
690    ///
691    /// Matrix versions that can't be parsed to a `MatrixVersion`, and features with the boolean
692    /// value set to `false` are discarded.
693    pub fn from_parts(versions: &[String], unstable_features: &BTreeMap<String, bool>) -> Self {
694        Self {
695            versions: versions.iter().flat_map(|s| s.parse::<MatrixVersion>()).collect(),
696            features: unstable_features
697                .iter()
698                .filter(|(_, enabled)| **enabled)
699                .map(|(feature, _)| feature.as_str().into())
700                .collect(),
701        }
702    }
703}
704
705/// The Matrix features supported by Ruma.
706///
707/// Features that are not behind a cargo feature are features that are part of the Matrix
708/// specification and that Ruma still supports, like the unstable version of an endpoint or a stable
709/// feature. Features behind a cargo feature are only supported when this feature is enabled.
710#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
711#[derive(Clone, StringEnum, Hash)]
712#[non_exhaustive]
713pub enum FeatureFlag {
714    /// `fi.mau.msc2246` ([MSC])
715    ///
716    /// Asynchronous media uploads.
717    ///
718    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/2246
719    #[ruma_enum(rename = "fi.mau.msc2246")]
720    Msc2246,
721
722    /// `org.matrix.msc2432` ([MSC])
723    ///
724    /// Updated semantics for publishing room aliases.
725    ///
726    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/2432
727    #[ruma_enum(rename = "org.matrix.msc2432")]
728    Msc2432,
729
730    /// `fi.mau.msc2659` ([MSC])
731    ///
732    /// Application service ping endpoint.
733    ///
734    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/2659
735    #[ruma_enum(rename = "fi.mau.msc2659")]
736    Msc2659,
737
738    /// `fi.mau.msc2659` ([MSC])
739    ///
740    /// Stable version of the application service ping endpoint.
741    ///
742    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/2659
743    #[ruma_enum(rename = "fi.mau.msc2659.stable")]
744    Msc2659Stable,
745
746    /// `uk.half-shot.msc2666.query_mutual_rooms` ([MSC])
747    ///
748    /// Get rooms in common with another user.
749    ///
750    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/2666
751    #[ruma_enum(rename = "uk.half-shot.msc2666.query_mutual_rooms")]
752    Msc2666,
753
754    /// `uk.half-shot.msc2666.query_mutual_rooms.stable` ([MSC])
755    ///
756    /// Get rooms in common with another user. (stable version)
757    ///
758    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/2666
759    #[ruma_enum(rename = "uk.half-shot.msc2666.query_mutual_rooms.stable")]
760    Msc2666Stable,
761
762    /// `org.matrix.msc3030` ([MSC])
763    ///
764    /// Jump to date API endpoint.
765    ///
766    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/3030
767    #[ruma_enum(rename = "org.matrix.msc3030")]
768    Msc3030,
769
770    /// `org.matrix.msc3882` ([MSC])
771    ///
772    /// Allow an existing session to sign in a new session.
773    ///
774    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/3882
775    #[ruma_enum(rename = "org.matrix.msc3882")]
776    Msc3882,
777
778    /// `org.matrix.msc3916` ([MSC])
779    ///
780    /// Authentication for media.
781    ///
782    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/3916
783    #[ruma_enum(rename = "org.matrix.msc3916")]
784    Msc3916,
785
786    /// `org.matrix.msc3916.stable` ([MSC])
787    ///
788    /// Stable version of authentication for media.
789    ///
790    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/3916
791    #[ruma_enum(rename = "org.matrix.msc3916.stable")]
792    Msc3916Stable,
793
794    /// `org.matrix.msc4108` ([MSC])
795    ///
796    /// Mechanism to allow OIDC sign in and E2EE set up via QR code.
797    ///
798    /// This is for the unstable 2024 version of the [MSC].
799    ///
800    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
801    #[cfg(feature = "unstable-msc4108")]
802    #[ruma_enum(rename = "org.matrix.msc4108")]
803    Msc4108,
804
805    /// `org.matrix.msc4140` ([MSC])
806    ///
807    /// Delayed events.
808    ///
809    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4140
810    #[cfg(feature = "unstable-msc4140")]
811    #[ruma_enum(rename = "org.matrix.msc4140")]
812    Msc4140,
813
814    /// `org.matrix.simplified_msc3575` ([MSC])
815    ///
816    /// Simplified Sliding Sync.
817    ///
818    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4186
819    #[cfg(feature = "unstable-msc4186")]
820    #[ruma_enum(rename = "org.matrix.simplified_msc3575")]
821    Msc4186,
822
823    /// `uk.timedout.msc4323` ([MSC])
824    ///
825    /// Suspend and lock endpoints.
826    ///
827    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4323
828    #[ruma_enum(rename = "uk.timedout.msc4323")]
829    Msc4323,
830
831    /// `org.matrix.msc4380_invite_permission_config` ([MSC])
832    ///
833    /// Invite Blocking.
834    ///
835    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4380
836    #[ruma_enum(rename = "org.matrix.msc4380")]
837    Msc4380,
838
839    /// `org.continuwuity.msc4484.unstable` ([MSC])
840    ///
841    /// Server administration OAuth scope.
842    ///
843    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4484
844    #[ruma_enum(rename = "org.continuwuity.msc4484.unstable")]
845    Msc4484,
846
847    /// `org.continuwuity.presence_v2.msc4495` ([MSC])
848    ///
849    /// Selective Presence.
850    ///
851    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4495
852    #[cfg(feature = "unstable-msc4495")]
853    #[ruma_enum(rename = "org.continuwuity.presence_v2.msc4495")]
854    Msc4495,
855
856    /// `uk.timedout.msc4494` ([MSC])
857    ///
858    /// Membership-based invite blocking
859    ///
860    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4494
861    #[cfg(feature = "unstable-msc4494")]
862    #[ruma_enum(rename = "uk.timedout.msc4494")]
863    Msc4494,
864
865    #[doc(hidden)]
866    _Custom(PrivOwnedStr),
867}
868
869/// An OAuth scope which grants access to some client-server API endpoints.
870///
871/// This enum does _not_ include the [device ID scope], which isn't really a scope (as it doesn't
872/// grant access to anything) but instead a way to reserve a specific device ID.
873///
874/// This type can hold any string which conforms to the format of an OAuth scope, as defined
875/// in [RFC 6749]. To build this with a custom value, convert it from a string with `::try_from()` /
876/// `.try_into()`. To check for values that are not available as a documented variant here, use its
877/// string representation, obtained through [`.as_str()`](Self::as_str()).
878///
879/// [device ID scope]: https://spec.matrix.org/v1.19/client-server-api/#device-id-allocation
880/// [RFC 6749]: https://datatracker.ietf.org/doc/html/rfc6749#section-3.3
881#[derive(
882    Clone,
883    Hash,
884    AsRefStr,
885    AsStrAsRefStr,
886    DisplayAsRefStr,
887    DebugAsRefStr,
888    SerializeAsRefStr,
889    EqAsRefStr,
890    OrdAsRefStr,
891)]
892#[non_exhaustive]
893pub enum OAuthClientScope {
894    /// Full access to all endpoints of the client-server API, unless explicitly noted.
895    #[ruma_enum(rename = "urn:matrix:client:api:*")]
896    ApiFullAccess,
897
898    /// Access to the endpoints in the [Server Administration] module.
899    ///
900    /// [Server Administration]: https://spec.matrix.org/v1.19/client-server-api/#server-administration
901    #[ruma_enum(rename = "urn:matrix:client:cc.c10y.msc4484.server_administration")]
902    #[cfg(feature = "unstable-msc4484")]
903    ServerAdministration,
904
905    #[doc(hidden)]
906    _Custom(PrivOwnedStr),
907}
908
909impl TryFrom<Cow<'_, str>> for OAuthClientScope {
910    type Error = ruma_identifiers_validation::Error;
911
912    fn try_from(value: Cow<'_, str>) -> Result<Self, Self::Error> {
913        match value.as_ref() {
914            "urn:matrix:client:api:*" | "urn:matrix:org.matrix.msc2967.client:api:*" => {
915                Ok(Self::ApiFullAccess)
916            }
917            #[cfg(feature = "unstable-msc4484")]
918            "urn:matrix:client:cc.c10y.msc4484.server_administration" => {
919                Ok(Self::ServerAdministration)
920            }
921            _ => {
922                ruma_identifiers_validation::oauth_scope::validate(&value)?;
923                let inner: Box<str> = value.into();
924                Ok(Self::_Custom(PrivOwnedStr(inner)))
925            }
926        }
927    }
928}
929
930impl TryFrom<&str> for OAuthClientScope {
931    type Error = ruma_identifiers_validation::Error;
932
933    fn try_from(value: &str) -> Result<Self, Self::Error> {
934        Self::try_from(Cow::Borrowed(value))
935    }
936}
937
938impl FromStr for OAuthClientScope {
939    type Err = ruma_identifiers_validation::Error;
940
941    fn from_str(value: &str) -> Result<Self, Self::Err> {
942        Self::try_from(Cow::Borrowed(value))
943    }
944}
945
946impl<'de> Deserialize<'de> for OAuthClientScope {
947    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
948    where
949        D: serde::Deserializer<'de>,
950    {
951        OAuthClientScope::try_from(deserialize_cow_str(deserializer)?).map_err(de::Error::custom)
952    }
953}
954
955#[cfg(test)]
956mod tests {
957    use ruma_identifiers_validation::Error;
958
959    use super::OAuthClientScope;
960
961    #[test]
962    fn parse_oauth_scope() {
963        OAuthClientScope::try_from("urn:example:hunter2").unwrap();
964        OAuthClientScope::try_from(
965            "urn:COMPLAINTS@ruma.dev:(([[{{<<'|~=.=~|'>>}}]])):+,*-./#$%&!^_`;:???????",
966        )
967        .unwrap();
968
969        assert_eq!(OAuthClientScope::try_from(""), Err(Error::Empty));
970        assert_eq!(OAuthClientScope::try_from("urn: :3"), Err(Error::InvalidCharacters));
971        assert_eq!(OAuthClientScope::try_from("urn:⚱️"), Err(Error::InvalidCharacters));
972    }
973}