ruma_common/api/
metadata.rs

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