diff --git a/crates/ruma-common/src/api.rs b/crates/ruma-common/src/api.rs index 1d153b64..b08d09db 100644 --- a/crates/ruma-common/src/api.rs +++ b/crates/ruma-common/src/api.rs @@ -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}; diff --git a/crates/ruma-common/src/api/error.rs b/crates/ruma-common/src/api/error.rs index 706d51f9..3f3ae0b8 100644 --- a/crates/ruma-common/src/api/error.rs +++ b/crates/ruma-common/src/api/error.rs @@ -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 {} diff --git a/crates/ruma-common/src/api/metadata.rs b/crates/ruma-common/src/api/metadata.rs index 3eb3c67b..5fca2476 100644 --- a/crates/ruma-common/src/api/metadata.rs +++ b/crates/ruma-common/src/api/metadata.rs @@ -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, - - /// 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, - - /// 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, + /// 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 { - 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, + + /// 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, +} + +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 { + 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) + ); } } diff --git a/crates/ruma-common/tests/api/manual_endpoint_impl.rs b/crates/ruma-common/tests/api/manual_endpoint_impl.rs index 00acd641..227f32dd 100644 --- a/crates/ruma-common/tests/api/manual_endpoint_impl.rs +++ b/crates/ruma-common/tests/api/manual_endpoint_impl.rs @@ -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 { diff --git a/crates/ruma-common/tests/api/ui/01-api-sanity-check.rs b/crates/ruma-common/tests/api/ui/01-api-sanity-check.rs index f338d59b..536be0f4 100644 --- a/crates/ruma-common/tests/api/ui/01-api-sanity-check.rs +++ b/crates/ruma-common/tests/api/ui/01-api-sanity-check.rs @@ -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)); } diff --git a/crates/ruma-macros/src/api.rs b/crates/ruma-macros/src/api.rs index 7ea57710..e8210f08 100644 --- a/crates/ruma-macros/src/api.rs +++ b/crates/ruma-macros/src/api.rs @@ -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 { - path.split('/').filter_map(|s| s.strip_prefix(':').map(ToOwned::to_owned)).collect() -} diff --git a/crates/ruma-macros/src/api/api_metadata.rs b/crates/ruma-macros/src/api/api_metadata.rs index c84ecd61..2a2b20dc 100644 --- a/crates/ruma-macros/src/api/api_metadata.rs +++ b/crates/ruma-macros/src/api/api_metadata.rs @@ -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, - - /// The pre-v1.1 path field. - pub r0_path: Option, - - /// The stable path field. - pub stable_path: Option, - /// The rate_limited field. pub rate_limited: LitBool, /// The authentication field. pub authentication: AuthScheme, - /// The added field. - pub added: Option, - - /// The deprecated field. - pub deprecated: Option, - - /// The removed field. - pub removed: Option, + /// The version history field. + pub history: History, } fn set_field(field: &mut Option, 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, + misc: MiscVersioning, +} + +impl History { + // TODO(j0j0): remove after codebase conversion is complete + /// Construct a History object from legacy parts. + pub fn construct( + deprecated: Option, + removed: Option, + unstable_path: Option, + r0_path: Option, + 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 { + 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); } } diff --git a/crates/ruma-macros/src/api/version.rs b/crates/ruma-macros/src/api/version.rs index f24a5b6a..60fac77b 100644 --- a/crates/ruma-macros/src/api/version.rs +++ b/crates/ruma-macros/src/api/version.rs @@ -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 { let fl: LitFloat = input.parse()?;