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
838impl TryFrom<&str> for MatrixVersion {
839    type Error = UnknownVersionError;
840
841    fn try_from(value: &str) -> Result<MatrixVersion, Self::Error> {
842        use MatrixVersion::*;
843
844        Ok(match value {
845            // Identity service API versions between Matrix 1.0 and 1.1.
846            // They might match older client-server API versions but that should not be a problem in practice.
847            "r0.2.0" | "r0.2.1" | "r0.3.0" |
848            // Client-server API versions between Matrix 1.0 and 1.1.
849            "r0.5.0" | "r0.6.0" | "r0.6.1" => V1_0,
850            "v1.1" => V1_1,
851            "v1.2" => V1_2,
852            "v1.3" => V1_3,
853            "v1.4" => V1_4,
854            "v1.5" => V1_5,
855            "v1.6" => V1_6,
856            "v1.7" => V1_7,
857            "v1.8" => V1_8,
858            "v1.9" => V1_9,
859            "v1.10" => V1_10,
860            "v1.11" => V1_11,
861            "v1.12" => V1_12,
862            "v1.13" => V1_13,
863            "v1.14" => V1_14,
864            "v1.15" => V1_15,
865            _ => return Err(UnknownVersionError),
866        })
867    }
868}
869
870impl FromStr for MatrixVersion {
871    type Err = UnknownVersionError;
872
873    fn from_str(s: &str) -> Result<Self, Self::Err> {
874        Self::try_from(s)
875    }
876}
877
878impl MatrixVersion {
879    /// Checks whether a version is compatible with another.
880    ///
881    /// Currently, all versions of Matrix are considered backwards compatible with all the previous
882    /// versions, so this is equivalent to `self >= other`. This behaviour may change in the future,
883    /// if a new release is considered to be breaking compatibility with the previous ones.
884    ///
885    /// > âš  Matrix has a deprecation policy, and Matrix versioning is not as straightforward as this
886    /// > function makes it out to be. This function only exists to prune breaking changes between
887    /// > versions, and versions too new for `self`.
888    pub fn is_superset_of(self, other: Self) -> bool {
889        self >= other
890    }
891
892    /// Get a string representation of this Matrix version.
893    ///
894    /// This is the string that can be found in the response to one of the `GET /versions`
895    /// endpoints. Parsing this string will give the same variant.
896    ///
897    /// Returns `None` for [`MatrixVersion::V1_0`] because it can match several per-API versions.
898    pub const fn as_str(self) -> Option<&'static str> {
899        let string = match self {
900            MatrixVersion::V1_0 => return None,
901            MatrixVersion::V1_1 => "v1.1",
902            MatrixVersion::V1_2 => "v1.2",
903            MatrixVersion::V1_3 => "v1.3",
904            MatrixVersion::V1_4 => "v1.4",
905            MatrixVersion::V1_5 => "v1.5",
906            MatrixVersion::V1_6 => "v1.6",
907            MatrixVersion::V1_7 => "v1.7",
908            MatrixVersion::V1_8 => "v1.8",
909            MatrixVersion::V1_9 => "v1.9",
910            MatrixVersion::V1_10 => "v1.10",
911            MatrixVersion::V1_11 => "v1.11",
912            MatrixVersion::V1_12 => "v1.12",
913            MatrixVersion::V1_13 => "v1.13",
914            MatrixVersion::V1_14 => "v1.14",
915            MatrixVersion::V1_15 => "v1.15",
916        };
917
918        Some(string)
919    }
920
921    /// Decompose the Matrix version into its major and minor number.
922    const fn into_parts(self) -> (u8, u8) {
923        match self {
924            MatrixVersion::V1_0 => (1, 0),
925            MatrixVersion::V1_1 => (1, 1),
926            MatrixVersion::V1_2 => (1, 2),
927            MatrixVersion::V1_3 => (1, 3),
928            MatrixVersion::V1_4 => (1, 4),
929            MatrixVersion::V1_5 => (1, 5),
930            MatrixVersion::V1_6 => (1, 6),
931            MatrixVersion::V1_7 => (1, 7),
932            MatrixVersion::V1_8 => (1, 8),
933            MatrixVersion::V1_9 => (1, 9),
934            MatrixVersion::V1_10 => (1, 10),
935            MatrixVersion::V1_11 => (1, 11),
936            MatrixVersion::V1_12 => (1, 12),
937            MatrixVersion::V1_13 => (1, 13),
938            MatrixVersion::V1_14 => (1, 14),
939            MatrixVersion::V1_15 => (1, 15),
940        }
941    }
942
943    /// Try to turn a pair of (major, minor) version components back into a `MatrixVersion`.
944    const fn from_parts(major: u8, minor: u8) -> Result<Self, UnknownVersionError> {
945        match (major, minor) {
946            (1, 0) => Ok(MatrixVersion::V1_0),
947            (1, 1) => Ok(MatrixVersion::V1_1),
948            (1, 2) => Ok(MatrixVersion::V1_2),
949            (1, 3) => Ok(MatrixVersion::V1_3),
950            (1, 4) => Ok(MatrixVersion::V1_4),
951            (1, 5) => Ok(MatrixVersion::V1_5),
952            (1, 6) => Ok(MatrixVersion::V1_6),
953            (1, 7) => Ok(MatrixVersion::V1_7),
954            (1, 8) => Ok(MatrixVersion::V1_8),
955            (1, 9) => Ok(MatrixVersion::V1_9),
956            (1, 10) => Ok(MatrixVersion::V1_10),
957            (1, 11) => Ok(MatrixVersion::V1_11),
958            (1, 12) => Ok(MatrixVersion::V1_12),
959            (1, 13) => Ok(MatrixVersion::V1_13),
960            (1, 14) => Ok(MatrixVersion::V1_14),
961            (1, 15) => Ok(MatrixVersion::V1_15),
962            _ => Err(UnknownVersionError),
963        }
964    }
965
966    /// Constructor for use by the `metadata!` macro.
967    ///
968    /// Accepts string literals and parses them.
969    #[doc(hidden)]
970    pub const fn from_lit(lit: &'static str) -> Self {
971        use konst::{option, primitive::parse_u8, result, string};
972
973        let major: u8;
974        let minor: u8;
975
976        let mut lit_iter = string::split(lit, ".").next();
977
978        {
979            let (checked_first, checked_split) = option::unwrap!(lit_iter); // First iteration always succeeds
980
981            major = result::unwrap_or_else!(parse_u8(checked_first), |_| panic!(
982                "major version is not a valid number"
983            ));
984
985            lit_iter = checked_split.next();
986        }
987
988        match lit_iter {
989            Some((checked_second, checked_split)) => {
990                minor = result::unwrap_or_else!(parse_u8(checked_second), |_| panic!(
991                    "minor version is not a valid number"
992                ));
993
994                lit_iter = checked_split.next();
995            }
996            None => panic!("could not find dot to denote second number"),
997        }
998
999        if lit_iter.is_some() {
1000            panic!("version literal contains more than one dot")
1001        }
1002
1003        result::unwrap_or_else!(Self::from_parts(major, minor), |_| panic!(
1004            "not a valid version literal"
1005        ))
1006    }
1007
1008    // Internal function to do ordering in const-fn contexts
1009    const fn const_ord(&self, other: &Self) -> Ordering {
1010        let self_parts = self.into_parts();
1011        let other_parts = other.into_parts();
1012
1013        use konst::primitive::cmp::cmp_u8;
1014
1015        let major_ord = cmp_u8(self_parts.0, other_parts.0);
1016        if major_ord.is_ne() {
1017            major_ord
1018        } else {
1019            cmp_u8(self_parts.1, other_parts.1)
1020        }
1021    }
1022
1023    // Internal function to check if this version is the legacy (v1.0) version in const-fn contexts
1024    const fn is_legacy(&self) -> bool {
1025        let self_parts = self.into_parts();
1026
1027        use konst::primitive::cmp::cmp_u8;
1028
1029        cmp_u8(self_parts.0, 1).is_eq() && cmp_u8(self_parts.1, 0).is_eq()
1030    }
1031
1032    /// Get the default [`RoomVersionId`] for this `MatrixVersion`.
1033    pub fn default_room_version(&self) -> RoomVersionId {
1034        match self {
1035            // <https://spec.matrix.org/historical/index.html#complete-list-of-room-versions>
1036            MatrixVersion::V1_0
1037            // <https://spec.matrix.org/v1.1/rooms/#complete-list-of-room-versions>
1038            | MatrixVersion::V1_1
1039            // <https://spec.matrix.org/v1.2/rooms/#complete-list-of-room-versions>
1040            | MatrixVersion::V1_2 => RoomVersionId::V6,
1041            // <https://spec.matrix.org/v1.3/rooms/#complete-list-of-room-versions>
1042            MatrixVersion::V1_3
1043            // <https://spec.matrix.org/v1.4/rooms/#complete-list-of-room-versions>
1044            | MatrixVersion::V1_4
1045            // <https://spec.matrix.org/v1.5/rooms/#complete-list-of-room-versions>
1046            | MatrixVersion::V1_5 => RoomVersionId::V9,
1047            // <https://spec.matrix.org/v1.6/rooms/#complete-list-of-room-versions>
1048            MatrixVersion::V1_6
1049            // <https://spec.matrix.org/v1.7/rooms/#complete-list-of-room-versions>
1050            | MatrixVersion::V1_7
1051            // <https://spec.matrix.org/v1.8/rooms/#complete-list-of-room-versions>
1052            | MatrixVersion::V1_8
1053            // <https://spec.matrix.org/v1.9/rooms/#complete-list-of-room-versions>
1054            | MatrixVersion::V1_9
1055            // <https://spec.matrix.org/v1.10/rooms/#complete-list-of-room-versions>
1056            | MatrixVersion::V1_10
1057            // <https://spec.matrix.org/v1.11/rooms/#complete-list-of-room-versions>
1058            | MatrixVersion::V1_11
1059            // <https://spec.matrix.org/v1.12/rooms/#complete-list-of-room-versions>
1060            | MatrixVersion::V1_12
1061            // <https://spec.matrix.org/v1.13/rooms/#complete-list-of-room-versions>
1062            | MatrixVersion::V1_13 => RoomVersionId::V10,
1063            // <https://spec.matrix.org/v1.14/rooms/#complete-list-of-room-versions>
1064            | MatrixVersion::V1_14
1065            // <https://spec.matrix.org/v1.15/rooms/#complete-list-of-room-versions>
1066            | MatrixVersion::V1_15 => RoomVersionId::V11,
1067        }
1068    }
1069}
1070
1071/// A selector for a stable path of an endpoint.
1072#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1073#[allow(clippy::exhaustive_enums)]
1074pub enum StablePathSelector {
1075    /// The path is available with the given stable feature.
1076    Feature(&'static str),
1077
1078    /// The path was added in the given Matrix version.
1079    Version(MatrixVersion),
1080
1081    /// The path is available via a stable feature and was added in a Matrix version.
1082    FeatureAndVersion {
1083        /// The stable feature that adds support for the path.
1084        feature: &'static str,
1085        /// The Matrix version when the path was added.
1086        version: MatrixVersion,
1087    },
1088}
1089
1090impl StablePathSelector {
1091    /// The feature that adds support for the path, if any.
1092    pub const fn feature(self) -> Option<&'static str> {
1093        match self {
1094            Self::Feature(feature) | Self::FeatureAndVersion { feature, .. } => Some(feature),
1095            _ => None,
1096        }
1097    }
1098
1099    /// The Matrix version when the path was added, if any.
1100    pub const fn version(self) -> Option<MatrixVersion> {
1101        match self {
1102            Self::Version(version) | Self::FeatureAndVersion { version, .. } => Some(version),
1103            _ => None,
1104        }
1105    }
1106}
1107
1108impl From<MatrixVersion> for StablePathSelector {
1109    fn from(value: MatrixVersion) -> Self {
1110        Self::Version(value)
1111    }
1112}
1113
1114/// The list of Matrix versions and features supported by a homeserver.
1115#[derive(Debug, Clone)]
1116#[allow(clippy::exhaustive_structs)]
1117pub struct SupportedVersions {
1118    /// The Matrix versions that are supported by the homeserver.
1119    ///
1120    /// This set contains only known versions.
1121    pub versions: BTreeSet<MatrixVersion>,
1122
1123    /// The features that are supported by the homeserver.
1124    ///
1125    /// This matches the `unstable_features` field of the `/versions` endpoint, without the boolean
1126    /// value.
1127    pub features: BTreeSet<FeatureFlag>,
1128}
1129
1130impl SupportedVersions {
1131    /// Construct a `SupportedVersions` from the parts of a `/versions` response.
1132    ///
1133    /// Matrix versions that can't be parsed to a `MatrixVersion`, and features with the boolean
1134    /// value set to `false` are discarded.
1135    pub fn from_parts(versions: &[String], unstable_features: &BTreeMap<String, bool>) -> Self {
1136        Self {
1137            versions: versions.iter().flat_map(|s| s.parse::<MatrixVersion>()).collect(),
1138            features: unstable_features
1139                .iter()
1140                .filter(|(_, enabled)| **enabled)
1141                .map(|(feature, _)| feature.as_str().into())
1142                .collect(),
1143        }
1144    }
1145}
1146
1147/// The Matrix features supported by Ruma.
1148///
1149/// Features that are not behind a cargo feature are features that are part of the Matrix
1150/// specification and that Ruma still supports, like the unstable version of an endpoint or a stable
1151/// feature. Features behind a cargo feature are only supported when this feature is enabled.
1152#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
1153#[derive(Clone, StringEnum, PartialEqAsRefStr, Eq, Hash, PartialOrdAsRefStr, OrdAsRefStr)]
1154#[non_exhaustive]
1155pub enum FeatureFlag {
1156    /// `fi.mau.msc2246` ([MSC])
1157    ///
1158    /// Asynchronous media uploads.
1159    ///
1160    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/2246
1161    #[ruma_enum(rename = "fi.mau.msc2246")]
1162    Msc2246,
1163
1164    /// `org.matrix.msc2432` ([MSC])
1165    ///
1166    /// Updated semantics for publishing room aliases.
1167    ///
1168    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/2432
1169    #[ruma_enum(rename = "org.matrix.msc2432")]
1170    Msc2432,
1171
1172    /// `fi.mau.msc2659` ([MSC])
1173    ///
1174    /// Application service ping endpoint.
1175    ///
1176    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/2659
1177    #[ruma_enum(rename = "fi.mau.msc2659")]
1178    Msc2659,
1179
1180    /// `fi.mau.msc2659` ([MSC])
1181    ///
1182    /// Stable version of the application service ping endpoint.
1183    ///
1184    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/2659
1185    #[ruma_enum(rename = "fi.mau.msc2659.stable")]
1186    Msc2659Stable,
1187
1188    /// `uk.half-shot.msc2666.query_mutual_rooms` ([MSC])
1189    ///
1190    /// Get rooms in common with another user.
1191    ///
1192    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/2666
1193    #[cfg(feature = "unstable-msc2666")]
1194    #[ruma_enum(rename = "uk.half-shot.msc2666.query_mutual_rooms")]
1195    Msc2666,
1196
1197    /// `org.matrix.msc3030` ([MSC])
1198    ///
1199    /// Jump to date API endpoint.
1200    ///
1201    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/3030
1202    #[ruma_enum(rename = "org.matrix.msc3030")]
1203    Msc3030,
1204
1205    /// `org.matrix.msc3882` ([MSC])
1206    ///
1207    /// Allow an existing session to sign in a new session.
1208    ///
1209    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/3882
1210    #[ruma_enum(rename = "org.matrix.msc3882")]
1211    Msc3882,
1212
1213    /// `org.matrix.msc3916` ([MSC])
1214    ///
1215    /// Authentication for media.
1216    ///
1217    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/3916
1218    #[ruma_enum(rename = "org.matrix.msc3916")]
1219    Msc3916,
1220
1221    /// `org.matrix.msc3916.stable` ([MSC])
1222    ///
1223    /// Stable version of authentication for media.
1224    ///
1225    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/3916
1226    #[ruma_enum(rename = "org.matrix.msc3916.stable")]
1227    Msc3916Stable,
1228
1229    /// `org.matrix.msc4108` ([MSC])
1230    ///
1231    /// Mechanism to allow OIDC sign in and E2EE set up via QR code.
1232    ///
1233    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
1234    #[cfg(feature = "unstable-msc4108")]
1235    #[ruma_enum(rename = "org.matrix.msc4108")]
1236    Msc4108,
1237
1238    /// `org.matrix.msc4140` ([MSC])
1239    ///
1240    /// Delayed events.
1241    ///
1242    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4140
1243    #[cfg(feature = "unstable-msc4140")]
1244    #[ruma_enum(rename = "org.matrix.msc4140")]
1245    Msc4140,
1246
1247    /// `org.matrix.simplified_msc3575` ([MSC])
1248    ///
1249    /// Simplified Sliding Sync.
1250    ///
1251    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4186
1252    #[cfg(feature = "unstable-msc4186")]
1253    #[ruma_enum(rename = "org.matrix.simplified_msc3575")]
1254    Msc4186,
1255
1256    #[doc(hidden)]
1257    _Custom(PrivOwnedStr),
1258}
1259
1260#[cfg(test)]
1261mod tests {
1262    use std::collections::{BTreeMap, BTreeSet};
1263
1264    use assert_matches2::assert_matches;
1265    use http::Method;
1266
1267    use super::{
1268        AuthScheme,
1269        MatrixVersion::{self, V1_0, V1_1, V1_2, V1_3},
1270        Metadata, StablePathSelector, SupportedVersions, VersionHistory,
1271    };
1272    use crate::api::error::IntoHttpError;
1273
1274    fn stable_only_metadata(
1275        stable_paths: &'static [(StablePathSelector, &'static str)],
1276    ) -> Metadata {
1277        Metadata {
1278            method: Method::GET,
1279            rate_limited: false,
1280            authentication: AuthScheme::None,
1281            history: VersionHistory {
1282                unstable_paths: &[],
1283                stable_paths,
1284                deprecated: None,
1285                removed: None,
1286            },
1287        }
1288    }
1289
1290    fn version_only_supported(versions: &[MatrixVersion]) -> SupportedVersions {
1291        SupportedVersions {
1292            versions: versions.iter().copied().collect(),
1293            features: BTreeSet::new(),
1294        }
1295    }
1296
1297    // TODO add test that can hook into tracing and verify the deprecation warning is emitted
1298
1299    #[test]
1300    fn make_simple_endpoint_url() {
1301        let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s")]);
1302        let url = meta
1303            .make_endpoint_url(&version_only_supported(&[V1_0]), "https://example.org", &[], "")
1304            .unwrap();
1305        assert_eq!(url, "https://example.org/s");
1306    }
1307
1308    #[test]
1309    fn make_endpoint_url_with_path_args() {
1310        let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
1311        let url = meta
1312            .make_endpoint_url(
1313                &version_only_supported(&[V1_0]),
1314                "https://example.org",
1315                &[&"123"],
1316                "",
1317            )
1318            .unwrap();
1319        assert_eq!(url, "https://example.org/s/123");
1320    }
1321
1322    #[test]
1323    fn make_endpoint_url_with_path_args_with_dash() {
1324        let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
1325        let url = meta
1326            .make_endpoint_url(
1327                &version_only_supported(&[V1_0]),
1328                "https://example.org",
1329                &[&"my-path"],
1330                "",
1331            )
1332            .unwrap();
1333        assert_eq!(url, "https://example.org/s/my-path");
1334    }
1335
1336    #[test]
1337    fn make_endpoint_url_with_path_args_with_reserved_char() {
1338        let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
1339        let url = meta
1340            .make_endpoint_url(
1341                &version_only_supported(&[V1_0]),
1342                "https://example.org",
1343                &[&"#path"],
1344                "",
1345            )
1346            .unwrap();
1347        assert_eq!(url, "https://example.org/s/%23path");
1348    }
1349
1350    #[test]
1351    fn make_endpoint_url_with_query() {
1352        let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s/")]);
1353        let url = meta
1354            .make_endpoint_url(
1355                &version_only_supported(&[V1_0]),
1356                "https://example.org",
1357                &[],
1358                "foo=bar",
1359            )
1360            .unwrap();
1361        assert_eq!(url, "https://example.org/s/?foo=bar");
1362    }
1363
1364    #[test]
1365    #[should_panic]
1366    fn make_endpoint_url_wrong_num_path_args() {
1367        let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
1368        _ = meta.make_endpoint_url(
1369            &version_only_supported(&[V1_0]),
1370            "https://example.org",
1371            &[],
1372            "",
1373        );
1374    }
1375
1376    const EMPTY: VersionHistory =
1377        VersionHistory { unstable_paths: &[], stable_paths: &[], deprecated: None, removed: None };
1378
1379    #[test]
1380    fn select_version() {
1381        let version_supported = version_only_supported(&[V1_0, V1_1]);
1382        let superset_supported = version_only_supported(&[V1_1]);
1383
1384        // With version only.
1385        let hist =
1386            VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_0), "/s")], ..EMPTY };
1387        assert_matches!(hist.select_path(&version_supported), Ok("/s"));
1388        assert!(hist.is_supported(&version_supported));
1389        assert_matches!(hist.select_path(&superset_supported), Ok("/s"));
1390        assert!(hist.is_supported(&superset_supported));
1391
1392        // With feature and version.
1393        let hist = VersionHistory {
1394            stable_paths: &[(
1395                StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_0 },
1396                "/s",
1397            )],
1398            ..EMPTY
1399        };
1400        assert_matches!(hist.select_path(&version_supported), Ok("/s"));
1401        assert!(hist.is_supported(&version_supported));
1402        assert_matches!(hist.select_path(&superset_supported), Ok("/s"));
1403        assert!(hist.is_supported(&superset_supported));
1404
1405        // Select latest stable version.
1406        let hist = VersionHistory {
1407            stable_paths: &[
1408                (StablePathSelector::Version(V1_0), "/s_v1"),
1409                (StablePathSelector::Version(V1_1), "/s_v2"),
1410            ],
1411            ..EMPTY
1412        };
1413        assert_matches!(hist.select_path(&version_supported), Ok("/s_v2"));
1414        assert!(hist.is_supported(&version_supported));
1415
1416        // With unstable feature.
1417        let unstable_supported = SupportedVersions {
1418            versions: [V1_0].into(),
1419            features: ["org.boo.unstable".into()].into(),
1420        };
1421        let hist = VersionHistory {
1422            unstable_paths: &[(Some("org.boo.unstable"), "/u")],
1423            stable_paths: &[(StablePathSelector::Version(V1_0), "/s")],
1424            ..EMPTY
1425        };
1426        assert_matches!(hist.select_path(&unstable_supported), Ok("/s"));
1427        assert!(hist.is_supported(&unstable_supported));
1428    }
1429
1430    #[test]
1431    fn select_stable_feature() {
1432        let supported = SupportedVersions {
1433            versions: [V1_1].into(),
1434            features: ["org.boo.unstable".into(), "org.boo.stable".into()].into(),
1435        };
1436
1437        // With feature only.
1438        let hist = VersionHistory {
1439            unstable_paths: &[(Some("org.boo.unstable"), "/u")],
1440            stable_paths: &[(StablePathSelector::Feature("org.boo.stable"), "/s")],
1441            ..EMPTY
1442        };
1443        assert_matches!(hist.select_path(&supported), Ok("/s"));
1444        assert!(hist.is_supported(&supported));
1445
1446        // With feature and version.
1447        let hist = VersionHistory {
1448            unstable_paths: &[(Some("org.boo.unstable"), "/u")],
1449            stable_paths: &[(
1450                StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_3 },
1451                "/s",
1452            )],
1453            ..EMPTY
1454        };
1455        assert_matches!(hist.select_path(&supported), Ok("/s"));
1456        assert!(hist.is_supported(&supported));
1457    }
1458
1459    #[test]
1460    fn select_unstable_feature() {
1461        let supported = SupportedVersions {
1462            versions: [V1_1].into(),
1463            features: ["org.boo.unstable".into()].into(),
1464        };
1465
1466        let hist = VersionHistory {
1467            unstable_paths: &[(Some("org.boo.unstable"), "/u")],
1468            stable_paths: &[(
1469                StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_3 },
1470                "/s",
1471            )],
1472            ..EMPTY
1473        };
1474        assert_matches!(hist.select_path(&supported), Ok("/u"));
1475        assert!(hist.is_supported(&supported));
1476    }
1477
1478    #[test]
1479    fn select_unstable_fallback() {
1480        let supported = version_only_supported(&[V1_0]);
1481        let hist = VersionHistory { unstable_paths: &[(None, "/u")], ..EMPTY };
1482        assert_matches!(hist.select_path(&supported), Ok("/u"));
1483        assert!(!hist.is_supported(&supported));
1484    }
1485
1486    #[test]
1487    fn select_r0() {
1488        let supported = version_only_supported(&[V1_0]);
1489        let hist =
1490            VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_0), "/r")], ..EMPTY };
1491        assert_matches!(hist.select_path(&supported), Ok("/r"));
1492        assert!(hist.is_supported(&supported));
1493    }
1494
1495    #[test]
1496    fn select_removed_err() {
1497        let supported = version_only_supported(&[V1_3]);
1498        let hist = VersionHistory {
1499            stable_paths: &[
1500                (StablePathSelector::Version(V1_0), "/r"),
1501                (StablePathSelector::Version(V1_1), "/s"),
1502            ],
1503            unstable_paths: &[(None, "/u")],
1504            deprecated: Some(V1_2),
1505            removed: Some(V1_3),
1506        };
1507        assert_matches!(hist.select_path(&supported), Err(IntoHttpError::EndpointRemoved(V1_3)));
1508        assert!(!hist.is_supported(&supported));
1509    }
1510
1511    #[test]
1512    fn partially_removed_but_stable() {
1513        let supported = version_only_supported(&[V1_2]);
1514        let hist = VersionHistory {
1515            stable_paths: &[
1516                (StablePathSelector::Version(V1_0), "/r"),
1517                (StablePathSelector::Version(V1_1), "/s"),
1518            ],
1519            unstable_paths: &[],
1520            deprecated: Some(V1_2),
1521            removed: Some(V1_3),
1522        };
1523        assert_matches!(hist.select_path(&supported), Ok("/s"));
1524        assert!(hist.is_supported(&supported));
1525    }
1526
1527    #[test]
1528    fn no_unstable() {
1529        let supported = version_only_supported(&[V1_0]);
1530        let hist =
1531            VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_1), "/s")], ..EMPTY };
1532        assert_matches!(hist.select_path(&supported), Err(IntoHttpError::NoUnstablePath));
1533        assert!(!hist.is_supported(&supported));
1534    }
1535
1536    #[test]
1537    fn version_literal() {
1538        const LIT: MatrixVersion = MatrixVersion::from_lit("1.0");
1539
1540        assert_eq!(LIT, V1_0);
1541    }
1542
1543    #[test]
1544    fn parse_as_str_sanity() {
1545        let version = MatrixVersion::try_from("r0.5.0").unwrap();
1546        assert_eq!(version, V1_0);
1547        assert_eq!(version.as_str(), None);
1548
1549        let version = MatrixVersion::try_from("v1.1").unwrap();
1550        assert_eq!(version, V1_1);
1551        assert_eq!(version.as_str(), Some("v1.1"));
1552    }
1553
1554    #[test]
1555    fn supported_versions_from_parts() {
1556        let empty_features = BTreeMap::new();
1557
1558        let none = &[];
1559        let none_supported = SupportedVersions::from_parts(none, &empty_features);
1560        assert_eq!(none_supported.versions, BTreeSet::new());
1561        assert_eq!(none_supported.features, BTreeSet::new());
1562
1563        let single_known = &["r0.6.0".to_owned()];
1564        let single_known_supported = SupportedVersions::from_parts(single_known, &empty_features);
1565        assert_eq!(single_known_supported.versions, BTreeSet::from([V1_0]));
1566        assert_eq!(single_known_supported.features, BTreeSet::new());
1567
1568        let multiple_known = &["v1.1".to_owned(), "r0.6.0".to_owned(), "r0.6.1".to_owned()];
1569        let multiple_known_supported =
1570            SupportedVersions::from_parts(multiple_known, &empty_features);
1571        assert_eq!(multiple_known_supported.versions, BTreeSet::from([V1_0, V1_1]));
1572        assert_eq!(multiple_known_supported.features, BTreeSet::new());
1573
1574        let single_unknown = &["v0.0".to_owned()];
1575        let single_unknown_supported =
1576            SupportedVersions::from_parts(single_unknown, &empty_features);
1577        assert_eq!(single_unknown_supported.versions, BTreeSet::new());
1578        assert_eq!(single_unknown_supported.features, BTreeSet::new());
1579
1580        let mut features = BTreeMap::new();
1581        features.insert("org.bar.enabled_1".to_owned(), true);
1582        features.insert("org.bar.disabled".to_owned(), false);
1583        features.insert("org.bar.enabled_2".to_owned(), true);
1584
1585        let features_supported = SupportedVersions::from_parts(single_known, &features);
1586        assert_eq!(features_supported.versions, BTreeSet::from([V1_0]));
1587        assert_eq!(
1588            features_supported.features,
1589            ["org.bar.enabled_1".into(), "org.bar.enabled_2".into()].into()
1590        );
1591    }
1592
1593    #[test]
1594    fn supported_versions_from_parts_order() {
1595        let empty_features = BTreeMap::new();
1596
1597        let sorted = &[
1598            "r0.0.1".to_owned(),
1599            "r0.5.0".to_owned(),
1600            "r0.6.0".to_owned(),
1601            "r0.6.1".to_owned(),
1602            "v1.1".to_owned(),
1603            "v1.2".to_owned(),
1604        ];
1605        let sorted_supported = SupportedVersions::from_parts(sorted, &empty_features);
1606        assert_eq!(sorted_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));
1607
1608        let sorted_reverse = &[
1609            "v1.2".to_owned(),
1610            "v1.1".to_owned(),
1611            "r0.6.1".to_owned(),
1612            "r0.6.0".to_owned(),
1613            "r0.5.0".to_owned(),
1614            "r0.0.1".to_owned(),
1615        ];
1616        let sorted_reverse_supported =
1617            SupportedVersions::from_parts(sorted_reverse, &empty_features);
1618        assert_eq!(sorted_reverse_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));
1619
1620        let random_order = &[
1621            "v1.1".to_owned(),
1622            "r0.6.1".to_owned(),
1623            "r0.5.0".to_owned(),
1624            "r0.6.0".to_owned(),
1625            "r0.0.1".to_owned(),
1626            "v1.2".to_owned(),
1627        ];
1628        let random_order_supported = SupportedVersions::from_parts(random_order, &empty_features);
1629        assert_eq!(random_order_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));
1630    }
1631
1632    #[test]
1633    #[should_panic]
1634    fn make_endpoint_url_with_path_args_old_syntax() {
1635        let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s/:x")]);
1636        let url = meta
1637            .make_endpoint_url(
1638                &version_only_supported(&[V1_0]),
1639                "https://example.org",
1640                &[&"123"],
1641                "",
1642            )
1643            .unwrap();
1644        assert_eq!(url, "https://example.org/s/123");
1645    }
1646}