ruma_common/api/
metadata.rs

1use std::{
2    cmp::Ordering,
3    collections::{BTreeMap, BTreeSet},
4    fmt::{Display, Write},
5    str::FromStr,
6};
7
8use bytes::BufMut;
9use http::{
10    header::{self, HeaderName, HeaderValue},
11    Method,
12};
13use percent_encoding::utf8_percent_encode;
14use ruma_macros::{OrdAsRefStr, PartialEqAsRefStr, PartialOrdAsRefStr, StringEnum};
15use tracing::warn;
16
17use super::{
18    error::{IntoHttpError, UnknownVersionError},
19    AuthScheme, SendAccessToken,
20};
21use crate::{
22    percent_encode::PATH_PERCENT_ENCODE_SET, serde::slice_to_buf, PrivOwnedStr, RoomVersionId,
23};
24
25/// Convenient constructor for [`Metadata`] constants.
26///
27/// ## Definition
28///
29/// The definition of the macro is made to look like a struct, with the following fields:
30///
31/// * `method` - The HTTP method to use for the endpoint. Its value must be one of the associated
32///   constants of [`http::Method`]. In most cases it should be one of `GET`, `POST`, `PUT` or
33///   `DELETE`.
34/// * `rate_limited` - Whether the endpoint should be rate-limited, according to the specification.
35///   Its value must be a `bool`.
36/// * `authentication` - The type of authentication that is required for the endpoint, according to
37///   the specification. Its value must be one of the variants of [`AuthScheme`].
38/// * `history` - The history of the paths of the endpoint. Its definition is made to look like
39///   match arms and must include at least one arm.
40///
41///   The match arms accept the following syntax:
42///
43///   * `unstable => "unstable/endpoint/path/{variable}"` - An unstable version of the endpoint as
44///     defined in the MSC that adds it, if the MSC does **NOT** define an unstable feature in the
45///     `unstable_features` field of the client-server API's `/versions` endpoint.
46///   * `unstable("org.bar.unstable_feature") => "unstable/endpoint/path/{variable}"` - An unstable
47///     version of the endpoint as defined in the MSC that adds it, if the MSC defines an unstable
48///     feature in the `unstable_features` field of the client-server API's `/versions` endpoint.
49///   * `1.0 | stable("org.bar.feature.stable") => "stable/endpoint/path/{variable}"` - A stable
50///     version of the endpoint as defined in an MSC or the Matrix specification. The match arm can
51///     be a Matrix version, a stable feature, or both separated by `|`.
52///
53///     A stable feature can be defined in an MSC alongside an unstable feature, and can be found in
54///     the `unstable_features` field of the client-server API's `/versions` endpoint. It is meant
55///     to be used by homeservers if they want to declare stable support for a feature before they
56///     can declare support for a whole Matrix version that supports it.
57///
58///   * `1.2 => deprecated` - The Matrix version that deprecated the endpoint, if any. It must be
59///     preceded by a match arm with a stable path and a different Matrix version.
60///   * `1.3 => removed` - The Matrix version that removed the endpoint, if any. It must be preceded
61///     by a match arm with a deprecation and a different Matrix version.
62///
63///   A Matrix version is a `float` representation of the version that looks like `major.minor`.
64///   It must match one of the variants of [`MatrixVersion`]. For example `1.0` matches
65///   [`MatrixVersion::V1_0`], `1.1` matches [`MatrixVersion::V1_1`], etc.
66///
67///   It is expected that the match arms are ordered by descending age. Usually the older unstable
68///   paths would be before the newer unstable paths, then we would find the stable paths, and
69///   finally the deprecation and removal.
70///
71///   The following checks occur at compile time:
72///
73///   * All unstable and stable paths contain the same variables (or lack thereof).
74///   * Matrix versions in match arms are all different and in ascending order.
75///
76/// ## Example
77///
78/// ```
79/// use ruma_common::{api::Metadata, metadata};
80/// const METADATA: Metadata = metadata! {
81///     method: GET,
82///     rate_limited: true,
83///     authentication: AccessToken,
84///
85///     history: {
86///         unstable => "/_matrix/unstable/org.bar.msc9000/baz",
87///         unstable("org.bar.msc9000.v1") => "/_matrix/unstable/org.bar.msc9000.v1/qux",
88///         1.0 | stable("org.bar.msc9000.stable") => "/_matrix/media/r0/qux",
89///         1.1 => "/_matrix/media/v3/qux",
90///         1.2 => deprecated,
91///         1.3 => removed,
92///     }
93/// };
94/// ```
95#[doc(hidden)]
96#[macro_export]
97macro_rules! metadata {
98    ( $( $field:ident: $rhs:tt ),+ $(,)? ) => {
99        $crate::api::Metadata {
100            $( $field: $crate::metadata!(@field $field: $rhs) ),+
101        }
102    };
103
104    ( @field method: $method:ident ) => { $crate::exports::http::Method::$method };
105
106    ( @field authentication: $scheme:ident ) => { $crate::api::AuthScheme::$scheme };
107
108    ( @field history: {
109        $( unstable $(($unstable_feature:literal))? => $unstable_path:literal, )*
110        $( stable ($stable_feature_only:literal) => $stable_feature_path:literal, )?
111        $( $( $version:literal $(| stable ($stable_feature:literal))? => $rhs:tt, )+ )?
112    } ) => {
113        $crate::metadata! {
114            @history_impl
115            [ $( $unstable_path $(= $unstable_feature)? ),* ]
116            $( stable ($stable_feature_only) => $stable_feature_path, )?
117            // Flip left and right to avoid macro parsing ambiguities
118            $( $( $rhs = $version $(| stable ($stable_feature))? ),+ )?
119        }
120    };
121
122    // Simple literal case: used for description, name, rate_limited
123    ( @field $_field:ident: $rhs:expr ) => { $rhs };
124
125    ( @history_impl
126        [ $( $unstable_path:literal $(= $unstable_feature:literal)? ),* ]
127        $( stable ($stable_feature_only:literal) => $stable_feature_path:literal, )?
128        $(
129            $( $stable_path:literal = $version:literal $(| stable ($stable_feature:literal))? ),+
130            $(,
131                deprecated = $deprecated_version:literal
132                $(, removed = $removed_version:literal )?
133            )?
134        )?
135    ) => {
136        $crate::api::VersionHistory::new(
137            &[ $(($crate::metadata!(@optional_feature $($unstable_feature)?), $unstable_path)),* ],
138            &[
139                $((
140                    $crate::metadata!(@stable_path_selector stable($stable_feature_only)),
141                    $stable_feature_path
142                ),)?
143                $($((
144                    $crate::metadata!(@stable_path_selector $version $(| stable($stable_feature))?),
145                    $stable_path
146                )),+)?
147            ],
148            $crate::metadata!(@optional_version $($( $deprecated_version )?)?),
149            $crate::metadata!(@optional_version $($($( $removed_version )?)?)?),
150        )
151    };
152
153    ( @optional_feature ) => { None };
154    ( @optional_feature $feature:literal ) => { Some($feature) };
155    ( @stable_path_selector stable($feature:literal)) => { $crate::api::StablePathSelector::Feature($feature) };
156    ( @stable_path_selector $version:literal | stable($feature:literal)) => {
157        $crate::api::StablePathSelector::FeatureAndVersion {
158            feature: $feature,
159            version: $crate::api::MatrixVersion::from_lit(stringify!($version)),
160        }
161    };
162    ( @stable_path_selector $version:literal) => { $crate::api::StablePathSelector::Version($crate::api::MatrixVersion::from_lit(stringify!($version))) };
163    ( @optional_version ) => { None };
164    ( @optional_version $version:literal ) => { Some($crate::api::MatrixVersion::from_lit(stringify!($version))) }
165}
166
167/// Metadata about an API endpoint.
168#[derive(Clone, Debug, PartialEq, Eq)]
169#[allow(clippy::exhaustive_structs)]
170pub struct Metadata {
171    /// The HTTP method used by this endpoint.
172    pub method: Method,
173
174    /// Whether or not this endpoint is rate limited by the server.
175    pub rate_limited: bool,
176
177    /// What authentication scheme the server uses for this endpoint.
178    pub authentication: AuthScheme,
179
180    /// All info pertaining to an endpoint's (historic) paths, deprecation version, and removal.
181    pub history: VersionHistory,
182}
183
184impl Metadata {
185    /// Returns an empty request body for this Matrix request.
186    ///
187    /// For `GET` requests, it returns an entirely empty buffer, for others it returns an empty JSON
188    /// object (`{}`).
189    pub fn empty_request_body<B>(&self) -> B
190    where
191        B: Default + BufMut,
192    {
193        if self.method == Method::GET {
194            Default::default()
195        } else {
196            slice_to_buf(b"{}")
197        }
198    }
199
200    /// Transform the `SendAccessToken` into an access token if the endpoint requires it, or if it
201    /// is `SendAccessToken::Force`.
202    ///
203    /// Fails if the endpoint requires an access token but the parameter is `SendAccessToken::None`,
204    /// or if the access token can't be converted to a [`HeaderValue`].
205    pub fn authorization_header(
206        &self,
207        access_token: SendAccessToken<'_>,
208    ) -> Result<Option<(HeaderName, HeaderValue)>, IntoHttpError> {
209        Ok(match self.authentication {
210            AuthScheme::None => match access_token.get_not_required_for_endpoint() {
211                Some(token) => Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?)),
212                None => None,
213            },
214
215            AuthScheme::AccessToken => {
216                let token = access_token
217                    .get_required_for_endpoint()
218                    .ok_or(IntoHttpError::NeedsAuthentication)?;
219
220                Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?))
221            }
222
223            AuthScheme::AccessTokenOptional => match access_token.get_required_for_endpoint() {
224                Some(token) => Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?)),
225                None => None,
226            },
227
228            AuthScheme::AppserviceToken => {
229                let token = access_token
230                    .get_required_for_appservice()
231                    .ok_or(IntoHttpError::NeedsAuthentication)?;
232
233                Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?))
234            }
235
236            AuthScheme::AppserviceTokenOptional => match access_token.get_required_for_appservice()
237            {
238                Some(token) => Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?)),
239                None => None,
240            },
241
242            AuthScheme::ServerSignatures => None,
243        })
244    }
245
246    /// Generate the endpoint URL for this endpoint.
247    pub fn make_endpoint_url(
248        &self,
249        considering: &SupportedVersions,
250        base_url: &str,
251        path_args: &[&dyn Display],
252        query_string: &str,
253    ) -> Result<String, IntoHttpError> {
254        let path_with_placeholders = self.history.select_path(considering)?;
255
256        let mut res = base_url.strip_suffix('/').unwrap_or(base_url).to_owned();
257        let mut segments = path_with_placeholders.split('/');
258        let mut path_args = path_args.iter();
259
260        let first_segment = segments.next().expect("split iterator is never empty");
261        assert!(first_segment.is_empty(), "endpoint paths must start with '/'");
262
263        for segment in segments {
264            if Self::extract_endpoint_path_segment_variable(segment).is_some() {
265                let arg = path_args
266                    .next()
267                    .expect("number of placeholders must match number of arguments")
268                    .to_string();
269                let arg = utf8_percent_encode(&arg, PATH_PERCENT_ENCODE_SET);
270
271                write!(res, "/{arg}").expect("writing to a String using fmt::Write can't fail");
272            } else {
273                res.reserve(segment.len() + 1);
274                res.push('/');
275                res.push_str(segment);
276            }
277        }
278
279        if !query_string.is_empty() {
280            res.push('?');
281            res.push_str(query_string);
282        }
283
284        Ok(res)
285    }
286
287    /// The list of path parameters in the metadata.
288    ///
289    /// Used for `#[test]`s generated by the API macros.
290    #[doc(hidden)]
291    pub fn _path_parameters(&self) -> Vec<&'static str> {
292        let path = self.history.all_paths().next().unwrap();
293        path.split('/').filter_map(Self::extract_endpoint_path_segment_variable).collect()
294    }
295
296    /// Extract the variable of the given endpoint path segment.
297    ///
298    /// The supported syntax for an endpoint path segment variable is `{var}`.
299    ///
300    /// Returns the name of the variable if one was found in the segment, `None` if no variable was
301    /// found.
302    ///
303    /// Panics if:
304    ///
305    /// * The segment begins with `{` but doesn't end with `}`.
306    /// * The segment ends with `}` but doesn't begin with `{`.
307    /// * The segment begins with `:`, which matches the old syntax for endpoint path segment
308    ///   variables.
309    fn extract_endpoint_path_segment_variable(segment: &str) -> Option<&str> {
310        if segment.starts_with(':') {
311            panic!(
312                "endpoint paths syntax has changed and segment variables must be wrapped by `{{}}`"
313            );
314        }
315
316        if let Some(var) = segment.strip_prefix('{').map(|s| {
317            s.strip_suffix('}')
318                .expect("endpoint path segment variable braces mismatch: missing ending `}`")
319        }) {
320            return Some(var);
321        }
322
323        if segment.ends_with('}') {
324            panic!("endpoint path segment variable braces mismatch: missing starting `{{`");
325        }
326
327        None
328    }
329}
330
331/// The complete history of this endpoint as far as Ruma knows, together with all variants on
332/// versions stable and unstable.
333///
334/// The amount and positioning of path variables are the same over all path variants.
335#[derive(Clone, Debug, PartialEq, Eq)]
336#[allow(clippy::exhaustive_structs)]
337pub struct VersionHistory {
338    /// A list of unstable paths over this endpoint's history, mapped to optional unstable
339    /// features.
340    ///
341    /// For endpoint querying purposes, the last item will be used as a fallback.
342    unstable_paths: &'static [(Option<&'static str>, &'static str)],
343
344    /// A list of stable paths, mapped to selectors.
345    ///
346    /// Sorted (ascending) by Matrix version.
347    stable_paths: &'static [(StablePathSelector, &'static str)],
348
349    /// The Matrix version that deprecated this endpoint.
350    ///
351    /// Deprecation often precedes one Matrix version before removal.
352    ///
353    /// This will make [`try_into_http_request`](super::OutgoingRequest::try_into_http_request)
354    /// emit a warning, see the corresponding documentation for more information.
355    deprecated: Option<MatrixVersion>,
356
357    /// The Matrix version that removed this endpoint.
358    ///
359    /// This will make [`try_into_http_request`](super::OutgoingRequest::try_into_http_request)
360    /// emit an error, see the corresponding documentation for more information.
361    removed: Option<MatrixVersion>,
362}
363
364impl VersionHistory {
365    /// Constructs an instance of [`VersionHistory`], erroring on compilation if it does not pass
366    /// invariants.
367    ///
368    /// Specifically, this checks the following invariants:
369    ///
370    /// * Path arguments are equal (in order, amount, and argument name) in all path strings
371    /// * In `stable_paths`:
372    ///   * Matrix versions are in ascending order
373    ///   * No matrix version is referenced twice
374    /// * `deprecated`'s version comes after the latest version mentioned in `stable_paths`, except
375    ///   for version 1.0, and only if any stable path is defined
376    /// * `removed` comes after `deprecated`, or after the latest referenced `stable_paths`, like
377    ///   `deprecated`
378    ///
379    /// ## Arguments
380    ///
381    /// * `unstable_paths` - List of unstable paths for the endpoint, mapped to optional unstable
382    ///   features.
383    /// * `stable_paths` - List of stable paths for the endpoint, mapped to selectors.
384    /// * `deprecated` - The Matrix version that deprecated the endpoint, if any.
385    /// * `removed` - The Matrix version that removed the endpoint, if any.
386    pub const fn new(
387        unstable_paths: &'static [(Option<&'static str>, &'static str)],
388        stable_paths: &'static [(StablePathSelector, &'static str)],
389        deprecated: Option<MatrixVersion>,
390        removed: Option<MatrixVersion>,
391    ) -> Self {
392        use konst::{iter, slice, string};
393
394        const fn check_path_is_valid(path: &'static str) {
395            iter::for_each!(path_b in slice::iter(path.as_bytes()) => {
396                match *path_b {
397                    0x21..=0x7E => {},
398                    _ => panic!("path contains invalid (non-ascii or whitespace) characters")
399                }
400            });
401        }
402
403        const fn check_path_args_equal(first: &'static str, second: &'static str) {
404            let mut second_iter = string::split(second, "/").next();
405
406            iter::for_each!(first_s in string::split(first, "/") => {
407                if let Some(first_arg) = string::strip_prefix(first_s, ":") {
408                    let second_next_arg: Option<&'static str> = loop {
409                        let Some((second_s, second_n_iter)) = second_iter else {
410                            break None;
411                        };
412
413                        let maybe_second_arg = string::strip_prefix(second_s, ":");
414
415                        second_iter = second_n_iter.next();
416
417                        if let Some(second_arg) = maybe_second_arg {
418                            break Some(second_arg);
419                        }
420                    };
421
422                    if let Some(second_next_arg) = second_next_arg {
423                        if !string::eq_str(second_next_arg, first_arg) {
424                            panic!("Path Arguments do not match");
425                        }
426                    } else {
427                        panic!("Amount of Path Arguments do not match");
428                    }
429                }
430            });
431
432            // If second iterator still has some values, empty first.
433            while let Some((second_s, second_n_iter)) = second_iter {
434                if string::starts_with(second_s, ":") {
435                    panic!("Amount of Path Arguments do not match");
436                }
437                second_iter = second_n_iter.next();
438            }
439        }
440
441        // The path we're going to use to compare all other paths with
442        let ref_path: &str = if let Some((_, s)) = unstable_paths.first() {
443            s
444        } else if let Some((_, s)) = stable_paths.first() {
445            s
446        } else {
447            panic!("No paths supplied")
448        };
449
450        iter::for_each!(unstable_path in slice::iter(unstable_paths) => {
451            check_path_is_valid(unstable_path.1);
452            check_path_args_equal(ref_path, unstable_path.1);
453        });
454
455        let mut prev_seen_version: Option<MatrixVersion> = None;
456
457        iter::for_each!(version_path in slice::iter(stable_paths) => {
458            check_path_is_valid(version_path.1);
459            check_path_args_equal(ref_path, version_path.1);
460
461            if let Some(current_version) = version_path.0.version() {
462                if let Some(prev_seen_version) = prev_seen_version {
463                    let cmp_result = current_version.const_ord(&prev_seen_version);
464
465                    if cmp_result.is_eq() {
466                        // Found a duplicate, current == previous
467                        panic!("Duplicate matrix version in stable_paths")
468                    } else if cmp_result.is_lt() {
469                        // Found an older version, current < previous
470                        panic!("No ascending order in stable_paths")
471                    }
472                }
473
474                prev_seen_version = Some(current_version);
475            }
476        });
477
478        if let Some(deprecated) = deprecated {
479            if let Some(prev_seen_version) = prev_seen_version {
480                let ord_result = prev_seen_version.const_ord(&deprecated);
481                if !deprecated.is_legacy() && ord_result.is_eq() {
482                    // prev_seen_version == deprecated, except for 1.0.
483                    // It is possible that an endpoint was both made stable and deprecated in the
484                    // legacy versions.
485                    panic!("deprecated version is equal to latest stable path version")
486                } else if ord_result.is_gt() {
487                    // prev_seen_version > deprecated
488                    panic!("deprecated version is older than latest stable path version")
489                }
490            } else {
491                panic!("Defined deprecated version while no stable path exists")
492            }
493        }
494
495        if let Some(removed) = removed {
496            if let Some(deprecated) = deprecated {
497                let ord_result = deprecated.const_ord(&removed);
498                if ord_result.is_eq() {
499                    // deprecated == removed
500                    panic!("removed version is equal to deprecated version")
501                } else if ord_result.is_gt() {
502                    // deprecated > removed
503                    panic!("removed version is older than deprecated version")
504                }
505            } else {
506                panic!("Defined removed version while no deprecated version exists")
507            }
508        }
509
510        VersionHistory { unstable_paths, stable_paths, deprecated, removed }
511    }
512
513    /// Whether the homeserver advertises support for a path in this [`VersionHistory`].
514    ///
515    /// Returns `true` if any version or feature in the given [`SupportedVersions`] matches a path
516    /// in this history, unless the endpoint was removed.
517    ///
518    /// Note that this is likely to return false negatives, since some endpoints don't specify a
519    /// stable or unstable feature, and homeservers should not advertise support for a Matrix
520    /// version unless they support all of its features.
521    pub fn is_supported(&self, considering: &SupportedVersions) -> bool {
522        match self.versioning_decision_for(&considering.versions) {
523            VersioningDecision::Removed => false,
524            VersioningDecision::Version { .. } => true,
525            VersioningDecision::Feature => self.feature_path(&considering.features).is_some(),
526        }
527    }
528
529    /// Picks the right path (or an error) according to the supported versions and features of a
530    /// homeserver.
531    fn select_path(&self, considering: &SupportedVersions) -> Result<&'static str, IntoHttpError> {
532        match self.versioning_decision_for(&considering.versions) {
533            VersioningDecision::Removed => Err(IntoHttpError::EndpointRemoved(
534                self.removed.expect("VersioningDecision::Removed implies metadata.removed"),
535            )),
536            VersioningDecision::Version { any_deprecated, all_deprecated, any_removed } => {
537                if any_removed {
538                    if all_deprecated {
539                        warn!(
540                            "endpoint is removed in some (and deprecated in ALL) \
541                             of the following versions: {:?}",
542                            considering.versions
543                        );
544                    } else if any_deprecated {
545                        warn!(
546                            "endpoint is removed (and deprecated) in some of the \
547                             following versions: {:?}",
548                            considering.versions
549                        );
550                    } else {
551                        unreachable!("any_removed implies *_deprecated");
552                    }
553                } else if all_deprecated {
554                    warn!(
555                        "endpoint is deprecated in ALL of the following versions: {:?}",
556                        considering.versions
557                    );
558                } else if any_deprecated {
559                    warn!(
560                        "endpoint is deprecated in some of the following versions: {:?}",
561                        considering.versions
562                    );
563                }
564
565                Ok(self
566                    .version_path(&considering.versions)
567                    .expect("VersioningDecision::Version implies that a version path exists"))
568            }
569            VersioningDecision::Feature => self
570                .feature_path(&considering.features)
571                .or_else(|| self.unstable())
572                .ok_or(IntoHttpError::NoUnstablePath),
573        }
574    }
575
576    /// Decide which kind of endpoint to use given the supported versions of a homeserver.
577    ///
578    /// Returns:
579    ///
580    /// - `Removed` if the endpoint is removed in all supported versions.
581    /// - `Version` if the endpoint is stable or deprecated in at least one supported version.
582    /// - `Feature` in all other cases, to look if a feature path is supported, or use the last
583    ///   unstable path as a fallback.
584    ///
585    /// If resulting [`VersioningDecision`] is `Stable`, it will also detail if any version denoted
586    /// deprecation or removal.
587    pub fn versioning_decision_for(
588        &self,
589        versions: &BTreeSet<MatrixVersion>,
590    ) -> VersioningDecision {
591        let is_superset_any =
592            |version: MatrixVersion| versions.iter().any(|v| v.is_superset_of(version));
593        let is_superset_all =
594            |version: MatrixVersion| versions.iter().all(|v| v.is_superset_of(version));
595
596        // Check if all versions removed this endpoint.
597        if self.removed.is_some_and(is_superset_all) {
598            return VersioningDecision::Removed;
599        }
600
601        // Check if *any* version marks this endpoint as stable.
602        if self.added_in().is_some_and(is_superset_any) {
603            let all_deprecated = self.deprecated.is_some_and(is_superset_all);
604
605            return VersioningDecision::Version {
606                any_deprecated: all_deprecated || self.deprecated.is_some_and(is_superset_any),
607                all_deprecated,
608                any_removed: self.removed.is_some_and(is_superset_any),
609            };
610        }
611
612        VersioningDecision::Feature
613    }
614
615    /// Returns the *first* version this endpoint was added in.
616    ///
617    /// Is `None` when this endpoint is unstable/unreleased.
618    pub fn added_in(&self) -> Option<MatrixVersion> {
619        self.stable_paths.iter().find_map(|(v, _)| v.version())
620    }
621
622    /// Returns the Matrix version that deprecated this endpoint, if any.
623    pub fn deprecated_in(&self) -> Option<MatrixVersion> {
624        self.deprecated
625    }
626
627    /// Returns the Matrix version that removed this endpoint, if any.
628    pub fn removed_in(&self) -> Option<MatrixVersion> {
629        self.removed
630    }
631
632    /// Picks the last unstable path, if it exists.
633    pub fn unstable(&self) -> Option<&'static str> {
634        self.unstable_paths.last().map(|(_, path)| *path)
635    }
636
637    /// Returns all path variants in canon form, for use in server routers.
638    pub fn all_paths(&self) -> impl Iterator<Item = &'static str> {
639        self.unstable_paths().map(|(_, path)| path).chain(self.stable_paths().map(|(_, path)| path))
640    }
641
642    /// Returns all unstable path variants in canon form, with optional corresponding feature.
643    pub fn unstable_paths(&self) -> impl Iterator<Item = (Option<&'static str>, &'static str)> {
644        self.unstable_paths.iter().copied()
645    }
646
647    /// Returns all version path variants in canon form, with corresponding selector.
648    pub fn stable_paths(&self) -> impl Iterator<Item = (StablePathSelector, &'static str)> {
649        self.stable_paths.iter().copied()
650    }
651
652    /// The path that should be used to query the endpoint, given a set of supported versions.
653    ///
654    /// Picks the latest path that the versions accept.
655    ///
656    /// Returns an endpoint in the following format;
657    /// - `/_matrix/client/versions`
658    /// - `/_matrix/client/hello/{world}` (`{world}` is a path replacement parameter)
659    ///
660    /// Note: This doesn't handle endpoint removals, check with
661    /// [`versioning_decision_for`](VersionHistory::versioning_decision_for) to see if this endpoint
662    /// is still available.
663    pub fn version_path(&self, versions: &BTreeSet<MatrixVersion>) -> Option<&'static str> {
664        let version_paths = self
665            .stable_paths
666            .iter()
667            .filter_map(|(selector, path)| selector.version().map(|version| (version, path)));
668
669        // Reverse the iterator, to check the "latest" version first.
670        for (ver, path) in version_paths.rev() {
671            // Check if any of the versions are equal or greater than the version the path needs.
672            if versions.iter().any(|v| v.is_superset_of(ver)) {
673                return Some(path);
674            }
675        }
676
677        None
678    }
679
680    /// The path that should be used to query the endpoint, given a list of supported features.
681    pub fn feature_path(&self, supported_features: &BTreeSet<FeatureFlag>) -> Option<&'static str> {
682        let unstable_feature_paths = self
683            .unstable_paths
684            .iter()
685            .filter_map(|(feature, path)| feature.map(|feature| (feature, path)));
686        let stable_feature_paths = self
687            .stable_paths
688            .iter()
689            .filter_map(|(selector, path)| selector.feature().map(|feature| (feature, path)));
690
691        // Reverse the iterator, to check the "latest" features first.
692        for (feature, path) in unstable_feature_paths.chain(stable_feature_paths).rev() {
693            // Return the path of the first supported feature.
694            if supported_features.iter().any(|supported| supported.as_str() == feature) {
695                return Some(path);
696            }
697        }
698
699        None
700    }
701}
702
703/// A versioning "decision" derived from a set of Matrix versions.
704#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
705#[allow(clippy::exhaustive_enums)]
706pub enum VersioningDecision {
707    /// A feature path should be used, or a fallback.
708    Feature,
709
710    /// A path with a Matrix version should be used.
711    Version {
712        /// If any version denoted deprecation.
713        any_deprecated: bool,
714
715        /// If *all* versions denoted deprecation.
716        all_deprecated: bool,
717
718        /// If any version denoted removal.
719        any_removed: bool,
720    },
721
722    /// This endpoint was removed in all versions, it should not be used.
723    Removed,
724}
725
726/// The Matrix versions Ruma currently understands to exist.
727///
728/// Matrix, since fall 2021, has a quarterly release schedule, using a global `vX.Y` versioning
729/// scheme. Usually `Y` is bumped for new backwards compatible changes, but `X` can be bumped
730/// instead when a large number of `Y` changes feel deserving of a major version increase.
731///
732/// Every new version denotes stable support for endpoints in a *relatively* backwards-compatible
733/// manner.
734///
735/// Matrix has a deprecation policy, read more about it here: <https://spec.matrix.org/latest/#deprecation-policy>.
736///
737/// Ruma keeps track of when endpoints are added, deprecated, and removed. It'll automatically
738/// select the right endpoint stability variation to use depending on which Matrix versions you
739/// pass to [`try_into_http_request`](super::OutgoingRequest::try_into_http_request), see its
740/// respective documentation for more information.
741///
742/// The `PartialOrd` and `Ord` implementations of this type sort the variants by release date. A
743/// newer release is greater than an older release.
744///
745/// `MatrixVersion::is_superset_of()` is used to keep track of compatibility between versions.
746#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
747#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
748pub enum MatrixVersion {
749    /// Matrix 1.0 was a release prior to the global versioning system and does not correspond to a
750    /// version of the Matrix specification.
751    ///
752    /// It matches the following per-API versions:
753    ///
754    /// * Client-Server API: r0.5.0 to r0.6.1
755    /// * Identity Service API: r0.2.0 to r0.3.0
756    ///
757    /// The other APIs are not supported because they do not have a `GET /versions` endpoint.
758    ///
759    /// See <https://spec.matrix.org/latest/#legacy-versioning>.
760    V1_0,
761
762    /// Version 1.1 of the Matrix specification, released in Q4 2021.
763    ///
764    /// See <https://spec.matrix.org/v1.1/>.
765    V1_1,
766
767    /// Version 1.2 of the Matrix specification, released in Q1 2022.
768    ///
769    /// See <https://spec.matrix.org/v1.2/>.
770    V1_2,
771
772    /// Version 1.3 of the Matrix specification, released in Q2 2022.
773    ///
774    /// See <https://spec.matrix.org/v1.3/>.
775    V1_3,
776
777    /// Version 1.4 of the Matrix specification, released in Q3 2022.
778    ///
779    /// See <https://spec.matrix.org/v1.4/>.
780    V1_4,
781
782    /// Version 1.5 of the Matrix specification, released in Q4 2022.
783    ///
784    /// See <https://spec.matrix.org/v1.5/>.
785    V1_5,
786
787    /// Version 1.6 of the Matrix specification, released in Q1 2023.
788    ///
789    /// See <https://spec.matrix.org/v1.6/>.
790    V1_6,
791
792    /// Version 1.7 of the Matrix specification, released in Q2 2023.
793    ///
794    /// See <https://spec.matrix.org/v1.7/>.
795    V1_7,
796
797    /// Version 1.8 of the Matrix specification, released in Q3 2023.
798    ///
799    /// See <https://spec.matrix.org/v1.8/>.
800    V1_8,
801
802    /// Version 1.9 of the Matrix specification, released in Q4 2023.
803    ///
804    /// See <https://spec.matrix.org/v1.9/>.
805    V1_9,
806
807    /// Version 1.10 of the Matrix specification, released in Q1 2024.
808    ///
809    /// See <https://spec.matrix.org/v1.10/>.
810    V1_10,
811
812    /// Version 1.11 of the Matrix specification, released in Q2 2024.
813    ///
814    /// See <https://spec.matrix.org/v1.11/>.
815    V1_11,
816
817    /// Version 1.12 of the Matrix specification, released in Q3 2024.
818    ///
819    /// See <https://spec.matrix.org/v1.12/>.
820    V1_12,
821
822    /// Version 1.13 of the Matrix specification, released in Q4 2024.
823    ///
824    /// See <https://spec.matrix.org/v1.13/>.
825    V1_13,
826
827    /// Version 1.14 of the Matrix specification, released in Q1 2025.
828    ///
829    /// See <https://spec.matrix.org/v1.14/>.
830    V1_14,
831
832    /// Version 1.15 of the Matrix specification, released in Q2 2025.
833    ///
834    /// See <https://spec.matrix.org/v1.15/>.
835    V1_15,
836
837    /// Version 1.16 of the Matrix specification, released in Q3 2025.
838    ///
839    /// See <https://spec.matrix.org/v1.16/>.
840    V1_16,
841}
842
843impl TryFrom<&str> for MatrixVersion {
844    type Error = UnknownVersionError;
845
846    fn try_from(value: &str) -> Result<MatrixVersion, Self::Error> {
847        use MatrixVersion::*;
848
849        Ok(match value {
850            // Identity service API versions between Matrix 1.0 and 1.1.
851            // They might match older client-server API versions but that should not be a problem in practice.
852            "r0.2.0" | "r0.2.1" | "r0.3.0" |
853            // Client-server API versions between Matrix 1.0 and 1.1.
854            "r0.5.0" | "r0.6.0" | "r0.6.1" => V1_0,
855            "v1.1" => V1_1,
856            "v1.2" => V1_2,
857            "v1.3" => V1_3,
858            "v1.4" => V1_4,
859            "v1.5" => V1_5,
860            "v1.6" => V1_6,
861            "v1.7" => V1_7,
862            "v1.8" => V1_8,
863            "v1.9" => V1_9,
864            "v1.10" => V1_10,
865            "v1.11" => V1_11,
866            "v1.12" => V1_12,
867            "v1.13" => V1_13,
868            "v1.14" => V1_14,
869            "v1.15" => V1_15,
870            "v1.16" => V1_16,
871            _ => return Err(UnknownVersionError),
872        })
873    }
874}
875
876impl FromStr for MatrixVersion {
877    type Err = UnknownVersionError;
878
879    fn from_str(s: &str) -> Result<Self, Self::Err> {
880        Self::try_from(s)
881    }
882}
883
884impl MatrixVersion {
885    /// Checks whether a version is compatible with another.
886    ///
887    /// Currently, all versions of Matrix are considered backwards compatible with all the previous
888    /// versions, so this is equivalent to `self >= other`. This behaviour may change in the future,
889    /// if a new release is considered to be breaking compatibility with the previous ones.
890    ///
891    /// > âš  Matrix has a deprecation policy, and Matrix versioning is not as straightforward as this
892    /// > function makes it out to be. This function only exists to prune breaking changes between
893    /// > versions, and versions too new for `self`.
894    pub fn is_superset_of(self, other: Self) -> bool {
895        self >= other
896    }
897
898    /// Get a string representation of this Matrix version.
899    ///
900    /// This is the string that can be found in the response to one of the `GET /versions`
901    /// endpoints. Parsing this string will give the same variant.
902    ///
903    /// Returns `None` for [`MatrixVersion::V1_0`] because it can match several per-API versions.
904    pub const fn as_str(self) -> Option<&'static str> {
905        let string = match self {
906            MatrixVersion::V1_0 => return None,
907            MatrixVersion::V1_1 => "v1.1",
908            MatrixVersion::V1_2 => "v1.2",
909            MatrixVersion::V1_3 => "v1.3",
910            MatrixVersion::V1_4 => "v1.4",
911            MatrixVersion::V1_5 => "v1.5",
912            MatrixVersion::V1_6 => "v1.6",
913            MatrixVersion::V1_7 => "v1.7",
914            MatrixVersion::V1_8 => "v1.8",
915            MatrixVersion::V1_9 => "v1.9",
916            MatrixVersion::V1_10 => "v1.10",
917            MatrixVersion::V1_11 => "v1.11",
918            MatrixVersion::V1_12 => "v1.12",
919            MatrixVersion::V1_13 => "v1.13",
920            MatrixVersion::V1_14 => "v1.14",
921            MatrixVersion::V1_15 => "v1.15",
922            MatrixVersion::V1_16 => "v1.16",
923        };
924
925        Some(string)
926    }
927
928    /// Decompose the Matrix version into its major and minor number.
929    const fn into_parts(self) -> (u8, u8) {
930        match self {
931            MatrixVersion::V1_0 => (1, 0),
932            MatrixVersion::V1_1 => (1, 1),
933            MatrixVersion::V1_2 => (1, 2),
934            MatrixVersion::V1_3 => (1, 3),
935            MatrixVersion::V1_4 => (1, 4),
936            MatrixVersion::V1_5 => (1, 5),
937            MatrixVersion::V1_6 => (1, 6),
938            MatrixVersion::V1_7 => (1, 7),
939            MatrixVersion::V1_8 => (1, 8),
940            MatrixVersion::V1_9 => (1, 9),
941            MatrixVersion::V1_10 => (1, 10),
942            MatrixVersion::V1_11 => (1, 11),
943            MatrixVersion::V1_12 => (1, 12),
944            MatrixVersion::V1_13 => (1, 13),
945            MatrixVersion::V1_14 => (1, 14),
946            MatrixVersion::V1_15 => (1, 15),
947            MatrixVersion::V1_16 => (1, 16),
948        }
949    }
950
951    /// Try to turn a pair of (major, minor) version components back into a `MatrixVersion`.
952    const fn from_parts(major: u8, minor: u8) -> Result<Self, UnknownVersionError> {
953        match (major, minor) {
954            (1, 0) => Ok(MatrixVersion::V1_0),
955            (1, 1) => Ok(MatrixVersion::V1_1),
956            (1, 2) => Ok(MatrixVersion::V1_2),
957            (1, 3) => Ok(MatrixVersion::V1_3),
958            (1, 4) => Ok(MatrixVersion::V1_4),
959            (1, 5) => Ok(MatrixVersion::V1_5),
960            (1, 6) => Ok(MatrixVersion::V1_6),
961            (1, 7) => Ok(MatrixVersion::V1_7),
962            (1, 8) => Ok(MatrixVersion::V1_8),
963            (1, 9) => Ok(MatrixVersion::V1_9),
964            (1, 10) => Ok(MatrixVersion::V1_10),
965            (1, 11) => Ok(MatrixVersion::V1_11),
966            (1, 12) => Ok(MatrixVersion::V1_12),
967            (1, 13) => Ok(MatrixVersion::V1_13),
968            (1, 14) => Ok(MatrixVersion::V1_14),
969            (1, 15) => Ok(MatrixVersion::V1_15),
970            (1, 16) => Ok(MatrixVersion::V1_16),
971            _ => Err(UnknownVersionError),
972        }
973    }
974
975    /// Constructor for use by the `metadata!` macro.
976    ///
977    /// Accepts string literals and parses them.
978    #[doc(hidden)]
979    pub const fn from_lit(lit: &'static str) -> Self {
980        use konst::{option, primitive::parse_u8, result, string};
981
982        let major: u8;
983        let minor: u8;
984
985        let mut lit_iter = string::split(lit, ".").next();
986
987        {
988            let (checked_first, checked_split) = option::unwrap!(lit_iter); // First iteration always succeeds
989
990            major = result::unwrap_or_else!(parse_u8(checked_first), |_| panic!(
991                "major version is not a valid number"
992            ));
993
994            lit_iter = checked_split.next();
995        }
996
997        match lit_iter {
998            Some((checked_second, checked_split)) => {
999                minor = result::unwrap_or_else!(parse_u8(checked_second), |_| panic!(
1000                    "minor version is not a valid number"
1001                ));
1002
1003                lit_iter = checked_split.next();
1004            }
1005            None => panic!("could not find dot to denote second number"),
1006        }
1007
1008        if lit_iter.is_some() {
1009            panic!("version literal contains more than one dot")
1010        }
1011
1012        result::unwrap_or_else!(Self::from_parts(major, minor), |_| panic!(
1013            "not a valid version literal"
1014        ))
1015    }
1016
1017    // Internal function to do ordering in const-fn contexts
1018    const fn const_ord(&self, other: &Self) -> Ordering {
1019        let self_parts = self.into_parts();
1020        let other_parts = other.into_parts();
1021
1022        use konst::primitive::cmp::cmp_u8;
1023
1024        let major_ord = cmp_u8(self_parts.0, other_parts.0);
1025        if major_ord.is_ne() {
1026            major_ord
1027        } else {
1028            cmp_u8(self_parts.1, other_parts.1)
1029        }
1030    }
1031
1032    // Internal function to check if this version is the legacy (v1.0) version in const-fn contexts
1033    const fn is_legacy(&self) -> bool {
1034        let self_parts = self.into_parts();
1035
1036        use konst::primitive::cmp::cmp_u8;
1037
1038        cmp_u8(self_parts.0, 1).is_eq() && cmp_u8(self_parts.1, 0).is_eq()
1039    }
1040
1041    /// Get the default [`RoomVersionId`] for this `MatrixVersion`.
1042    pub fn default_room_version(&self) -> RoomVersionId {
1043        match self {
1044            // <https://spec.matrix.org/historical/index.html#complete-list-of-room-versions>
1045            MatrixVersion::V1_0
1046            // <https://spec.matrix.org/v1.1/rooms/#complete-list-of-room-versions>
1047            | MatrixVersion::V1_1
1048            // <https://spec.matrix.org/v1.2/rooms/#complete-list-of-room-versions>
1049            | MatrixVersion::V1_2 => RoomVersionId::V6,
1050            // <https://spec.matrix.org/v1.3/rooms/#complete-list-of-room-versions>
1051            MatrixVersion::V1_3
1052            // <https://spec.matrix.org/v1.4/rooms/#complete-list-of-room-versions>
1053            | MatrixVersion::V1_4
1054            // <https://spec.matrix.org/v1.5/rooms/#complete-list-of-room-versions>
1055            | MatrixVersion::V1_5 => RoomVersionId::V9,
1056            // <https://spec.matrix.org/v1.6/rooms/#complete-list-of-room-versions>
1057            MatrixVersion::V1_6
1058            // <https://spec.matrix.org/v1.7/rooms/#complete-list-of-room-versions>
1059            | MatrixVersion::V1_7
1060            // <https://spec.matrix.org/v1.8/rooms/#complete-list-of-room-versions>
1061            | MatrixVersion::V1_8
1062            // <https://spec.matrix.org/v1.9/rooms/#complete-list-of-room-versions>
1063            | MatrixVersion::V1_9
1064            // <https://spec.matrix.org/v1.10/rooms/#complete-list-of-room-versions>
1065            | MatrixVersion::V1_10
1066            // <https://spec.matrix.org/v1.11/rooms/#complete-list-of-room-versions>
1067            | MatrixVersion::V1_11
1068            // <https://spec.matrix.org/v1.12/rooms/#complete-list-of-room-versions>
1069            | MatrixVersion::V1_12
1070            // <https://spec.matrix.org/v1.13/rooms/#complete-list-of-room-versions>
1071            | MatrixVersion::V1_13 => RoomVersionId::V10,
1072            // <https://spec.matrix.org/v1.14/rooms/#complete-list-of-room-versions>
1073            | MatrixVersion::V1_14
1074            // <https://spec.matrix.org/v1.15/rooms/#complete-list-of-room-versions>
1075            | MatrixVersion::V1_15 => RoomVersionId::V11,
1076            // <https://spec.matrix.org/v1.16/rooms/#complete-list-of-room-versions>
1077            MatrixVersion::V1_16 => RoomVersionId::V12,
1078        }
1079    }
1080}
1081
1082/// A selector for a stable path of an endpoint.
1083#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1084#[allow(clippy::exhaustive_enums)]
1085pub enum StablePathSelector {
1086    /// The path is available with the given stable feature.
1087    Feature(&'static str),
1088
1089    /// The path was added in the given Matrix version.
1090    Version(MatrixVersion),
1091
1092    /// The path is available via a stable feature and was added in a Matrix version.
1093    FeatureAndVersion {
1094        /// The stable feature that adds support for the path.
1095        feature: &'static str,
1096        /// The Matrix version when the path was added.
1097        version: MatrixVersion,
1098    },
1099}
1100
1101impl StablePathSelector {
1102    /// The feature that adds support for the path, if any.
1103    pub const fn feature(self) -> Option<&'static str> {
1104        match self {
1105            Self::Feature(feature) | Self::FeatureAndVersion { feature, .. } => Some(feature),
1106            _ => None,
1107        }
1108    }
1109
1110    /// The Matrix version when the path was added, if any.
1111    pub const fn version(self) -> Option<MatrixVersion> {
1112        match self {
1113            Self::Version(version) | Self::FeatureAndVersion { version, .. } => Some(version),
1114            _ => None,
1115        }
1116    }
1117}
1118
1119impl From<MatrixVersion> for StablePathSelector {
1120    fn from(value: MatrixVersion) -> Self {
1121        Self::Version(value)
1122    }
1123}
1124
1125/// The list of Matrix versions and features supported by a homeserver.
1126#[derive(Debug, Clone)]
1127#[allow(clippy::exhaustive_structs)]
1128pub struct SupportedVersions {
1129    /// The Matrix versions that are supported by the homeserver.
1130    ///
1131    /// This set contains only known versions.
1132    pub versions: BTreeSet<MatrixVersion>,
1133
1134    /// The features that are supported by the homeserver.
1135    ///
1136    /// This matches the `unstable_features` field of the `/versions` endpoint, without the boolean
1137    /// value.
1138    pub features: BTreeSet<FeatureFlag>,
1139}
1140
1141impl SupportedVersions {
1142    /// Construct a `SupportedVersions` from the parts of a `/versions` response.
1143    ///
1144    /// Matrix versions that can't be parsed to a `MatrixVersion`, and features with the boolean
1145    /// value set to `false` are discarded.
1146    pub fn from_parts(versions: &[String], unstable_features: &BTreeMap<String, bool>) -> Self {
1147        Self {
1148            versions: versions.iter().flat_map(|s| s.parse::<MatrixVersion>()).collect(),
1149            features: unstable_features
1150                .iter()
1151                .filter(|(_, enabled)| **enabled)
1152                .map(|(feature, _)| feature.as_str().into())
1153                .collect(),
1154        }
1155    }
1156}
1157
1158/// The Matrix features supported by Ruma.
1159///
1160/// Features that are not behind a cargo feature are features that are part of the Matrix
1161/// specification and that Ruma still supports, like the unstable version of an endpoint or a stable
1162/// feature. Features behind a cargo feature are only supported when this feature is enabled.
1163#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
1164#[derive(Clone, StringEnum, PartialEqAsRefStr, Eq, Hash, PartialOrdAsRefStr, OrdAsRefStr)]
1165#[non_exhaustive]
1166pub enum FeatureFlag {
1167    /// `fi.mau.msc2246` ([MSC])
1168    ///
1169    /// Asynchronous media uploads.
1170    ///
1171    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/2246
1172    #[ruma_enum(rename = "fi.mau.msc2246")]
1173    Msc2246,
1174
1175    /// `org.matrix.msc2432` ([MSC])
1176    ///
1177    /// Updated semantics for publishing room aliases.
1178    ///
1179    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/2432
1180    #[ruma_enum(rename = "org.matrix.msc2432")]
1181    Msc2432,
1182
1183    /// `fi.mau.msc2659` ([MSC])
1184    ///
1185    /// Application service ping endpoint.
1186    ///
1187    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/2659
1188    #[ruma_enum(rename = "fi.mau.msc2659")]
1189    Msc2659,
1190
1191    /// `fi.mau.msc2659` ([MSC])
1192    ///
1193    /// Stable version of the application service ping endpoint.
1194    ///
1195    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/2659
1196    #[ruma_enum(rename = "fi.mau.msc2659.stable")]
1197    Msc2659Stable,
1198
1199    /// `uk.half-shot.msc2666.query_mutual_rooms` ([MSC])
1200    ///
1201    /// Get rooms in common with another user.
1202    ///
1203    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/2666
1204    #[cfg(feature = "unstable-msc2666")]
1205    #[ruma_enum(rename = "uk.half-shot.msc2666.query_mutual_rooms")]
1206    Msc2666,
1207
1208    /// `org.matrix.msc3030` ([MSC])
1209    ///
1210    /// Jump to date API endpoint.
1211    ///
1212    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/3030
1213    #[ruma_enum(rename = "org.matrix.msc3030")]
1214    Msc3030,
1215
1216    /// `org.matrix.msc3882` ([MSC])
1217    ///
1218    /// Allow an existing session to sign in a new session.
1219    ///
1220    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/3882
1221    #[ruma_enum(rename = "org.matrix.msc3882")]
1222    Msc3882,
1223
1224    /// `org.matrix.msc3916` ([MSC])
1225    ///
1226    /// Authentication for media.
1227    ///
1228    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/3916
1229    #[ruma_enum(rename = "org.matrix.msc3916")]
1230    Msc3916,
1231
1232    /// `org.matrix.msc3916.stable` ([MSC])
1233    ///
1234    /// Stable version of authentication for media.
1235    ///
1236    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/3916
1237    #[ruma_enum(rename = "org.matrix.msc3916.stable")]
1238    Msc3916Stable,
1239
1240    /// `org.matrix.msc4108` ([MSC])
1241    ///
1242    /// Mechanism to allow OIDC sign in and E2EE set up via QR code.
1243    ///
1244    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
1245    #[cfg(feature = "unstable-msc4108")]
1246    #[ruma_enum(rename = "org.matrix.msc4108")]
1247    Msc4108,
1248
1249    /// `org.matrix.msc4140` ([MSC])
1250    ///
1251    /// Delayed events.
1252    ///
1253    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4140
1254    #[cfg(feature = "unstable-msc4140")]
1255    #[ruma_enum(rename = "org.matrix.msc4140")]
1256    Msc4140,
1257
1258    /// `org.matrix.simplified_msc3575` ([MSC])
1259    ///
1260    /// Simplified Sliding Sync.
1261    ///
1262    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4186
1263    #[cfg(feature = "unstable-msc4186")]
1264    #[ruma_enum(rename = "org.matrix.simplified_msc3575")]
1265    Msc4186,
1266
1267    #[doc(hidden)]
1268    _Custom(PrivOwnedStr),
1269}
1270
1271#[cfg(test)]
1272mod tests {
1273    use std::collections::{BTreeMap, BTreeSet};
1274
1275    use assert_matches2::assert_matches;
1276    use http::Method;
1277
1278    use super::{
1279        AuthScheme,
1280        MatrixVersion::{self, V1_0, V1_1, V1_2, V1_3},
1281        Metadata, StablePathSelector, SupportedVersions, VersionHistory,
1282    };
1283    use crate::api::error::IntoHttpError;
1284
1285    fn stable_only_metadata(
1286        stable_paths: &'static [(StablePathSelector, &'static str)],
1287    ) -> Metadata {
1288        Metadata {
1289            method: Method::GET,
1290            rate_limited: false,
1291            authentication: AuthScheme::None,
1292            history: VersionHistory {
1293                unstable_paths: &[],
1294                stable_paths,
1295                deprecated: None,
1296                removed: None,
1297            },
1298        }
1299    }
1300
1301    fn version_only_supported(versions: &[MatrixVersion]) -> SupportedVersions {
1302        SupportedVersions {
1303            versions: versions.iter().copied().collect(),
1304            features: BTreeSet::new(),
1305        }
1306    }
1307
1308    // TODO add test that can hook into tracing and verify the deprecation warning is emitted
1309
1310    #[test]
1311    fn make_simple_endpoint_url() {
1312        let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s")]);
1313        let url = meta
1314            .make_endpoint_url(&version_only_supported(&[V1_0]), "https://example.org", &[], "")
1315            .unwrap();
1316        assert_eq!(url, "https://example.org/s");
1317    }
1318
1319    #[test]
1320    fn make_endpoint_url_with_path_args() {
1321        let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
1322        let url = meta
1323            .make_endpoint_url(
1324                &version_only_supported(&[V1_0]),
1325                "https://example.org",
1326                &[&"123"],
1327                "",
1328            )
1329            .unwrap();
1330        assert_eq!(url, "https://example.org/s/123");
1331    }
1332
1333    #[test]
1334    fn make_endpoint_url_with_path_args_with_dash() {
1335        let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
1336        let url = meta
1337            .make_endpoint_url(
1338                &version_only_supported(&[V1_0]),
1339                "https://example.org",
1340                &[&"my-path"],
1341                "",
1342            )
1343            .unwrap();
1344        assert_eq!(url, "https://example.org/s/my-path");
1345    }
1346
1347    #[test]
1348    fn make_endpoint_url_with_path_args_with_reserved_char() {
1349        let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
1350        let url = meta
1351            .make_endpoint_url(
1352                &version_only_supported(&[V1_0]),
1353                "https://example.org",
1354                &[&"#path"],
1355                "",
1356            )
1357            .unwrap();
1358        assert_eq!(url, "https://example.org/s/%23path");
1359    }
1360
1361    #[test]
1362    fn make_endpoint_url_with_query() {
1363        let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s/")]);
1364        let url = meta
1365            .make_endpoint_url(
1366                &version_only_supported(&[V1_0]),
1367                "https://example.org",
1368                &[],
1369                "foo=bar",
1370            )
1371            .unwrap();
1372        assert_eq!(url, "https://example.org/s/?foo=bar");
1373    }
1374
1375    #[test]
1376    #[should_panic]
1377    fn make_endpoint_url_wrong_num_path_args() {
1378        let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
1379        _ = meta.make_endpoint_url(
1380            &version_only_supported(&[V1_0]),
1381            "https://example.org",
1382            &[],
1383            "",
1384        );
1385    }
1386
1387    const EMPTY: VersionHistory =
1388        VersionHistory { unstable_paths: &[], stable_paths: &[], deprecated: None, removed: None };
1389
1390    #[test]
1391    fn select_version() {
1392        let version_supported = version_only_supported(&[V1_0, V1_1]);
1393        let superset_supported = version_only_supported(&[V1_1]);
1394
1395        // With version only.
1396        let hist =
1397            VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_0), "/s")], ..EMPTY };
1398        assert_matches!(hist.select_path(&version_supported), Ok("/s"));
1399        assert!(hist.is_supported(&version_supported));
1400        assert_matches!(hist.select_path(&superset_supported), Ok("/s"));
1401        assert!(hist.is_supported(&superset_supported));
1402
1403        // With feature and version.
1404        let hist = VersionHistory {
1405            stable_paths: &[(
1406                StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_0 },
1407                "/s",
1408            )],
1409            ..EMPTY
1410        };
1411        assert_matches!(hist.select_path(&version_supported), Ok("/s"));
1412        assert!(hist.is_supported(&version_supported));
1413        assert_matches!(hist.select_path(&superset_supported), Ok("/s"));
1414        assert!(hist.is_supported(&superset_supported));
1415
1416        // Select latest stable version.
1417        let hist = VersionHistory {
1418            stable_paths: &[
1419                (StablePathSelector::Version(V1_0), "/s_v1"),
1420                (StablePathSelector::Version(V1_1), "/s_v2"),
1421            ],
1422            ..EMPTY
1423        };
1424        assert_matches!(hist.select_path(&version_supported), Ok("/s_v2"));
1425        assert!(hist.is_supported(&version_supported));
1426
1427        // With unstable feature.
1428        let unstable_supported = SupportedVersions {
1429            versions: [V1_0].into(),
1430            features: ["org.boo.unstable".into()].into(),
1431        };
1432        let hist = VersionHistory {
1433            unstable_paths: &[(Some("org.boo.unstable"), "/u")],
1434            stable_paths: &[(StablePathSelector::Version(V1_0), "/s")],
1435            ..EMPTY
1436        };
1437        assert_matches!(hist.select_path(&unstable_supported), Ok("/s"));
1438        assert!(hist.is_supported(&unstable_supported));
1439    }
1440
1441    #[test]
1442    fn select_stable_feature() {
1443        let supported = SupportedVersions {
1444            versions: [V1_1].into(),
1445            features: ["org.boo.unstable".into(), "org.boo.stable".into()].into(),
1446        };
1447
1448        // With feature only.
1449        let hist = VersionHistory {
1450            unstable_paths: &[(Some("org.boo.unstable"), "/u")],
1451            stable_paths: &[(StablePathSelector::Feature("org.boo.stable"), "/s")],
1452            ..EMPTY
1453        };
1454        assert_matches!(hist.select_path(&supported), Ok("/s"));
1455        assert!(hist.is_supported(&supported));
1456
1457        // With feature and version.
1458        let hist = VersionHistory {
1459            unstable_paths: &[(Some("org.boo.unstable"), "/u")],
1460            stable_paths: &[(
1461                StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_3 },
1462                "/s",
1463            )],
1464            ..EMPTY
1465        };
1466        assert_matches!(hist.select_path(&supported), Ok("/s"));
1467        assert!(hist.is_supported(&supported));
1468    }
1469
1470    #[test]
1471    fn select_unstable_feature() {
1472        let supported = SupportedVersions {
1473            versions: [V1_1].into(),
1474            features: ["org.boo.unstable".into()].into(),
1475        };
1476
1477        let hist = VersionHistory {
1478            unstable_paths: &[(Some("org.boo.unstable"), "/u")],
1479            stable_paths: &[(
1480                StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_3 },
1481                "/s",
1482            )],
1483            ..EMPTY
1484        };
1485        assert_matches!(hist.select_path(&supported), Ok("/u"));
1486        assert!(hist.is_supported(&supported));
1487    }
1488
1489    #[test]
1490    fn select_unstable_fallback() {
1491        let supported = version_only_supported(&[V1_0]);
1492        let hist = VersionHistory { unstable_paths: &[(None, "/u")], ..EMPTY };
1493        assert_matches!(hist.select_path(&supported), Ok("/u"));
1494        assert!(!hist.is_supported(&supported));
1495    }
1496
1497    #[test]
1498    fn select_r0() {
1499        let supported = version_only_supported(&[V1_0]);
1500        let hist =
1501            VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_0), "/r")], ..EMPTY };
1502        assert_matches!(hist.select_path(&supported), Ok("/r"));
1503        assert!(hist.is_supported(&supported));
1504    }
1505
1506    #[test]
1507    fn select_removed_err() {
1508        let supported = version_only_supported(&[V1_3]);
1509        let hist = VersionHistory {
1510            stable_paths: &[
1511                (StablePathSelector::Version(V1_0), "/r"),
1512                (StablePathSelector::Version(V1_1), "/s"),
1513            ],
1514            unstable_paths: &[(None, "/u")],
1515            deprecated: Some(V1_2),
1516            removed: Some(V1_3),
1517        };
1518        assert_matches!(hist.select_path(&supported), Err(IntoHttpError::EndpointRemoved(V1_3)));
1519        assert!(!hist.is_supported(&supported));
1520    }
1521
1522    #[test]
1523    fn partially_removed_but_stable() {
1524        let supported = version_only_supported(&[V1_2]);
1525        let hist = VersionHistory {
1526            stable_paths: &[
1527                (StablePathSelector::Version(V1_0), "/r"),
1528                (StablePathSelector::Version(V1_1), "/s"),
1529            ],
1530            unstable_paths: &[],
1531            deprecated: Some(V1_2),
1532            removed: Some(V1_3),
1533        };
1534        assert_matches!(hist.select_path(&supported), Ok("/s"));
1535        assert!(hist.is_supported(&supported));
1536    }
1537
1538    #[test]
1539    fn no_unstable() {
1540        let supported = version_only_supported(&[V1_0]);
1541        let hist =
1542            VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_1), "/s")], ..EMPTY };
1543        assert_matches!(hist.select_path(&supported), Err(IntoHttpError::NoUnstablePath));
1544        assert!(!hist.is_supported(&supported));
1545    }
1546
1547    #[test]
1548    fn version_literal() {
1549        const LIT: MatrixVersion = MatrixVersion::from_lit("1.0");
1550
1551        assert_eq!(LIT, V1_0);
1552    }
1553
1554    #[test]
1555    fn parse_as_str_sanity() {
1556        let version = MatrixVersion::try_from("r0.5.0").unwrap();
1557        assert_eq!(version, V1_0);
1558        assert_eq!(version.as_str(), None);
1559
1560        let version = MatrixVersion::try_from("v1.1").unwrap();
1561        assert_eq!(version, V1_1);
1562        assert_eq!(version.as_str(), Some("v1.1"));
1563    }
1564
1565    #[test]
1566    fn supported_versions_from_parts() {
1567        let empty_features = BTreeMap::new();
1568
1569        let none = &[];
1570        let none_supported = SupportedVersions::from_parts(none, &empty_features);
1571        assert_eq!(none_supported.versions, BTreeSet::new());
1572        assert_eq!(none_supported.features, BTreeSet::new());
1573
1574        let single_known = &["r0.6.0".to_owned()];
1575        let single_known_supported = SupportedVersions::from_parts(single_known, &empty_features);
1576        assert_eq!(single_known_supported.versions, BTreeSet::from([V1_0]));
1577        assert_eq!(single_known_supported.features, BTreeSet::new());
1578
1579        let multiple_known = &["v1.1".to_owned(), "r0.6.0".to_owned(), "r0.6.1".to_owned()];
1580        let multiple_known_supported =
1581            SupportedVersions::from_parts(multiple_known, &empty_features);
1582        assert_eq!(multiple_known_supported.versions, BTreeSet::from([V1_0, V1_1]));
1583        assert_eq!(multiple_known_supported.features, BTreeSet::new());
1584
1585        let single_unknown = &["v0.0".to_owned()];
1586        let single_unknown_supported =
1587            SupportedVersions::from_parts(single_unknown, &empty_features);
1588        assert_eq!(single_unknown_supported.versions, BTreeSet::new());
1589        assert_eq!(single_unknown_supported.features, BTreeSet::new());
1590
1591        let mut features = BTreeMap::new();
1592        features.insert("org.bar.enabled_1".to_owned(), true);
1593        features.insert("org.bar.disabled".to_owned(), false);
1594        features.insert("org.bar.enabled_2".to_owned(), true);
1595
1596        let features_supported = SupportedVersions::from_parts(single_known, &features);
1597        assert_eq!(features_supported.versions, BTreeSet::from([V1_0]));
1598        assert_eq!(
1599            features_supported.features,
1600            ["org.bar.enabled_1".into(), "org.bar.enabled_2".into()].into()
1601        );
1602    }
1603
1604    #[test]
1605    fn supported_versions_from_parts_order() {
1606        let empty_features = BTreeMap::new();
1607
1608        let sorted = &[
1609            "r0.0.1".to_owned(),
1610            "r0.5.0".to_owned(),
1611            "r0.6.0".to_owned(),
1612            "r0.6.1".to_owned(),
1613            "v1.1".to_owned(),
1614            "v1.2".to_owned(),
1615        ];
1616        let sorted_supported = SupportedVersions::from_parts(sorted, &empty_features);
1617        assert_eq!(sorted_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));
1618
1619        let sorted_reverse = &[
1620            "v1.2".to_owned(),
1621            "v1.1".to_owned(),
1622            "r0.6.1".to_owned(),
1623            "r0.6.0".to_owned(),
1624            "r0.5.0".to_owned(),
1625            "r0.0.1".to_owned(),
1626        ];
1627        let sorted_reverse_supported =
1628            SupportedVersions::from_parts(sorted_reverse, &empty_features);
1629        assert_eq!(sorted_reverse_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));
1630
1631        let random_order = &[
1632            "v1.1".to_owned(),
1633            "r0.6.1".to_owned(),
1634            "r0.5.0".to_owned(),
1635            "r0.6.0".to_owned(),
1636            "r0.0.1".to_owned(),
1637            "v1.2".to_owned(),
1638        ];
1639        let random_order_supported = SupportedVersions::from_parts(random_order, &empty_features);
1640        assert_eq!(random_order_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));
1641    }
1642
1643    #[test]
1644    #[should_panic]
1645    fn make_endpoint_url_with_path_args_old_syntax() {
1646        let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s/:x")]);
1647        let url = meta
1648            .make_endpoint_url(
1649                &version_only_supported(&[V1_0]),
1650                "https://example.org",
1651                &[&"123"],
1652                "",
1653            )
1654            .unwrap();
1655        assert_eq!(url, "https://example.org/s/123");
1656    }
1657}