876 lines
32 KiB
Rust
876 lines
32 KiB
Rust
use std::{
|
|
cmp::Ordering,
|
|
fmt::{self, Display, Write},
|
|
str::FromStr,
|
|
};
|
|
|
|
use bytes::BufMut;
|
|
use http::{
|
|
header::{self, HeaderName, HeaderValue},
|
|
Method,
|
|
};
|
|
use percent_encoding::utf8_percent_encode;
|
|
use tracing::warn;
|
|
|
|
use super::{
|
|
error::{IntoHttpError, UnknownVersionError},
|
|
AuthScheme, SendAccessToken,
|
|
};
|
|
use crate::{percent_encode::PATH_PERCENT_ENCODE_SET, serde::slice_to_buf, RoomVersionId};
|
|
|
|
/// Metadata about an API endpoint.
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
#[allow(clippy::exhaustive_structs)]
|
|
pub struct Metadata {
|
|
/// The HTTP method used by this endpoint.
|
|
pub method: Method,
|
|
|
|
/// Whether or not this endpoint is rate limited by the server.
|
|
pub rate_limited: bool,
|
|
|
|
/// What authentication scheme the server uses for this endpoint.
|
|
pub authentication: AuthScheme,
|
|
|
|
/// All info pertaining to an endpoint's (historic) paths, deprecation version, and removal.
|
|
pub history: VersionHistory,
|
|
}
|
|
|
|
impl Metadata {
|
|
/// Returns an empty request body for this Matrix request.
|
|
///
|
|
/// For `GET` requests, it returns an entirely empty buffer, for others it returns an empty JSON
|
|
/// object (`{}`).
|
|
pub fn empty_request_body<B>(&self) -> B
|
|
where
|
|
B: Default + BufMut,
|
|
{
|
|
if self.method == Method::GET {
|
|
Default::default()
|
|
} else {
|
|
slice_to_buf(b"{}")
|
|
}
|
|
}
|
|
|
|
/// Transform the `SendAccessToken` into an access token if the endpoint requires it, or if it
|
|
/// is `SendAccessToken::Force`.
|
|
///
|
|
/// Fails if the endpoint requires an access token but the parameter is `SendAccessToken::None`,
|
|
/// or if the access token can't be converted to a [`HeaderValue`].
|
|
pub fn authorization_header(
|
|
&self,
|
|
access_token: SendAccessToken<'_>,
|
|
) -> Result<Option<(HeaderName, HeaderValue)>, IntoHttpError> {
|
|
Ok(match self.authentication {
|
|
AuthScheme::None => match access_token.get_not_required_for_endpoint() {
|
|
Some(token) => Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?)),
|
|
None => None,
|
|
},
|
|
|
|
AuthScheme::AccessToken => {
|
|
let token = access_token
|
|
.get_required_for_endpoint()
|
|
.ok_or(IntoHttpError::NeedsAuthentication)?;
|
|
|
|
Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?))
|
|
}
|
|
|
|
AuthScheme::AccessTokenOptional => match access_token.get_required_for_endpoint() {
|
|
Some(token) => Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?)),
|
|
None => None,
|
|
},
|
|
|
|
AuthScheme::AppserviceToken => match access_token.get_required_for_appservice() {
|
|
Some(token) => Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?)),
|
|
None => None,
|
|
},
|
|
|
|
AuthScheme::ServerSignatures => None,
|
|
})
|
|
}
|
|
|
|
/// Generate the endpoint URL for this endpoint.
|
|
pub fn make_endpoint_url(
|
|
&self,
|
|
versions: &[MatrixVersion],
|
|
base_url: &str,
|
|
path_args: &[&dyn Display],
|
|
query_string: &str,
|
|
) -> Result<String, IntoHttpError> {
|
|
let path_with_placeholders = self.history.select_path(versions)?;
|
|
|
|
let mut res = base_url.strip_suffix('/').unwrap_or(base_url).to_owned();
|
|
let mut segments = path_with_placeholders.split('/');
|
|
let mut path_args = path_args.iter();
|
|
|
|
let first_segment = segments.next().expect("split iterator is never empty");
|
|
assert!(first_segment.is_empty(), "endpoint paths must start with '/'");
|
|
|
|
for segment in segments {
|
|
if segment.starts_with(':') {
|
|
let arg = path_args
|
|
.next()
|
|
.expect("number of placeholders must match number of arguments")
|
|
.to_string();
|
|
let arg = utf8_percent_encode(&arg, PATH_PERCENT_ENCODE_SET);
|
|
|
|
write!(res, "/{arg}").expect("writing to a String using fmt::Write can't fail");
|
|
} else {
|
|
res.reserve(segment.len() + 1);
|
|
res.push('/');
|
|
res.push_str(segment);
|
|
}
|
|
}
|
|
|
|
if !query_string.is_empty() {
|
|
res.push('?');
|
|
res.push_str(query_string);
|
|
}
|
|
|
|
Ok(res)
|
|
}
|
|
|
|
// Used for generated `#[test]`s
|
|
#[doc(hidden)]
|
|
pub fn _path_parameters(&self) -> Vec<&'static str> {
|
|
let path = self.history.all_paths().next().unwrap();
|
|
path.split('/').filter_map(|segment| segment.strip_prefix(':')).collect()
|
|
}
|
|
}
|
|
|
|
/// The complete history of this endpoint as far as Ruma knows, together with all variants on
|
|
/// versions stable and unstable.
|
|
///
|
|
/// The amount and positioning of path variables are the same over all path variants.
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
#[allow(clippy::exhaustive_structs)]
|
|
pub struct VersionHistory {
|
|
/// A list of unstable paths over this endpoint's history.
|
|
///
|
|
/// For endpoint querying purposes, the last item will be used.
|
|
unstable_paths: &'static [&'static str],
|
|
|
|
/// A list of path versions, mapped to Matrix versions.
|
|
///
|
|
/// Sorted (ascending) by Matrix version, will not mix major versions.
|
|
stable_paths: &'static [(MatrixVersion, &'static str)],
|
|
|
|
/// The Matrix version that deprecated this endpoint.
|
|
///
|
|
/// Deprecation often precedes one Matrix version before removal.
|
|
///
|
|
/// This will make [`try_into_http_request`](super::OutgoingRequest::try_into_http_request)
|
|
/// emit a warning, see the corresponding documentation for more information.
|
|
deprecated: Option<MatrixVersion>,
|
|
|
|
/// The Matrix version that removed this endpoint.
|
|
///
|
|
/// This will make [`try_into_http_request`](super::OutgoingRequest::try_into_http_request)
|
|
/// emit an error, see the corresponding documentation for more information.
|
|
removed: Option<MatrixVersion>,
|
|
}
|
|
|
|
impl VersionHistory {
|
|
/// Constructs an instance of [`VersionHistory`], erroring on compilation if it does not pass
|
|
/// invariants.
|
|
///
|
|
/// Specifically, this checks the following invariants:
|
|
/// - Path Arguments are equal (in order, amount, and argument name) in all path strings
|
|
/// - In stable_paths:
|
|
/// - matrix versions are in ascending order
|
|
/// - no matrix version is referenced twice
|
|
/// - deprecated's version comes after the latest version mentioned in stable_paths, except for
|
|
/// version 1.0, and only if any stable path is defined
|
|
/// - removed comes after deprecated, or after the latest referenced stable_paths, like
|
|
/// deprecated
|
|
pub const fn new(
|
|
unstable_paths: &'static [&'static str],
|
|
stable_paths: &'static [(MatrixVersion, &'static str)],
|
|
deprecated: Option<MatrixVersion>,
|
|
removed: Option<MatrixVersion>,
|
|
) -> Self {
|
|
use konst::{iter, slice, string};
|
|
|
|
const fn check_path_is_valid(path: &'static str) {
|
|
iter::for_each!(path_b in slice::iter(path.as_bytes()) => {
|
|
match *path_b {
|
|
0x21..=0x7E => {},
|
|
_ => panic!("path contains invalid (non-ascii or whitespace) characters")
|
|
}
|
|
});
|
|
}
|
|
|
|
const fn check_path_args_equal(first: &'static str, second: &'static str) {
|
|
let mut second_iter = string::split(second, "/").next();
|
|
|
|
iter::for_each!(first_s in string::split(first, "/") => {
|
|
if let Some(first_arg) = string::strip_prefix(first_s, ":") {
|
|
let second_next_arg: Option<&'static str> = loop {
|
|
let (second_s, second_n_iter) = match second_iter {
|
|
Some(tuple) => tuple,
|
|
None => break None,
|
|
};
|
|
|
|
let maybe_second_arg = string::strip_prefix(second_s, ":");
|
|
|
|
second_iter = second_n_iter.next();
|
|
|
|
if let Some(second_arg) = maybe_second_arg {
|
|
break Some(second_arg);
|
|
}
|
|
};
|
|
|
|
if let Some(second_next_arg) = second_next_arg {
|
|
if !string::eq_str(second_next_arg, first_arg) {
|
|
panic!("Path Arguments do not match");
|
|
}
|
|
} else {
|
|
panic!("Amount of Path Arguments do not match");
|
|
}
|
|
}
|
|
});
|
|
|
|
// If second iterator still has some values, empty first.
|
|
while let Some((second_s, second_n_iter)) = second_iter {
|
|
if string::starts_with(second_s, ":") {
|
|
panic!("Amount of Path Arguments do not match");
|
|
}
|
|
second_iter = second_n_iter.next();
|
|
}
|
|
}
|
|
|
|
// The path we're going to use to compare all other paths with
|
|
let ref_path: &str = if let Some(s) = unstable_paths.first() {
|
|
s
|
|
} else if let Some((_, s)) = stable_paths.first() {
|
|
s
|
|
} else {
|
|
panic!("No paths supplied")
|
|
};
|
|
|
|
iter::for_each!(unstable_path in slice::iter(unstable_paths) => {
|
|
check_path_is_valid(unstable_path);
|
|
check_path_args_equal(ref_path, unstable_path);
|
|
});
|
|
|
|
let mut prev_seen_version: Option<MatrixVersion> = None;
|
|
|
|
iter::for_each!(stable_path in slice::iter(stable_paths) => {
|
|
check_path_is_valid(stable_path.1);
|
|
check_path_args_equal(ref_path, stable_path.1);
|
|
|
|
let current_version = stable_path.0;
|
|
|
|
if let Some(prev_seen_version) = prev_seen_version {
|
|
let cmp_result = current_version.const_ord(&prev_seen_version);
|
|
|
|
if cmp_result.is_eq() {
|
|
// Found a duplicate, current == previous
|
|
panic!("Duplicate matrix version in stable_paths")
|
|
} else if cmp_result.is_lt() {
|
|
// Found an older version, current < previous
|
|
panic!("No ascending order in stable_paths")
|
|
}
|
|
}
|
|
|
|
prev_seen_version = Some(current_version);
|
|
});
|
|
|
|
if let Some(deprecated) = deprecated {
|
|
if let Some(prev_seen_version) = prev_seen_version {
|
|
let ord_result = prev_seen_version.const_ord(&deprecated);
|
|
if !deprecated.is_legacy() && ord_result.is_eq() {
|
|
// prev_seen_version == deprecated, except for 1.0.
|
|
// It is possible that an endpoint was both made stable and deprecated in the
|
|
// legacy versions.
|
|
panic!("deprecated version is equal to latest stable path version")
|
|
} else if ord_result.is_gt() {
|
|
// prev_seen_version > deprecated
|
|
panic!("deprecated version is older than latest stable path version")
|
|
}
|
|
} else {
|
|
panic!("Defined deprecated version while no stable path exists")
|
|
}
|
|
}
|
|
|
|
if let Some(removed) = removed {
|
|
if let Some(deprecated) = deprecated {
|
|
let ord_result = deprecated.const_ord(&removed);
|
|
if ord_result.is_eq() {
|
|
// deprecated == removed
|
|
panic!("removed version is equal to deprecated version")
|
|
} else if ord_result.is_gt() {
|
|
// deprecated > removed
|
|
panic!("removed version is older than deprecated version")
|
|
}
|
|
} else {
|
|
panic!("Defined removed version while no deprecated version exists")
|
|
}
|
|
}
|
|
|
|
VersionHistory { unstable_paths, stable_paths, deprecated, removed }
|
|
}
|
|
|
|
// This function helps picks the right path (or an error) from a set of Matrix versions.
|
|
fn select_path(&self, versions: &[MatrixVersion]) -> Result<&'static str, IntoHttpError> {
|
|
match self.versioning_decision_for(versions) {
|
|
VersioningDecision::Removed => Err(IntoHttpError::EndpointRemoved(
|
|
self.removed.expect("VersioningDecision::Removed implies metadata.removed"),
|
|
)),
|
|
VersioningDecision::Stable { any_deprecated, all_deprecated, any_removed } => {
|
|
if any_removed {
|
|
if all_deprecated {
|
|
warn!(
|
|
"endpoint is removed in some (and deprecated in ALL) \
|
|
of the following versions: {versions:?}",
|
|
);
|
|
} else if any_deprecated {
|
|
warn!(
|
|
"endpoint is removed (and deprecated) in some of the \
|
|
following versions: {versions:?}",
|
|
);
|
|
} else {
|
|
unreachable!("any_removed implies *_deprecated");
|
|
}
|
|
} else if all_deprecated {
|
|
warn!(
|
|
"endpoint is deprecated in ALL of the following versions: \
|
|
{versions:?}",
|
|
);
|
|
} else if any_deprecated {
|
|
warn!(
|
|
"endpoint is deprecated in some of the following versions: \
|
|
{versions:?}",
|
|
);
|
|
}
|
|
|
|
Ok(self
|
|
.stable_endpoint_for(versions)
|
|
.expect("VersioningDecision::Stable implies that a stable path exists"))
|
|
}
|
|
VersioningDecision::Unstable => self.unstable().ok_or(IntoHttpError::NoUnstablePath),
|
|
}
|
|
}
|
|
|
|
/// Will decide how a particular set of Matrix versions sees an endpoint.
|
|
///
|
|
/// It will only return `Deprecated` or `Removed` if all versions denote it.
|
|
///
|
|
/// In other words, if in any version it tells it supports the endpoint in a stable fashion,
|
|
/// this will return `Stable`, even if some versions in this set will denote deprecation or
|
|
/// removal.
|
|
///
|
|
/// If resulting [`VersioningDecision`] is `Stable`, it will also detail if any version denoted
|
|
/// deprecation or removal.
|
|
pub fn versioning_decision_for(&self, versions: &[MatrixVersion]) -> VersioningDecision {
|
|
let greater_or_equal_any =
|
|
|version: MatrixVersion| versions.iter().any(|v| v.is_superset_of(version));
|
|
let greater_or_equal_all =
|
|
|version: MatrixVersion| versions.iter().all(|v| v.is_superset_of(version));
|
|
|
|
// Check if all versions removed this endpoint.
|
|
if self.removed.is_some_and(greater_or_equal_all) {
|
|
return VersioningDecision::Removed;
|
|
}
|
|
|
|
// Check if *any* version marks this endpoint as stable.
|
|
if self.added_in().is_some_and(greater_or_equal_any) {
|
|
let all_deprecated = self.deprecated.is_some_and(greater_or_equal_all);
|
|
|
|
return VersioningDecision::Stable {
|
|
any_deprecated: all_deprecated || self.deprecated.is_some_and(greater_or_equal_any),
|
|
all_deprecated,
|
|
any_removed: self.removed.is_some_and(greater_or_equal_any),
|
|
};
|
|
}
|
|
|
|
VersioningDecision::Unstable
|
|
}
|
|
|
|
/// Returns the *first* version this endpoint was added in.
|
|
///
|
|
/// Is `None` when this endpoint is unstable/unreleased.
|
|
pub fn added_in(&self) -> Option<MatrixVersion> {
|
|
self.stable_paths.first().map(|(v, _)| *v)
|
|
}
|
|
|
|
/// Returns the Matrix version that deprecated this endpoint, if any.
|
|
pub fn deprecated_in(&self) -> Option<MatrixVersion> {
|
|
self.deprecated
|
|
}
|
|
|
|
/// Returns the Matrix version that removed this endpoint, if any.
|
|
pub fn removed_in(&self) -> Option<MatrixVersion> {
|
|
self.removed
|
|
}
|
|
|
|
/// Picks the last unstable path, if it exists.
|
|
pub fn unstable(&self) -> Option<&'static str> {
|
|
self.unstable_paths.last().copied()
|
|
}
|
|
|
|
/// Returns all path variants in canon form, for use in server routers.
|
|
pub fn all_paths(&self) -> impl Iterator<Item = &'static str> {
|
|
self.unstable_paths().chain(self.stable_paths().map(|(_, path)| path))
|
|
}
|
|
|
|
/// Returns all unstable path variants in canon form.
|
|
pub fn unstable_paths(&self) -> impl Iterator<Item = &'static str> {
|
|
self.unstable_paths.iter().copied()
|
|
}
|
|
|
|
/// Returns all stable path variants in canon form, with corresponding Matrix version.
|
|
pub fn stable_paths(&self) -> impl Iterator<Item = (MatrixVersion, &'static str)> {
|
|
self.stable_paths.iter().map(|(version, data)| (*version, *data))
|
|
}
|
|
|
|
/// The path that should be used to query the endpoint, given a series of versions.
|
|
///
|
|
/// This will pick the latest path that the version accepts.
|
|
///
|
|
/// This will return an endpoint in the following format;
|
|
/// - `/_matrix/client/versions`
|
|
/// - `/_matrix/client/hello/:world` (`:world` is a path replacement parameter)
|
|
///
|
|
/// Note: This will not keep in mind endpoint removals, check with
|
|
/// [`versioning_decision_for`](VersionHistory::versioning_decision_for) to see if this endpoint
|
|
/// is still available.
|
|
pub fn stable_endpoint_for(&self, versions: &[MatrixVersion]) -> Option<&'static str> {
|
|
// Go reverse, to check the "latest" version first.
|
|
for (ver, path) in self.stable_paths.iter().rev() {
|
|
// Check if any of the versions are equal or greater than the version the path needs.
|
|
if versions.iter().any(|v| v.is_superset_of(*ver)) {
|
|
return Some(path);
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
}
|
|
|
|
/// A versioning "decision" derived from a set of Matrix versions.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
#[allow(clippy::exhaustive_enums)]
|
|
pub enum VersioningDecision {
|
|
/// The unstable endpoint should be used.
|
|
Unstable,
|
|
|
|
/// The stable endpoint should be used.
|
|
Stable {
|
|
/// If any version denoted deprecation.
|
|
any_deprecated: bool,
|
|
|
|
/// If *all* versions denoted deprecation.
|
|
all_deprecated: bool,
|
|
|
|
/// If any version denoted removal.
|
|
any_removed: bool,
|
|
},
|
|
|
|
/// This endpoint was removed in all versions, it should not be used.
|
|
Removed,
|
|
}
|
|
|
|
/// The Matrix versions Ruma currently understands to exist.
|
|
///
|
|
/// Matrix, since fall 2021, has a quarterly release schedule, using a global `vX.Y` versioning
|
|
/// scheme.
|
|
///
|
|
/// Every new minor version denotes stable support for endpoints in a *relatively*
|
|
/// backwards-compatible manner.
|
|
///
|
|
/// Matrix has a deprecation policy, read more about it here: <https://spec.matrix.org/latest/#deprecation-policy>.
|
|
///
|
|
/// Ruma keeps track of when endpoints are added, deprecated, and removed. It'll automatically
|
|
/// select the right endpoint stability variation to use depending on which Matrix versions you
|
|
/// pass to [`try_into_http_request`](super::OutgoingRequest::try_into_http_request), see its
|
|
/// respective documentation for more information.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
|
pub enum MatrixVersion {
|
|
/// Version 1.0 of the Matrix specification.
|
|
///
|
|
/// Retroactively defined as <https://spec.matrix.org/latest/#legacy-versioning>.
|
|
V1_0,
|
|
|
|
/// Version 1.1 of the Matrix specification, released in Q4 2021.
|
|
///
|
|
/// See <https://spec.matrix.org/v1.1/>.
|
|
V1_1,
|
|
|
|
/// Version 1.2 of the Matrix specification, released in Q1 2022.
|
|
///
|
|
/// See <https://spec.matrix.org/v1.2/>.
|
|
V1_2,
|
|
|
|
/// Version 1.3 of the Matrix specification, released in Q2 2022.
|
|
///
|
|
/// See <https://spec.matrix.org/v1.3/>.
|
|
V1_3,
|
|
|
|
/// Version 1.4 of the Matrix specification, released in Q3 2022.
|
|
///
|
|
/// See <https://spec.matrix.org/v1.4/>.
|
|
V1_4,
|
|
|
|
/// Version 1.5 of the Matrix specification, released in Q4 2022.
|
|
///
|
|
/// See <https://spec.matrix.org/v1.5/>.
|
|
V1_5,
|
|
|
|
/// Version 1.6 of the Matrix specification, released in Q1 2023.
|
|
///
|
|
/// See <https://spec.matrix.org/v1.6/>.
|
|
V1_6,
|
|
|
|
/// Version 1.7 of the Matrix specification, released in Q2 2023.
|
|
///
|
|
/// See <https://spec.matrix.org/v1.7/>.
|
|
V1_7,
|
|
|
|
/// Version 1.8 of the Matrix specification, released in Q3 2023.
|
|
///
|
|
/// See <https://spec.matrix.org/v1.8/>.
|
|
V1_8,
|
|
|
|
/// Version 1.9 of the Matrix specification, released in Q4 2023.
|
|
///
|
|
/// See <https://spec.matrix.org/v1.9/>.
|
|
V1_9,
|
|
|
|
/// Version 1.10 of the Matrix specification, released in Q1 2024.
|
|
///
|
|
/// See <https://spec.matrix.org/v1.10/>.
|
|
V1_10,
|
|
|
|
/// Version 1.11 of the Matrix specification, released in Q2 2024.
|
|
///
|
|
/// See <https://spec.matrix.org/v1.11/>.
|
|
V1_11,
|
|
}
|
|
|
|
impl TryFrom<&str> for MatrixVersion {
|
|
type Error = UnknownVersionError;
|
|
|
|
fn try_from(value: &str) -> Result<MatrixVersion, Self::Error> {
|
|
use MatrixVersion::*;
|
|
|
|
Ok(match value {
|
|
// FIXME: these are likely not entirely correct; https://github.com/ruma/ruma/issues/852
|
|
"v1.0" |
|
|
// Additional definitions according to https://spec.matrix.org/latest/#legacy-versioning
|
|
"r0.5.0" | "r0.6.0" | "r0.6.1" => V1_0,
|
|
"v1.1" => V1_1,
|
|
"v1.2" => V1_2,
|
|
"v1.3" => V1_3,
|
|
"v1.4" => V1_4,
|
|
"v1.5" => V1_5,
|
|
"v1.6" => V1_6,
|
|
"v1.7" => V1_7,
|
|
"v1.8" => V1_8,
|
|
"v1.9" => V1_9,
|
|
"v1.10" => V1_10,
|
|
"v1.11" => V1_11,
|
|
_ => return Err(UnknownVersionError),
|
|
})
|
|
}
|
|
}
|
|
|
|
impl FromStr for MatrixVersion {
|
|
type Err = UnknownVersionError;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
Self::try_from(s)
|
|
}
|
|
}
|
|
|
|
impl MatrixVersion {
|
|
/// Checks whether a version is compatible with another.
|
|
///
|
|
/// A is compatible with B as long as B is equal or less, so long as A and B have the same
|
|
/// major versions.
|
|
///
|
|
/// For example, v1.2 is compatible with v1.1, as it is likely only some additions of
|
|
/// endpoints on top of v1.1, but v1.1 would not be compatible with v1.2, as v1.1
|
|
/// cannot represent all of v1.2, in a manner similar to set theory.
|
|
///
|
|
/// Warning: Matrix has a deprecation policy, and Matrix versioning is not as
|
|
/// straight-forward as this function makes it out to be. This function only exists
|
|
/// to prune major version differences, and versions too new for `self`.
|
|
///
|
|
/// This (considering if major versions are the same) is equivalent to a `self >= other`
|
|
/// check.
|
|
pub fn is_superset_of(self, other: Self) -> bool {
|
|
let (major_l, minor_l) = self.into_parts();
|
|
let (major_r, minor_r) = other.into_parts();
|
|
major_l == major_r && minor_l >= minor_r
|
|
}
|
|
|
|
/// Decompose the Matrix version into its major and minor number.
|
|
pub const fn into_parts(self) -> (u8, u8) {
|
|
match self {
|
|
MatrixVersion::V1_0 => (1, 0),
|
|
MatrixVersion::V1_1 => (1, 1),
|
|
MatrixVersion::V1_2 => (1, 2),
|
|
MatrixVersion::V1_3 => (1, 3),
|
|
MatrixVersion::V1_4 => (1, 4),
|
|
MatrixVersion::V1_5 => (1, 5),
|
|
MatrixVersion::V1_6 => (1, 6),
|
|
MatrixVersion::V1_7 => (1, 7),
|
|
MatrixVersion::V1_8 => (1, 8),
|
|
MatrixVersion::V1_9 => (1, 9),
|
|
MatrixVersion::V1_10 => (1, 10),
|
|
MatrixVersion::V1_11 => (1, 11),
|
|
}
|
|
}
|
|
|
|
/// Try to turn a pair of (major, minor) version components back into a `MatrixVersion`.
|
|
pub const fn from_parts(major: u8, minor: u8) -> Result<Self, UnknownVersionError> {
|
|
match (major, minor) {
|
|
(1, 0) => Ok(MatrixVersion::V1_0),
|
|
(1, 1) => Ok(MatrixVersion::V1_1),
|
|
(1, 2) => Ok(MatrixVersion::V1_2),
|
|
(1, 3) => Ok(MatrixVersion::V1_3),
|
|
(1, 4) => Ok(MatrixVersion::V1_4),
|
|
(1, 5) => Ok(MatrixVersion::V1_5),
|
|
(1, 6) => Ok(MatrixVersion::V1_6),
|
|
(1, 7) => Ok(MatrixVersion::V1_7),
|
|
(1, 8) => Ok(MatrixVersion::V1_8),
|
|
(1, 9) => Ok(MatrixVersion::V1_9),
|
|
(1, 10) => Ok(MatrixVersion::V1_10),
|
|
(1, 11) => Ok(MatrixVersion::V1_11),
|
|
_ => Err(UnknownVersionError),
|
|
}
|
|
}
|
|
|
|
/// Constructor for use by the `metadata!` macro.
|
|
///
|
|
/// Accepts string literals and parses them.
|
|
#[doc(hidden)]
|
|
pub const fn from_lit(lit: &'static str) -> Self {
|
|
use konst::{option, primitive::parse_u8, result, string};
|
|
|
|
let major: u8;
|
|
let minor: u8;
|
|
|
|
let mut lit_iter = string::split(lit, ".").next();
|
|
|
|
{
|
|
let (checked_first, checked_split) = option::unwrap!(lit_iter); // First iteration always succeeds
|
|
|
|
major = result::unwrap_or_else!(parse_u8(checked_first), |_| panic!(
|
|
"major version is not a valid number"
|
|
));
|
|
|
|
lit_iter = checked_split.next();
|
|
}
|
|
|
|
match lit_iter {
|
|
Some((checked_second, checked_split)) => {
|
|
minor = result::unwrap_or_else!(parse_u8(checked_second), |_| panic!(
|
|
"minor version is not a valid number"
|
|
));
|
|
|
|
lit_iter = checked_split.next();
|
|
}
|
|
None => panic!("could not find dot to denote second number"),
|
|
}
|
|
|
|
if lit_iter.is_some() {
|
|
panic!("version literal contains more than one dot")
|
|
}
|
|
|
|
result::unwrap_or_else!(Self::from_parts(major, minor), |_| panic!(
|
|
"not a valid version literal"
|
|
))
|
|
}
|
|
|
|
// Internal function to do ordering in const-fn contexts
|
|
const fn const_ord(&self, other: &Self) -> Ordering {
|
|
let self_parts = self.into_parts();
|
|
let other_parts = other.into_parts();
|
|
|
|
use konst::primitive::cmp::cmp_u8;
|
|
|
|
let major_ord = cmp_u8(self_parts.0, other_parts.0);
|
|
if major_ord.is_ne() {
|
|
major_ord
|
|
} else {
|
|
cmp_u8(self_parts.1, other_parts.1)
|
|
}
|
|
}
|
|
|
|
// Internal function to check if this version is the legacy (v1.0) version in const-fn contexts
|
|
const fn is_legacy(&self) -> bool {
|
|
let self_parts = self.into_parts();
|
|
|
|
use konst::primitive::cmp::cmp_u8;
|
|
|
|
cmp_u8(self_parts.0, 1).is_eq() && cmp_u8(self_parts.1, 0).is_eq()
|
|
}
|
|
|
|
/// Get the default [`RoomVersionId`] for this `MatrixVersion`.
|
|
pub fn default_room_version(&self) -> RoomVersionId {
|
|
match self {
|
|
// <https://spec.matrix.org/historical/index.html#complete-list-of-room-versions>
|
|
MatrixVersion::V1_0
|
|
// <https://spec.matrix.org/v1.1/rooms/#complete-list-of-room-versions>
|
|
| MatrixVersion::V1_1
|
|
// <https://spec.matrix.org/v1.2/rooms/#complete-list-of-room-versions>
|
|
| MatrixVersion::V1_2 => RoomVersionId::V6,
|
|
// <https://spec.matrix.org/v1.3/rooms/#complete-list-of-room-versions>
|
|
MatrixVersion::V1_3
|
|
// <https://spec.matrix.org/v1.4/rooms/#complete-list-of-room-versions>
|
|
| MatrixVersion::V1_4
|
|
// <https://spec.matrix.org/v1.5/rooms/#complete-list-of-room-versions>
|
|
| MatrixVersion::V1_5 => RoomVersionId::V9,
|
|
// <https://spec.matrix.org/v1.6/rooms/#complete-list-of-room-versions>
|
|
MatrixVersion::V1_6
|
|
// <https://spec.matrix.org/v1.7/rooms/#complete-list-of-room-versions>
|
|
| MatrixVersion::V1_7
|
|
// <https://spec.matrix.org/v1.8/rooms/#complete-list-of-room-versions>
|
|
| MatrixVersion::V1_8
|
|
// <https://spec.matrix.org/v1.9/rooms/#complete-list-of-room-versions>
|
|
| MatrixVersion::V1_9
|
|
// <https://spec.matrix.org/v1.10/rooms/#complete-list-of-room-versions>
|
|
| MatrixVersion::V1_10
|
|
// <https://spec.matrix.org/v1.11/rooms/#complete-list-of-room-versions>
|
|
| MatrixVersion::V1_11 => RoomVersionId::V10,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for MatrixVersion {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
let (major, minor) = self.into_parts();
|
|
f.write_str(&format!("v{major}.{minor}"))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use assert_matches2::assert_matches;
|
|
use http::Method;
|
|
|
|
use super::{
|
|
AuthScheme,
|
|
MatrixVersion::{self, V1_0, V1_1, V1_2, V1_3},
|
|
Metadata, VersionHistory,
|
|
};
|
|
use crate::api::error::IntoHttpError;
|
|
|
|
fn stable_only_metadata(stable_paths: &'static [(MatrixVersion, &'static str)]) -> Metadata {
|
|
Metadata {
|
|
method: Method::GET,
|
|
rate_limited: false,
|
|
authentication: AuthScheme::None,
|
|
history: VersionHistory {
|
|
unstable_paths: &[],
|
|
stable_paths,
|
|
deprecated: None,
|
|
removed: None,
|
|
},
|
|
}
|
|
}
|
|
|
|
// TODO add test that can hook into tracing and verify the deprecation warning is emitted
|
|
|
|
#[test]
|
|
fn make_simple_endpoint_url() {
|
|
let meta = stable_only_metadata(&[(V1_0, "/s")]);
|
|
let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], "").unwrap();
|
|
assert_eq!(url, "https://example.org/s");
|
|
}
|
|
|
|
#[test]
|
|
fn make_endpoint_url_with_path_args() {
|
|
let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
|
|
let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[&"123"], "").unwrap();
|
|
assert_eq!(url, "https://example.org/s/123");
|
|
}
|
|
|
|
#[test]
|
|
fn make_endpoint_url_with_path_args_with_dash() {
|
|
let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
|
|
let url =
|
|
meta.make_endpoint_url(&[V1_0], "https://example.org", &[&"my-path"], "").unwrap();
|
|
assert_eq!(url, "https://example.org/s/my-path");
|
|
}
|
|
|
|
#[test]
|
|
fn make_endpoint_url_with_path_args_with_reserved_char() {
|
|
let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
|
|
let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[&"#path"], "").unwrap();
|
|
assert_eq!(url, "https://example.org/s/%23path");
|
|
}
|
|
|
|
#[test]
|
|
fn make_endpoint_url_with_query() {
|
|
let meta = stable_only_metadata(&[(V1_0, "/s/")]);
|
|
let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], "foo=bar").unwrap();
|
|
assert_eq!(url, "https://example.org/s/?foo=bar");
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic]
|
|
fn make_endpoint_url_wrong_num_path_args() {
|
|
let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
|
|
_ = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], "");
|
|
}
|
|
|
|
const EMPTY: VersionHistory =
|
|
VersionHistory { unstable_paths: &[], stable_paths: &[], deprecated: None, removed: None };
|
|
|
|
#[test]
|
|
fn select_latest_stable() {
|
|
let hist = VersionHistory { stable_paths: &[(V1_1, "/s")], ..EMPTY };
|
|
assert_matches!(hist.select_path(&[V1_0, V1_1]), Ok("/s"));
|
|
}
|
|
|
|
#[test]
|
|
fn select_unstable() {
|
|
let hist = VersionHistory { unstable_paths: &["/u"], ..EMPTY };
|
|
assert_matches!(hist.select_path(&[V1_0]), Ok("/u"));
|
|
}
|
|
|
|
#[test]
|
|
fn select_r0() {
|
|
let hist = VersionHistory { stable_paths: &[(V1_0, "/r")], ..EMPTY };
|
|
assert_matches!(hist.select_path(&[V1_0]), Ok("/r"));
|
|
}
|
|
|
|
#[test]
|
|
fn select_removed_err() {
|
|
let hist = VersionHistory {
|
|
stable_paths: &[(V1_0, "/r"), (V1_1, "/s")],
|
|
unstable_paths: &["/u"],
|
|
deprecated: Some(V1_2),
|
|
removed: Some(V1_3),
|
|
};
|
|
assert_matches!(hist.select_path(&[V1_3]), Err(IntoHttpError::EndpointRemoved(V1_3)));
|
|
}
|
|
|
|
#[test]
|
|
fn partially_removed_but_stable() {
|
|
let hist = VersionHistory {
|
|
stable_paths: &[(V1_0, "/r"), (V1_1, "/s")],
|
|
unstable_paths: &[],
|
|
deprecated: Some(V1_2),
|
|
removed: Some(V1_3),
|
|
};
|
|
assert_matches!(hist.select_path(&[V1_2]), Ok("/s"));
|
|
}
|
|
|
|
#[test]
|
|
fn no_unstable() {
|
|
let hist = VersionHistory { stable_paths: &[(V1_1, "/s")], ..EMPTY };
|
|
assert_matches!(hist.select_path(&[V1_0]), Err(IntoHttpError::NoUnstablePath));
|
|
}
|
|
|
|
#[test]
|
|
fn version_literal() {
|
|
const LIT: MatrixVersion = MatrixVersion::from_lit("1.0");
|
|
|
|
assert_eq!(LIT, V1_0);
|
|
}
|
|
}
|