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.
477///
478/// Every new minor version denotes stable support for endpoints in a *relatively*
479/// backwards-compatible manner.
480///
481/// Matrix has a deprecation policy, read more about it here: <https://spec.matrix.org/latest/#deprecation-policy>.
482///
483/// Ruma keeps track of when endpoints are added, deprecated, and removed. It'll automatically
484/// select the right endpoint stability variation to use depending on which Matrix versions you
485/// pass to [`try_into_http_request`](super::OutgoingRequest::try_into_http_request), see its
486/// respective documentation for more information.
487#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
488#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
489pub enum MatrixVersion {
490    /// Version 1.0 of the Matrix specification.
491    ///
492    /// Retroactively defined as <https://spec.matrix.org/latest/#legacy-versioning>.
493    V1_0,
494
495    /// Version 1.1 of the Matrix specification, released in Q4 2021.
496    ///
497    /// See <https://spec.matrix.org/v1.1/>.
498    V1_1,
499
500    /// Version 1.2 of the Matrix specification, released in Q1 2022.
501    ///
502    /// See <https://spec.matrix.org/v1.2/>.
503    V1_2,
504
505    /// Version 1.3 of the Matrix specification, released in Q2 2022.
506    ///
507    /// See <https://spec.matrix.org/v1.3/>.
508    V1_3,
509
510    /// Version 1.4 of the Matrix specification, released in Q3 2022.
511    ///
512    /// See <https://spec.matrix.org/v1.4/>.
513    V1_4,
514
515    /// Version 1.5 of the Matrix specification, released in Q4 2022.
516    ///
517    /// See <https://spec.matrix.org/v1.5/>.
518    V1_5,
519
520    /// Version 1.6 of the Matrix specification, released in Q1 2023.
521    ///
522    /// See <https://spec.matrix.org/v1.6/>.
523    V1_6,
524
525    /// Version 1.7 of the Matrix specification, released in Q2 2023.
526    ///
527    /// See <https://spec.matrix.org/v1.7/>.
528    V1_7,
529
530    /// Version 1.8 of the Matrix specification, released in Q3 2023.
531    ///
532    /// See <https://spec.matrix.org/v1.8/>.
533    V1_8,
534
535    /// Version 1.9 of the Matrix specification, released in Q4 2023.
536    ///
537    /// See <https://spec.matrix.org/v1.9/>.
538    V1_9,
539
540    /// Version 1.10 of the Matrix specification, released in Q1 2024.
541    ///
542    /// See <https://spec.matrix.org/v1.10/>.
543    V1_10,
544
545    /// Version 1.11 of the Matrix specification, released in Q2 2024.
546    ///
547    /// See <https://spec.matrix.org/v1.11/>.
548    V1_11,
549
550    /// Version 1.12 of the Matrix specification, released in Q3 2024.
551    ///
552    /// See <https://spec.matrix.org/v1.12/>.
553    V1_12,
554
555    /// Version 1.13 of the Matrix specification, released in Q4 2024.
556    ///
557    /// See <https://spec.matrix.org/v1.13/>.
558    V1_13,
559}
560
561impl TryFrom<&str> for MatrixVersion {
562    type Error = UnknownVersionError;
563
564    fn try_from(value: &str) -> Result<MatrixVersion, Self::Error> {
565        use MatrixVersion::*;
566
567        Ok(match value {
568            // FIXME: these are likely not entirely correct; https://github.com/ruma/ruma/issues/852
569            "v1.0" |
570            // Additional definitions according to https://spec.matrix.org/latest/#legacy-versioning
571            "r0.5.0" | "r0.6.0" | "r0.6.1" => V1_0,
572            "v1.1" => V1_1,
573            "v1.2" => V1_2,
574            "v1.3" => V1_3,
575            "v1.4" => V1_4,
576            "v1.5" => V1_5,
577            "v1.6" => V1_6,
578            "v1.7" => V1_7,
579            "v1.8" => V1_8,
580            "v1.9" => V1_9,
581            "v1.10" => V1_10,
582            "v1.11" => V1_11,
583            "v1.12" => V1_12,
584            "v1.13" => V1_13,
585            _ => return Err(UnknownVersionError),
586        })
587    }
588}
589
590impl FromStr for MatrixVersion {
591    type Err = UnknownVersionError;
592
593    fn from_str(s: &str) -> Result<Self, Self::Err> {
594        Self::try_from(s)
595    }
596}
597
598impl MatrixVersion {
599    /// Checks whether a version is compatible with another.
600    ///
601    /// A is compatible with B as long as B is equal or less, so long as A and B have the same
602    /// major versions.
603    ///
604    /// For example, v1.2 is compatible with v1.1, as it is likely only some additions of
605    /// endpoints on top of v1.1, but v1.1 would not be compatible with v1.2, as v1.1
606    /// cannot represent all of v1.2, in a manner similar to set theory.
607    ///
608    /// Warning: Matrix has a deprecation policy, and Matrix versioning is not as
609    /// straight-forward as this function makes it out to be. This function only exists
610    /// to prune major version differences, and versions too new for `self`.
611    ///
612    /// This (considering if major versions are the same) is equivalent to a `self >= other`
613    /// check.
614    pub fn is_superset_of(self, other: Self) -> bool {
615        let (major_l, minor_l) = self.into_parts();
616        let (major_r, minor_r) = other.into_parts();
617        major_l == major_r && minor_l >= minor_r
618    }
619
620    /// Decompose the Matrix version into its major and minor number.
621    pub const fn into_parts(self) -> (u8, u8) {
622        match self {
623            MatrixVersion::V1_0 => (1, 0),
624            MatrixVersion::V1_1 => (1, 1),
625            MatrixVersion::V1_2 => (1, 2),
626            MatrixVersion::V1_3 => (1, 3),
627            MatrixVersion::V1_4 => (1, 4),
628            MatrixVersion::V1_5 => (1, 5),
629            MatrixVersion::V1_6 => (1, 6),
630            MatrixVersion::V1_7 => (1, 7),
631            MatrixVersion::V1_8 => (1, 8),
632            MatrixVersion::V1_9 => (1, 9),
633            MatrixVersion::V1_10 => (1, 10),
634            MatrixVersion::V1_11 => (1, 11),
635            MatrixVersion::V1_12 => (1, 12),
636            MatrixVersion::V1_13 => (1, 13),
637        }
638    }
639
640    /// Try to turn a pair of (major, minor) version components back into a `MatrixVersion`.
641    pub const fn from_parts(major: u8, minor: u8) -> Result<Self, UnknownVersionError> {
642        match (major, minor) {
643            (1, 0) => Ok(MatrixVersion::V1_0),
644            (1, 1) => Ok(MatrixVersion::V1_1),
645            (1, 2) => Ok(MatrixVersion::V1_2),
646            (1, 3) => Ok(MatrixVersion::V1_3),
647            (1, 4) => Ok(MatrixVersion::V1_4),
648            (1, 5) => Ok(MatrixVersion::V1_5),
649            (1, 6) => Ok(MatrixVersion::V1_6),
650            (1, 7) => Ok(MatrixVersion::V1_7),
651            (1, 8) => Ok(MatrixVersion::V1_8),
652            (1, 9) => Ok(MatrixVersion::V1_9),
653            (1, 10) => Ok(MatrixVersion::V1_10),
654            (1, 11) => Ok(MatrixVersion::V1_11),
655            (1, 12) => Ok(MatrixVersion::V1_12),
656            (1, 13) => Ok(MatrixVersion::V1_13),
657            _ => Err(UnknownVersionError),
658        }
659    }
660
661    /// Constructor for use by the `metadata!` macro.
662    ///
663    /// Accepts string literals and parses them.
664    #[doc(hidden)]
665    pub const fn from_lit(lit: &'static str) -> Self {
666        use konst::{option, primitive::parse_u8, result, string};
667
668        let major: u8;
669        let minor: u8;
670
671        let mut lit_iter = string::split(lit, ".").next();
672
673        {
674            let (checked_first, checked_split) = option::unwrap!(lit_iter); // First iteration always succeeds
675
676            major = result::unwrap_or_else!(parse_u8(checked_first), |_| panic!(
677                "major version is not a valid number"
678            ));
679
680            lit_iter = checked_split.next();
681        }
682
683        match lit_iter {
684            Some((checked_second, checked_split)) => {
685                minor = result::unwrap_or_else!(parse_u8(checked_second), |_| panic!(
686                    "minor version is not a valid number"
687                ));
688
689                lit_iter = checked_split.next();
690            }
691            None => panic!("could not find dot to denote second number"),
692        }
693
694        if lit_iter.is_some() {
695            panic!("version literal contains more than one dot")
696        }
697
698        result::unwrap_or_else!(Self::from_parts(major, minor), |_| panic!(
699            "not a valid version literal"
700        ))
701    }
702
703    // Internal function to do ordering in const-fn contexts
704    const fn const_ord(&self, other: &Self) -> Ordering {
705        let self_parts = self.into_parts();
706        let other_parts = other.into_parts();
707
708        use konst::primitive::cmp::cmp_u8;
709
710        let major_ord = cmp_u8(self_parts.0, other_parts.0);
711        if major_ord.is_ne() {
712            major_ord
713        } else {
714            cmp_u8(self_parts.1, other_parts.1)
715        }
716    }
717
718    // Internal function to check if this version is the legacy (v1.0) version in const-fn contexts
719    const fn is_legacy(&self) -> bool {
720        let self_parts = self.into_parts();
721
722        use konst::primitive::cmp::cmp_u8;
723
724        cmp_u8(self_parts.0, 1).is_eq() && cmp_u8(self_parts.1, 0).is_eq()
725    }
726
727    /// Get the default [`RoomVersionId`] for this `MatrixVersion`.
728    pub fn default_room_version(&self) -> RoomVersionId {
729        match self {
730            // <https://spec.matrix.org/historical/index.html#complete-list-of-room-versions>
731            MatrixVersion::V1_0
732            // <https://spec.matrix.org/v1.1/rooms/#complete-list-of-room-versions>
733            | MatrixVersion::V1_1
734            // <https://spec.matrix.org/v1.2/rooms/#complete-list-of-room-versions>
735            | MatrixVersion::V1_2 => RoomVersionId::V6,
736            // <https://spec.matrix.org/v1.3/rooms/#complete-list-of-room-versions>
737            MatrixVersion::V1_3
738            // <https://spec.matrix.org/v1.4/rooms/#complete-list-of-room-versions>
739            | MatrixVersion::V1_4
740            // <https://spec.matrix.org/v1.5/rooms/#complete-list-of-room-versions>
741            | MatrixVersion::V1_5 => RoomVersionId::V9,
742            // <https://spec.matrix.org/v1.6/rooms/#complete-list-of-room-versions>
743            MatrixVersion::V1_6
744            // <https://spec.matrix.org/v1.7/rooms/#complete-list-of-room-versions>
745            | MatrixVersion::V1_7
746            // <https://spec.matrix.org/v1.8/rooms/#complete-list-of-room-versions>
747            | MatrixVersion::V1_8
748            // <https://spec.matrix.org/v1.9/rooms/#complete-list-of-room-versions>
749            | MatrixVersion::V1_9
750            // <https://spec.matrix.org/v1.10/rooms/#complete-list-of-room-versions>
751            | MatrixVersion::V1_10
752            // <https://spec.matrix.org/v1.11/rooms/#complete-list-of-room-versions>
753            | MatrixVersion::V1_11
754            // <https://spec.matrix.org/v1.12/rooms/#complete-list-of-room-versions>
755            | MatrixVersion::V1_12
756            // <https://spec.matrix.org/v1.13/rooms/#complete-list-of-room-versions>
757            | MatrixVersion::V1_13 => RoomVersionId::V10,
758        }
759    }
760}
761
762impl Display for MatrixVersion {
763    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
764        let (major, minor) = self.into_parts();
765        f.write_str(&format!("v{major}.{minor}"))
766    }
767}
768
769#[cfg(test)]
770mod tests {
771    use assert_matches2::assert_matches;
772    use http::Method;
773
774    use super::{
775        AuthScheme,
776        MatrixVersion::{self, V1_0, V1_1, V1_2, V1_3},
777        Metadata, VersionHistory,
778    };
779    use crate::api::error::IntoHttpError;
780
781    fn stable_only_metadata(stable_paths: &'static [(MatrixVersion, &'static str)]) -> Metadata {
782        Metadata {
783            method: Method::GET,
784            rate_limited: false,
785            authentication: AuthScheme::None,
786            history: VersionHistory {
787                unstable_paths: &[],
788                stable_paths,
789                deprecated: None,
790                removed: None,
791            },
792        }
793    }
794
795    // TODO add test that can hook into tracing and verify the deprecation warning is emitted
796
797    #[test]
798    fn make_simple_endpoint_url() {
799        let meta = stable_only_metadata(&[(V1_0, "/s")]);
800        let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], "").unwrap();
801        assert_eq!(url, "https://example.org/s");
802    }
803
804    #[test]
805    fn make_endpoint_url_with_path_args() {
806        let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
807        let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[&"123"], "").unwrap();
808        assert_eq!(url, "https://example.org/s/123");
809    }
810
811    #[test]
812    fn make_endpoint_url_with_path_args_with_dash() {
813        let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
814        let url =
815            meta.make_endpoint_url(&[V1_0], "https://example.org", &[&"my-path"], "").unwrap();
816        assert_eq!(url, "https://example.org/s/my-path");
817    }
818
819    #[test]
820    fn make_endpoint_url_with_path_args_with_reserved_char() {
821        let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
822        let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[&"#path"], "").unwrap();
823        assert_eq!(url, "https://example.org/s/%23path");
824    }
825
826    #[test]
827    fn make_endpoint_url_with_query() {
828        let meta = stable_only_metadata(&[(V1_0, "/s/")]);
829        let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], "foo=bar").unwrap();
830        assert_eq!(url, "https://example.org/s/?foo=bar");
831    }
832
833    #[test]
834    #[should_panic]
835    fn make_endpoint_url_wrong_num_path_args() {
836        let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
837        _ = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], "");
838    }
839
840    const EMPTY: VersionHistory =
841        VersionHistory { unstable_paths: &[], stable_paths: &[], deprecated: None, removed: None };
842
843    #[test]
844    fn select_latest_stable() {
845        let hist = VersionHistory { stable_paths: &[(V1_1, "/s")], ..EMPTY };
846        assert_matches!(hist.select_path(&[V1_0, V1_1]), Ok("/s"));
847    }
848
849    #[test]
850    fn select_unstable() {
851        let hist = VersionHistory { unstable_paths: &["/u"], ..EMPTY };
852        assert_matches!(hist.select_path(&[V1_0]), Ok("/u"));
853    }
854
855    #[test]
856    fn select_r0() {
857        let hist = VersionHistory { stable_paths: &[(V1_0, "/r")], ..EMPTY };
858        assert_matches!(hist.select_path(&[V1_0]), Ok("/r"));
859    }
860
861    #[test]
862    fn select_removed_err() {
863        let hist = VersionHistory {
864            stable_paths: &[(V1_0, "/r"), (V1_1, "/s")],
865            unstable_paths: &["/u"],
866            deprecated: Some(V1_2),
867            removed: Some(V1_3),
868        };
869        assert_matches!(hist.select_path(&[V1_3]), Err(IntoHttpError::EndpointRemoved(V1_3)));
870    }
871
872    #[test]
873    fn partially_removed_but_stable() {
874        let hist = VersionHistory {
875            stable_paths: &[(V1_0, "/r"), (V1_1, "/s")],
876            unstable_paths: &[],
877            deprecated: Some(V1_2),
878            removed: Some(V1_3),
879        };
880        assert_matches!(hist.select_path(&[V1_2]), Ok("/s"));
881    }
882
883    #[test]
884    fn no_unstable() {
885        let hist = VersionHistory { stable_paths: &[(V1_1, "/s")], ..EMPTY };
886        assert_matches!(hist.select_path(&[V1_0]), Err(IntoHttpError::NoUnstablePath));
887    }
888
889    #[test]
890    fn version_literal() {
891        const LIT: MatrixVersion = MatrixVersion::from_lit("1.0");
892
893        assert_eq!(LIT, V1_0);
894    }
895}