ruma_common/api/
metadata.rs

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