event_content_collection!: implement parsing input and generating an enum

This commit is contained in:
Ragotzy.devin 2020-05-29 14:38:55 -04:00 committed by Jonas Platte
parent 7ec162678e
commit 3814690b29
No known key found for this signature in database
GPG Key ID: 7D261D771D915378
4 changed files with 161 additions and 64 deletions

View File

@ -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<TokenStream> {
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::<Vec<_>>();
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::<String>();
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::<String>();
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<Attribute>,
/// 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<LitStr>,
}
impl Parse for RumaCollectionInput {
fn parse(input: ParseStream<'_>) -> parse::Result<Self> {
let attrs = input.call(Attribute::parse_outer)?;
// name field
input.parse::<kw::name>()?;
input.parse::<Token![:]>()?;
// the name of our collection enum
let name: Ident = input.parse()?;
input.parse::<Token![,]>()?;
// events field
input.parse::<kw::events>()?;
input.parse::<Token![:]>()?;
// an array of event names `["m.room.whatever"]`
let ev_array = input.parse::<syn::ExprArray>()?;
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::<syn::Result<_>>()?;
Ok(Self {
attrs,
name,
events,
})
}
}
}

View File

@ -178,16 +178,6 @@ fn populate_state_fields(content_name: Ident, fields: Vec<Field>) -> Vec<Field>
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<Field, Token![,]>`
/// from a `TokenStream`.
///

View File

@ -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.

View File

@ -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<CanonicalAliasEventContent>),
// /// m.room.create
// RoomCreate(StateEvent<CreateEventContent>),
// /// m.room.encryption
// RoomEncryption(StateEvent<EncryptionEventContent>),
// /// m.room.guest_access
// RoomGuestAccess(StateEvent<GuestAccessEventContent>),
// /// m.room.history_visibility
// RoomHistoryVisibility(StateEvent<HistoryVisibilityEventContent>),
// /// m.room.join_rules
// RoomJoinRules(StateEvent<JoinRulesEventContent>),
// /// m.room.member
// RoomMember(StateEvent<MemberEventContent>),
// /// m.room.name
// RoomName(StateEvent<NameEventContent>),
// /// m.room.pinned_events
// RoomPinnedEvents(StateEvent<PinnedEventsEventContent>),
// /// m.room.power_levels
// RoomPowerLevels(StateEvent<PinnedEventsEventContent>),
// /// m.room.server_acl
// RoomServerAcl(StateEvent<ServerAclEventContent>),
// /// m.room.third_party_invite
// RoomThirdPartyInvite(StateEvent<ThirdPartyInviteEventContent>),
// /// m.room.tombstone
// RoomTombstone(StateEvent<TombstoneEventContent>),
// /// m.room.topic
// RoomTopic(StateEvent<TopicEventContent>),
// /// Any state event that is not part of the specification.
// CustomState(StateEvent<CustomEventContent>),
event_content_collection! {
/// A state event.
name: AnyStateEventContent,
events: ["m.room.aliases", "m.room.avatar"]
}
/// State event.