api: Replace path fields in Metadata with new VersionHistory type
Co-authored-by: Jonathan de Jong <jonathan@automatia.nl>
This commit is contained in:
parent
451a50a77b
commit
ec31badd84
@ -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};
|
||||
|
||||
|
@ -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 {}
|
||||
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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()?;
|
||||
|
Loading…
x
Reference in New Issue
Block a user