api: Replace path fields in Metadata with new VersionHistory type

Co-authored-by: Jonathan de Jong <jonathan@automatia.nl>
This commit is contained in:
Jonas Platte 2022-10-20 19:29:51 +02:00
parent 451a50a77b
commit ec31badd84
No known key found for this signature in database
GPG Key ID: 7D261D771D915378
8 changed files with 445 additions and 269 deletions

View File

@ -197,7 +197,7 @@ pub use ruma_macros::ruma_api;
pub mod error;
mod metadata;
pub use metadata::{MatrixVersion, Metadata, VersioningDecision};
pub use metadata::{MatrixVersion, Metadata, VersionHistory, VersioningDecision};
use error::{FromHttpRequestError, FromHttpResponseError, IntoHttpError};

View File

@ -292,3 +292,23 @@ impl fmt::Display for UnknownVersionError {
}
impl StdError for UnknownVersionError {}
/// An error that happens when an incorrect amount of arguments have been passed to PathData parts
/// formatting.
#[derive(Debug)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct IncorrectArgumentCount {
/// The expected amount of arguments.
pub expected: usize,
/// The amount of arguments received.
pub got: usize,
}
impl fmt::Display for IncorrectArgumentCount {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "incorrect path argument count, expected {}, got {}", self.expected, self.got)
}
}
impl StdError for IncorrectArgumentCount {}

View File

