From 0dcdb57c29228339f028c7925f680cb373857080 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Tue, 8 Feb 2022 10:52:43 +0100 Subject: [PATCH] api: Add `added`, `deprecated`, and `removed` metadata fields --- crates/ruma-api-macros/src/api.rs | 9 ++- crates/ruma-api-macros/src/api/metadata.rs | 65 ++++++++++++++++++- crates/ruma-api-macros/src/lib.rs | 1 + crates/ruma-api-macros/src/util.rs | 9 ++- crates/ruma-api-macros/src/version.rs | 46 +++++++++++++ crates/ruma-api/src/lib.rs | 19 ++++++ crates/ruma-api/tests/manual_endpoint_impl.rs | 3 + crates/ruma-api/tests/ruma_api.rs | 2 + .../ruma-api/tests/ui/01-api-sanity-check.rs | 13 +++- .../tests/ui/08-deprecated-without-added.rs | 23 +++++++ .../ui/08-deprecated-without-added.stderr | 13 ++++ .../ui/09-removed-without-deprecated copy.rs | 23 +++++++ .../tests/ui/09-removed-without-deprecated.rs | 23 +++++++ .../ui/09-removed-without-deprecated.stderr | 13 ++++ 14 files changed, 256 insertions(+), 6 deletions(-) create mode 100644 crates/ruma-api-macros/src/version.rs create mode 100644 crates/ruma-api/tests/ui/08-deprecated-without-added.rs create mode 100644 crates/ruma-api/tests/ui/08-deprecated-without-added.stderr create mode 100644 crates/ruma-api/tests/ui/09-removed-without-deprecated copy.rs create mode 100644 crates/ruma-api/tests/ui/09-removed-without-deprecated.rs create mode 100644 crates/ruma-api/tests/ui/09-removed-without-deprecated.stderr diff --git a/crates/ruma-api-macros/src/api.rs b/crates/ruma-api-macros/src/api.rs index 561764d1..a27e8caf 100644 --- a/crates/ruma-api-macros/src/api.rs +++ b/crates/ruma-api-macros/src/api.rs @@ -60,8 +60,7 @@ impl Api { } }) .collect(); - let authentication: TokenStream = self - .metadata + let authentication: TokenStream = metadata .authentication .iter() .map(|r| { @@ -73,6 +72,9 @@ impl Api { } }) .collect(); + 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 error_ty = self .error_ty @@ -90,6 +92,9 @@ impl Api { method: #http::Method::#method, name: #name, path: #path, + added: #added, + deprecated: #deprecated, + removed: #removed, #rate_limited #authentication }; diff --git a/crates/ruma-api-macros/src/api/metadata.rs b/crates/ruma-api-macros/src/api/metadata.rs index 878a43af..443252cd 100644 --- a/crates/ruma-api-macros/src/api/metadata.rs +++ b/crates/ruma-api-macros/src/api/metadata.rs @@ -7,7 +7,7 @@ use syn::{ Attribute, Ident, LitBool, LitStr, Token, }; -use crate::{auth_scheme::AuthScheme, util}; +use crate::{auth_scheme::AuthScheme, util, version::MatrixVersionLiteral}; mod kw { syn::custom_keyword!(metadata); @@ -17,6 +17,9 @@ mod kw { syn::custom_keyword!(path); syn::custom_keyword!(rate_limited); syn::custom_keyword!(authentication); + syn::custom_keyword!(added); + syn::custom_keyword!(deprecated); + syn::custom_keyword!(removed); } /// A field of Metadata that contains attribute macros @@ -47,6 +50,15 @@ pub struct Metadata { /// The authentication field. pub authentication: Vec>, + + /// The added field. + pub added: Option, + + /// The deprecated field. + pub deprecated: Option, + + /// The removed field. + pub removed: Option, } fn set_field(field: &mut Option, value: T) -> syn::Result<()> { @@ -80,6 +92,9 @@ impl Parse for Metadata { let mut path = None; let mut rate_limited = vec![]; let mut authentication = vec![]; + let mut added = None; + let mut deprecated = None; + let mut removed = None; for field_value in field_values { match field_value { @@ -93,12 +108,39 @@ impl Parse for Metadata { FieldValue::Authentication(value, attrs) => { authentication.push(MetadataField { attrs, value }); } + FieldValue::Added(v) => set_field(&mut added, v)?, + FieldValue::Deprecated(v) => set_field(&mut deprecated, v)?, + FieldValue::Removed(v) => set_field(&mut removed, v)?, } } let missing_field = |name| syn::Error::new_spanned(metadata_kw, format!("missing field `{}`", name)); + 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", + )); + } + } + Ok(Self { description: description.ok_or_else(|| missing_field("description"))?, method: method.ok_or_else(|| missing_field("method"))?, @@ -114,6 +156,9 @@ impl Parse for Metadata { } else { authentication }, + added, + deprecated, + removed, }) } } @@ -125,6 +170,9 @@ enum Field { Path, RateLimited, Authentication, + Added, + Deprecated, + Removed, } impl Parse for Field { @@ -149,6 +197,15 @@ impl Parse for Field { } else if lookahead.peek(kw::authentication) { let _: kw::authentication = input.parse()?; Ok(Self::Authentication) + } else if lookahead.peek(kw::added) { + let _: kw::added = input.parse()?; + Ok(Self::Added) + } else if lookahead.peek(kw::deprecated) { + let _: kw::deprecated = input.parse()?; + Ok(Self::Deprecated) + } else if lookahead.peek(kw::removed) { + let _: kw::removed = input.parse()?; + Ok(Self::Removed) } else { Err(lookahead.error()) } @@ -162,6 +219,9 @@ enum FieldValue { Path(LitStr), RateLimited(LitBool, Vec), Authentication(AuthScheme, Vec), + Added(MatrixVersionLiteral), + Deprecated(MatrixVersionLiteral), + Removed(MatrixVersionLiteral), } impl Parse for FieldValue { @@ -196,6 +256,9 @@ impl Parse for FieldValue { } Field::RateLimited => Self::RateLimited(input.parse()?, attrs), Field::Authentication => Self::Authentication(input.parse()?, attrs), + Field::Added => Self::Added(input.parse()?), + Field::Deprecated => Self::Deprecated(input.parse()?), + Field::Removed => Self::Removed(input.parse()?), }) } } diff --git a/crates/ruma-api-macros/src/lib.rs b/crates/ruma-api-macros/src/lib.rs index e8e80c0f..4f7e9544 100644 --- a/crates/ruma-api-macros/src/lib.rs +++ b/crates/ruma-api-macros/src/lib.rs @@ -19,6 +19,7 @@ mod auth_scheme; mod request; mod response; mod util; +mod version; use api::Api; use request::expand_derive_request; diff --git a/crates/ruma-api-macros/src/util.rs b/crates/ruma-api-macros/src/util.rs index 0d21ab00..eabda89f 100644 --- a/crates/ruma-api-macros/src/util.rs +++ b/crates/ruma-api-macros/src/util.rs @@ -4,7 +4,7 @@ use std::collections::BTreeSet; use proc_macro2::TokenStream; use proc_macro_crate::{crate_name, FoundCrate}; -use quote::{format_ident, quote}; +use quote::{format_ident, quote, ToTokens}; use syn::{parse_quote, visit::Visit, AttrStyle, Attribute, Lifetime, NestedMeta, Type}; pub fn import_ruma_api() -> TokenStream { @@ -25,6 +25,13 @@ pub fn import_ruma_api() -> TokenStream { } } +pub fn map_option_literal(ver: &Option) -> TokenStream { + match ver { + Some(v) => quote! { ::std::option::Option::Some(#v) }, + None => quote! { ::std::option::Option::None }, + } +} + pub fn is_valid_endpoint_path(string: &str) -> bool { string.as_bytes().iter().all(|b| (0x21..=0x7E).contains(b)) } diff --git a/crates/ruma-api-macros/src/version.rs b/crates/ruma-api-macros/src/version.rs new file mode 100644 index 00000000..dafc90ed --- /dev/null +++ b/crates/ruma-api-macros/src/version.rs @@ -0,0 +1,46 @@ +use std::{convert::TryInto, num::NonZeroU8}; + +use proc_macro2::TokenStream; +use quote::{format_ident, quote, ToTokens}; +use syn::{parse::Parse, Error, LitFloat}; + +#[derive(Clone)] +pub struct MatrixVersionLiteral { + major: NonZeroU8, + minor: u8, +} + +impl Parse for MatrixVersionLiteral { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + let fl: LitFloat = input.parse()?; + + if !fl.suffix().is_empty() { + return Err(Error::new_spanned( + fl, + "matrix version has to be only two positive numbers separated by a `.`", + )); + } + + let ver_vec: Vec = fl.to_string().split('.').map(&str::to_owned).collect(); + + let ver: [String; 2] = ver_vec.try_into().map_err(|_| { + Error::new_spanned(&fl, "did not contain only both an X and Y value like X.Y") + })?; + + let major: NonZeroU8 = ver[0].parse().map_err(|e| { + Error::new_spanned(&fl, format!("major number failed to parse as >0 number: {}", e)) + })?; + let minor: u8 = ver[1] + .parse() + .map_err(|e| Error::new_spanned(&fl, format!("minor number failed to parse: {}", e)))?; + + Ok(Self { major, minor }) + } +} + +impl ToTokens for MatrixVersionLiteral { + fn to_tokens(&self, tokens: &mut TokenStream) { + let variant = format_ident!("V{}_{}", u8::from(self.major), self.minor); + tokens.extend(quote! { ::ruma_api::MatrixVersion::#variant }); + } +} diff --git a/crates/ruma-api/src/lib.rs b/crates/ruma-api/src/lib.rs index 04c467db..012f2e26 100644 --- a/crates/ruma-api/src/lib.rs +++ b/crates/ruma-api/src/lib.rs @@ -424,6 +424,25 @@ pub struct Metadata { /// 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. + // TODO add once try_into_http_request has been altered; + // This will make `try_into_http_request` emit a warning, + // see the corresponding documentation for more information. + pub deprecated: Option, + + /// The matrix version that removed this endpoint. + // TODO add once try_into_http_request has been altered; + // This will make `try_into_http_request` emit an error, + // see the corresponding documentation for more information. + pub removed: Option, } /// The Matrix versions Ruma currently understands to exist. diff --git a/crates/ruma-api/tests/manual_endpoint_impl.rs b/crates/ruma-api/tests/manual_endpoint_impl.rs index e965c841..cc876aec 100644 --- a/crates/ruma-api/tests/manual_endpoint_impl.rs +++ b/crates/ruma-api/tests/manual_endpoint_impl.rs @@ -31,6 +31,9 @@ const METADATA: Metadata = Metadata { path: "/_matrix/client/r0/directory/room/:room_alias", rate_limited: false, authentication: AuthScheme::None, + added: None, + deprecated: None, + removed: None, }; impl OutgoingRequest for Request { diff --git a/crates/ruma-api/tests/ruma_api.rs b/crates/ruma-api/tests/ruma_api.rs index eb62d882..b7c536c7 100644 --- a/crates/ruma-api/tests/ruma_api.rs +++ b/crates/ruma-api/tests/ruma_api.rs @@ -8,4 +8,6 @@ fn ui() { t.pass("tests/ui/05-request-only.rs"); t.pass("tests/ui/06-response-only.rs"); t.compile_fail("tests/ui/07-error-type-attribute.rs"); + t.compile_fail("tests/ui/08-deprecated-without-added.rs"); + t.compile_fail("tests/ui/09-removed-without-deprecated.rs"); } diff --git a/crates/ruma-api/tests/ui/01-api-sanity-check.rs b/crates/ruma-api/tests/ui/01-api-sanity-check.rs index 0382b21e..2d4253f8 100644 --- a/crates/ruma-api/tests/ui/01-api-sanity-check.rs +++ b/crates/ruma-api/tests/ui/01-api-sanity-check.rs @@ -1,6 +1,6 @@ use ruma_api::ruma_api; -use ruma_serde::Raw; use ruma_events::{tag::TagEvent, AnyRoomEvent}; +use ruma_serde::Raw; ruma_api! { metadata: { @@ -10,6 +10,9 @@ ruma_api! { path: "/_matrix/some/endpoint/:baz", rate_limited: false, authentication: None, + added: 1.0, + deprecated: 1.1, + removed: 1.2, } request: { @@ -50,4 +53,10 @@ ruma_api! { } } -fn main() {} +fn main() { + use ruma_api::MatrixVersion; + + assert_eq!(METADATA.added, Some(MatrixVersion::V1_0)); + assert_eq!(METADATA.deprecated, Some(MatrixVersion::V1_1)); + assert_eq!(METADATA.removed, Some(MatrixVersion::V1_2)); +} diff --git a/crates/ruma-api/tests/ui/08-deprecated-without-added.rs b/crates/ruma-api/tests/ui/08-deprecated-without-added.rs new file mode 100644 index 00000000..502f96e9 --- /dev/null +++ b/crates/ruma-api/tests/ui/08-deprecated-without-added.rs @@ -0,0 +1,23 @@ +use ruma_api::ruma_api; + +ruma_api! { + metadata: { + description: "This will fail.", + method: GET, + name: "invalid_versions", + path: "/a/path", + rate_limited: false, + authentication: None, + + deprecated: 1.1, + } + + request: { + #[ruma_api(query_map)] + pub fields: Vec<(String, String)>, + } + + response: {} +} + +fn main() {} diff --git a/crates/ruma-api/tests/ui/08-deprecated-without-added.stderr b/crates/ruma-api/tests/ui/08-deprecated-without-added.stderr new file mode 100644 index 00000000..a08a9b57 --- /dev/null +++ b/crates/ruma-api/tests/ui/08-deprecated-without-added.stderr @@ -0,0 +1,13 @@ +error: deprecated version is defined while added version is not defined + --> tests/ui/08-deprecated-without-added.rs:3:1 + | +3 | / ruma_api! { +4 | | metadata: { +5 | | description: "This will fail.", +6 | | method: GET, +... | +20 | | response: {} +21 | | } + | |_^ + | + = note: this error originates in the macro `ruma_api` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/ruma-api/tests/ui/09-removed-without-deprecated copy.rs b/crates/ruma-api/tests/ui/09-removed-without-deprecated copy.rs new file mode 100644 index 00000000..4702abe9 --- /dev/null +++ b/crates/ruma-api/tests/ui/09-removed-without-deprecated copy.rs @@ -0,0 +1,23 @@ +use ruma_api::ruma_api; + +ruma_api! { + metadata: { + description: "This will fail.", + method: GET, + name: "invalid_versions", + path: "/a/path", + rate_limited: false, + authentication: None, + + removed: 1.1, + } + + request: { + #[ruma_api(query_map)] + pub fields: Vec<(String, String)>, + } + + response: {} +} + +fn main() {} diff --git a/crates/ruma-api/tests/ui/09-removed-without-deprecated.rs b/crates/ruma-api/tests/ui/09-removed-without-deprecated.rs new file mode 100644 index 00000000..4702abe9 --- /dev/null +++ b/crates/ruma-api/tests/ui/09-removed-without-deprecated.rs @@ -0,0 +1,23 @@ +use ruma_api::ruma_api; + +ruma_api! { + metadata: { + description: "This will fail.", + method: GET, + name: "invalid_versions", + path: "/a/path", + rate_limited: false, + authentication: None, + + removed: 1.1, + } + + request: { + #[ruma_api(query_map)] + pub fields: Vec<(String, String)>, + } + + response: {} +} + +fn main() {} diff --git a/crates/ruma-api/tests/ui/09-removed-without-deprecated.stderr b/crates/ruma-api/tests/ui/09-removed-without-deprecated.stderr new file mode 100644 index 00000000..c554a000 --- /dev/null +++ b/crates/ruma-api/tests/ui/09-removed-without-deprecated.stderr @@ -0,0 +1,13 @@ +error: removed version is defined while deprecated version is not defined + --> tests/ui/09-removed-without-deprecated.rs:3:1 + | +3 | / ruma_api! { +4 | | metadata: { +5 | | description: "This will fail.", +6 | | method: GET, +... | +20 | | response: {} +21 | | } + | |_^ + | + = note: this error originates in the macro `ruma_api` (in Nightly builds, run with -Z macro-backtrace for more info)