//! Details of the `metadata` section of the procedural macro. use proc_macro2::TokenStream; use quote::{quote, ToTokens}; use syn::{ braced, parse::{Parse, ParseStream}, Ident, LitBool, LitStr, Token, }; use super::{auth_scheme::AuthScheme, util, version::MatrixVersionLiteral}; mod kw { syn::custom_keyword!(metadata); syn::custom_keyword!(description); syn::custom_keyword!(method); syn::custom_keyword!(name); syn::custom_keyword!(unstable_path); syn::custom_keyword!(r0_path); syn::custom_keyword!(stable_path); syn::custom_keyword!(rate_limited); syn::custom_keyword!(authentication); syn::custom_keyword!(added); syn::custom_keyword!(deprecated); syn::custom_keyword!(removed); } /// The result of processing the `metadata` section of the macro. pub struct Metadata { /// The description field. pub description: LitStr, /// The method field. pub method: Ident, /// The name field. pub name: LitStr, /// The rate_limited field. pub rate_limited: LitBool, /// The authentication field. pub authentication: AuthScheme, /// The version history field. pub history: History, } fn set_field(field: &mut Option, value: T) -> syn::Result<()> { match field { Some(existing_value) => { let mut error = syn::Error::new_spanned(value, "duplicate field assignment"); error.combine(syn::Error::new_spanned(existing_value, "first one here")); Err(error) } None => { *field = Some(value); Ok(()) } } } impl Parse for Metadata { fn parse(input: ParseStream<'_>) -> syn::Result { let metadata_kw: kw::metadata = input.parse()?; let _: Token![:] = input.parse()?; let field_values; braced!(field_values in input); let field_values = field_values.parse_terminated::(FieldValue::parse)?; let mut description = None; let mut method = None; let mut name = None; let mut unstable_path = None; let mut r0_path = None; let mut stable_path = None; let mut rate_limited = None; let mut authentication = None; let mut added = None; let mut deprecated = None; let mut removed = None; for field_value in field_values { match field_value { FieldValue::Description(d) => set_field(&mut description, d)?, FieldValue::Method(m) => set_field(&mut method, m)?, FieldValue::Name(n) => set_field(&mut name, n)?, FieldValue::UnstablePath(p) => set_field(&mut unstable_path, p)?, FieldValue::R0Path(p) => set_field(&mut r0_path, p)?, FieldValue::StablePath(p) => set_field(&mut stable_path, p)?, FieldValue::RateLimited(rl) => set_field(&mut rate_limited, rl)?, FieldValue::Authentication(a) => set_field(&mut authentication, a)?, 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}`")); // 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(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", )); } } 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 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( 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"))?, rate_limited: rate_limited.ok_or_else(|| missing_field("rate_limited"))?, authentication: authentication.ok_or_else(|| missing_field("authentication"))?, history, }) } } enum Field { Description, Method, Name, UnstablePath, R0Path, StablePath, RateLimited, Authentication, Added, Deprecated, Removed, } impl Parse for Field { fn parse(input: ParseStream<'_>) -> syn::Result { let lookahead = input.lookahead1(); if lookahead.peek(kw::description) { let _: kw::description = input.parse()?; Ok(Self::Description) } else if lookahead.peek(kw::method) { let _: kw::method = input.parse()?; Ok(Self::Method) } else if lookahead.peek(kw::name) { let _: kw::name = input.parse()?; Ok(Self::Name) } else if lookahead.peek(kw::unstable_path) { let _: kw::unstable_path = input.parse()?; Ok(Self::UnstablePath) } else if lookahead.peek(kw::r0_path) { let _: kw::r0_path = input.parse()?; Ok(Self::R0Path) } else if lookahead.peek(kw::stable_path) { let _: kw::stable_path = input.parse()?; Ok(Self::StablePath) } else if lookahead.peek(kw::rate_limited) { let _: kw::rate_limited = input.parse()?; Ok(Self::RateLimited) } 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()) } } } enum FieldValue { Description(LitStr), Method(Ident), Name(LitStr), UnstablePath(EndpointPath), R0Path(EndpointPath), StablePath(EndpointPath), RateLimited(LitBool), Authentication(AuthScheme), Added(MatrixVersionLiteral), Deprecated(MatrixVersionLiteral), Removed(MatrixVersionLiteral), } impl Parse for FieldValue { fn parse(input: ParseStream<'_>) -> syn::Result { let field: Field = input.parse()?; let _: Token![:] = input.parse()?; Ok(match field { Field::Description => Self::Description(input.parse()?), Field::Method => Self::Method(input.parse()?), Field::Name => Self::Name(input.parse()?), Field::UnstablePath => Self::UnstablePath(input.parse()?), Field::R0Path => Self::R0Path(input.parse()?), Field::StablePath => Self::StablePath(input.parse()?), Field::RateLimited => Self::RateLimited(input.parse()?), Field::Authentication => Self::Authentication(input.parse()?), Field::Added => Self::Added(input.parse()?), Field::Deprecated => Self::Deprecated(input.parse()?), Field::Removed => Self::Removed(input.parse()?), }) } } #[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::new( &[ #(#unstable),* ], &[ #(#versioned),* ], #deprecated, #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 { fn parse(input: ParseStream<'_>) -> syn::Result { let path: LitStr = input.parse()?; if util::is_valid_endpoint_path(&path.value()) { Ok(Self(path)) } else { Err(syn::Error::new_spanned( &path, "path may only contain printable ASCII characters with no spaces", )) } } } impl ToTokens for EndpointPath { fn to_tokens(&self, tokens: &mut TokenStream) { self.0.to_tokens(tokens); } }