@ -26,82 +26,17 @@ pub struct Metadata {
/// A unique identifier for this endpoint.
pub name: &'static str,
/// The unstable path of this endpoint's URL, often `None`, used for developmental
/// purposes.
pub unstable_path: Option<&'static str>,
/// The pre-v1.1 version of this endpoint's URL, `None` for post-v1.1 endpoints,
/// supplemental to `stable_path`.
pub r0_path: Option<&'static str>,
/// The path of this endpoint's URL, with variable names where path parameters should be
/// filled in during a request.
pub stable_path: Option<&'static str>,
/// 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,
/// The matrix version that this endpoint was added in.
///
/// Is None when this endpoint is unstable/unreleased.
pub added: Option<MatrixVersion>,
/// 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.
pub 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.
pub removed: Option<MatrixVersion>,
/// All info pertaining to an endpoint's (historic) paths, deprecation version, and removal.
pub history: VersionHistory,
}
impl Metadata {
/// Will decide how a particular set of matrix versions sees an endpoint.
///
/// It will pick `Stable` over `R0` and `Unstable`. It'll return `Deprecated` or `Removed` only
/// 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.map(greater_or_equal_all).unwrap_or(false) {
return VersioningDecision::Removed;
}
// Check if *any* version marks this endpoint as stable.
if self.added.map(greater_or_equal_any).unwrap_or(false) {
let all_deprecated = self.deprecated.map(greater_or_equal_all).unwrap_or(false);
return VersioningDecision::Stable {
any_deprecated: all_deprecated
|| self.deprecated.map(greater_or_equal_any).unwrap_or(false),
all_deprecated,
any_removed: self.removed.map(greater_or_equal_any).unwrap_or(false),
};
}
VersioningDecision::Unstable
}
/// Generate the endpoint URL for this endpoint.
pub fn make_endpoint_url(
&self,
@ -110,7 +45,7 @@ impl Metadata {
path_args: &[&dyn Display],
query_string: Option<&str>,
) -> Result<String, IntoHttpError> {
let path_with_placeholders = self.select_path(versions)?;
let path_with_placeholders = self.history.select_path(versions, self.name)?;
let mut res = base_url.strip_suffix('/').unwrap_or(base_url).to_owned();
let mut segments = path_with_placeholders.split('/');
@ -142,9 +77,47 @@ impl Metadata {
Ok(res)
}
}
// This function helps picks the right path (or an error) from a set of matrix versions.
fn select_path(&self, versions: &[MatrixVersion]) -> Result<&str, IntoHttpError> {
/// 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)]
#[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.
pub unstable_paths: &'static [&'static str],
/// A list of path versions, mapped to Matrix versions.
///
/// Sorted (ascending) by Matrix version, will not mix major versions.
pub 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.
pub 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.
pub removed: Option<MatrixVersion>,
}
impl VersionHistory {
// This function helps picks the right path (or an error) from a set of Matrix versions.
fn select_path(
&self,
versions: &[MatrixVersion],
name: &str,
) -> Result<&'static str, IntoHttpError> {
match self.versioning_decision_for(versions) {
VersioningDecision::Removed => Err(IntoHttpError::EndpointRemoved(
self.removed.expect("VersioningDecision::Removed implies metadata.removed"),
@ -153,55 +126,134 @@ impl Metadata {
if any_removed {
if all_deprecated {
warn!(
"endpoint {} is removed in some (and deprecated in ALL) \
of the following versions: {:?}",
self.name, versions
"endpoint {name} 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: {:?}",
self.name, versions
"endpoint {name} 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: {:?}",
self.name, versions
"endpoint {name} is deprecated in ALL of the following versions: \
{versions:?}",
);
} else if any_deprecated {
warn!(
"endpoint {} is deprecated in some of the following versions: {:?}",
self.name, versions
"endpoint {name} is deprecated in some of the following versions: \
{versions:?}",
);
}
if let Some(r0) = self.r0_path {
if versions.iter().all(|&v| v == MatrixVersion::V1_0) {
// Endpoint was added in 1.0, we return the r0 variant.
return Ok(r0);
}
}
Ok(self.stable_path.expect("metadata.added enforces the stable path to exist"))
Ok(self
.stable_endpoint_for(versions)
.expect("VersioningDecision::Stable implies that a stable path exists"))
}
VersioningDecision::Unstable => self.unstable_path.ok_or(IntoHttpError::NoUnstablePath),
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.map_or(false, greater_or_equal_all) {
return VersioningDecision::Removed;
}
// Check if *any* version marks this endpoint as stable.
if self.added_version().map_or(false, greater_or_equal_any) {
let all_deprecated = self.deprecated.map_or(false, greater_or_equal_all);
return VersioningDecision::Stable {
any_deprecated: all_deprecated
|| self.deprecated.map_or(false, greater_or_equal_any),
all_deprecated,
any_removed: self.removed.map_or(false, greater_or_equal_any),
};
}
VersioningDecision::Unstable
}
/// Will return the *first* version this path was added in.
///
/// Is None when this endpoint is unstable/unreleased.
pub fn added_version(&self) -> Option<MatrixVersion> {
self.stable_paths.first().map(|(x, _)| *x)
}
/// 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) -> Vec<&'static str> {
let unstable = self.unstable_paths.iter().copied();
let stable = self.stable_paths.iter().map(|(_, y)| *y);
unstable.chain(stable).collect()
}
/// Returns all unstable path variants in canon form.
pub fn all_unstable_paths(&self) -> Vec<&'static str> {
self.unstable_paths.to_owned()
}
/// Returns all stable path variants in canon form, with corresponding Matrix version.
pub fn all_versioned_stable_paths(&self) -> Vec<(MatrixVersion, &'static str)> {
self.stable_paths.iter().map(|(version, data)| (*version, *data)).collect()
}
/// 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.
/// 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.
///
/// Note, in the special case that all versions note [v1.0](MatrixVersion::V1_0), and the
/// [`r0_path`](Metadata::r0_path) is not `None`, that path should be used.
Stable {
/// If any version denoted deprecation.
any_deprecated: bool,
@ -212,6 +264,7 @@ pub enum VersioningDecision {
/// If any version denoted removal.
any_removed: bool,
},
/// This endpoint was removed in all versions, it should not be used.
Removed,
}
@ -363,44 +416,46 @@ mod tests {
use super::{
AuthScheme,
MatrixVersion::{V1_0, V1_1, V1_2},
Metadata,
MatrixVersion::{self, V1_0, V1_1, V1_2, V1_3},
Metadata, VersionHistory,
};
use crate::api::error::IntoHttpError;
const BASE: Metadata = Metadata {
description: "",
method: Method::GET,
name: "test_endpoint",
unstable_path: None,
r0_path: None,
stable_path: None,
rate_limited: false,
authentication: AuthScheme::None,
added: None,
deprecated: None,
removed: None,
};
fn stable_only_metadata(stable_paths: &'static [(MatrixVersion, &'static str)]) -> Metadata {
Metadata {
description: "",
method: Method::GET,
name: "test_endpoint",
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 = Metadata { added: Some(V1_0), stable_path: Some("/s"), ..BASE };
let meta = stable_only_metadata(&[(V1_0, "/s")]);
let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], None).unwrap();
assert_eq!(url, "https://example.org/s");
}
#[test]
fn make_endpoint_url_with_path_args() {
let meta = Metadata { added: Some(V1_0), stable_path: Some("/s/:x"), ..BASE };
let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[&"123"], None).unwrap();
assert_eq!(url, "https://example.org/s/123");
}
#[test]
fn make_endpoint_url_with_query() {
let meta = Metadata { added: Some(V1_0), stable_path: Some("/s/"), ..BASE };
let meta = stable_only_metadata(&[(V1_0, "/s/")]);
let url =
meta.make_endpoint_url(&[V1_0], "https://example.org", &[], Some("foo=bar")).unwrap();
assert_eq!(url, "https://example.org/s/?foo=bar");
@ -409,59 +464,62 @@ mod tests {
#[test]
#[should_panic]
fn make_endpoint_url_wrong_num_path_args() {
let meta = Metadata { added: Some(V1_0), stable_path: Some("/s/:x"), ..BASE };
let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
_ = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], None);
}
const EMPTY: VersionHistory =
VersionHistory { unstable_paths: &[], stable_paths: &[], deprecated: None, removed: None };
#[test]
fn select_stable() {
let meta = Metadata { added: Some(V1_1), stable_path: Some("s"), ..BASE };
assert_matches!(meta.select_path(&[V1_0, V1_1]), Ok("s"));
fn select_latest_stable() {
let hist = VersionHistory { stable_paths: &[(V1_1, "/s")], ..EMPTY };
assert_matches!(hist.select_path(&[V1_0, V1_1], "test_endpoint"), Ok("/s"));
}
#[test]
fn select_unstable() {
let meta = Metadata { unstable_path: Some("u"), ..BASE };
assert_matches!(meta.select_path(&[V1_0]), Ok("u"));
let hist = VersionHistory { unstable_paths: &["/u"], ..EMPTY };
assert_matches!(hist.select_path(&[V1_0], "test_endpoint"), Ok("/u"));
}
#[test]
fn select_r0() {
let meta = Metadata { added: Some(V1_0), r0_path: Some("r"), ..BASE };
assert_matches!(meta.select_path(&[V1_0]), Ok("r"));
let hist = VersionHistory { stable_paths: &[(V1_0, "/r")], ..EMPTY };
assert_matches!(hist.select_path(&[V1_0], "test_endpoint"), Ok("/r"));
}
#[test]
fn select_removed_err() {
let meta = Metadata {
added: Some(V1_0),
deprecated: Some(V1_1),
removed: Some(V1_2),
unstable_path: Some("u"),
r0_path: Some("r"),
stable_path: Some("s"),
..BASE
let hist = VersionHistory {
stable_paths: &[(V1_0, "/r"), (V1_1, "/s")],
unstable_paths: &["/u"],
deprecated: Some(V1_2),
removed: Some(V1_3),
};
assert_matches!(meta.select_path(&[V1_2]), Err(IntoHttpError::EndpointRemoved(V1_2)));
assert_matches!(
hist.select_path(&[V1_3], "test_endpoint"),
Err(IntoHttpError::EndpointRemoved(V1_3))
);
}
#[test]
fn partially_removed_but_stable() {
let meta = Metadata {
added: Some(V1_0),
deprecated: Some(V1_1),
removed: Some(V1_2),
r0_path: Some("r"),
stable_path: Some("s"),
..BASE
let hist = VersionHistory {
stable_paths: &[(V1_0, "/r"), (V1_1, "/s")],
unstable_paths: &[],
deprecated: Some(V1_2),
removed: Some(V1_3),
};
assert_matches!(meta.select_path(&[V1_1]), Ok("s"));
assert_matches!(hist.select_path(&[V1_2], "test_endpoint"), Ok("/s"));
}
#[test]
fn no_unstable() {
let meta =
Metadata { added: Some(V1_1), r0_path: Some("r"), stable_path: Some("s"), ..BASE };
assert_matches!(meta.select_path(&[V1_0]), Err(IntoHttpError::NoUnstablePath));
let hist = VersionHistory { stable_paths: &[(V1_1, "/s")], ..EMPTY };
assert_matches!(
hist.select_path(&[V1_0], "test_endpoint"),
Err(IntoHttpError::NoUnstablePath)
);
}
}

View File

@ -10,7 +10,7 @@ use ruma_common::{
FromHttpRequestError, FromHttpResponseError, IntoHttpError, MatrixError, ServerError,
},
AuthScheme, EndpointError, IncomingRequest, IncomingResponse, MatrixVersion, Metadata,
OutgoingRequest, OutgoingResponse, SendAccessToken,
OutgoingRequest, OutgoingResponse, SendAccessToken, VersionHistory,
},
OwnedRoomAliasId, OwnedRoomId,
};
@ -27,14 +27,18 @@ const METADATA: Metadata = Metadata {
description: "Add an alias to a room.",
method: Method::PUT,
name: "create_alias",
unstable_path: Some("/_matrix/client/unstable/directory/room/:room_alias"),
r0_path: Some("/_matrix/client/r0/directory/room/:room_alias"),
stable_path: Some("/_matrix/client/v3/directory/room/:room_alias"),
rate_limited: false,
authentication: AuthScheme::None,
added: Some(MatrixVersion::V1_0),
deprecated: Some(MatrixVersion::V1_1),
removed: Some(MatrixVersion::V1_2),
history: VersionHistory {
unstable_paths: &["/_matrix/client/unstable/directory/room/:room_alias"],
stable_paths: &[
(MatrixVersion::V1_0, "/_matrix/client/r0/directory/room/:room_alias"),
(MatrixVersion::V1_1, "/_matrix/client/v3/directory/room/:room_alias"),
],
deprecated: Some(MatrixVersion::V1_2),
removed: Some(MatrixVersion::V1_3),
},
};
impl OutgoingRequest for Request {

View File

@ -12,7 +12,7 @@ ruma_api! {
name: "some_endpoint",
unstable_path: "/_matrix/some/msc1234/endpoint/:baz",
r0_path: "/_matrix/some/r0/endpoint/:baz",
stable_path: "/_matrix/some/v1/endpoint/:baz",
stable_path: "/_matrix/some/v3/endpoint/:baz",
rate_limited: false,
authentication: None,
added: 1.0,
@ -61,11 +61,16 @@ ruma_api! {
fn main() {
use ruma_common::api::MatrixVersion;
assert_eq!(METADATA.unstable_path, Some("/_matrix/some/msc1234/endpoint/:baz"));
assert_eq!(METADATA.r0_path, Some("/_matrix/some/r0/endpoint/:baz"));
assert_eq!(METADATA.stable_path, Some("/_matrix/some/v1/endpoint/:baz"));
assert_eq!(METADATA.history.all_unstable_paths(), &["/_matrix/some/msc1234/endpoint/:baz"],);
assert_eq!(
METADATA.history.all_versioned_stable_paths(),
&[
(MatrixVersion::V1_0, "/_matrix/some/r0/endpoint/:baz"),
(MatrixVersion::V1_1, "/_matrix/some/v3/endpoint/:baz")
]
);
assert_eq!(METADATA.added, Some(MatrixVersion::V1_0));
assert_eq!(METADATA.deprecated, Some(MatrixVersion::V1_1));
assert_eq!(METADATA.removed, Some(MatrixVersion::V1_2));
assert_eq!(METADATA.history.added_version(), Some(MatrixVersion::V1_0));
assert_eq!(METADATA.history.deprecated, Some(MatrixVersion::V1_1));
assert_eq!(METADATA.history.removed, Some(MatrixVersion::V1_2));
}

View File

@ -65,14 +65,9 @@ impl Api {
let description = &metadata.description;
let method = &metadata.method;
let name = &metadata.name;
let unstable_path = util::map_option_literal(&metadata.unstable_path);
let r0_path = util::map_option_literal(&metadata.r0_path);
let stable_path = util::map_option_literal(&metadata.stable_path);
let rate_limited = &self.metadata.rate_limited;
let authentication = &self.metadata.authentication;
let added = util::map_option_literal(&metadata.added);
let deprecated = util::map_option_literal(&metadata.deprecated);
let removed = util::map_option_literal(&metadata.removed);
let history = &self.metadata.history;
let error_ty = self.error_ty.map_or_else(
|| quote! { #ruma_common::api::error::MatrixError },
@ -93,14 +88,9 @@ impl Api {
description: #description,
method: #http::Method::#method,
name: #name,
unstable_path: #unstable_path,
r0_path: #r0_path,
stable_path: #stable_path,
added: #added,
deprecated: #deprecated,
removed: #removed,
rate_limited: #rate_limited,
authentication: #ruma_common::api::AuthScheme::#authentication,
history: #history,
};
#request
@ -112,20 +102,15 @@ impl Api {
}
fn check_paths(&self) -> syn::Result<()> {
let mut path_iter = self
.metadata
.unstable_path
.iter()
.chain(&self.metadata.r0_path)
.chain(&self.metadata.stable_path);
let mut path_iter = self.metadata.history.entries.iter().filter_map(|entry| entry.path());
let path = path_iter.next().ok_or_else(|| {
syn::Error::new(Span::call_site(), "at least one path metadata field must be set")
})?;
let path_args = get_path_args(&path.value());
let path_args = path.args();
for extra_path in path_iter {
let extra_path_args = get_path_args(&extra_path.value());
let extra_path_args = extra_path.args();
if extra_path_args != path_args {
return Err(syn::Error::new(
Span::call_site(),
@ -270,7 +255,3 @@ fn ensure_feature_presence() -> Option<&'static syn::Error> {
RESULT.as_ref().err()
}
fn get_path_args(path: &str) -> Vec<String> {
path.split('/').filter_map(|s| s.strip_prefix(':').map(ToOwned::to_owned)).collect()
}

View File

@ -1,6 +1,7 @@
//! Details of the `metadata` section of the procedural macro.
use quote::ToTokens;
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::{
braced,
parse::{Parse, ParseStream},
@ -35,29 +36,14 @@ pub struct Metadata {
/// The name field.
pub name: LitStr,
/// The unstable path field.
pub unstable_path: Option<EndpointPath>,
/// The pre-v1.1 path field.
pub r0_path: Option<EndpointPath>,
/// The stable path field.
pub stable_path: Option<EndpointPath>,
/// The rate_limited field.
pub rate_limited: LitBool,
/// The authentication field.
pub authentication: AuthScheme,
/// The added field.
pub added: Option<MatrixVersionLiteral>,
/// The deprecated field.
pub deprecated: Option<MatrixVersionLiteral>,
/// The removed field.
pub removed: Option<MatrixVersionLiteral>,
/// The version history field.
pub history: History,
}
fn set_field<T: ToTokens>(field: &mut Option<T>, value: T) -> syn::Result<()> {
@ -116,97 +102,98 @@ impl Parse for Metadata {
let missing_field =
|name| syn::Error::new_spanned(metadata_kw, format!("missing field `{}`", name));
let stable_or_r0 = stable_path.as_ref().or(r0_path.as_ref());
// Construct the History object.
let history = {
let stable_or_r0 = stable_path.as_ref().or(r0_path.as_ref());
if let Some(path) = stable_or_r0 {
if added.is_none() {
return Err(syn::Error::new_spanned(
path,
"stable path was defined, while `added` version was not defined",
));
if let Some(path) = stable_or_r0 {
if added.is_none() {
return Err(syn::Error::new_spanned(
path,
"stable path was defined, while `added` version was not defined",
));
}
}
}
if let Some(deprecated) = &deprecated {
if added.is_none() {
return Err(syn::Error::new_spanned(
deprecated,
"deprecated version is defined while added version is not defined",
));
if let Some(deprecated) = &deprecated {
if added.is_none() {
return Err(syn::Error::new_spanned(
deprecated,
"deprecated version is defined while added version is not defined",
));
}
}
}
// note: It is possible that matrix will remove endpoints in a single version, while not
// having a deprecation version inbetween, but that would not be allowed by their own
// deprecation policy, so lets just assume there's always a deprecation version before a
// removal one.
//
// If matrix does so anyways, we can just alter this.
if let Some(removed) = &removed {
if deprecated.is_none() {
return Err(syn::Error::new_spanned(
removed,
"removed version is defined while deprecated version is not defined",
));
// Note: It is possible that Matrix will remove endpoints in a single version, while
// not having a deprecation version inbetween, but that would not be allowed by their
// own deprecation policy, so lets just assume there's always a deprecation version
// before a removal one.
//
// If Matrix does so anyways, we can just alter this.
if let Some(removed) = &removed {
if deprecated.is_none() {
return Err(syn::Error::new_spanned(
removed,
"removed version is defined while deprecated version is not defined",
));
}
}
}
if let Some(added) = &added {
if stable_or_r0.is_none() {
return Err(syn::Error::new_spanned(
added,
"added version is defined, but no stable or r0 path exists",
));
if let Some(added) = &added {
if stable_or_r0.is_none() {
return Err(syn::Error::new_spanned(
added,
"added version is defined, but no stable or r0 path exists",
));
}
}
}
if let Some(r0) = &r0_path {
let added = added.as_ref().expect("we error if r0 or stable is defined without added");
if let Some(r0) = &r0_path {
let added =
added.as_ref().expect("we error if r0 or stable is defined without added");
if added.major.get() == 1 && added.minor > 0 {
if added.major.get() == 1 && added.minor > 0 {
return Err(syn::Error::new_spanned(
r0,
"r0 defined while added version is newer than v1.0",
));
}
if stable_path.is_none() {
return Err(syn::Error::new_spanned(r0, "r0 defined without stable path"));
}
if !r0.value().contains("/r0/") {
return Err(syn::Error::new_spanned(r0, "r0 endpoint does not contain /r0/"));
}
}
if let Some(stable) = &stable_path {
if stable.value().contains("/r0/") {
return Err(syn::Error::new_spanned(
stable,
"stable endpoint contains /r0/ (did you make a copy-paste error?)",
));
}
}
if unstable_path.is_none() && r0_path.is_none() && stable_path.is_none() {
return Err(syn::Error::new_spanned(
r0,
"r0 defined while added version is newer than v1.0",
metadata_kw,
"need to define one of [r0_path, stable_path, unstable_path]",
));
}
if stable_path.is_none() {
return Err(syn::Error::new_spanned(r0, "r0 defined without stable path"));
}
if !r0.value().contains("/r0/") {
return Err(syn::Error::new_spanned(r0, "r0 endpoint does not contain /r0/"));
}
}
if let Some(stable) = &stable_path {
if stable.value().contains("/r0/") {
return Err(syn::Error::new_spanned(
stable,
"stable endpoint contains /r0/ (did you make a copy-paste error?)",
));
}
}
if unstable_path.is_none() && r0_path.is_none() && stable_path.is_none() {
return Err(syn::Error::new_spanned(
metadata_kw,
"need to define one of [r0_path, stable_path, unstable_path]",
));
}
History::construct(deprecated, removed, unstable_path, r0_path, stable_path.zip(added))
};
Ok(Self {
description: description.ok_or_else(|| missing_field("description"))?,
method: method.ok_or_else(|| missing_field("method"))?,
name: name.ok_or_else(|| missing_field("name"))?,
unstable_path,
r0_path,
stable_path,
rate_limited: rate_limited.ok_or_else(|| missing_field("rate_limited"))?,
authentication: authentication.ok_or_else(|| missing_field("authentication"))?,
added,
deprecated,
removed,
history,
})
}
}
@ -303,13 +290,127 @@ impl Parse for FieldValue {
}
}
#[derive(Clone)]
#[derive(Debug, PartialEq)]
pub struct History {
pub(super) entries: Vec<HistoryEntry>,
misc: MiscVersioning,
}
impl History {
// TODO(j0j0): remove after codebase conversion is complete
/// Construct a History object from legacy parts.
pub fn construct(
deprecated: Option<MatrixVersionLiteral>,
removed: Option<MatrixVersionLiteral>,
unstable_path: Option<EndpointPath>,
r0_path: Option<EndpointPath>,
stable_path_and_version: Option<(EndpointPath, MatrixVersionLiteral)>,
) -> Self {
// Unfortunately can't `use` associated constants
const V1_0: MatrixVersionLiteral = MatrixVersionLiteral::V1_0;
let unstable = unstable_path.map(|path| HistoryEntry::Unstable { path });
let r0 = r0_path.map(|path| HistoryEntry::Stable { path, version: V1_0 });
let stable = stable_path_and_version.map(|(path, mut version)| {
// If added in 1.0 as r0, the new stable path must be from 1.1
if r0.is_some() && version == V1_0 {
version = MatrixVersionLiteral::V1_1;
}
HistoryEntry::Stable { path, version }
});
let misc = match (deprecated, removed) {
(None, None) => MiscVersioning::None,
(Some(deprecated), None) => MiscVersioning::Deprecated(deprecated),
(Some(deprecated), Some(removed)) => MiscVersioning::Removed { deprecated, removed },
(None, Some(_)) => unreachable!("removed implies deprecated"),
};
let entries = [unstable, r0, stable].into_iter().flatten().collect();
History { entries, misc }
}
}
#[derive(Debug, PartialEq)]
pub enum MiscVersioning {
None,
Deprecated(MatrixVersionLiteral),
Removed { deprecated: MatrixVersionLiteral, removed: MatrixVersionLiteral },
}
impl ToTokens for History {
fn to_tokens(&self, tokens: &mut TokenStream) {
fn endpointpath_to_pathdata_ts(endpoint: &EndpointPath) -> String {
endpoint.value()
}
let unstable = self.entries.iter().filter_map(|e| match e {
HistoryEntry::Unstable { path } => Some(endpointpath_to_pathdata_ts(path)),
_ => None,
});
let versioned = self.entries.iter().filter_map(|e| match e {
HistoryEntry::Stable { path, version } => {
let path = endpointpath_to_pathdata_ts(path);
Some(quote! {( #version, #path )})
}
_ => None,
});
let (deprecated, removed) = match &self.misc {
MiscVersioning::None => (None, None),
MiscVersioning::Deprecated(deprecated) => (Some(deprecated), None),
MiscVersioning::Removed { deprecated, removed } => (Some(deprecated), Some(removed)),
};
let deprecated = util::map_option_literal(&deprecated);
let removed = util::map_option_literal(&removed);
tokens.extend(quote! {
::ruma_common::api::VersionHistory {
unstable_paths: &[ #(#unstable),* ],
stable_paths: &[ #(#versioned),* ],
deprecated: #deprecated,
removed: #removed,
}
});
}
}
#[derive(Debug, PartialEq)]
// Unused variants will be constructed when the macro input is updated
#[allow(dead_code)]
pub enum HistoryEntry {
Unstable { path: EndpointPath },
Stable { version: MatrixVersionLiteral, path: EndpointPath },
Deprecated { version: MatrixVersionLiteral },
Removed { version: MatrixVersionLiteral },
}
impl HistoryEntry {
pub(super) fn path(&self) -> Option<&EndpointPath> {
Some(match self {
HistoryEntry::Stable { version: _, path } => path,
HistoryEntry::Unstable { path } => path,
_ => return None,
})
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct EndpointPath(LitStr);
impl EndpointPath {
pub fn value(&self) -> String {
self.0.value()
}
pub fn args(&self) -> Vec<String> {
self.value().split('/').filter_map(|s| s.strip_prefix(':')).map(String::from).collect()
}
}
impl Parse for EndpointPath {
@ -328,7 +429,7 @@ impl Parse for EndpointPath {
}
impl ToTokens for EndpointPath {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.0.to_tokens(tokens);
}
}

View File

@ -4,12 +4,19 @@ use proc_macro2::TokenStream;
use quote::{format_ident, quote, ToTokens};
use syn::{parse::Parse, Error, LitFloat};
#[derive(Clone)]
#[derive(Clone, Debug, PartialEq)]
pub struct MatrixVersionLiteral {
pub(crate) major: NonZeroU8,
pub(crate) minor: u8,
}
const ONE: NonZeroU8 = unsafe { NonZeroU8::new_unchecked(1) };
impl MatrixVersionLiteral {
pub const V1_0: Self = Self { major: ONE, minor: 0 };
pub const V1_1: Self = Self { major: ONE, minor: 1 };
}
impl Parse for MatrixVersionLiteral {
fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
let fl: LitFloat = input.parse()?;