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