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; pub mod error;
mod metadata; mod metadata;
pub use metadata::{MatrixVersion, Metadata, VersioningDecision}; pub use metadata::{MatrixVersion, Metadata, VersionHistory, VersioningDecision};
use error::{FromHttpRequestError, FromHttpResponseError, IntoHttpError}; use error::{FromHttpRequestError, FromHttpResponseError, IntoHttpError};

View File

@ -292,3 +292,23 @@ impl fmt::Display for UnknownVersionError {
} }
impl StdError 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. /// A unique identifier for this endpoint.
pub name: &'static str, 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. /// Whether or not this endpoint is rate limited by the server.
pub rate_limited: bool, pub rate_limited: bool,
/// What authentication scheme the server uses for this endpoint. /// What authentication scheme the server uses for this endpoint.
pub authentication: AuthScheme, pub authentication: AuthScheme,
/// The matrix version that this endpoint was added in. /// All info pertaining to an endpoint's (historic) paths, deprecation version, and removal.
/// pub history: VersionHistory,
/// 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>,
} }
impl Metadata { 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. /// Generate the endpoint URL for this endpoint.
pub fn make_endpoint_url( pub fn make_endpoint_url(
&self, &self,
@ -110,7 +45,7 @@ impl Metadata {
path_args: &[&dyn Display], path_args: &[&dyn Display],
query_string: Option<&str>, query_string: Option<&str>,
) -> Result<String, IntoHttpError> { ) -> 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 res = base_url.strip_suffix('/').unwrap_or(base_url).to_owned();
let mut segments = path_with_placeholders.split('/'); let mut segments = path_with_placeholders.split('/');
@ -142,9 +77,47 @@ impl Metadata {
Ok(res) Ok(res)
} }
}
// This function helps picks the right path (or an error) from a set of matrix versions. /// The complete history of this endpoint as far as Ruma knows, together with all variants on
fn select_path(&self, versions: &[MatrixVersion]) -> Result<&str, IntoHttpError> { /// 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) { match self.versioning_decision_for(versions) {
VersioningDecision::Removed => Err(IntoHttpError::EndpointRemoved( VersioningDecision::Removed => Err(IntoHttpError::EndpointRemoved(
self.removed.expect("VersioningDecision::Removed implies metadata.removed"), self.removed.expect("VersioningDecision::Removed implies metadata.removed"),
@ -153,55 +126,134 @@ impl Metadata {
if any_removed { if any_removed {
if all_deprecated { if all_deprecated {
warn!( warn!(
"endpoint {} is removed in some (and deprecated in ALL) \ "endpoint {name} is removed in some (and deprecated in ALL) \
of the following versions: {:?}", of the following versions: {versions:?}",
self.name, versions
); );
} else if any_deprecated { } else if any_deprecated {
warn!( warn!(
"endpoint {} is removed (and deprecated) in some of the \ "endpoint {name} is removed (and deprecated) in some of the \
following versions: {:?}", following versions: {versions:?}",
self.name, versions
); );
} else { } else {
unreachable!("any_removed implies *_deprecated"); unreachable!("any_removed implies *_deprecated");
} }
} else if all_deprecated { } else if all_deprecated {
warn!( warn!(
"endpoint {} is deprecated in ALL of the following versions: {:?}", "endpoint {name} is deprecated in ALL of the following versions: \
self.name, versions {versions:?}",
); );
} else if any_deprecated { } else if any_deprecated {
warn!( warn!(
"endpoint {} is deprecated in some of the following versions: {:?}", "endpoint {name} is deprecated in some of the following versions: \
self.name, versions {versions:?}",
); );
} }
if let Some(r0) = self.r0_path { Ok(self
if versions.iter().all(|&v| v == MatrixVersion::V1_0) { .stable_endpoint_for(versions)
// Endpoint was added in 1.0, we return the r0 variant. .expect("VersioningDecision::Stable implies that a stable path exists"))
return Ok(r0);
}
}
Ok(self.stable_path.expect("metadata.added enforces the stable path to exist"))
} }
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)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[allow(clippy::exhaustive_enums)] #[allow(clippy::exhaustive_enums)]
pub enum VersioningDecision { pub enum VersioningDecision {
/// The unstable endpoint should be used. /// The unstable endpoint should be used.
Unstable, Unstable,
/// The stable endpoint should be used. /// 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 { Stable {
/// If any version denoted deprecation. /// If any version denoted deprecation.
any_deprecated: bool, any_deprecated: bool,
@ -212,6 +264,7 @@ pub enum VersioningDecision {
/// If any version denoted removal. /// If any version denoted removal.
any_removed: bool, any_removed: bool,
}, },
/// This endpoint was removed in all versions, it should not be used. /// This endpoint was removed in all versions, it should not be used.
Removed, Removed,
} }
@ -363,44 +416,46 @@ mod tests {
use super::{ use super::{
AuthScheme, AuthScheme,
MatrixVersion::{V1_0, V1_1, V1_2}, MatrixVersion::{self, V1_0, V1_1, V1_2, V1_3},
Metadata, Metadata, VersionHistory,
}; };
use crate::api::error::IntoHttpError; use crate::api::error::IntoHttpError;
const BASE: Metadata = Metadata { fn stable_only_metadata(stable_paths: &'static [(MatrixVersion, &'static str)]) -> Metadata {
description: "", Metadata {
method: Method::GET, description: "",
name: "test_endpoint", method: Method::GET,
unstable_path: None, name: "test_endpoint",
r0_path: None, rate_limited: false,
stable_path: None, authentication: AuthScheme::None,
rate_limited: false, history: VersionHistory {
authentication: AuthScheme::None, unstable_paths: &[],
added: None, stable_paths,
deprecated: None, deprecated: None,
removed: None, removed: None,
}; },
}
}
// TODO add test that can hook into tracing and verify the deprecation warning is emitted // TODO add test that can hook into tracing and verify the deprecation warning is emitted
#[test] #[test]
fn make_simple_endpoint_url() { 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(); let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], None).unwrap();
assert_eq!(url, "https://example.org/s"); assert_eq!(url, "https://example.org/s");
} }
#[test] #[test]
fn make_endpoint_url_with_path_args() { 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(); let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[&"123"], None).unwrap();
assert_eq!(url, "https://example.org/s/123"); assert_eq!(url, "https://example.org/s/123");
} }
#[test] #[test]
fn make_endpoint_url_with_query() { 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 = let url =
meta.make_endpoint_url(&[V1_0], "https://example.org", &[], Some("foo=bar")).unwrap(); meta.make_endpoint_url(&[V1_0], "https://example.org", &[], Some("foo=bar")).unwrap();
assert_eq!(url, "https://example.org/s/?foo=bar"); assert_eq!(url, "https://example.org/s/?foo=bar");
@ -409,59 +464,62 @@ mod tests {
#[test] #[test]
#[should_panic] #[should_panic]
fn make_endpoint_url_wrong_num_path_args() { 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); _ = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], None);
} }
const EMPTY: VersionHistory =
VersionHistory { unstable_paths: &[], stable_paths: &[], deprecated: None, removed: None };
#[test] #[test]
fn select_stable() { fn select_latest_stable() {
let meta = Metadata { added: Some(V1_1), stable_path: Some("s"), ..BASE }; let hist = VersionHistory { stable_paths: &[(V1_1, "/s")], ..EMPTY };
assert_matches!(meta.select_path(&[V1_0, V1_1]), Ok("s")); assert_matches!(hist.select_path(&[V1_0, V1_1], "test_endpoint"), Ok("/s"));
} }
#[test] #[test]
fn select_unstable() { fn select_unstable() {
let meta = Metadata { unstable_path: Some("u"), ..BASE }; let hist = VersionHistory { unstable_paths: &["/u"], ..EMPTY };
assert_matches!(meta.select_path(&[V1_0]), Ok("u")); assert_matches!(hist.select_path(&[V1_0], "test_endpoint"), Ok("/u"));
} }
#[test] #[test]
fn select_r0() { fn select_r0() {
let meta = Metadata { added: Some(V1_0), r0_path: Some("r"), ..BASE }; let hist = VersionHistory { stable_paths: &[(V1_0, "/r")], ..EMPTY };
assert_matches!(meta.select_path(&[V1_0]), Ok("r")); assert_matches!(hist.select_path(&[V1_0], "test_endpoint"), Ok("/r"));
} }
#[test] #[test]
fn select_removed_err() { fn select_removed_err() {
let meta = Metadata { let hist = VersionHistory {
added: Some(V1_0), stable_paths: &[(V1_0, "/r"), (V1_1, "/s")],
deprecated: Some(V1_1), unstable_paths: &["/u"],
removed: Some(V1_2), deprecated: Some(V1_2),
unstable_path: Some("u"), removed: Some(V1_3),
r0_path: Some("r"),
stable_path: Some("s"),
..BASE
}; };
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] #[test]
fn partially_removed_but_stable() { fn partially_removed_but_stable() {
let meta = Metadata { let hist = VersionHistory {
added: Some(V1_0), stable_paths: &[(V1_0, "/r"), (V1_1, "/s")],
deprecated: Some(V1_1), unstable_paths: &[],
removed: Some(V1_2), deprecated: Some(V1_2),
r0_path: Some("r"), removed: Some(V1_3),
stable_path: Some("s"),
..BASE
}; };
assert_matches!(meta.select_path(&[V1_1]), Ok("s")); assert_matches!(hist.select_path(&[V1_2], "test_endpoint"), Ok("/s"));
} }
#[test] #[test]
fn no_unstable() { fn no_unstable() {
let meta = let hist = VersionHistory { stable_paths: &[(V1_1, "/s")], ..EMPTY };
Metadata { added: Some(V1_1), r0_path: Some("r"), stable_path: Some("s"), ..BASE }; assert_matches!(
assert_matches!(meta.select_path(&[V1_0]), Err(IntoHttpError::NoUnstablePath)); hist.select_path(&[V1_0], "test_endpoint"),
Err(IntoHttpError::NoUnstablePath)
);
} }
} }

