From 3814690b2964a00af196e51bdfe2c47b9a0c048c Mon Sep 17 00:00:00 2001 From: "Ragotzy.devin" Date: Fri, 29 May 2020 14:38:55 -0400 Subject: [PATCH] event_content_collection!: implement parsing input and generating an enum --- ruma-events-macros/src/collection.rs | 142 +++++++++++++++++++++++++++ ruma-events-macros/src/gen.rs | 10 -- ruma-events-macros/src/lib.rs | 14 +++ src/state.rs | 59 +---------- 4 files changed, 161 insertions(+), 64 deletions(-) create mode 100644 ruma-events-macros/src/collection.rs diff --git a/ruma-events-macros/src/collection.rs b/ruma-events-macros/src/collection.rs new file mode 100644 index 00000000..94c90244 --- /dev/null +++ b/ruma-events-macros/src/collection.rs @@ -0,0 +1,142 @@ +//! Implementation of the collection type macro. + +use proc_macro2::TokenStream; +use quote::quote; +use syn::{Ident, LitStr}; + +use parse::RumaCollectionInput; + +/// Create a collection from `RumaCollectionInput. +pub fn expand_collection(input: RumaCollectionInput) -> syn::Result { + let attrs = &input.attrs; + let ident = &input.name; + + let variants = input + .events + .iter() + .map(|lit| { + let content_docstring = lit; + let var = to_camel_case(lit); + let content = to_event_content(lit); + + quote! { + #[doc = #content_docstring] + #var(#content) + } + }) + .collect::>(); + + let collection = quote! { + #( #attrs )* + #[derive(Clone, Debug, Serialize)] + #[serde(untagged)] + #[allow(clippy::large_enum_variant)] + pub enum #ident { + #( #variants ),* + } + }; + + Ok(collection) +} + +/// Splits the given `event_type` string on `.` and `_` removing the `m.` then +/// using only the event name append "EventContent". +fn to_event_content(name: &LitStr) -> Ident { + let span = name.span(); + let name = name.value(); + + assert_eq!(&name[..2], "m."); + + let event = name[2..].split('.').last().unwrap(); + + let event = event + .split('_') + .map(|s| s.chars().next().unwrap().to_uppercase().to_string() + &s[1..]) + .collect::(); + + let content_str = format!("{}EventContent", event); + Ident::new(&content_str, span) +} + +/// Splits the given `event_type` string on `.` and `_` removing the `m.room.` then +/// camel casing to give the `EventContent` struct name. +pub(crate) fn to_camel_case(name: &LitStr) -> Ident { + let span = name.span(); + let name = name.value(); + assert_eq!(&name[..2], "m."); + let s = name[2..] + .split(&['.', '_'] as &[char]) + .map(|s| s.chars().next().unwrap().to_uppercase().to_string() + &s[1..]) + .collect::(); + Ident::new(&s, span) +} + +/// Details of parsing input for the `event_content_collection` procedural macro. +pub mod parse { + use syn::{ + parse::{self, Parse, ParseStream}, + Attribute, Expr, ExprLit, Ident, Lit, LitStr, Token, + }; + + /// Custom keywords for the `event_content_collection!` macro + mod kw { + syn::custom_keyword!(name); + syn::custom_keyword!(events); + } + + /// The entire `event_content_collection!` macro structure directly as it appears in the source code.. + pub struct RumaCollectionInput { + /// Outer attributes on the field, such as a docstring. + pub attrs: Vec, + + /// The name of the event. + pub name: Ident, + + /// An array of valid matrix event types. This will generate the variants of the event content type "name". + /// There needs to be a corresponding variant in `ruma_events::EventType` for + /// this event (converted to a valid Rust-style type name by stripping `m.`, replacing the + /// remaining dots by underscores and then converting from snake_case to CamelCase). + pub events: Vec, + } + + impl Parse for RumaCollectionInput { + fn parse(input: ParseStream<'_>) -> parse::Result { + let attrs = input.call(Attribute::parse_outer)?; + // name field + input.parse::()?; + input.parse::()?; + // the name of our collection enum + let name: Ident = input.parse()?; + input.parse::()?; + + // events field + input.parse::()?; + input.parse::()?; + + // an array of event names `["m.room.whatever"]` + let ev_array = input.parse::()?; + let events = ev_array + .elems + .into_iter() + .map(|item| { + if let Expr::Lit(ExprLit { + lit: Lit::Str(lit_str), + .. + }) = item + { + Ok(lit_str) + } else { + let msg = "values of field `events` are required to be a string literal"; + Err(syn::Error::new_spanned(item, msg)) + } + }) + .collect::>()?; + + Ok(Self { + attrs, + name, + events, + }) + } + } +} diff --git a/ruma-events-macros/src/gen.rs b/ruma-events-macros/src/gen.rs index 477389aa..1ea34371 100644 --- a/ruma-events-macros/src/gen.rs +++ b/ruma-events-macros/src/gen.rs @@ -178,16 +178,6 @@ fn populate_state_fields(content_name: Ident, fields: Vec) -> Vec fields } -/// Splits the given `event_type` string on `.` and `_` removing the `m.` then -/// camel casing to give the `EventType` variant. -fn to_camel_case(name: String) -> String { - assert_eq!(&name[..2], "m."); - name[2..] - .split(&['.', '_'] as &[char]) - .map(|s| s.chars().next().unwrap().to_uppercase().to_string() + &s[1..]) - .collect() -} - /// A wrapper around `syn::Field` that makes it possible to parse `Punctuated` /// from a `TokenStream`. /// diff --git a/ruma-events-macros/src/lib.rs b/ruma-events-macros/src/lib.rs index 82dee2c8..0e20a11c 100644 --- a/ruma-events-macros/src/lib.rs +++ b/ruma-events-macros/src/lib.rs @@ -37,12 +37,14 @@ use quote::ToTokens; use syn::{parse_macro_input, DeriveInput}; use self::{ + collection::{expand_collection, parse::RumaCollectionInput}, event_content::{expand_message_event, expand_state_event}, from_raw::expand_from_raw, gen::RumaEvent, parse::RumaEventInput, }; +mod collection; mod event_content; mod from_raw; mod gen; @@ -141,6 +143,18 @@ pub fn ruma_event(input: TokenStream) -> TokenStream { ruma_event.into_token_stream().into() } +/// Generates a collection type to represent the various Matrix event types. +/// +/// This macro also implements the necessary traits for the type to serialize and deserialize itself. +// TODO more docs/example +#[proc_macro] +pub fn event_content_collection(input: TokenStream) -> TokenStream { + let ruma_collection_input = syn::parse_macro_input!(input as RumaCollectionInput); + expand_collection(ruma_collection_input) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + /// Generates an implementation of `ruma_events::FromRaw`. Only usable inside of `ruma_events`. /// Requires there to be a `raw` module in the same scope, with a type with the same name and fields /// as the one that this macro is used on. diff --git a/src/state.rs b/src/state.rs index ab5de494..1a3bbeb4 100644 --- a/src/state.rs +++ b/src/state.rs @@ -22,61 +22,12 @@ use crate::{ room::{aliases::AliasesEventContent, avatar::AvatarEventContent}, EventContent, RoomEventContent, StateEventContent, TryFromRaw, UnsignedData, }; +use ruma_events_macros::event_content_collection; -/// A state event. -#[derive(Clone, Debug, Serialize)] -#[serde(untagged)] -#[allow(clippy::large_enum_variant)] -pub enum AnyStateEventContent { - /// m.room.aliases - RoomAliases(AliasesEventContent), - - /// m.room.avatar - RoomAvatar(AvatarEventContent), - // /// m.room.canonical_alias - // RoomCanonicalAlias(StateEvent), - - // /// m.room.create - // RoomCreate(StateEvent), - - // /// m.room.encryption - // RoomEncryption(StateEvent), - - // /// m.room.guest_access - // RoomGuestAccess(StateEvent), - - // /// m.room.history_visibility - // RoomHistoryVisibility(StateEvent), - - // /// m.room.join_rules - // RoomJoinRules(StateEvent), - - // /// m.room.member - // RoomMember(StateEvent), - - // /// m.room.name - // RoomName(StateEvent), - - // /// m.room.pinned_events - // RoomPinnedEvents(StateEvent), - - // /// m.room.power_levels - // RoomPowerLevels(StateEvent), - - // /// m.room.server_acl - // RoomServerAcl(StateEvent), - - // /// m.room.third_party_invite - // RoomThirdPartyInvite(StateEvent), - - // /// m.room.tombstone - // RoomTombstone(StateEvent), - - // /// m.room.topic - // RoomTopic(StateEvent), - - // /// Any state event that is not part of the specification. - // CustomState(StateEvent), +event_content_collection! { + /// A state event. + name: AnyStateEventContent, + events: ["m.room.aliases", "m.room.avatar"] } /// State event.