ruma_common/api/
metadata.rs

1use std::{
2    cmp::Ordering,
3    collections::{BTreeMap, BTreeSet},
4    fmt::{self, 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/// Metadata about an API endpoint.
26#[derive(Clone, Debug, PartialEq, Eq)]
27#[allow(clippy::exhaustive_structs)]
28pub struct Metadata {
29    /// The HTTP method used by this endpoint.
30    pub method: Method,
31
32    /// Whether or not this endpoint is rate limited by the server.
33    pub rate_limited: bool,
34
35    /// What authentication scheme the server uses for this endpoint.
36    pub authentication: AuthScheme,
37
38    /// All info pertaining to an endpoint's (historic) paths, deprecation version, and removal.
39    pub history: VersionHistory,
40}
41
42impl Metadata {
43    /// Returns an empty request body for this Matrix request.
44    ///
45    /// For `GET` requests, it returns an entirely empty buffer, for others it returns an empty JSON
46    /// object (`{}`).
47    pub fn empty_request_body<B>(&self) -> B
48    where
49        B: Default + BufMut,
50    {
51        if self.method == Method::GET {
52            Default::default()
53        } else {
54            slice_to_buf(b"{}")
55        }
56    }
57
58    /// Transform the `SendAccessToken` into an access token if the endpoint requires it, or if it
59    /// is `SendAccessToken::Force`.
60    ///
61    /// Fails if the endpoint requires an access token but the parameter is `SendAccessToken::None`,
62    /// or if the access token can't be converted to a [`HeaderValue`].
63    pub fn authorization_header(
64        &self,
65        access_token: SendAccessToken<'_>,
66    ) -> Result<Option<(HeaderName, HeaderValue)>, IntoHttpError> {
67        Ok(match self.authentication {
68            AuthScheme::None => match access_token.get_not_required_for_endpoint() {
69                Some(token) => Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?)),
70                None => None,
71            },
72
73            AuthScheme::AccessToken => {
74                let token = access_token
75                    .get_required_for_endpoint()
76                    .ok_or(IntoHttpError::NeedsAuthentication)?;
77
78                Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?))
79            }
80
81            AuthScheme::AccessTokenOptional => match access_token.get_required_for_endpoint() {
82                Some(token) => Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?)),
83                None => None,
84            },
85
86            AuthScheme::AppserviceToken => match access_token.get_required_for_appservice() {
87                Some(token) => Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?)),
88                None => None,
89            },
90
91            AuthScheme::ServerSignatures => None,
92        })
93    }
94
95    /// Generate the endpoint URL for this endpoint.
96    pub fn make_endpoint_url(
97        &self,
98        versions: &[MatrixVersion],
99        base_url: &str,
100        path_args: &[&dyn Display],
101        query_string: &str,
102    ) -> Result<String, IntoHttpError> {
103        let path_with_placeholders = self.history.select_path(versions)?;
104
105        let mut res = base_url.strip_suffix('/').unwrap_or(base_url).to_owned();
106        let mut segments = path_with_placeholders.split('/');
107        let mut path_args = path_args.iter();
108
109        let first_segment = segments.next().expect("split iterator is never empty");
110        assert!(first_segment.is_empty(), "endpoint paths must start with '/'");
111
112        for segment in segments {
113            if segment.starts_with(':') {
114                let arg = path_args
115                    .next()
116                    .expect("number of placeholders must match number of arguments")
117                    .to_string();
118                let arg = utf8_percent_encode(&arg, PATH_PERCENT_ENCODE_SET);
119
120                write!(res, "/{arg}").expect("writing to a String using fmt::Write can't fail");
121            } else {
122                res.reserve(segment.len() + 1);
123                res.push('/');
124                res.push_str(segment);
125            }
126        }
127
128        if !query_string.is_empty() {
129            res.push('?');
130            res.push_str(query_string);
131        }
132
133        Ok(res)
134    }
135
136    // Used for generated `#[test]`s
137    #[doc(hidden)]
138    pub fn _path_parameters(&self) -> Vec<&'static str> {
139        let path = self.history.all_paths().next().unwrap();
140        path.split('/').filter_map(|segment| segment.strip_prefix(':')).collect()
141    }
142}
143
144/// The complete history of this endpoint as far as Ruma knows, together with all variants on
145/// versions stable and unstable.
146///
147/// The amount and positioning of path variables are the same over all path variants.
148#[derive(Clone, Debug, PartialEq, Eq)]
149#[allow(clippy::exhaustive_structs)]
150pub struct VersionHistory {
151    /// A list of unstable paths over this endpoint's history.
152    ///
153    /// For endpoint querying purposes, the last item will be used.
154    unstable_paths: &'static [&'static str],
155
156    /// A list of path versions, mapped to Matrix versions.
157    ///
158    /// Sorted (ascending) by Matrix version, will not mix major versions.
159    stable_paths: &'static [(MatrixVersion, &'static str)],
160
161    /// The Matrix version that deprecated this endpoint.
162    ///
163    /// Deprecation often precedes one Matrix version before removal.
164    ///
165    /// This will make [`try_into_http_request`](super::OutgoingRequest::try_into_http_request)
166    /// emit a warning, see the corresponding documentation for more information.
167    deprecated: Option<MatrixVersion>,
168
169    /// The Matrix version that removed this endpoint.
170    ///
171    /// This will make [`try_into_http_request`](super::OutgoingRequest::try_into_http_request)
172    /// emit an error, see the corresponding documentation for more information.
173    removed: Option<MatrixVersion>,
174}
175
176impl VersionHistory {
177    /// Constructs an instance of [`VersionHistory`], erroring on compilation if it does not pass
178    /// invariants.
179    ///
180    /// Specifically, this checks the following invariants:
181    /// - Path Arguments are equal (in order, amount, and argument name) in all path strings
182    /// - In stable_paths:
183    ///   - matrix versions are in ascending order
184    ///   - no matrix version is referenced twice
185    /// - deprecated's version comes after the latest version mentioned in stable_paths, except for
186    ///   version 1.0, and only if any stable path is defined
187    /// - removed comes after deprecated, or after the latest referenced stable_paths, like
188    ///   deprecated
189    pub const fn new(
190        unstable_paths: &'static [&'static str],
191        stable_paths: &'static [(MatrixVersion, &'static str)],
192        deprecated: Option<MatrixVersion>,
193        removed: Option<MatrixVersion>,
194    ) -> Self {
195        use konst::{iter, slice, string};
196
197        const fn check_path_is_valid(path: &'static str) {
198            iter::for_each!(path_b in slice::iter(path.as_bytes()) => {
199                match *path_b {
200                    0x21..=0x7E => {},
201                    _ => panic!("path contains invalid (non-ascii or whitespace) characters")
202                }
203            });
204        }
205
206        const fn check_path_args_equal(first: &'static str, second: &'static str) {
207            let mut second_iter = string::split(second, "/").next();
208
209            iter::for_each!(first_s in string::split(first, "/") => {
210                if let Some(first_arg) = string::strip_prefix(first_s, ":") {
211                    let second_next_arg: Option<&'static str> = loop {
212                        let (second_s, second_n_iter) = match second_iter {
213                            Some(tuple) => tuple,
214                            None => break None,
215                        };
216
217                        let maybe_second_arg = string::strip_prefix(second_s, ":");
218
219                        second_iter = second_n_iter.next();
220
221                        if let Some(second_arg) = maybe_second_arg {
222                            break Some(second_arg);
223                        }
224                    };
225
226                    if let Some(second_next_arg) = second_next_arg {
227                        if !string::eq_str(second_next_arg, first_arg) {
228                            panic!("Path Arguments do not match");
229                        }
230                    } else {
231                        panic!("Amount of Path Arguments do not match");
232                    }
233                }
234            });
235
236            // If second iterator still has some values, empty first.
237            while let Some((second_s, second_n_iter)) = second_iter {
238                if string::starts_with(second_s, ":") {
239                    panic!("Amount of Path Arguments do not match");
240                }
241                second_iter = second_n_iter.next();
242            }
243        }
244
245        // The path we're going to use to compare all other paths with
246        let ref_path: &str = if let Some(s) = unstable_paths.first() {
247            s
248        } else if let Some((_, s)) = stable_paths.first() {
249            s
250        } else {
251            panic!("No paths supplied")
252        };
253
254        iter::for_each!(unstable_path in slice::iter(unstable_paths) => {
255            check_path_is_valid(unstable_path);
256            check_path_args_equal(ref_path, unstable_path);
257        });
258
259        let mut prev_seen_version: Option<MatrixVersion> = None;
260
261        iter::for_each!(stable_path in slice::iter(stable_paths) => {
262            check_path_is_valid(stable_path.1);
263            check_path_args_equal(ref_path, stable_path.1);
264
265            let current_version = stable_path.0;
266
267            if let Some(prev_seen_version) = prev_seen_version {
268                let cmp_result = current_version.const_ord(&prev_seen_version);
269
270                if cmp_result.is_eq() {
271                    // Found a duplicate, current == previous
272                    panic!("Duplicate matrix version in stable_paths")
273                } else if cmp_result.is_lt() {
274                    // Found an older version, current < previous
275                    panic!("No ascending order in stable_paths")
276                }
277            }
278
279            prev_seen_version = Some(current_version);
280        });
281
282        if let Some(deprecated) = deprecated {
283            if let Some(prev_seen_version) = prev_seen_version {
284                let ord_result = prev_seen_version.const_ord(&deprecated);
285                if !deprecated.is_legacy() && ord_result.is_eq() {
286                    // prev_seen_version == deprecated, except for 1.0.
287                    // It is possible that an endpoint was both made stable and deprecated in the
288                    // legacy versions.
289                    panic!("deprecated version is equal to latest stable path version")
290                } else if ord_result.is_gt() {
291                    // prev_seen_version > deprecated
292                    panic!("deprecated version is older than latest stable path version")
293                }
294            } else {
295                panic!("Defined deprecated version while no stable path exists")
296            }
297        }
298
299        if let Some(removed) = removed {
300            if let Some(deprecated) = deprecated {
301                let ord_result = deprecated.const_ord(&removed);
302                if ord_result.is_eq() {
303                    // deprecated == removed
304                    panic!("removed version is equal to deprecated version")
305                } else if ord_result.is_gt() {
306                    // deprecated > removed
307                    panic!("removed version is older than deprecated version")
308                }
309            } else {
310                panic!("Defined removed version while no deprecated version exists")
311            }
312        }
313
314        VersionHistory { unstable_paths, stable_paths, deprecated, removed }
315    }
316
317    // This function helps picks the right path (or an error) from a set of Matrix versions.
318    fn select_path(&self, versions: &[MatrixVersion]) -> Result<&'static str, IntoHttpError> {
319        match self.versioning_decision_for(versions) {
320            VersioningDecision::Removed => Err(IntoHttpError::EndpointRemoved(
321                self.removed.expect("VersioningDecision::Removed implies metadata.removed"),
322            )),
323            VersioningDecision::Stable { any_deprecated, all_deprecated, any_removed } => {
324                if any_removed {
325                    if all_deprecated {
326                        warn!(
327                            "endpoint is removed in some (and deprecated in ALL) \
328                             of the following versions: {versions:?}",
329                        );
330                    } else if any_deprecated {
331                        warn!(
332                            "endpoint is removed (and deprecated) in some of the \
333                             following versions: {versions:?}",
334                        );
335                    } else {
336                        unreachable!("any_removed implies *_deprecated");
337                    }
338                } else if all_deprecated {
339                    warn!(
340                        "endpoint is deprecated in ALL of the following versions: \
341                         {versions:?}",
342                    );
343                } else if any_deprecated {
344                    warn!(
345                        "endpoint is deprecated in some of the following versions: \
346                         {versions:?}",
347                    );
348                }
349
350                Ok(self
351                    .stable_endpoint_for(versions)
352                    .expect("VersioningDecision::Stable implies that a stable path exists"))
353            }
354            VersioningDecision::Unstable => self.unstable().ok_or(IntoHttpError::NoUnstablePath),
355        }
356    }
357
358    /// Will decide how a particular set of Matrix versions sees an endpoint.
359    ///
360    /// It will only return `Deprecated` or `Removed` if all versions denote it.
361    ///
362    /// In other words, if in any version it tells it supports the endpoint in a stable fashion,
363    /// this will return `Stable`, even if some versions in this set will denote deprecation or
364    /// removal.
365    ///
366    /// If resulting [`VersioningDecision`] is `Stable`, it will also detail if any version denoted
367    /// deprecation or removal.
368    pub fn versioning_decision_for(&self, versions: &[MatrixVersion]) -> VersioningDecision {
369        let greater_or_equal_any =
370            |version: MatrixVersion| versions.iter().any(|v| v.is_superset_of(version));
371        let greater_or_equal_all =
372            |version: MatrixVersion| versions.iter().all(|v| v.is_superset_of(version));
373
374        // Check if all versions removed this endpoint.
375        if self.removed.is_some_and(greater_or_equal_all) {
376            return VersioningDecision::Removed;
377        }
378
379        // Check if *any* version marks this endpoint as stable.
380        if self.added_in().is_some_and(greater_or_equal_any) {
381            let all_deprecated = self.deprecated.is_some_and(greater_or_equal_all);
382
383            return VersioningDecision::Stable {
384                any_deprecated: all_deprecated || self.deprecated.is_some_and(greater_or_equal_any),
385                all_deprecated,
386                any_removed: self.removed.is_some_and(greater_or_equal_any),
387            };
388        }
389
390        VersioningDecision::Unstable
391    }
392
393    /// Returns the *first* version this endpoint was added in.
394    ///
395    /// Is `None` when this endpoint is unstable/unreleased.
396    pub fn added_in(&self) -> Option<MatrixVersion> {
397        self.stable_paths.first().map(|(v, _)| *v)
398    }
399
400    /// Returns the Matrix version that deprecated this endpoint, if any.
401    pub fn deprecated_in(&self) -> Option<MatrixVersion> {
402        self.deprecated
403    }
404
405    /// Returns the Matrix version that removed this endpoint, if any.
406    pub fn removed_in(&self) -> Option<MatrixVersion> {
407        self.removed
408    }
409
410    /// Picks the last unstable path, if it exists.
411    pub fn unstable(&self) -> Option<&'static str> {
412        self.unstable_paths.last().copied()
413    }
414
415    /// Returns all path variants in canon form, for use in server routers.
416    pub fn all_paths(&self) -> impl Iterator<Item = &'static str> {
417        self.unstable_paths().chain(self.stable_paths().map(|(_, path)| path))
418    }
419
420    /// Returns all unstable path variants in canon form.
421    pub fn unstable_paths(&self) -> impl Iterator<Item = &'static str> {
422        self.unstable_paths.iter().copied()
423    }
424
425    /// Returns all stable path variants in canon form, with corresponding Matrix version.
426    pub fn stable_paths(&self) -> impl Iterator<Item = (MatrixVersion, &'static str)> {
427        self.stable_paths.iter().map(|(version, data)| (*version, *data))
428    }
429
430    /// The path that should be used to query the endpoint, given a series of versions.
431    ///
432    /// This will pick the latest path that the version accepts.
433    ///
434    /// This will return an endpoint in the following format;
435    /// - `/_matrix/client/versions`
436    /// - `/_matrix/client/hello/:world` (`:world` is a path replacement parameter)
437    ///
438    /// Note: This will not keep in mind endpoint removals, check with
439    /// [`versioning_decision_for`](VersionHistory::versioning_decision_for) to see if this endpoint
440    /// is still available.
441    pub fn stable_endpoint_for(&self, versions: &[MatrixVersion]) -> Option<&'static str> {
442        // Go reverse, to check the "latest" version first.
443        for (ver, path) in self.stable_paths.iter().rev() {
444            // Check if any of the versions are equal or greater than the version the path needs.
445            if versions.iter().any(|v| v.is_superset_of(*ver)) {
446                return Some(path);
447            }
448        }
449
450        None
451    }
452}
453
454/// A versioning "decision" derived from a set of Matrix versions.
455#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
456#[allow(clippy::exhaustive_enums)]
457pub enum VersioningDecision {
458    /// The unstable endpoint should be used.
459    Unstable,
460
461    /// The stable endpoint should be used.
462    Stable {
463        /// If any version denoted deprecation.
464        any_deprecated: bool,
465
466        /// If *all* versions denoted deprecation.
467        all_deprecated: bool,
468
469        /// If any version denoted removal.
470        any_removed: bool,
471    },
472
473    /// This endpoint was removed in all versions, it should not be used.
474    Removed,
475}
476
477/// The Matrix versions Ruma currently understands to exist.
478///
479/// Matrix, since fall 2021, has a quarterly release schedule, using a global `vX.Y` versioning
480/// scheme. Usually `Y` is bumped for new backwards compatible changes, but `X` can be bumped
481/// instead when a large number of `Y` changes feel deserving of a major version increase.
482///
483/// Every new version denotes stable support for endpoints in a *relatively* backwards-compatible
484/// manner.
485///
486/// Matrix has a deprecation policy, read more about it here: <https://spec.matrix.org/latest/#deprecation-policy>.
487///
488/// Ruma keeps track of when endpoints are added, deprecated, and removed. It'll automatically
489/// select the right endpoint stability variation to use depending on which Matrix versions you
490/// pass to [`try_into_http_request`](super::OutgoingRequest::try_into_http_request), see its
491/// respective documentation for more information.
492///
493/// The `PartialOrd` and `Ord` implementations of this type sort the variants by release date. A
494/// newer release is greater than an older release.
495///
496/// `MatrixVersion::is_superset_of()` is used to keep track of compatibility between versions.
497#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
498#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
499pub enum MatrixVersion {
500    /// Matrix 1.0 was a release prior to the global versioning system and does not correspond to a
501    /// version of the Matrix specification.
502    ///
503    /// It matches the following per-API versions:
504    ///
505    /// * Client-Server API: r0.5.0 to r0.6.1
506    /// * Identity Service API: r0.2.0 to r0.3.0
507    ///
508    /// The other APIs are not supported because they do not have a `GET /versions` endpoint.
509    ///
510    /// See <https://spec.matrix.org/latest/#legacy-versioning>.
511    V1_0,
512
513    /// Version 1.1 of the Matrix specification, released in Q4 2021.
514    ///
515    /// See <https://spec.matrix.org/v1.1/>.
516    V1_1,
517
518    /// Version 1.2 of the Matrix specification, released in Q1 2022.
519    ///
520    /// See <https://spec.matrix.org/v1.2/>.
521    V1_2,
522
523    /// Version 1.3 of the Matrix specification, released in Q2 2022.
524    ///
525    /// See <https://spec.matrix.org/v1.3/>.
526    V1_3,
527
528    /// Version 1.4 of the Matrix specification, released in Q3 2022.
529    ///
530    /// See <https://spec.matrix.org/v1.4/>.
531    V1_4,
532
533    /// Version 1.5 of the Matrix specification, released in Q4 2022.
534    ///
535    /// See <https://spec.matrix.org/v1.5/>.
536    V1_5,
537
538    /// Version 1.6 of the Matrix specification, released in Q1 2023.
539    ///
540    /// See <https://spec.matrix.org/v1.6/>.
541    V1_6,
542
543    /// Version 1.7 of the Matrix specification, released in Q2 2023.
544    ///
545    /// See <https://spec.matrix.org/v1.7/>.
546    V1_7,
547
548    /// Version 1.8 of the Matrix specification, released in Q3 2023.
549    ///
550    /// See <https://spec.matrix.org/v1.8/>.
551    V1_8,
552
553    /// Version 1.9 of the Matrix specification, released in Q4 2023.
554    ///
555    /// See <https://spec.matrix.org/v1.9/>.
556    V1_9,
557
558    /// Version 1.10 of the Matrix specification, released in Q1 2024.
559    ///
560    /// See <https://spec.matrix.org/v1.10/>.
561    V1_10,
562
563    /// Version 1.11 of the Matrix specification, released in Q2 2024.
564    ///
565    /// See <https://spec.matrix.org/v1.11/>.
566    V1_11,
567
568    /// Version 1.12 of the Matrix specification, released in Q3 2024.
569    ///
570    /// See <https://spec.matrix.org/v1.12/>.
571    V1_12,
572
573    /// Version 1.13 of the Matrix specification, released in Q4 2024.
574    ///
575    /// See <https://spec.matrix.org/v1.13/>.
576    V1_13,
577
578    /// Version 1.14 of the Matrix specification, released in Q1 2025.
579    ///
580    /// See <https://spec.matrix.org/v1.14/>.
581    V1_14,
582
583    /// Version 1.15 of the Matrix specification, released in Q2 2025.
584    ///
585    /// See <https://spec.matrix.org/v1.15/>.
586    V1_15,
587}
588
589impl TryFrom<&str> for MatrixVersion {
590    type Error = UnknownVersionError;
591
592    fn try_from(value: &str) -> Result<MatrixVersion, Self::Error> {
593        use MatrixVersion::*;
594
595        Ok(match value {
596            // Identity service API versions between Matrix 1.0 and 1.1.
597            // They might match older client-server API versions but that should not be a problem in practice.
598            "r0.2.0" | "r0.2.1" | "r0.3.0" |
599            // Client-server API versions between Matrix 1.0 and 1.1.
600            "r0.5.0" | "r0.6.0" | "r0.6.1" => V1_0,
601            "v1.1" => V1_1,
602            "v1.2" => V1_2,
603            "v1.3" => V1_3,
604            "v1.4" => V1_4,
605            "v1.5" => V1_5,
606            "v1.6" => V1_6,
607            "v1.7" => V1_7,
608            "v1.8" => V1_8,
609            "v1.9" => V1_9,
610            "v1.10" => V1_10,
611            "v1.11" => V1_11,
612            "v1.12" => V1_12,
613            "v1.13" => V1_13,
614            "v1.14" => V1_14,
615            "v1.15" => V1_15,
616            _ => return Err(UnknownVersionError),
617        })
618    }
619}
620
621impl FromStr for MatrixVersion {
622    type Err = UnknownVersionError;
623
624    fn from_str(s: &str) -> Result<Self, Self::Err> {
625        Self::try_from(s)
626    }
627}
628
629impl MatrixVersion {
630    /// Checks whether a version is compatible with another.
631    ///
632    /// Currently, all versions of Matrix are considered backwards compatible with all the previous
633    /// versions, so this is equivalent to `self >= other`. This behaviour may change in the future,
634    /// if a new release is considered to be breaking compatibility with the previous ones.
635    ///
636    /// > ⚠ Matrix has a deprecation policy, and Matrix versioning is not as straightforward as this
637    /// > function makes it out to be. This function only exists to prune breaking changes between
638    /// > versions, and versions too new for `self`.
639    pub fn is_superset_of(self, other: Self) -> bool {
640        self >= other
641    }
642
643    /// Decompose the Matrix version into its major and minor number.
644    pub const fn into_parts(self) -> (u8, u8) {
645        match self {
646            MatrixVersion::V1_0 => (1, 0),
647            MatrixVersion::V1_1 => (1, 1),
648            MatrixVersion::V1_2 => (1, 2),
649            MatrixVersion::V1_3 => (1, 3),
650            MatrixVersion::V1_4 => (1, 4),
651            MatrixVersion::V1_5 => (1, 5),
652            MatrixVersion::V1_6 => (1, 6),
653            MatrixVersion::V1_7 => (1, 7),
654            MatrixVersion::V1_8 => (1, 8),
655            MatrixVersion::V1_9 => (1, 9),
656            MatrixVersion::V1_10 => (1, 10),
657            MatrixVersion::V1_11 => (1, 11),
658            MatrixVersion::V1_12 => (1, 12),
659            MatrixVersion::V1_13 => (1, 13),
660            MatrixVersion::V1_14 => (1, 14),
661            MatrixVersion::V1_15 => (1, 15),
662        }
663    }
664
665    /// Try to turn a pair of (major, minor) version components back into a `MatrixVersion`.
666    pub const fn from_parts(major: u8, minor: u8) -> Result<Self, UnknownVersionError> {
667        match (major, minor) {
668            (1, 0) => Ok(MatrixVersion::V1_0),
669            (1, 1) => Ok(MatrixVersion::V1_1),
670            (1, 2) => Ok(MatrixVersion::V1_2),
671            (1, 3) => Ok(MatrixVersion::V1_3),
672            (1, 4) => Ok(MatrixVersion::V1_4),
673            (1, 5) => Ok(MatrixVersion::V1_5),
674            (1, 6) => Ok(MatrixVersion::V1_6),
675            (1, 7) => Ok(MatrixVersion::V1_7),
676            (1, 8) => Ok(MatrixVersion::V1_8),
677            (1, 9) => Ok(MatrixVersion::V1_9),
678            (1, 10) => Ok(MatrixVersion::V1_10),
679            (1, 11) => Ok(MatrixVersion::V1_11),
680            (1, 12) => Ok(MatrixVersion::V1_12),
681            (1, 13) => Ok(MatrixVersion::V1_13),
682            (1, 14) => Ok(MatrixVersion::V1_14),
683            (1, 15) => Ok(MatrixVersion::V1_15),
684            _ => Err(UnknownVersionError),
685        }
686    }
687
688    /// Constructor for use by the `metadata!` macro.
689    ///
690    /// Accepts string literals and parses them.
691    #[doc(hidden)]
692    pub const fn from_lit(lit: &'static str) -> Self {
693        use konst::{option, primitive::parse_u8, result, string};
694
695        let major: u8;
696        let minor: u8;
697
698        let mut lit_iter = string::split(lit, ".").next();
699
700        {
701            let (checked_first, checked_split) = option::unwrap!(lit_iter); // First iteration always succeeds
702
703            major = result::unwrap_or_else!(parse_u8(checked_first), |_| panic!(
704                "major version is not a valid number"
705            ));
706
707            lit_iter = checked_split.next();
708        }
709
710        match lit_iter {
711            Some((checked_second, checked_split)) => {
712                minor = result::unwrap_or_else!(parse_u8(checked_second), |_| panic!(
713                    "minor version is not a valid number"
714                ));
715
716                lit_iter = checked_split.next();
717            }
718            None => panic!("could not find dot to denote second number"),
719        }
720
721        if lit_iter.is_some() {
722            panic!("version literal contains more than one dot")
723        }
724
725        result::unwrap_or_else!(Self::from_parts(major, minor), |_| panic!(
726            "not a valid version literal"
727        ))
728    }
729
730    // Internal function to do ordering in const-fn contexts
731    const fn const_ord(&self, other: &Self) -> Ordering {
732        let self_parts = self.into_parts();
733        let other_parts = other.into_parts();
734
735        use konst::primitive::cmp::cmp_u8;
736
737        let major_ord = cmp_u8(self_parts.0, other_parts.0);
738        if major_ord.is_ne() {
739            major_ord
740        } else {
741            cmp_u8(self_parts.1, other_parts.1)
742        }
743    }
744
745    // Internal function to check if this version is the legacy (v1.0) version in const-fn contexts
746    const fn is_legacy(&self) -> bool {
747        let self_parts = self.into_parts();
748
749        use konst::primitive::cmp::cmp_u8;
750
751        cmp_u8(self_parts.0, 1).is_eq() && cmp_u8(self_parts.1, 0).is_eq()
752    }
753
754    /// Get the default [`RoomVersionId`] for this `MatrixVersion`.
755    pub fn default_room_version(&self) -> RoomVersionId {
756        match self {
757            // <https://spec.matrix.org/historical/index.html#complete-list-of-room-versions>
758            MatrixVersion::V1_0
759            // <https://spec.matrix.org/v1.1/rooms/#complete-list-of-room-versions>
760            | MatrixVersion::V1_1
761            // <https://spec.matrix.org/v1.2/rooms/#complete-list-of-room-versions>
762            | MatrixVersion::V1_2 => RoomVersionId::V6,
763            // <https://spec.matrix.org/v1.3/rooms/#complete-list-of-room-versions>
764            MatrixVersion::V1_3
765            // <https://spec.matrix.org/v1.4/rooms/#complete-list-of-room-versions>
766            | MatrixVersion::V1_4
767            // <https://spec.matrix.org/v1.5/rooms/#complete-list-of-room-versions>
768            | MatrixVersion::V1_5 => RoomVersionId::V9,
769            // <https://spec.matrix.org/v1.6/rooms/#complete-list-of-room-versions>
770            MatrixVersion::V1_6
771            // <https://spec.matrix.org/v1.7/rooms/#complete-list-of-room-versions>
772            | MatrixVersion::V1_7
773            // <https://spec.matrix.org/v1.8/rooms/#complete-list-of-room-versions>
774            | MatrixVersion::V1_8
775            // <https://spec.matrix.org/v1.9/rooms/#complete-list-of-room-versions>
776            | MatrixVersion::V1_9
777            // <https://spec.matrix.org/v1.10/rooms/#complete-list-of-room-versions>
778            | MatrixVersion::V1_10
779            // <https://spec.matrix.org/v1.11/rooms/#complete-list-of-room-versions>
780            | MatrixVersion::V1_11
781            // <https://spec.matrix.org/v1.12/rooms/#complete-list-of-room-versions>
782            | MatrixVersion::V1_12
783            // <https://spec.matrix.org/v1.13/rooms/#complete-list-of-room-versions>
784            | MatrixVersion::V1_13 => RoomVersionId::V10,
785            // <https://spec.matrix.org/v1.14/rooms/#complete-list-of-room-versions>
786            | MatrixVersion::V1_14
787            // <https://spec.matrix.org/v1.15/rooms/#complete-list-of-room-versions>
788            | MatrixVersion::V1_15 => RoomVersionId::V11,
789        }
790    }
791}
792
793impl Display for MatrixVersion {
794    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
795        let (major, minor) = self.into_parts();
796        f.write_str(&format!("v{major}.{minor}"))
797    }
798}
799
800/// The list of Matrix versions and features supported by a homeserver.
801#[derive(Debug, Clone)]
802#[allow(clippy::exhaustive_structs)]
803pub struct SupportedVersions {
804    /// The Matrix versions that are supported by the homeserver.
805    ///
806    /// This array contains only known versions.
807    pub versions: Box<[MatrixVersion]>,
808
809    /// The features that are supported by the homeserver.
810    ///
811    /// This matches the `unstable_features` field of the `/versions` endpoint, without the boolean
812    /// value.
813    pub features: BTreeSet<FeatureFlag>,
814}
815
816impl SupportedVersions {
817    /// Construct a `SupportedVersions` from the parts of a `/versions` response.
818    ///
819    /// Matrix versions that can't be parsed to a `MatrixVersion`, and features with the boolean
820    /// value set to `false` are discarded.
821    pub fn from_parts(versions: &[String], unstable_features: &BTreeMap<String, bool>) -> Self {
822        Self {
823            versions: versions
824                .iter()
825                // Parse, discard unknown versions
826                .flat_map(|s| s.parse::<MatrixVersion>())
827                // Map to key-value pairs where the key is the major-minor representation
828                // (which can be used as a BTreeMap unlike MatrixVersion itself)
829                .map(|v| (v.into_parts(), v))
830                // Collect to BTreeMap
831                .collect::<BTreeMap<_, _>>()
832                // Return an iterator over just the values (`MatrixVersion`s)
833                .into_values()
834                .collect(),
835            features: unstable_features
836                .iter()
837                .filter(|(_, enabled)| **enabled)
838                .map(|(feature, _)| feature.as_str().into())
839                .collect(),
840        }
841    }
842}
843
844/// The Matrix features supported by Ruma.
845///
846/// Features that are not behind a cargo feature are features that are part of the Matrix
847/// specification and that Ruma still supports, like the unstable version of an endpoint or a stable
848/// feature. Features behind a cargo feature are only supported when this feature is enabled.
849#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
850#[derive(Clone, StringEnum, PartialEqAsRefStr, Eq, Hash, PartialOrdAsRefStr, OrdAsRefStr)]
851#[non_exhaustive]
852pub enum FeatureFlag {
853    /// `fi.mau.msc2246` ([MSC])
854    ///
855    /// Asynchronous media uploads.
856    ///
857    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/2246
858    #[ruma_enum(rename = "fi.mau.msc2246")]
859    Msc2246,
860
861    /// `org.matrix.msc2432` ([MSC])
862    ///
863    /// Updated semantics for publishing room aliases.
864    ///
865    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/2432
866    #[ruma_enum(rename = "org.matrix.msc2432")]
867    Msc2432,
868
869    /// `fi.mau.msc2659` ([MSC])
870    ///
871    /// Application service ping endpoint.
872    ///
873    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/2659
874    #[ruma_enum(rename = "fi.mau.msc2659")]
875    Msc2659,
876
877    /// `fi.mau.msc2659` ([MSC])
878    ///
879    /// Stable version of the application service ping endpoint.
880    ///
881    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/2659
882    #[ruma_enum(rename = "fi.mau.msc2659.stable")]
883    Msc2659Stable,
884
885    /// `uk.half-shot.msc2666.query_mutual_rooms` ([MSC])
886    ///
887    /// Get rooms in common with another user.
888    ///
889    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/2666
890    #[cfg(feature = "unstable-msc2666")]
891    #[ruma_enum(rename = "uk.half-shot.msc2666.query_mutual_rooms")]
892    Msc2666,
893
894    /// `org.matrix.msc3030` ([MSC])
895    ///
896    /// Jump to date API endpoint.
897    ///
898    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/3030
899    #[ruma_enum(rename = "org.matrix.msc3030")]
900    Msc3030,
901
902    /// `org.matrix.msc3882` ([MSC])
903    ///
904    /// Allow an existing session to sign in a new session.
905    ///
906    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/3882
907    #[ruma_enum(rename = "org.matrix.msc3882")]
908    Msc3882,
909
910    /// `org.matrix.msc3916` ([MSC])
911    ///
912    /// Authentication for media.
913    ///
914    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/3916
915    #[ruma_enum(rename = "org.matrix.msc3916")]
916    Msc3916,
917
918    /// `org.matrix.msc3916.stable` ([MSC])
919    ///
920    /// Stable version of authentication for media.
921    ///
922    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/3916
923    #[ruma_enum(rename = "org.matrix.msc3916.stable")]
924    Msc3916Stable,
925
926    /// `org.matrix.msc4108` ([MSC])
927    ///
928    /// Mechanism to allow OIDC sign in and E2EE set up via QR code.
929    ///
930    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
931    #[cfg(feature = "unstable-msc4108")]
932    #[ruma_enum(rename = "org.matrix.msc4108")]
933    Msc4108,
934
935    /// `org.matrix.msc4140` ([MSC])
936    ///
937    /// Delayed events.
938    ///
939    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4140
940    #[cfg(feature = "unstable-msc4140")]
941    #[ruma_enum(rename = "org.matrix.msc4140")]
942    Msc4140,
943
944    /// `org.matrix.simplified_msc3575` ([MSC])
945    ///
946    /// Simplified Sliding Sync.
947    ///
948    /// [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4186
949    #[cfg(feature = "unstable-msc4186")]
950    #[ruma_enum(rename = "org.matrix.simplified_msc3575")]
951    Msc4186,
952
953    #[doc(hidden)]
954    _Custom(PrivOwnedStr),
955}
956
957#[cfg(test)]
958mod tests {
959    use std::collections::{BTreeMap, BTreeSet};
960
961    use assert_matches2::assert_matches;
962    use http::Method;
963
964    use super::{
965        AuthScheme,
966        MatrixVersion::{self, V1_0, V1_1, V1_2, V1_3},
967        Metadata, SupportedVersions, VersionHistory,
968    };
969    use crate::api::error::IntoHttpError;
970
971    fn stable_only_metadata(stable_paths: &'static [(MatrixVersion, &'static str)]) -> Metadata {
972        Metadata {
973            method: Method::GET,
974            rate_limited: false,
975            authentication: AuthScheme::None,
976            history: VersionHistory {
977                unstable_paths: &[],
978                stable_paths,
979                deprecated: None,
980                removed: None,
981            },
982        }
983    }
984
985    // TODO add test that can hook into tracing and verify the deprecation warning is emitted
986
987    #[test]
988    fn make_simple_endpoint_url() {
989        let meta = stable_only_metadata(&[(V1_0, "/s")]);
990        let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], "").unwrap();
991        assert_eq!(url, "https://example.org/s");
992    }
993
994    #[test]
995    fn make_endpoint_url_with_path_args() {
996        let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
997        let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[&"123"], "").unwrap();
998        assert_eq!(url, "https://example.org/s/123");
999    }
1000
1001    #[test]
1002    fn make_endpoint_url_with_path_args_with_dash() {
1003        let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
1004        let url =
1005            meta.make_endpoint_url(&[V1_0], "https://example.org", &[&"my-path"], "").unwrap();
1006        assert_eq!(url, "https://example.org/s/my-path");
1007    }
1008
1009    #[test]
1010    fn make_endpoint_url_with_path_args_with_reserved_char() {
1011        let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
1012        let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[&"#path"], "").unwrap();
1013        assert_eq!(url, "https://example.org/s/%23path");
1014    }
1015
1016    #[test]
1017    fn make_endpoint_url_with_query() {
1018        let meta = stable_only_metadata(&[(V1_0, "/s/")]);
1019        let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], "foo=bar").unwrap();
1020        assert_eq!(url, "https://example.org/s/?foo=bar");
1021    }
1022
1023    #[test]
1024    #[should_panic]
1025    fn make_endpoint_url_wrong_num_path_args() {
1026        let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
1027        _ = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], "");
1028    }
1029
1030    const EMPTY: VersionHistory =
1031        VersionHistory { unstable_paths: &[], stable_paths: &[], deprecated: None, removed: None };
1032
1033    #[test]
1034    fn select_latest_stable() {
1035        let hist = VersionHistory { stable_paths: &[(V1_1, "/s")], ..EMPTY };
1036        assert_matches!(hist.select_path(&[V1_0, V1_1]), Ok("/s"));
1037    }
1038
1039    #[test]
1040    fn select_unstable() {
1041        let hist = VersionHistory { unstable_paths: &["/u"], ..EMPTY };
1042        assert_matches!(hist.select_path(&[V1_0]), Ok("/u"));
1043    }
1044
1045    #[test]
1046    fn select_r0() {
1047        let hist = VersionHistory { stable_paths: &[(V1_0, "/r")], ..EMPTY };
1048        assert_matches!(hist.select_path(&[V1_0]), Ok("/r"));
1049    }
1050
1051    #[test]
1052    fn select_removed_err() {
1053        let hist = VersionHistory {
1054            stable_paths: &[(V1_0, "/r"), (V1_1, "/s")],
1055            unstable_paths: &["/u"],
1056            deprecated: Some(V1_2),
1057            removed: Some(V1_3),
1058        };
1059        assert_matches!(hist.select_path(&[V1_3]), Err(IntoHttpError::EndpointRemoved(V1_3)));
1060    }
1061
1062    #[test]
1063    fn partially_removed_but_stable() {
1064        let hist = VersionHistory {
1065            stable_paths: &[(V1_0, "/r"), (V1_1, "/s")],
1066            unstable_paths: &[],
1067            deprecated: Some(V1_2),
1068            removed: Some(V1_3),
1069        };
1070        assert_matches!(hist.select_path(&[V1_2]), Ok("/s"));
1071    }
1072
1073    #[test]
1074    fn no_unstable() {
1075        let hist = VersionHistory { stable_paths: &[(V1_1, "/s")], ..EMPTY };
1076        assert_matches!(hist.select_path(&[V1_0]), Err(IntoHttpError::NoUnstablePath));
1077    }
1078
1079    #[test]
1080    fn version_literal() {
1081        const LIT: MatrixVersion = MatrixVersion::from_lit("1.0");
1082
1083        assert_eq!(LIT, V1_0);
1084    }
1085
1086    #[test]
1087    fn supported_versions_from_parts() {
1088        let empty_features = BTreeMap::new();
1089
1090        let none = &[];
1091        let none_supported = SupportedVersions::from_parts(none, &empty_features);
1092        assert_eq!(none_supported.versions.as_ref(), &[]);
1093        assert_eq!(none_supported.features, BTreeSet::new());
1094
1095        let single_known = &["r0.6.0".to_owned()];
1096        let single_known_supported = SupportedVersions::from_parts(single_known, &empty_features);
1097        assert_eq!(single_known_supported.versions.as_ref(), &[V1_0]);
1098        assert_eq!(single_known_supported.features, BTreeSet::new());
1099
1100        let multiple_known = &["v1.1".to_owned(), "r0.6.0".to_owned(), "r0.6.1".to_owned()];
1101        let multiple_known_supported =
1102            SupportedVersions::from_parts(multiple_known, &empty_features);
1103        assert_eq!(multiple_known_supported.versions.as_ref(), &[V1_0, V1_1]);
1104        assert_eq!(multiple_known_supported.features, BTreeSet::new());
1105
1106        let single_unknown = &["v0.0".to_owned()];
1107        let single_unknown_supported =
1108            SupportedVersions::from_parts(single_unknown, &empty_features);
1109        assert_eq!(single_unknown_supported.versions.as_ref(), &[]);
1110        assert_eq!(single_unknown_supported.features, BTreeSet::new());
1111
1112        let mut features = BTreeMap::new();
1113        features.insert("org.bar.enabled_1".to_owned(), true);
1114        features.insert("org.bar.disabled".to_owned(), false);
1115        features.insert("org.bar.enabled_2".to_owned(), true);
1116
1117        let features_supported = SupportedVersions::from_parts(single_known, &features);
1118        assert_eq!(features_supported.versions.as_ref(), &[V1_0]);
1119        assert_eq!(
1120            features_supported.features,
1121            ["org.bar.enabled_1".into(), "org.bar.enabled_2".into()].into()
1122        );
1123    }
1124}