diff --git a/crates/ruma-common/tests/events/event_content.rs b/crates/ruma-common/tests/events/event_content.rs index 1ab09d34..e1a1be67 100644 --- a/crates/ruma-common/tests/events/event_content.rs +++ b/crates/ruma-common/tests/events/event_content.rs @@ -4,4 +4,5 @@ fn ui() { t.pass("tests/events/ui/01-content-sanity-check.rs"); t.compile_fail("tests/events/ui/02-no-event-type.rs"); t.compile_fail("tests/events/ui/03-invalid-event-type.rs"); + t.pass("tests/events/ui/10-content-wildcard.rs"); } diff --git a/crates/ruma-common/tests/events/ui/03-invalid-event-type.stderr b/crates/ruma-common/tests/events/ui/03-invalid-event-type.stderr index ac499103..e25bfccf 100644 --- a/crates/ruma-common/tests/events/ui/03-invalid-event-type.stderr +++ b/crates/ruma-common/tests/events/ui/03-invalid-event-type.stderr @@ -1,19 +1,19 @@ error: no event type attribute found, add `#[ruma_event(type = "any.room.event", kind = Kind)]` below the event content derive - --> $DIR/03-invalid-event-type.rs:4:48 + --> tests/events/ui/03-invalid-event-type.rs:4:48 | 4 | #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] | ^^^^^^^^^^^^ | = note: this error originates in the derive macro `EventContent` (in Nightly builds, run with -Z macro-backtrace for more info) -error: expected one of: `type`, `kind`, `skip_redaction`, `custom_redacted` - --> $DIR/03-invalid-event-type.rs:11:14 +error: expected one of: `type`, `kind`, `skip_redaction`, `custom_redacted`, `type_fragment` + --> tests/events/ui/03-invalid-event-type.rs:11:14 | 11 | #[ruma_event(event = "m.macro.test", kind = State)] | ^^^^^ error: cannot find attribute `not_ruma_event` in this scope - --> $DIR/03-invalid-event-type.rs:5:3 + --> tests/events/ui/03-invalid-event-type.rs:5:3 | 5 | #[not_ruma_event(type = "m.macro.test", kind = State)] | ^^^^^^^^^^^^^^ help: a derive helper attribute with a similar name exists: `ruma_event` diff --git a/crates/ruma-common/tests/events/ui/10-content-wildcard.rs b/crates/ruma-common/tests/events/ui/10-content-wildcard.rs new file mode 100644 index 00000000..1ecdd340 --- /dev/null +++ b/crates/ruma-common/tests/events/ui/10-content-wildcard.rs @@ -0,0 +1,18 @@ +use ruma_macros::EventContent; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize, EventContent)] +#[ruma_event(type = "m.macro.test.*", kind = GlobalAccountData)] +pub struct MacroTestContent { + #[ruma_event(type_fragment)] + pub frag: String, +} + +fn main() { + use ruma_common::events::EventContent; + + assert_eq!( + MacroTestContent { frag: "foo".to_owned() }.event_type().as_str(), + "m.macro.test.foo" + ); +} diff --git a/crates/ruma-macros/src/events/event_content.rs b/crates/ruma-macros/src/events/event_content.rs index a2c423aa..86d55f2e 100644 --- a/crates/ruma-macros/src/events/event_content.rs +++ b/crates/ruma-macros/src/events/event_content.rs @@ -6,7 +6,7 @@ use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote}; use syn::{ parse::{Parse, ParseStream}, - DeriveInput, Ident, LitStr, Token, + DeriveInput, Field, Ident, LitStr, Token, }; use crate::util::m_prefix_name_to_type_name; @@ -20,6 +20,7 @@ mod kw { syn::custom_keyword!(custom_redacted); // The kind of event content this is. syn::custom_keyword!(kind); + syn::custom_keyword!(type_fragment); } /// Parses attributes for `*EventContent` derives. @@ -38,6 +39,10 @@ enum EventMeta { /// This attribute signals that the events redacted form is manually implemented and should not /// be generated. CustomRedacted, + + /// The given field holds a part of the event type (replaces the `*` in a `m.foo.*` event + /// type). + TypeFragment, } impl EventMeta { @@ -73,6 +78,9 @@ impl Parse for EventMeta { } else if lookahead.peek(kw::custom_redacted) { let _: kw::custom_redacted = input.parse()?; Ok(EventMeta::CustomRedacted) + } else if lookahead.peek(kw::type_fragment) { + let _: kw::type_fragment = input.parse()?; + Ok(EventMeta::TypeFragment) } else { Err(lookahead.error()) } @@ -149,7 +157,7 @@ pub fn expand_event_content( let ident = &input.ident; let fields = match &input.data { - syn::Data::Struct(syn::DataStruct { fields, .. }) => fields, + syn::Data::Struct(syn::DataStruct { fields, .. }) => fields.iter(), _ => { return Err(syn::Error::new( Span::call_site(), @@ -158,15 +166,38 @@ pub fn expand_event_content( } }; + let event_type_s = event_type.value(); + let prefix = event_type_s.strip_suffix(".*"); + + if prefix.unwrap_or(&event_type_s).contains('*') { + return Err(syn::Error::new_spanned( + event_type, + "event type may only contain `*` as part of a `.*` suffix", + )); + } + + if prefix.is_some() && !event_kind.map_or(false, |k| k.is_account_data()) { + return Err(syn::Error::new_spanned( + event_type, + "only account data events may contain a `.*` suffix", + )); + } + // We only generate redacted content structs for state and message-like events let redacted_event_content = needs_redacted(&content_attr, event_kind) .then(|| { - generate_redacted_event_content(ident, fields, event_type, event_kind, ruma_common) + generate_redacted_event_content( + ident, + fields.clone(), + event_type, + event_kind, + ruma_common, + ) }) .transpose()?; let event_content_impl = - generate_event_content_impl(ident, event_type, event_kind, ruma_common)?; + generate_event_content_impl(ident, fields, event_type, event_kind, ruma_common)?; let static_event_content_impl = event_kind .map(|k| generate_static_event_content_impl(ident, k, false, event_type, ruma_common)); let type_aliases = event_kind @@ -181,13 +212,18 @@ pub fn expand_event_content( }) } -fn generate_redacted_event_content( +fn generate_redacted_event_content<'a>( ident: &Ident, - fields: &syn::Fields, + fields: impl Iterator, event_type: &LitStr, event_kind: Option, ruma_common: &TokenStream, ) -> syn::Result { + assert!( + !event_type.value().contains('*'), + "Event type shouldn't contain a `*`, this should have been checked previously" + ); + let serde = quote! { #ruma_common::exports::serde }; let serde_json = quote! { #ruma_common::exports::serde_json }; @@ -195,7 +231,6 @@ fn generate_redacted_event_content( let redacted_ident = format_ident!("Redacted{}", ident); let kept_redacted_fields: Vec<_> = fields - .iter() .map(|f| { let mut keep_field = false; let attrs = f @@ -217,7 +252,7 @@ fn generate_redacted_event_content( .collect::>()?; if keep_field { - Ok(Some(syn::Field { attrs, ..f.clone() })) + Ok(Some(Field { attrs, ..f.clone() })) } else { Ok(None) } @@ -260,8 +295,13 @@ fn generate_redacted_event_content( } }); - let redacted_event_content = - generate_event_content_impl(&redacted_ident, event_type, event_kind, ruma_common)?; + let redacted_event_content = generate_event_content_impl( + &redacted_ident, + kept_redacted_fields.iter(), + event_type, + event_kind, + ruma_common, + )?; let static_event_content_impl = event_kind.map(|k| { generate_static_event_content_impl(&redacted_ident, k, true, event_type, ruma_common) @@ -371,8 +411,9 @@ fn generate_event_type_aliases( Ok(type_aliases) } -fn generate_event_content_impl( +fn generate_event_content_impl<'a>( ident: &Ident, + mut fields: impl Iterator, event_type: &LitStr, event_kind: Option, ruma_common: &TokenStream, @@ -387,7 +428,40 @@ fn generate_event_content_impl( let i = kind.to_event_type_enum(); event_type_ty_decl = None; event_type_ty = quote! { #ruma_common::events::#i }; - event_type_fn_impl = quote! { ::std::convert::From::from(#event_type) }; + event_type_fn_impl = match event_type.value().strip_suffix(".*") { + Some(type_prefix) => { + let type_fragment_field = fields + .find_map(|f| { + f.attrs.iter().filter(|a| a.path.is_ident("ruma_event")).find_map(|a| { + match a.parse_args() { + Ok(EventMeta::TypeFragment) => Some(Ok(f)), + Ok(_) => None, + Err(e) => Some(Err(e)), + } + }) + }) + .transpose()?; + + let f = type_fragment_field + .ok_or_else(|| { + syn::Error::new_spanned( + event_type, + "event type with a `.*` suffix requires there to be a \ + `#[ruma_event(type_fragment)]` field", + ) + })? + .ident + .as_ref() + .expect("type fragment field needs to have a name"); + + let format = type_prefix.to_owned() + ".{}"; + + quote! { + ::std::convert::From::from(::std::format!(#format, self.#f)) + } + } + None => quote! { ::std::convert::From::from(#event_type) }, + }; } None => { let camel_case_type_name = m_prefix_name_to_type_name(event_type)?; diff --git a/crates/ruma-macros/src/events/event_parse.rs b/crates/ruma-macros/src/events/event_parse.rs index 11b0800b..af6b031b 100644 --- a/crates/ruma-macros/src/events/event_parse.rs +++ b/crates/ruma-macros/src/events/event_parse.rs @@ -127,6 +127,10 @@ impl IdentFragment for EventKindVariation { } impl EventKind { + pub fn is_account_data(self) -> bool { + matches!(self, Self::GlobalAccountData | Self::RoomAccountData) + } + pub fn try_to_event_ident(self, var: EventKindVariation) -> Option { use EventKindVariation as V; diff --git a/crates/ruma-macros/src/util.rs b/crates/ruma-macros/src/util.rs index f1864547..fbc964fe 100644 --- a/crates/ruma-macros/src/util.rs +++ b/crates/ruma-macros/src/util.rs @@ -47,6 +47,8 @@ pub(crate) fn m_prefix_name_to_type_name(name: &LitStr) -> syn::Result { })?; let s: String = name + .strip_suffix(".*") + .unwrap_or(name) .split(&['.', '_'] as &[char]) .map(|s| s.chars().next().unwrap().to_uppercase().to_string() + &s[1..]) .collect();