use std::{
cmp::Ordering,
collections::{BTreeMap, BTreeSet},
fmt::{Display, Write},
str::FromStr,
};
use bytes::BufMut;
use http::{
header::{self, HeaderName, HeaderValue},
Method,
};
use percent_encoding::utf8_percent_encode;
use ruma_macros::{OrdAsRefStr, PartialEqAsRefStr, PartialOrdAsRefStr, StringEnum};
use tracing::warn;
use super::{
error::{IntoHttpError, UnknownVersionError},
AuthScheme, SendAccessToken,
};
use crate::{
percent_encode::PATH_PERCENT_ENCODE_SET, serde::slice_to_buf, PrivOwnedStr, RoomVersionId,
};
#[doc(hidden)]
#[macro_export]
macro_rules! metadata {
( $( $field:ident: $rhs:tt ),+ $(,)? ) => {
$crate::api::Metadata {
$( $field: $crate::metadata!(@field $field: $rhs) ),+
}
};
( @field method: $method:ident ) => { $crate::exports::http::Method::$method };
( @field authentication: $scheme:ident ) => { $crate::api::AuthScheme::$scheme };
( @field history: {
$( unstable $(($unstable_feature:literal))? => $unstable_path:literal, )*
$( stable ($stable_feature_only:literal) => $stable_feature_path:literal, )?
$( $( $version:literal $(| stable ($stable_feature:literal))? => $rhs:tt, )+ )?
} ) => {
$crate::metadata! {
@history_impl
[ $( $unstable_path $(= $unstable_feature)? ),* ]
$( stable ($stable_feature_only) => $stable_feature_path, )?
$( $( $rhs = $version $(| stable ($stable_feature))? ),+ )?
}
};
( @field $_field:ident: $rhs:expr ) => { $rhs };
( @history_impl
[ $( $unstable_path:literal $(= $unstable_feature:literal)? ),* ]
$( stable ($stable_feature_only:literal) => $stable_feature_path:literal, )?
$(
$( $stable_path:literal = $version:literal $(| stable ($stable_feature:literal))? ),+
$(,
deprecated = $deprecated_version:literal
$(, removed = $removed_version:literal )?
)?
)?
) => {
$crate::api::VersionHistory::new(
&[ $(($crate::metadata!(@optional_feature $($unstable_feature)?), $unstable_path)),* ],
&[
$((
$crate::metadata!(@stable_path_selector stable($stable_feature_only)),
$stable_feature_path
),)?
$($((
$crate::metadata!(@stable_path_selector $version $(| stable($stable_feature))?),
$stable_path
)),+)?
],
$crate::metadata!(@optional_version $($( $deprecated_version )?)?),
$crate::metadata!(@optional_version $($($( $removed_version )?)?)?),
)
};
( @optional_feature ) => { None };
( @optional_feature $feature:literal ) => { Some($feature) };
( @stable_path_selector stable($feature:literal)) => { $crate::api::StablePathSelector::Feature($feature) };
( @stable_path_selector $version:literal | stable($feature:literal)) => {
$crate::api::StablePathSelector::FeatureAndVersion {
feature: $feature,
version: $crate::api::MatrixVersion::from_lit(stringify!($version)),
}
};
( @stable_path_selector $version:literal) => { $crate::api::StablePathSelector::Version($crate::api::MatrixVersion::from_lit(stringify!($version))) };
( @optional_version ) => { None };
( @optional_version $version:literal ) => { Some($crate::api::MatrixVersion::from_lit(stringify!($version))) }
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[allow(clippy::exhaustive_structs)]
pub struct Metadata {
pub method: Method,
pub rate_limited: bool,
pub authentication: AuthScheme,
pub history: VersionHistory,
}
impl Metadata {
pub fn empty_request_body<B>(&self) -> B
where
B: Default + BufMut,
{
if self.method == Method::GET {
Default::default()
} else {
slice_to_buf(b"{}")
}
}
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 => {
let token = access_token
.get_required_for_appservice()
.ok_or(IntoHttpError::NeedsAuthentication)?;
Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?))
}
AuthScheme::AppserviceTokenOptional => match access_token.get_required_for_appservice()
{
Some(token) => Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?)),
None => None,
},
AuthScheme::ServerSignatures => None,
})
}
pub fn make_endpoint_url(
&self,
considering: &SupportedVersions,
base_url: &str,
path_args: &[&dyn Display],
query_string: &str,
) -> Result<String, IntoHttpError> {
let path_with_placeholders = self.history.select_path(considering)?;
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 Self::extract_endpoint_path_segment_variable(segment).is_some() {
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)
}
#[doc(hidden)]
pub fn _path_parameters(&self) -> Vec<&'static str> {
let path = self.history.all_paths().next().unwrap();
path.split('/').filter_map(Self::extract_endpoint_path_segment_variable).collect()
}
fn extract_endpoint_path_segment_variable(segment: &str) -> Option<&str> {
if segment.starts_with(':') {
panic!(
"endpoint paths syntax has changed and segment variables must be wrapped by `{{}}`"
);
}
if let Some(var) = segment.strip_prefix('{').map(|s| {
s.strip_suffix('}')
.expect("endpoint path segment variable braces mismatch: missing ending `}`")
}) {
return Some(var);
}
if segment.ends_with('}') {
panic!("endpoint path segment variable braces mismatch: missing starting `{{`");
}
None
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[allow(clippy::exhaustive_structs)]
pub struct VersionHistory {
unstable_paths: &'static [(Option<&'static str>, &'static str)],
stable_paths: &'static [(StablePathSelector, &'static str)],
deprecated: Option<MatrixVersion>,
removed: Option<MatrixVersion>,
}
impl VersionHistory {
pub const fn new(
unstable_paths: &'static [(Option<&'static str>, &'static str)],
stable_paths: &'static [(StablePathSelector, &'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 Some((second_s, second_n_iter)) = second_iter else {
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");
}
}
});
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();
}
}
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.1);
check_path_args_equal(ref_path, unstable_path.1);
});
let mut prev_seen_version: Option<MatrixVersion> = None;
iter::for_each!(version_path in slice::iter(stable_paths) => {
check_path_is_valid(version_path.1);
check_path_args_equal(ref_path, version_path.1);
if let Some(current_version) = version_path.0.version() {
if let Some(prev_seen_version) = prev_seen_version {
let cmp_result = current_version.const_ord(&prev_seen_version);
if cmp_result.is_eq() {
panic!("Duplicate matrix version in stable_paths")
} else if cmp_result.is_lt() {
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() {
panic!("deprecated version is equal to latest stable path version")
} else if ord_result.is_gt() {
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() {
panic!("removed version is equal to deprecated version")
} else if ord_result.is_gt() {
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 }
}
pub fn is_supported(&self, considering: &SupportedVersions) -> bool {
match self.versioning_decision_for(&considering.versions) {
VersioningDecision::Removed => false,
VersioningDecision::Version { .. } => true,
VersioningDecision::Feature => self.feature_path(&considering.features).is_some(),
}
}
fn select_path(&self, considering: &SupportedVersions) -> Result<&'static str, IntoHttpError> {
match self.versioning_decision_for(&considering.versions) {
VersioningDecision::Removed => Err(IntoHttpError::EndpointRemoved(
self.removed.expect("VersioningDecision::Removed implies metadata.removed"),
)),
VersioningDecision::Version { 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: {:?}",
considering.versions
);
} else if any_deprecated {
warn!(
"endpoint is removed (and deprecated) in some of the \
following versions: {:?}",
considering.versions
);
} else {
unreachable!("any_removed implies *_deprecated");
}
} else if all_deprecated {
warn!(
"endpoint is deprecated in ALL of the following versions: {:?}",
considering.versions
);
} else if any_deprecated {
warn!(
"endpoint is deprecated in some of the following versions: {:?}",
considering.versions
);
}
Ok(self
.version_path(&considering.versions)
.expect("VersioningDecision::Version implies that a version path exists"))
}
VersioningDecision::Feature => self
.feature_path(&considering.features)
.or_else(|| self.unstable())
.ok_or(IntoHttpError::NoUnstablePath),
}
}
pub fn versioning_decision_for(
&self,
versions: &BTreeSet<MatrixVersion>,
) -> VersioningDecision {
let is_superset_any =
|version: MatrixVersion| versions.iter().any(|v| v.is_superset_of(version));
let is_superset_all =
|version: MatrixVersion| versions.iter().all(|v| v.is_superset_of(version));
if self.removed.is_some_and(is_superset_all) {
return VersioningDecision::Removed;
}
if self.added_in().is_some_and(is_superset_any) {
let all_deprecated = self.deprecated.is_some_and(is_superset_all);
return VersioningDecision::Version {
any_deprecated: all_deprecated || self.deprecated.is_some_and(is_superset_any),
all_deprecated,
any_removed: self.removed.is_some_and(is_superset_any),
};
}
VersioningDecision::Feature
}
pub fn added_in(&self) -> Option<MatrixVersion> {
self.stable_paths.iter().find_map(|(v, _)| v.version())
}
pub fn deprecated_in(&self) -> Option<MatrixVersion> {
self.deprecated
}
pub fn removed_in(&self) -> Option<MatrixVersion> {
self.removed
}
pub fn unstable(&self) -> Option<&'static str> {
self.unstable_paths.last().map(|(_, path)| *path)
}
pub fn all_paths(&self) -> impl Iterator<Item = &'static str> {
self.unstable_paths().map(|(_, path)| path).chain(self.stable_paths().map(|(_, path)| path))
}
pub fn unstable_paths(&self) -> impl Iterator<Item = (Option<&'static str>, &'static str)> {
self.unstable_paths.iter().copied()
}
pub fn stable_paths(&self) -> impl Iterator<Item = (StablePathSelector, &'static str)> {
self.stable_paths.iter().copied()
}
pub fn version_path(&self, versions: &BTreeSet<MatrixVersion>) -> Option<&'static str> {
let version_paths = self
.stable_paths
.iter()
.filter_map(|(selector, path)| selector.version().map(|version| (version, path)));
for (ver, path) in version_paths.rev() {
if versions.iter().any(|v| v.is_superset_of(ver)) {
return Some(path);
}
}
None
}
pub fn feature_path(&self, supported_features: &BTreeSet<FeatureFlag>) -> Option<&'static str> {
let unstable_feature_paths = self
.unstable_paths
.iter()
.filter_map(|(feature, path)| feature.map(|feature| (feature, path)));
let stable_feature_paths = self
.stable_paths
.iter()
.filter_map(|(selector, path)| selector.feature().map(|feature| (feature, path)));
for (feature, path) in unstable_feature_paths.chain(stable_feature_paths).rev() {
if supported_features.iter().any(|supported| supported.as_str() == feature) {
return Some(path);
}
}
None
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[allow(clippy::exhaustive_enums)]
pub enum VersioningDecision {
Feature,
Version {
any_deprecated: bool,
all_deprecated: bool,
any_removed: bool,
},
Removed,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub enum MatrixVersion {
V1_0,
V1_1,
V1_2,
V1_3,
V1_4,
V1_5,
V1_6,
V1_7,
V1_8,
V1_9,
V1_10,
V1_11,
V1_12,
V1_13,
V1_14,
V1_15,
}
impl TryFrom<&str> for MatrixVersion {
type Error = UnknownVersionError;
fn try_from(value: &str) -> Result<MatrixVersion, Self::Error> {
use MatrixVersion::*;
Ok(match value {
"r0.2.0" | "r0.2.1" | "r0.3.0" |
"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,
"v1.12" => V1_12,
"v1.13" => V1_13,
"v1.14" => V1_14,
"v1.15" => V1_15,
_ => 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 {
pub fn is_superset_of(self, other: Self) -> bool {
self >= other
}
pub const fn as_str(self) -> Option<&'static str> {
let string = match self {
MatrixVersion::V1_0 => return None,
MatrixVersion::V1_1 => "v1.1",
MatrixVersion::V1_2 => "v1.2",
MatrixVersion::V1_3 => "v1.3",
MatrixVersion::V1_4 => "v1.4",
MatrixVersion::V1_5 => "v1.5",
MatrixVersion::V1_6 => "v1.6",
MatrixVersion::V1_7 => "v1.7",
MatrixVersion::V1_8 => "v1.8",
MatrixVersion::V1_9 => "v1.9",
MatrixVersion::V1_10 => "v1.10",
MatrixVersion::V1_11 => "v1.11",
MatrixVersion::V1_12 => "v1.12",
MatrixVersion::V1_13 => "v1.13",
MatrixVersion::V1_14 => "v1.14",
MatrixVersion::V1_15 => "v1.15",
};
Some(string)
}
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),
MatrixVersion::V1_12 => (1, 12),
MatrixVersion::V1_13 => (1, 13),
MatrixVersion::V1_14 => (1, 14),
MatrixVersion::V1_15 => (1, 15),
}
}
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),
(1, 12) => Ok(MatrixVersion::V1_12),
(1, 13) => Ok(MatrixVersion::V1_13),
(1, 14) => Ok(MatrixVersion::V1_14),
(1, 15) => Ok(MatrixVersion::V1_15),
_ => Err(UnknownVersionError),
}
}
#[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);
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"
))
}
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)
}
}
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()
}
pub fn default_room_version(&self) -> RoomVersionId {
match self {
MatrixVersion::V1_0
| MatrixVersion::V1_1
| MatrixVersion::V1_2 => RoomVersionId::V6,
MatrixVersion::V1_3
| MatrixVersion::V1_4
| MatrixVersion::V1_5 => RoomVersionId::V9,
MatrixVersion::V1_6
| MatrixVersion::V1_7
| MatrixVersion::V1_8
| MatrixVersion::V1_9
| MatrixVersion::V1_10
| MatrixVersion::V1_11
| MatrixVersion::V1_12
| MatrixVersion::V1_13 => RoomVersionId::V10,
| MatrixVersion::V1_14
| MatrixVersion::V1_15 => RoomVersionId::V11,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(clippy::exhaustive_enums)]
pub enum StablePathSelector {
Feature(&'static str),
Version(MatrixVersion),
FeatureAndVersion {
feature: &'static str,
version: MatrixVersion,
},
}
impl StablePathSelector {
pub const fn feature(self) -> Option<&'static str> {
match self {
Self::Feature(feature) | Self::FeatureAndVersion { feature, .. } => Some(feature),
_ => None,
}
}
pub const fn version(self) -> Option<MatrixVersion> {
match self {
Self::Version(version) | Self::FeatureAndVersion { version, .. } => Some(version),
_ => None,
}
}
}
impl From<MatrixVersion> for StablePathSelector {
fn from(value: MatrixVersion) -> Self {
Self::Version(value)
}
}
#[derive(Debug, Clone)]
#[allow(clippy::exhaustive_structs)]
pub struct SupportedVersions {
pub versions: BTreeSet<MatrixVersion>,
pub features: BTreeSet<FeatureFlag>,
}
impl SupportedVersions {
pub fn from_parts(versions: &[String], unstable_features: &BTreeMap<String, bool>) -> Self {
Self {
versions: versions.iter().flat_map(|s| s.parse::<MatrixVersion>()).collect(),
features: unstable_features
.iter()
.filter(|(_, enabled)| **enabled)
.map(|(feature, _)| feature.as_str().into())
.collect(),
}
}
}
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
#[derive(Clone, StringEnum, PartialEqAsRefStr, Eq, Hash, PartialOrdAsRefStr, OrdAsRefStr)]
#[non_exhaustive]
pub enum FeatureFlag {
#[ruma_enum(rename = "fi.mau.msc2246")]
Msc2246,
#[ruma_enum(rename = "org.matrix.msc2432")]
Msc2432,
#[ruma_enum(rename = "fi.mau.msc2659")]
Msc2659,
#[ruma_enum(rename = "fi.mau.msc2659.stable")]
Msc2659Stable,
#[cfg(feature = "unstable-msc2666")]
#[ruma_enum(rename = "uk.half-shot.msc2666.query_mutual_rooms")]
Msc2666,
#[ruma_enum(rename = "org.matrix.msc3030")]
Msc3030,
#[ruma_enum(rename = "org.matrix.msc3882")]
Msc3882,
#[ruma_enum(rename = "org.matrix.msc3916")]
Msc3916,
#[ruma_enum(rename = "org.matrix.msc3916.stable")]
Msc3916Stable,
#[cfg(feature = "unstable-msc4108")]
#[ruma_enum(rename = "org.matrix.msc4108")]
Msc4108,
#[cfg(feature = "unstable-msc4140")]
#[ruma_enum(rename = "org.matrix.msc4140")]
Msc4140,
#[cfg(feature = "unstable-msc4186")]
#[ruma_enum(rename = "org.matrix.simplified_msc3575")]
Msc4186,
#[doc(hidden)]
_Custom(PrivOwnedStr),
}
#[cfg(test)]
mod tests {
use std::collections::{BTreeMap, BTreeSet};
use assert_matches2::assert_matches;
use http::Method;
use super::{
AuthScheme,
MatrixVersion::{self, V1_0, V1_1, V1_2, V1_3},
Metadata, StablePathSelector, SupportedVersions, VersionHistory,
};
use crate::api::error::IntoHttpError;
fn stable_only_metadata(
stable_paths: &'static [(StablePathSelector, &'static str)],
) -> Metadata {
Metadata {
method: Method::GET,
rate_limited: false,
authentication: AuthScheme::None,
history: VersionHistory {
unstable_paths: &[],
stable_paths,
deprecated: None,
removed: None,
},
}
}
fn version_only_supported(versions: &[MatrixVersion]) -> SupportedVersions {
SupportedVersions {
versions: versions.iter().copied().collect(),
features: BTreeSet::new(),
}
}
#[test]
fn make_simple_endpoint_url() {
let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s")]);
let url = meta
.make_endpoint_url(&version_only_supported(&[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(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
let url = meta
.make_endpoint_url(
&version_only_supported(&[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(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
let url = meta
.make_endpoint_url(
&version_only_supported(&[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(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
let url = meta
.make_endpoint_url(
&version_only_supported(&[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(&[(StablePathSelector::Version(V1_0), "/s/")]);
let url = meta
.make_endpoint_url(
&version_only_supported(&[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(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
_ = meta.make_endpoint_url(
&version_only_supported(&[V1_0]),
"https://example.org",
&[],
"",
);
}
const EMPTY: VersionHistory =
VersionHistory { unstable_paths: &[], stable_paths: &[], deprecated: None, removed: None };
#[test]
fn select_version() {
let version_supported = version_only_supported(&[V1_0, V1_1]);
let superset_supported = version_only_supported(&[V1_1]);
let hist =
VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_0), "/s")], ..EMPTY };
assert_matches!(hist.select_path(&version_supported), Ok("/s"));
assert!(hist.is_supported(&version_supported));
assert_matches!(hist.select_path(&superset_supported), Ok("/s"));
assert!(hist.is_supported(&superset_supported));
let hist = VersionHistory {
stable_paths: &[(
StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_0 },
"/s",
)],
..EMPTY
};
assert_matches!(hist.select_path(&version_supported), Ok("/s"));
assert!(hist.is_supported(&version_supported));
assert_matches!(hist.select_path(&superset_supported), Ok("/s"));
assert!(hist.is_supported(&superset_supported));
let hist = VersionHistory {
stable_paths: &[
(StablePathSelector::Version(V1_0), "/s_v1"),
(StablePathSelector::Version(V1_1), "/s_v2"),
],
..EMPTY
};
assert_matches!(hist.select_path(&version_supported), Ok("/s_v2"));
assert!(hist.is_supported(&version_supported));
let unstable_supported = SupportedVersions {
versions: [V1_0].into(),
features: ["org.boo.unstable".into()].into(),
};
let hist = VersionHistory {
unstable_paths: &[(Some("org.boo.unstable"), "/u")],
stable_paths: &[(StablePathSelector::Version(V1_0), "/s")],
..EMPTY
};
assert_matches!(hist.select_path(&unstable_supported), Ok("/s"));
assert!(hist.is_supported(&unstable_supported));
}
#[test]
fn select_stable_feature() {
let supported = SupportedVersions {
versions: [V1_1].into(),
features: ["org.boo.unstable".into(), "org.boo.stable".into()].into(),
};
let hist = VersionHistory {
unstable_paths: &[(Some("org.boo.unstable"), "/u")],
stable_paths: &[(StablePathSelector::Feature("org.boo.stable"), "/s")],
..EMPTY
};
assert_matches!(hist.select_path(&supported), Ok("/s"));
assert!(hist.is_supported(&supported));
let hist = VersionHistory {
unstable_paths: &[(Some("org.boo.unstable"), "/u")],
stable_paths: &[(
StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_3 },
"/s",
)],
..EMPTY
};
assert_matches!(hist.select_path(&supported), Ok("/s"));
assert!(hist.is_supported(&supported));
}
#[test]
fn select_unstable_feature() {
let supported = SupportedVersions {
versions: [V1_1].into(),
features: ["org.boo.unstable".into()].into(),
};
let hist = VersionHistory {
unstable_paths: &[(Some("org.boo.unstable"), "/u")],
stable_paths: &[(
StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_3 },
"/s",
)],
..EMPTY
};
assert_matches!(hist.select_path(&supported), Ok("/u"));
assert!(hist.is_supported(&supported));
}
#[test]
fn select_unstable_fallback() {
let supported = version_only_supported(&[V1_0]);
let hist = VersionHistory { unstable_paths: &[(None, "/u")], ..EMPTY };
assert_matches!(hist.select_path(&supported), Ok("/u"));
assert!(!hist.is_supported(&supported));
}
#[test]
fn select_r0() {
let supported = version_only_supported(&[V1_0]);
let hist =
VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_0), "/r")], ..EMPTY };
assert_matches!(hist.select_path(&supported), Ok("/r"));
assert!(hist.is_supported(&supported));
}
#[test]
fn select_removed_err() {
let supported = version_only_supported(&[V1_3]);
let hist = VersionHistory {
stable_paths: &[
(StablePathSelector::Version(V1_0), "/r"),
(StablePathSelector::Version(V1_1), "/s"),
],
unstable_paths: &[(None, "/u")],
deprecated: Some(V1_2),
removed: Some(V1_3),
};
assert_matches!(hist.select_path(&supported), Err(IntoHttpError::EndpointRemoved(V1_3)));
assert!(!hist.is_supported(&supported));
}
#[test]
fn partially_removed_but_stable() {
let supported = version_only_supported(&[V1_2]);
let hist = VersionHistory {
stable_paths: &[
(StablePathSelector::Version(V1_0), "/r"),
(StablePathSelector::Version(V1_1), "/s"),
],
unstable_paths: &[],
deprecated: Some(V1_2),
removed: Some(V1_3),
};
assert_matches!(hist.select_path(&supported), Ok("/s"));
assert!(hist.is_supported(&supported));
}
#[test]
fn no_unstable() {
let supported = version_only_supported(&[V1_0]);
let hist =
VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_1), "/s")], ..EMPTY };
assert_matches!(hist.select_path(&supported), Err(IntoHttpError::NoUnstablePath));
assert!(!hist.is_supported(&supported));
}
#[test]
fn version_literal() {
const LIT: MatrixVersion = MatrixVersion::from_lit("1.0");
assert_eq!(LIT, V1_0);
}
#[test]
fn parse_as_str_sanity() {
let version = MatrixVersion::try_from("r0.5.0").unwrap();
assert_eq!(version, V1_0);
assert_eq!(version.as_str(), None);
let version = MatrixVersion::try_from("v1.1").unwrap();
assert_eq!(version, V1_1);
assert_eq!(version.as_str(), Some("v1.1"));
}
#[test]
fn supported_versions_from_parts() {
let empty_features = BTreeMap::new();
let none = &[];
let none_supported = SupportedVersions::from_parts(none, &empty_features);
assert_eq!(none_supported.versions, BTreeSet::new());
assert_eq!(none_supported.features, BTreeSet::new());
let single_known = &["r0.6.0".to_owned()];
let single_known_supported = SupportedVersions::from_parts(single_known, &empty_features);
assert_eq!(single_known_supported.versions, BTreeSet::from([V1_0]));
assert_eq!(single_known_supported.features, BTreeSet::new());
let multiple_known = &["v1.1".to_owned(), "r0.6.0".to_owned(), "r0.6.1".to_owned()];
let multiple_known_supported =
SupportedVersions::from_parts(multiple_known, &empty_features);
assert_eq!(multiple_known_supported.versions, BTreeSet::from([V1_0, V1_1]));
assert_eq!(multiple_known_supported.features, BTreeSet::new());
let single_unknown = &["v0.0".to_owned()];
let single_unknown_supported =
SupportedVersions::from_parts(single_unknown, &empty_features);
assert_eq!(single_unknown_supported.versions, BTreeSet::new());
assert_eq!(single_unknown_supported.features, BTreeSet::new());
let mut features = BTreeMap::new();
features.insert("org.bar.enabled_1".to_owned(), true);
features.insert("org.bar.disabled".to_owned(), false);
features.insert("org.bar.enabled_2".to_owned(), true);
let features_supported = SupportedVersions::from_parts(single_known, &features);
assert_eq!(features_supported.versions, BTreeSet::from([V1_0]));
assert_eq!(
features_supported.features,
["org.bar.enabled_1".into(), "org.bar.enabled_2".into()].into()
);
}
#[test]
fn supported_versions_from_parts_order() {
let empty_features = BTreeMap::new();
let sorted = &[
"r0.0.1".to_owned(),
"r0.5.0".to_owned(),
"r0.6.0".to_owned(),
"r0.6.1".to_owned(),
"v1.1".to_owned(),
"v1.2".to_owned(),
];
let sorted_supported = SupportedVersions::from_parts(sorted, &empty_features);
assert_eq!(sorted_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));
let sorted_reverse = &[
"v1.2".to_owned(),
"v1.1".to_owned(),
"r0.6.1".to_owned(),
"r0.6.0".to_owned(),
"r0.5.0".to_owned(),
"r0.0.1".to_owned(),
];
let sorted_reverse_supported =
SupportedVersions::from_parts(sorted_reverse, &empty_features);
assert_eq!(sorted_reverse_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));
let random_order = &[
"v1.1".to_owned(),
"r0.6.1".to_owned(),
"r0.5.0".to_owned(),
"r0.6.0".to_owned(),
"r0.0.1".to_owned(),
"v1.2".to_owned(),
];
let random_order_supported = SupportedVersions::from_parts(random_order, &empty_features);
assert_eq!(random_order_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));
}
#[test]
#[should_panic]
fn make_endpoint_url_with_path_args_old_syntax() {
let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s/:x")]);
let url = meta
.make_endpoint_url(
&version_only_supported(&[V1_0]),
"https://example.org",
&[&"123"],
"",
)
.unwrap();
assert_eq!(url, "https://example.org/s/123");
}
}