View File

@ -10,7 +10,7 @@ use ruma_common::{
FromHttpRequestError, FromHttpResponseError, IntoHttpError, MatrixError, ServerError, FromHttpRequestError, FromHttpResponseError, IntoHttpError, MatrixError, ServerError,
}, },
AuthScheme, EndpointError, IncomingRequest, IncomingResponse, MatrixVersion, Metadata, AuthScheme, EndpointError, IncomingRequest, IncomingResponse, MatrixVersion, Metadata,
OutgoingRequest, OutgoingResponse, SendAccessToken, OutgoingRequest, OutgoingResponse, SendAccessToken, VersionHistory,
}, },
OwnedRoomAliasId, OwnedRoomId, OwnedRoomAliasId, OwnedRoomId,
}; };
@ -27,14 +27,18 @@ const METADATA: Metadata = Metadata {
description: "Add an alias to a room.", description: "Add an alias to a room.",
method: Method::PUT, method: Method::PUT,
name: "create_alias", 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, rate_limited: false,
authentication: AuthScheme::None, authentication: AuthScheme::None,
added: Some(MatrixVersion::V1_0),
deprecated: Some(MatrixVersion::V1_1), history: VersionHistory {
removed: Some(MatrixVersion::V1_2), 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 { impl OutgoingRequest for Request {

View File

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

View File

@ -65,14 +65,9 @@ impl Api {
let description = &metadata.description; let description = &metadata.description;
let method = &metadata.method; let method = &metadata.method;
let name = &metadata.name; 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 rate_limited = &self.metadata.rate_limited;
let authentication = &self.metadata.authentication; let authentication = &self.metadata.authentication;
let added = util::map_option_literal(&metadata.added); let history = &self.metadata.history;
let deprecated = util::map_option_literal(&metadata.deprecated);
let removed = util::map_option_literal(&metadata.removed);
let error_ty = self.error_ty.map_or_else( let error_ty = self.error_ty.map_or_else(
|| quote! { #ruma_common::api::error::MatrixError }, || quote! { #ruma_common::api::error::MatrixError },
@ -93,14 +88,9 @@ impl Api {
description: #description, description: #description,
method: #http::Method::#method, method: #http::Method::#method,
name: #name, name: #name,
unstable_path: #unstable_path,
r0_path: #r0_path,
stable_path: #stable_path,
added: #added,
deprecated: #deprecated,
removed: #removed,
rate_limited: #rate_limited, rate_limited: #rate_limited,
authentication: #ruma_common::api::AuthScheme::#authentication, authentication: #ruma_common::api::AuthScheme::#authentication,
history: #history,
}; };
#request #request
@ -112,20 +102,15 @@ impl Api {
} }
fn check_paths(&self) -> syn::Result<()> { fn check_paths(&self) -> syn::Result<()> {
let mut path_iter = self let mut path_iter = self.metadata.history.entries.iter().filter_map(|entry| entry.path());
.metadata
.unstable_path
.iter()
.chain(&self.metadata.r0_path)
.chain(&self.metadata.stable_path);
let path = path_iter.next().ok_or_else(|| { let path = path_iter.next().ok_or_else(|| {
syn::Error::new(Span::call_site(), "at least one path metadata field must be set") 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 { 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 { if extra_path_args != path_args {
return Err(syn::Error::new( return Err(syn::Error::new(
Span::call_site(), Span::call_site(),
@ -270,7 +255,3 @@ fn ensure_feature_presence() -> Option<&'static syn::Error> {
RESULT.as_ref().err() 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. //! Details of the `metadata` section of the procedural macro.
use quote::ToTokens; use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::{ use syn::{
braced, braced,
parse::{Parse, ParseStream}, parse::{Parse, ParseStream},
@ -35,29 +36,14 @@ pub struct Metadata {
/// The name field. /// The name field.
pub name: LitStr, 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. /// The rate_limited field.
pub rate_limited: LitBool, pub rate_limited: LitBool,
/// The authentication field. /// The authentication field.
pub authentication: AuthScheme, pub authentication: AuthScheme,
/// The added field. /// The version history field.
pub added: Option<MatrixVersionLiteral>, pub history: History,
/// The deprecated field.
pub deprecated: Option<MatrixVersionLiteral>,
/// The removed field.
pub removed: Option<MatrixVersionLiteral>,
} }
fn set_field<T: ToTokens>(field: &mut Option<T>, value: T) -> syn::Result<()> { fn set_field<T: ToTokens>(field: &mut Option<T>, value: T) -> syn::Result<()> {
@ -116,97 +102,98 @@ impl Parse for Metadata {
let missing_field = let missing_field =
|name| syn::Error::new_spanned(metadata_kw, format!("missing field `{}`", name)); |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 let Some(path) = stable_or_r0 {
if added.is_none() { if added.is_none() {
return Err(syn::Error::new_spanned( return Err(syn::Error::new_spanned(
path, path,
"stable path was defined, while `added` version was not defined", "stable path was defined, while `added` version was not defined",
)); ));
}
} }
}
if let Some(deprecated) = &deprecated { if let Some(deprecated) = &deprecated {
if added.is_none() { if added.is_none() {
return Err(syn::Error::new_spanned( return Err(syn::Error::new_spanned(
deprecated, deprecated,
"deprecated version is defined while added version is not defined", "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 // Note: It is possible that Matrix will remove endpoints in a single version, while
// having a deprecation version inbetween, but that would not be allowed by their own // not having a deprecation version inbetween, but that would not be allowed by their
// deprecation policy, so lets just assume there's always a deprecation version before a // own deprecation policy, so lets just assume there's always a deprecation version
// removal one. // before a removal one.
// //
// If matrix does so anyways, we can just alter this. // If Matrix does so anyways, we can just alter this.
if let Some(removed) = &removed { if let Some(removed) = &removed {
if deprecated.is_none() { if deprecated.is_none() {
return Err(syn::Error::new_spanned( return Err(syn::Error::new_spanned(
removed, removed,
"removed version is defined while deprecated version is not defined", "removed version is defined while deprecated version is not defined",
)); ));
}
} }
}
if let Some(added) = &added { if let Some(added) = &added {
if stable_or_r0.is_none() { if stable_or_r0.is_none() {
return Err(syn::Error::new_spanned( return Err(syn::Error::new_spanned(
added, added,
"added version is defined, but no stable or r0 path exists", "added version is defined, but no stable or r0 path exists",
)); ));
}
} }
}
if let Some(r0) = &r0_path { if let Some(r0) = &r0_path {
let added = added.as_ref().expect("we error if r0 or stable is defined without added"); 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( return Err(syn::Error::new_spanned(
r0, metadata_kw,
"r0 defined while added version is newer than v1.0", "need to define one of [r0_path, stable_path, unstable_path]",
)); ));
} }
if stable_path.is_none() { History::construct(deprecated, removed, unstable_path, r0_path, stable_path.zip(added))
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]",
));
}
Ok(Self { Ok(Self {
description: description.ok_or_else(|| missing_field("description"))?, description: description.ok_or_else(|| missing_field("description"))?,
method: method.ok_or_else(|| missing_field("method"))?, method: method.ok_or_else(|| missing_field("method"))?,
name: name.ok_or_else(|| missing_field("name"))?, 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"))?, rate_limited: rate_limited.ok_or_else(|| missing_field("rate_limited"))?,
authentication: authentication.ok_or_else(|| missing_field("authentication"))?, authentication: authentication.ok_or_else(|| missing_field("authentication"))?,
added, history,
deprecated,
removed,
}) })
} }
} }
@ -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); pub struct EndpointPath(LitStr);
impl EndpointPath { impl EndpointPath {
pub fn value(&self) -> String { pub fn value(&self) -> String {
self.0.value() 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 { impl Parse for EndpointPath {
@ -328,7 +429,7 @@ impl Parse for EndpointPath {
} }
impl ToTokens 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); self.0.to_tokens(tokens);
} }
} }

View File

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