diff --git a/crates/ruma-api/src/lib.rs b/crates/ruma-api/src/lib.rs index 72142db5..04c467db 100644 --- a/crates/ruma-api/src/lib.rs +++ b/crates/ruma-api/src/lib.rs @@ -18,7 +18,11 @@ #[cfg(not(all(feature = "client", feature = "server")))] compile_error!("ruma_api's Cargo features only exist as a workaround are not meant to be disabled"); -use std::{convert::TryInto as _, error::Error as StdError}; +use std::{ + convert::{TryFrom, TryInto as _}, + error::Error as StdError, + fmt::{self, Display}, +}; use bytes::BufMut; use http::Method; @@ -421,3 +425,140 @@ pub struct Metadata { /// What authentication scheme the server uses for this endpoint. pub authentication: AuthScheme, } + +/// 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: . +// TODO add the following once `EndpointPath` and added/deprecated/removed macros are in place; +// 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 version you pass +// it with [`EndpointPath`], see its respective documentation for more. +#[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 . + V1_0, + + /// Version 1.1 of the Matrix specification, released in Q4 2021. + /// + /// See . + V1_1, + + /// Version 1.2 of the Matrix specification, released in Q1 2022. + /// + /// See . + V1_2, +} + +/// An error that happens when Ruma cannot understand a Matrix version. +#[derive(Debug)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct UnknownVersionError; + +impl Display for UnknownVersionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Version string was unknown.") + } +} + +impl StdError for UnknownVersionError {} + +impl TryFrom<&str> for MatrixVersion { + type Error = UnknownVersionError; + + fn try_from(value: &str) -> Result { + 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/v1.2/#legacy-versioning + "r0.5.0" | "r0.6.0" | "r0.6.1" => V1_0, + "v1.1" => V1_1, + "v1.2" => V1_2, + _ => return Err(UnknownVersionError), + }) + } +} + +impl Display for MatrixVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let r = self.repr(); + + f.write_str(&format!("v{}.{}", r.major, r.minor)) + } +} + +// Internal-only structure to abstract away version representations into Major-Minor bits. +// +// This is not represented on MatrixVersion due to the major footguns it exposes, +// maybe in the future this'll be merged into it, but not now. +#[derive(PartialEq)] +struct VersionRepr { + major: u8, + minor: u8, +} + +impl VersionRepr { + fn new(major: u8, minor: u8) -> Self { + VersionRepr { major, minor } + } +} + +// We don't expose this on MatrixVersion due to the subtleties of non-total ordering semantics +// the Matrix versions have; we cannot guarantee ordering between major versions, and only between +// minor versions of the same major one. +// +// This means that V2_0 > V1_0 returns false, and V2_0 < V1_0 too. +// +// This sort of behavior has to be pre-emptively known by the programmer, which is the definition of +// a gotcha/footgun. +// +// As such, we're not including it in the public API (only using it via `MatrixVersion::compatible`) +// until we find a way to introduce it in a way that exposes the ambiguities to the programmer. +impl PartialOrd for VersionRepr { + fn partial_cmp(&self, other: &Self) -> Option { + if self.major != other.major { + // Ordering between major versions is non-total. + return None; + } + self.minor.partial_cmp(&other.minor) + } +} + +impl MatrixVersion { + /// Checks wether 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 { + self.repr() >= other.repr() + } + + // Internal function to desugar the enum to a version repr + fn repr(&self) -> VersionRepr { + match self { + MatrixVersion::V1_0 => VersionRepr::new(1, 0), + MatrixVersion::V1_1 => VersionRepr::new(1, 1), + MatrixVersion::V1_2 => VersionRepr::new(1, 2), + } + } +} diff --git a/crates/ruma-client-api/src/unversioned/get_supported_versions.rs b/crates/ruma-client-api/src/unversioned/get_supported_versions.rs index e51e0f2e..18df4c15 100644 --- a/crates/ruma-client-api/src/unversioned/get_supported_versions.rs +++ b/crates/ruma-client-api/src/unversioned/get_supported_versions.rs @@ -1,8 +1,8 @@ //! [GET /_matrix/client/versions](https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-versions) -use std::collections::BTreeMap; +use std::{collections::BTreeMap, convert::TryInto as _}; -use ruma_api::ruma_api; +use ruma_api::{ruma_api, MatrixVersion}; ruma_api! { metadata: { @@ -41,4 +41,19 @@ impl Response { pub fn new(versions: Vec) -> Self { Self { versions, unstable_features: BTreeMap::new() } } + + /// Extracts known Matrix versions from this response. + /// + /// Matrix versions that Ruma cannot parse, or does not know about, are discarded. + pub fn known_versions(&self) -> Vec { + let mut versions = vec![]; + for s in &self.versions { + if let Ok(ver) = s.as_str().try_into() { + if !versions.contains(&ver) { + versions.push(ver) + } + } + } + versions + } }