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
584impl TryFrom<&str> for MatrixVersion {
585    type Error = UnknownVersionError;
586
587    fn try_from(value: &str) -> Result<MatrixVersion, Self::Error> {
588        use MatrixVersion::*;
589
590        Ok(match value {
591            // Identity service API versions between Matrix 1.0 and 1.1.
592            // They might match older client-server API versions but that should not be a problem in practice.
593            "r0.2.0" | "r0.2.1" | "r0.3.0" |
594            // Client-server API versions between Matrix 1.0 and 1.1.
595            "r0.5.0" | "r0.6.0" | "r0.6.1" => V1_0,
596            "v1.1" => V1_1,
597            "v1.2" => V1_2,
598            "v1.3" => V1_3,
599            "v1.4" => V1_4,
600            "v1.5" => V1_5,
601            "v1.6" => V1_6,
602            "v1.7" => V1_7,
603            "v1.8" => V1_8,
604            "v1.9" => V1_9,
605            "v1.10" => V1_10,
606            "v1.11" => V1_11,
607            "v1.12" => V1_12,
608            "v1.13" => V1_13,
609            _ => return Err(UnknownVersionError),
610        })
611    }
612}
613
614impl FromStr for MatrixVersion {
615    type Err = UnknownVersionError;
616
617    fn from_str(s: &str) -> Result<Self, Self::Err> {
618        Self::try_from(s)
619    }
620}
621
622impl MatrixVersion {
623    /// Checks whether a version is compatible with another.
624    ///
625    /// Currently, all versions of Matrix are considered backwards compatible with all the previous
626    /// versions, so this is equivalent to `self >= other`. This behaviour may change in the future,
627    /// if a new release is considered to be breaking compatibility with the previous ones.
628    ///
629    /// > ⚠ Matrix has a deprecation policy, and Matrix versioning is not as straightforward as this
630    /// > function makes it out to be. This function only exists to prune breaking changes between
631    /// > versions, and versions too new for `self`.
632    pub fn is_superset_of(self, other: Self) -> bool {
633        self >= other
634    }
635
636    /// Get a string representation of this Matrix version.
637    ///
638    /// This is the string that can be found in the response to one of the `GET /versions`
639    /// endpoints. Parsing this string will give the same variant.
640    ///
641    /// Returns `None` for [`MatrixVersion::V1_0`] because it can match several per-API versions.
642    pub const fn as_str(self) -> Option<&'static str> {
643        let string = match self {
644            MatrixVersion::V1_0 => return None,
645            MatrixVersion::V1_1 => "v1.1",
646            MatrixVersion::V1_2 => "v1.2",
647            MatrixVersion::V1_3 => "v1.3",
648            MatrixVersion::V1_4 => "v1.4",
649            MatrixVersion::V1_5 => "v1.5",
650            MatrixVersion::V1_6 => "v1.6",
651            MatrixVersion::V1_7 => "v1.7",
652            MatrixVersion::V1_8 => "v1.8",
653            MatrixVersion::V1_9 => "v1.9",
654            MatrixVersion::V1_10 => "v1.10",
655            MatrixVersion::V1_11 => "v1.11",
656            MatrixVersion::V1_12 => "v1.12",
657            MatrixVersion::V1_13 => "v1.13",
658        };
659
660        Some(string)
661    }
662
663    /// Decompose the Matrix version into its major and minor number.
664    const fn into_parts(self) -> (u8, u8) {
665        match self {
666            MatrixVersion::V1_0 => (1, 0),
667            MatrixVersion::V1_1 => (1, 1),
668            MatrixVersion::V1_2 => (1, 2),
669            MatrixVersion::V1_3 => (1, 3),
670            MatrixVersion::V1_4 => (1, 4),
671            MatrixVersion::V1_5 => (1, 5),
672            MatrixVersion::V1_6 => (1, 6),
673            MatrixVersion::V1_7 => (1, 7),
674            MatrixVersion::V1_8 => (1, 8),
675            MatrixVersion::V1_9 => (1, 9),
676            MatrixVersion::V1_10 => (1, 10),
677            MatrixVersion::V1_11 => (1, 11),
678            MatrixVersion::V1_12 => (1, 12),
679            MatrixVersion::V1_13 => (1, 13),
680        }
681    }
682
683    /// Try to turn a pair of (major, minor) version components back into a `MatrixVersion`.
684    const fn from_parts(major: u8, minor: u8) -> Result<Self, UnknownVersionError> {
685        match (major, minor) {
686            (1, 0) => Ok(MatrixVersion::V1_0),
687            (1, 1) => Ok(MatrixVersion::V1_1),
688            (1, 2) => Ok(MatrixVersion::V1_2),
689            (1, 3) => Ok(MatrixVersion::V1_3),
690            (1, 4) => Ok(MatrixVersion::V1_4),
691            (1, 5) => Ok(MatrixVersion::V1_5),
692            (1, 6) => Ok(MatrixVersion::V1_6),
693            (1, 7) => Ok(MatrixVersion::V1_7),
694            (1, 8) => Ok(MatrixVersion::V1_8),
695            (1, 9) => Ok(MatrixVersion::V1_9),
696            (1, 10) => Ok(MatrixVersion::V1_10),
697            (1, 11) => Ok(MatrixVersion::V1_11),
698            (1, 12) => Ok(MatrixVersion::V1_12),
699            (1, 13) => Ok(MatrixVersion::V1_13),
700            _ => Err(UnknownVersionError),
701        }
702    }
703
704    /// Constructor for use by the `metadata!` macro.
705    ///
706    /// Accepts string literals and parses them.
707    #[doc(hidden)]
708    pub const fn from_lit(lit: &'static str) -> Self {
709        use konst::{option, primitive::parse_u8, result, string};
710
711        let major: u8;
712        let minor: u8;
713
714        let mut lit_iter = string::split(lit, ".").next();
715
716        {
717            let (checked_first, checked_split) = option::unwrap!(lit_iter); // First iteration always succeeds
718
719            major = result::unwrap_or_else!(parse_u8(checked_first), |_| panic!(
720                "major version is not a valid number"
721            ));
722
723            lit_iter = checked_split.next();
724        }
725
726        match lit_iter {
727            Some((checked_second, checked_split)) => {
728                minor = result::unwrap_or_else!(parse_u8(checked_second), |_| panic!(
729                    "minor version is not a valid number"
730                ));
731
732                lit_iter = checked_split.next();
733            }
734            None => panic!("could not find dot to denote second number"),
735        }
736
737        if lit_iter.is_some() {
738            panic!("version literal contains more than one dot")
739        }
740
741        result::unwrap_or_else!(Self::from_parts(major, minor), |_| panic!(
742            "not a valid version literal"
743        ))
744    }
745
746    // Internal function to do ordering in const-fn contexts
747    const fn const_ord(&self, other: &Self) -> Ordering {
748        let self_parts = self.into_parts();
749        let other_parts = other.into_parts();
750
751        use konst::primitive::cmp::cmp_u8;
752
753        let major_ord = cmp_u8(self_parts.0, other_parts.0);
754        if major_ord.is_ne() {
755            major_ord
756        } else {
757            cmp_u8(self_parts.1, other_parts.1)
758        }
759    }
760
761    // Internal function to check if this version is the legacy (v1.0) version in const-fn contexts
762    const fn is_legacy(&self) -> bool {
763        let self_parts = self.into_parts();
764
765        use konst::primitive::cmp::cmp_u8;
766
767        cmp_u8(self_parts.0, 1).is_eq() && cmp_u8(self_parts.1, 0).is_eq()
768    }
769
770    /// Get the default [`RoomVersionId`] for this `MatrixVersion`.
771    pub fn default_room_version(&self) -> RoomVersionId {
772        match self {
773            // <https://spec.matrix.org/historical/index.html#complete-list-of-room-versions>
774            MatrixVersion::V1_0
775            // <https://spec.matrix.org/v1.1/rooms/#complete-list-of-room-versions>
776            | MatrixVersion::V1_1
777            // <https://spec.matrix.org/v1.2/rooms/#complete-list-of-room-versions>
778            | MatrixVersion::V1_2 => RoomVersionId::V6,
779            // <https://spec.matrix.org/v1.3/rooms/#complete-list-of-room-versions>
780            MatrixVersion::V1_3
781            // <https://spec.matrix.org/v1.4/rooms/#complete-list-of-room-versions>
782            | MatrixVersion::V1_4
783            // <https://spec.matrix.org/v1.5/rooms/#complete-list-of-room-versions>
784            | MatrixVersion::V1_5 => RoomVersionId::V9,
785            // <https://spec.matrix.org/v1.6/rooms/#complete-list-of-room-versions>
786            MatrixVersion::V1_6
787            // <https://spec.matrix.org/v1.7/rooms/#complete-list-of-room-versions>
788            | MatrixVersion::V1_7
789            // <https://spec.matrix.org/v1.8/rooms/#complete-list-of-room-versions>
790            | MatrixVersion::V1_8
791            // <https://spec.matrix.org/v1.9/rooms/#complete-list-of-room-versions>
792            | MatrixVersion::V1_9
793            // <https://spec.matrix.org/v1.10/rooms/#complete-list-of-room-versions>
794            | MatrixVersion::V1_10
795            // <https://spec.matrix.org/v1.11/rooms/#complete-list-of-room-versions>
796            | MatrixVersion::V1_11
797            // <https://spec.matrix.org/v1.12/rooms/#complete-list-of-room-versions>
798            | MatrixVersion::V1_12
799            // <https://spec.matrix.org/v1.13/rooms/#complete-list-of-room-versions>
800            | MatrixVersion::V1_13 => RoomVersionId::V10,
801        }
802    }
803}
804
805#[cfg(test)]
806mod tests {
807    use assert_matches2::assert_matches;
808    use http::Method;
809
810    use super::{
811        AuthScheme,
812        MatrixVersion::{self, V1_0, V1_1, V1_2, V1_3},
813        Metadata, VersionHistory,
814    };
815    use crate::api::error::IntoHttpError;
816
817    fn stable_only_metadata(stable_paths: &'static [(MatrixVersion, &'static str)]) -> Metadata {
818        Metadata {
819            method: Method::GET,
820            rate_limited: false,
821            authentication: AuthScheme::None,
822            history: VersionHistory {
823                unstable_paths: &[],
824                stable_paths,
825                deprecated: None,
826                removed: None,
827            },
828        }
829    }
830
831    // TODO add test that can hook into tracing and verify the deprecation warning is emitted
832
833    #[test]
834    fn make_simple_endpoint_url() {
835        let meta = stable_only_metadata(&[(V1_0, "/s")]);
836        let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], "").unwrap();
837        assert_eq!(url, "https://example.org/s");
838    }
839
840    #[test]
841    fn make_endpoint_url_with_path_args() {
842        let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
843        let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[&"123"], "").unwrap();
844        assert_eq!(url, "https://example.org/s/123");
845    }
846
847    #[test]
848    fn make_endpoint_url_with_path_args_with_dash() {
849        let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
850        let url =
851            meta.make_endpoint_url(&[V1_0], "https://example.org", &[&"my-path"], "").unwrap();
852        assert_eq!(url, "https://example.org/s/my-path");
853    }
854
855    #[test]
856    fn make_endpoint_url_with_path_args_with_reserved_char() {
857        let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
858        let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[&"#path"], "").unwrap();
859        assert_eq!(url, "https://example.org/s/%23path");
860    }
861
862    #[test]
863    fn make_endpoint_url_with_query() {
864        let meta = stable_only_metadata(&[(V1_0, "/s/")]);
865        let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], "foo=bar").unwrap();
866        assert_eq!(url, "https://example.org/s/?foo=bar");
867    }
868
869    #[test]
870    #[should_panic]
871    fn make_endpoint_url_wrong_num_path_args() {
872        let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
873        _ = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], "");
874    }
875
876    const EMPTY: VersionHistory =
877        VersionHistory { unstable_paths: &[], stable_paths: &[], deprecated: None, removed: None };
878
879    #[test]
880    fn select_latest_stable() {
881        let hist = VersionHistory { stable_paths: &[(V1_1, "/s")], ..EMPTY };
882        assert_matches!(hist.select_path(&[V1_0, V1_1]), Ok("/s"));
883    }
884
885    #[test]
886    fn select_unstable() {
887        let hist = VersionHistory { unstable_paths: &["/u"], ..EMPTY };
888        assert_matches!(hist.select_path(&[V1_0]), Ok("/u"));
889    }
890
891    #[test]
892    fn select_r0() {
893        let hist = VersionHistory { stable_paths: &[(V1_0, "/r")], ..EMPTY };
894        assert_matches!(hist.select_path(&[V1_0]), Ok("/r"));
895    }
896
897    #[test]
898    fn select_removed_err() {
899        let hist = VersionHistory {
900            stable_paths: &[(V1_0, "/r"), (V1_1, "/s")],
901            unstable_paths: &["/u"],
902            deprecated: Some(V1_2),
903            removed: Some(V1_3),
904        };
905        assert_matches!(hist.select_path(&[V1_3]), Err(IntoHttpError::EndpointRemoved(V1_3)));
906    }
907
908    #[test]
909    fn partially_removed_but_stable() {
910        let hist = VersionHistory {
911            stable_paths: &[(V1_0, "/r"), (V1_1, "/s")],
912            unstable_paths: &[],
913            deprecated: Some(V1_2),
914            removed: Some(V1_3),
915        };
916        assert_matches!(hist.select_path(&[V1_2]), Ok("/s"));
917    }
918
919    #[test]
920    fn no_unstable() {
921        let hist = VersionHistory { stable_paths: &[(V1_1, "/s")], ..EMPTY };
922        assert_matches!(hist.select_path(&[V1_0]), Err(IntoHttpError::NoUnstablePath));
923    }
924
925    #[test]
926    fn version_literal() {
927        const LIT: MatrixVersion = MatrixVersion::from_lit("1.0");
928
929        assert_eq!(LIT, V1_0);
930    }
931
932    #[test]
933    fn parse_as_str_sanity() {
934        let version = MatrixVersion::try_from("r0.5.0").unwrap();
935        assert_eq!(version, V1_0);
936        assert_eq!(version.as_str(), None);
937
938        let version = MatrixVersion::try_from("v1.1").unwrap();
939        assert_eq!(version, V1_1);
940        assert_eq!(version.as_str(), Some("v1.1"));
941    }
942}