From c2b1c9897bdbcac5f7fba9744b6e9c6c7a5ecf62 Mon Sep 17 00:00:00 2001 From: "Ragotzy.devin" Date: Tue, 9 Jun 2020 17:31:31 -0400 Subject: [PATCH] Any event enum macro implementation (also generates event content enums) --- ruma-events-macros/src/event_enum.rs | 263 +++++++++++++++++++++++++++ ruma-events-macros/src/lib.rs | 15 ++ src/call/candidates.rs | 5 + src/call/hangup.rs | 5 + src/call/invite.rs | 4 + src/content_enums.rs | 74 -------- src/enums.rs | 74 ++++++++ src/ignored_user_list.rs | 5 + src/lib.rs | 11 +- src/room/aliases.rs | 5 + src/room/avatar.rs | 4 + src/room/message.rs | 7 + src/room/message/feedback.rs | 5 + tests/event_enums.rs | 117 ++++++++++++ 14 files changed, 515 insertions(+), 79 deletions(-) create mode 100644 ruma-events-macros/src/event_enum.rs delete mode 100644 src/content_enums.rs create mode 100644 src/enums.rs create mode 100644 tests/event_enums.rs diff --git a/ruma-events-macros/src/event_enum.rs b/ruma-events-macros/src/event_enum.rs new file mode 100644 index 00000000..e6b20074 --- /dev/null +++ b/ruma-events-macros/src/event_enum.rs @@ -0,0 +1,263 @@ +//! Implementation of event enum and event content enum macros. + +use proc_macro2::TokenStream; +use quote::quote; +use syn::{ + parse::{self, Parse, ParseStream}, + Attribute, Expr, ExprLit, Ident, Lit, LitStr, Token, +}; + +/// Create a content enum from `EventEnumInput`. +pub fn expand_event_enum(input: EventEnumInput) -> syn::Result { + let attrs = &input.attrs; + let ident = &input.name; + let event_type_str = &input.events; + + let variants = input.events.iter().map(to_camel_case).collect::>(); + let content = input.events.iter().map(to_event_path).collect::>(); + + let event_enum = quote! { + #( #attrs )* + #[derive(Clone, Debug, ::serde::Serialize)] + #[serde(untagged)] + #[allow(clippy::large_enum_variant)] + pub enum #ident { + #( + #[doc = #event_type_str] + #variants(#content) + ),* + } + }; + + let event_deserialize_impl = quote! { + impl<'de> ::serde::de::Deserialize<'de> for #ident { + fn deserialize(deserializer: D) -> Result + where + D: ::serde::de::Deserializer<'de>, + { + use ::serde::de::Error as _; + + let json = ::serde_json::Value::deserialize(deserializer)?; + let ev_type: String = ::ruma_events::util::get_field(&json, "type")?; + match ev_type.as_str() { + #( + #event_type_str => { + let event = ::serde_json::from_value::<#content>(json).map_err(D::Error::custom)?; + Ok(#ident::#variants(event)) + }, + )* + _ => Err(D::Error::custom(format!("event type `{}` is not a valid event", ev_type))) + } + } + } + }; + + let event_content_enum = expand_content_enum(input)?; + + Ok(quote! { + #event_enum + + #event_deserialize_impl + + #event_content_enum + }) +} + +/// Create a content enum from `EventEnumInput`. +pub fn expand_content_enum(input: EventEnumInput) -> syn::Result { + let attrs = &input.attrs; + let ident = Ident::new( + &format!("{}Content", input.name.to_string()), + input.name.span(), + ); + let event_type_str = &input.events; + + let variants = input.events.iter().map(to_camel_case).collect::>(); + let content = input + .events + .iter() + .map(to_event_content_path) + .collect::>(); + + let content_enum = quote! { + #( #attrs )* + #[derive(Clone, Debug, ::serde::Serialize)] + #[serde(untagged)] + #[allow(clippy::large_enum_variant)] + pub enum #ident { + #( + #[doc = #event_type_str] + #variants(#content) + ),* + } + }; + + let event_content_impl = quote! { + impl ::ruma_events::EventContent for #ident { + fn event_type(&self) -> &str { + match self { + #( Self::#variants(content) => content.event_type() ),* + } + } + + fn from_parts(event_type: &str, input: Box<::serde_json::value::RawValue>) -> Result { + match event_type { + #( + #event_type_str => { + let content = #content::from_parts(event_type, input)?; + Ok(#ident::#variants(content)) + }, + )* + ev => Err(format!("event not supported {}", ev)), + } + } + } + }; + + let marker_trait_impls = marker_traits(&ident); + + Ok(quote! { + #content_enum + + #event_content_impl + + #marker_trait_impls + }) +} + +fn marker_traits(ident: &Ident) -> TokenStream { + match ident.to_string().as_str() { + "AnyStateEventContent" => quote! { + impl ::ruma_events::RoomEventContent for #ident {} + impl ::ruma_events::StateEventContent for #ident {} + }, + "AnyMessageEventContent" => quote! { + impl ::ruma_events::RoomEventContent for #ident {} + impl ::ruma_events::MessageEventContent for #ident {} + }, + "AnyEphemeralRoomEventContent" => quote! { + impl ::ruma_events::EphemeralRoomEventContent for #ident {} + }, + "AnyBasicEventContent" => quote! { + impl ::ruma_events::BasicEventContent for #ident {} + }, + _ => TokenStream::new(), + } +} + +fn to_event_path(name: &LitStr) -> TokenStream { + let span = name.span(); + let name = name.value(); + + assert_eq!(&name[..2], "m."); + + let path = name[2..].split('.').collect::>(); + + let event_str = path.last().unwrap(); + let event = event_str + .split('_') + .map(|s| s.chars().next().unwrap().to_uppercase().to_string() + &s[1..]) + .collect::(); + + let content_str = Ident::new(&format!("{}Event", event), span); + let path = path.iter().map(|s| Ident::new(s, span)); + quote! { + ::ruma_events::#( #path )::*::#content_str + } +} + +fn to_event_content_path(name: &LitStr) -> TokenStream { + let span = name.span(); + let name = name.value(); + + assert_eq!(&name[..2], "m."); + + let path = name[2..].split('.').collect::>(); + + let event_str = path.last().unwrap(); + let event = event_str + .split('_') + .map(|s| s.chars().next().unwrap().to_uppercase().to_string() + &s[1..]) + .collect::(); + + let content_str = Ident::new(&format!("{}EventContent", event), span); + let path = path.iter().map(|s| Ident::new(s, span)); + quote! { + ::ruma_events::#( #path )::*::#content_str + } +} + +/// Splits the given `event_type` string on `.` and `_` removing the `m.room.` then +/// camel casing to give the `Event` 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) +} + +/// Custom keywords for the `event_enum!` macro +mod kw { + syn::custom_keyword!(name); + syn::custom_keyword!(events); +} + +/// The entire `event_enum!` macro structure directly as it appears in the source code. +pub struct EventEnumInput { + /// 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 EventEnumInput { + fn parse(input: ParseStream<'_>) -> parse::Result { + let attrs = input.call(Attribute::parse_outer)?; + // name field + input.parse::()?; + input.parse::()?; + // the name of our event 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/lib.rs b/ruma-events-macros/src/lib.rs index fdfea401..a4bedf6c 100644 --- a/ruma-events-macros/src/lib.rs +++ b/ruma-events-macros/src/lib.rs @@ -21,6 +21,7 @@ use self::{ expand_basic_event_content, expand_ephemeral_room_event_content, expand_event_content, expand_message_event_content, expand_room_event_content, expand_state_event_content, }, + event_enum::{expand_event_enum, EventEnumInput}, gen::RumaEvent, parse::RumaEventInput, }; @@ -28,6 +29,7 @@ use self::{ mod content_enum; mod event; mod event_content; +mod event_enum; mod gen; mod parse; @@ -124,6 +126,19 @@ pub fn ruma_event(input: TokenStream) -> TokenStream { ruma_event.into_token_stream().into() } +/// Generates an enum 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_enum(input: TokenStream) -> TokenStream { + let event_enum_input = syn::parse_macro_input!(input as EventEnumInput); + expand_event_enum(event_enum_input) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + /// Generates a content enum to represent the various Matrix event types. /// /// This macro also implements the necessary traits for the type to serialize and deserialize diff --git a/src/call/candidates.rs b/src/call/candidates.rs index b3ef0add..befc3d65 100644 --- a/src/call/candidates.rs +++ b/src/call/candidates.rs @@ -4,8 +4,13 @@ use js_int::UInt; use ruma_events_macros::MessageEventContent; use serde::{Deserialize, Serialize}; +use crate::MessageEvent; + /// This event is sent by callers after sending an invite and by the callee after answering. Its /// purpose is to give the other party additional ICE candidates to try using to communicate. +pub type CandidatesEvent = MessageEvent; + +/// The payload for `CandidatesEvent`. #[derive(Clone, Debug, Deserialize, Serialize, MessageEventContent)] #[ruma_event(type = "m.call.candidates")] pub struct CandidatesEventContent { diff --git a/src/call/hangup.rs b/src/call/hangup.rs index 83abeb74..4dfe9c7c 100644 --- a/src/call/hangup.rs +++ b/src/call/hangup.rs @@ -5,8 +5,13 @@ use ruma_events_macros::MessageEventContent; use serde::{Deserialize, Serialize}; use strum::{Display, EnumString}; +use crate::MessageEvent; + /// Sent by either party to signal their termination of the call. This can be sent either once the /// call has has been established or before to abort the call. +pub type HangupEvent = MessageEvent; + +/// The payload for `HangupEvent`. #[derive(Clone, Debug, Deserialize, Serialize, MessageEventContent)] #[ruma_event(type = "m.call.hangup")] pub struct HangupEventContent { diff --git a/src/call/invite.rs b/src/call/invite.rs index 87813c5f..48f9157b 100644 --- a/src/call/invite.rs +++ b/src/call/invite.rs @@ -5,8 +5,12 @@ use ruma_events_macros::MessageEventContent; use serde::{Deserialize, Serialize}; use super::SessionDescription; +use crate::MessageEvent; /// This event is sent by the caller when they wish to establish a call. +pub type InviteEvent = MessageEvent; + +/// The payload for `InviteEvent`. #[derive(Clone, Debug, Deserialize, Serialize, MessageEventContent)] #[ruma_event(type = "m.call.invite")] pub struct InviteEventContent { diff --git a/src/content_enums.rs b/src/content_enums.rs deleted file mode 100644 index 671d2cb3..00000000 --- a/src/content_enums.rs +++ /dev/null @@ -1,74 +0,0 @@ -use ruma_events_macros::event_content_enum; - -event_content_enum! { - /// Any basic event's content. - name: AnyBasicEventContent, - events: [ - "m.direct", - "m.dummy", - "m.ignored_user_list", - "m.push_rules", - "m.room_key", - ] -} - -event_content_enum! { - /// Any ephemeral room event. - name: AnyEphemeralRoomEventContent, - events: [ "m.typing", "m.receipt" ] -} - -event_content_enum! { - /// Any message event's content. - name: AnyMessageEventContent, - events: [ - "m.call.answer", - "m.call.invite", - "m.call.hangup", - "m.call.candidates", - "m.room.message", - "m.room.message.feedback", - "m.sticker", - ] -} - -event_content_enum! { - /// Any state event's content. - name: AnyStateEventContent, - events: [ - "m.room.aliases", - "m.room.avatar", - "m.room.canonical_alias", - "m.room.create", - "m.room.encryption", - "m.room.guest_access", - "m.room.history_visibility", - "m.room.join_rules", - "m.room.member", - "m.room.name", - "m.room.pinned_events", - "m.room.power_levels", - "m.room.server_acl", - "m.room.third_party_invite", - "m.room.tombstone", - "m.room.topic", - ] -} - -event_content_enum! { - /// Any to-device event's content. - name: AnyToDeviceEventContent, - events: [ - "m.dummy", - "m.room_key", - //"m.room_key_request", - //"m.forwarded_room_key", - //"m.key.verification.request", - "m.key.verification.start", - //"m.key.verification.cancel", - //"m.key.verification.accept", - //"m.key.verification.key", - //"m.key.verification.mac", - //"m.room.encrypted", - ] -} diff --git a/src/enums.rs b/src/enums.rs new file mode 100644 index 00000000..6ac11ae9 --- /dev/null +++ b/src/enums.rs @@ -0,0 +1,74 @@ +use ruma_events_macros::event_enum; + +event_enum! { + /// Any basic event. + name: AnyBasicEvent, + events: [ + "m.direct", + "m.dummy", + "m.ignored_user_list", + "m.push_rules", + "m.room_key", + ] +} + +event_enum! { + /// Any ephemeral room event. + name: AnyEphemeralRoomEvent, + events: [ "m.typing", "m.receipt" ] +} + +event_enum! { + /// Any message event. + name: AnyMessageEvent, + events: [ + "m.call.answer", + "m.call.invite", + "m.call.hangup", + "m.call.candidates", + "m.room.message", + "m.room.message.feedback", + "m.sticker", + ] +} + +event_enum! { + /// Any state event. + name: AnyStateEvent, + events: [ + "m.room.aliases", + "m.room.avatar", + // "m.room.canonical_alias", + // "m.room.create", + // "m.room.encryption", + // "m.room.guest_access", + // "m.room.history_visibility", + // "m.room.join_rules", + // "m.room.member", + // "m.room.name", + // "m.room.pinned_events", + // "m.room.power_levels", + // "m.room.server_acl", + // "m.room.third_party_invite", + // "m.room.tombstone", + // "m.room.topic", + ] +} + +event_enum! { + /// Any to-device event. + name: AnyToDeviceEvent, + events: [ + "m.dummy", + "m.room_key", + //"m.room_key_request", + //"m.forwarded_room_key", + //"m.key.verification.request", + "m.key.verification.start", + //"m.key.verification.cancel", + //"m.key.verification.accept", + //"m.key.verification.key", + //"m.key.verification.mac", + //"m.room.encrypted", + ] +} diff --git a/src/ignored_user_list.rs b/src/ignored_user_list.rs index fd06c685..a77eb6d4 100644 --- a/src/ignored_user_list.rs +++ b/src/ignored_user_list.rs @@ -4,7 +4,12 @@ use ruma_events_macros::BasicEventContent; use ruma_identifiers::UserId; use serde::{Deserialize, Serialize}; +use crate::BasicEvent; + /// A list of users to ignore. +pub type IgnoredUserListEvent = BasicEvent; + +/// The payload for `IgnoredUserListEvent`. #[derive(Clone, Debug, Deserialize, Serialize, BasicEventContent)] #[ruma_event(type = "m.ignored_user_list")] pub struct IgnoredUserListEventContent { diff --git a/src/lib.rs b/src/lib.rs index ffa4724a..e2e5961a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -128,7 +128,7 @@ use self::room::redaction::RedactionEvent; pub use ruma_serde::empty::Empty; mod algorithm; -mod content_enums; +mod enums; mod error; mod event_kinds; mod event_type; @@ -160,11 +160,12 @@ pub mod typing; pub use self::{ algorithm::Algorithm, - content_enums::{ - AnyBasicEventContent, AnyEphemeralRoomEventContent, AnyMessageEventContent, - AnyStateEventContent, AnyToDeviceEventContent, - }, custom::{CustomBasicEvent, CustomMessageEvent, CustomStateEvent}, + enums::{ + AnyBasicEvent, AnyBasicEventContent, AnyEphemeralRoomEvent, AnyEphemeralRoomEventContent, + AnyMessageEvent, AnyMessageEventContent, AnyStateEvent, AnyStateEventContent, + AnyToDeviceEvent, AnyToDeviceEventContent, + }, error::{FromStrError, InvalidEvent, InvalidInput}, event_kinds::{ BasicEvent, EphemeralRoomEvent, MessageEvent, MessageEventStub, StateEvent, StateEventStub, diff --git a/src/room/aliases.rs b/src/room/aliases.rs index b29cdb3b..5dbe5673 100644 --- a/src/room/aliases.rs +++ b/src/room/aliases.rs @@ -4,7 +4,12 @@ use ruma_events_macros::StateEventContent; use ruma_identifiers::RoomAliasId; use serde::{Deserialize, Serialize}; +use crate::StateEvent; + /// Informs the room about what room aliases it has been given. +pub type AliasesEvent = StateEvent; + +/// The payload for `AliasesEvent`. #[derive(Clone, Debug, Deserialize, Serialize, StateEventContent)] #[ruma_event(type = "m.room.aliases")] pub struct AliasesEventContent { diff --git a/src/room/avatar.rs b/src/room/avatar.rs index f7f6cf49..5cd3539f 100644 --- a/src/room/avatar.rs +++ b/src/room/avatar.rs @@ -4,10 +4,14 @@ use ruma_events_macros::StateEventContent; use serde::{Deserialize, Serialize}; use super::ImageInfo; +use crate::StateEvent; /// A picture that is associated with the room. /// /// This can be displayed alongside the room information. +pub type AvatarEvent = StateEvent; + +/// The payload for `AvatarEvent`. #[derive(Clone, Debug, Deserialize, Serialize, StateEventContent)] #[ruma_event(type = "m.room.avatar")] pub struct AvatarEventContent { diff --git a/src/room/message.rs b/src/room/message.rs index 07584a25..b99e584b 100644 --- a/src/room/message.rs +++ b/src/room/message.rs @@ -9,6 +9,13 @@ use super::{EncryptedFile, ImageInfo, ThumbnailInfo}; pub mod feedback; +use crate::MessageEvent as OuterMessageEvent; + +/// This event is used when sending messages in a room. +/// +/// Messages are not limited to be text. +pub type MessageEvent = OuterMessageEvent; + /// The payload for `MessageEvent`. #[derive(Clone, Debug, Deserialize, Serialize, MessageEventContent)] #[ruma_event(type = "m.room.message")] diff --git a/src/room/message/feedback.rs b/src/room/message/feedback.rs index 3b0b6bdd..d36a2e97 100644 --- a/src/room/message/feedback.rs +++ b/src/room/message/feedback.rs @@ -5,10 +5,15 @@ use ruma_identifiers::EventId; use serde::{Deserialize, Serialize}; use strum::{Display, EnumString}; +use crate::MessageEvent; + /// An acknowledgement of a message. /// /// N.B.: Usage of this event is discouraged in favor of the receipts module. Most clients will /// not recognize this event. +pub type FeedbackEvent = MessageEvent; + +/// The payload for `FeedbackEvent`. #[derive(Clone, Debug, Deserialize, Serialize, MessageEventContent)] #[ruma_event(type = "m.room.message.feedback")] pub struct FeedbackEventContent { diff --git a/tests/event_enums.rs b/tests/event_enums.rs new file mode 100644 index 00000000..fd2e61d4 --- /dev/null +++ b/tests/event_enums.rs @@ -0,0 +1,117 @@ +use std::{ + convert::TryFrom, + time::{Duration, UNIX_EPOCH}, +}; + +use js_int::UInt; +use matches::assert_matches; +use ruma_identifiers::{EventId, RoomId, UserId}; +use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; + +use ruma_events::{ + call::{answer::AnswerEventContent, SessionDescription, SessionDescriptionType}, + room::{ImageInfo, ThumbnailInfo}, + sticker::StickerEventContent, + AnyMessageEvent, MessageEvent, UnsignedData, +}; + +#[test] +fn deserialize_message_event() { + let json_data = json!({ + "content": { + "answer": { + "type": "answer", + "sdp": "Hello" + }, + "call_id": "foofoo", + "version": 1 + }, + "event_id": "$h29iv0s8:example.com", + "origin_server_ts": 1, + "room_id": "!roomid:room.com", + "sender": "@carl:example.com", + "type": "m.call.answer" + }); + + assert_matches!( + from_json_value::(json_data) + .unwrap(), + AnyMessageEvent::CallAnswer(MessageEvent { + content: AnswerEventContent { + answer: SessionDescription { + session_type: SessionDescriptionType::Answer, + sdp, + }, + call_id, + version, + }, + event_id, + origin_server_ts, + room_id, + sender, + unsigned, + }) if sdp == "Hello" && call_id == "foofoo" && version == UInt::new(1).unwrap() + && event_id == EventId::try_from("$h29iv0s8:example.com").unwrap() + && origin_server_ts == UNIX_EPOCH + Duration::from_millis(1) + && room_id == RoomId::try_from("!roomid:room.com").unwrap() + && sender == UserId::try_from("@carl:example.com").unwrap() + && unsigned.is_empty() + ); +} + +#[test] +fn serialize_message_event() { + let aliases_event = AnyMessageEvent::Sticker(MessageEvent { + content: StickerEventContent { + body: "Hello".into(), + info: ImageInfo { + height: UInt::new(423), + width: UInt::new(1011), + mimetype: Some("image/png".into()), + size: UInt::new(84242), + thumbnail_info: Some(Box::new(ThumbnailInfo { + width: UInt::new(800), + height: UInt::new(334), + mimetype: Some("image/png".into()), + size: UInt::new(82595), + })), + thumbnail_url: Some("mxc://matrix.org".into()), + thumbnail_file: None, + }, + url: "http://www.matrix.org".into(), + }, + event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(), + origin_server_ts: UNIX_EPOCH + Duration::from_millis(1), + room_id: RoomId::try_from("!roomid:room.com").unwrap(), + sender: UserId::try_from("@carl:example.com").unwrap(), + unsigned: UnsignedData::default(), + }); + + let actual = to_json_value(&aliases_event).unwrap(); + let expected = json!({ + "content": { + "body": "Hello", + "info": { + "h": 423, + "mimetype": "image/png", + "size": 84242, + "thumbnail_info": { + "h": 334, + "mimetype": "image/png", + "size": 82595, + "w": 800 + }, + "thumbnail_url": "mxc://matrix.org", + "w": 1011 + }, + "url": "http://www.matrix.org" + }, + "event_id": "$h29iv0s8:example.com", + "origin_server_ts": 1, + "room_id": "!roomid:room.com", + "sender": "@carl:example.com", + "type": "m.sticker", + }); + + assert_eq!(actual, expected); +}