From 0381190bdb74594f5f040bdc8f9d77a93d9ec724 Mon Sep 17 00:00:00 2001 From: "Ragotzy.devin" Date: Mon, 15 Jun 2020 17:59:31 -0400 Subject: [PATCH] Add is_compatible method to event content enums to fix Any*Event deserialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … and add some benchmarks for it --- .gitignore | 2 +- ruma-events-macros/src/content_enum.rs | 16 ++ ruma-events/Cargo.toml | 5 + ruma-events/benches/event_deserialize.rs | 109 ++++++++++ ruma-events/src/enums.rs | 95 ++++++++- ruma-events/tests/enums.rs | 245 +++++++++++++++++++++++ 6 files changed, 466 insertions(+), 6 deletions(-) create mode 100644 ruma-events/benches/event_deserialize.rs create mode 100644 ruma-events/tests/enums.rs diff --git a/.gitignore b/.gitignore index 96ef6c0b..a9d37c56 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -/target +target Cargo.lock diff --git a/ruma-events-macros/src/content_enum.rs b/ruma-events-macros/src/content_enum.rs index 4a207a1f..e54bb999 100644 --- a/ruma-events-macros/src/content_enum.rs +++ b/ruma-events-macros/src/content_enum.rs @@ -8,9 +8,12 @@ use syn::{ }; /// Create a content enum from `ContentEnumInput`. +/// +/// This is the internals of the `event_content_enum!` macro. pub fn expand_content_enum(input: ContentEnumInput) -> 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::>(); @@ -54,11 +57,24 @@ pub fn expand_content_enum(input: ContentEnumInput) -> syn::Result } }; + let any_event_variant_impl = quote! { + impl #ident { + fn is_compatible(event_type: &str) -> bool { + match event_type { + #( #event_type_str => true, )* + _ => false, + } + } + } + }; + let marker_trait_impls = marker_traits(ident); Ok(quote! { #content_enum + #any_event_variant_impl + #event_content_impl #marker_trait_impls diff --git a/ruma-events/Cargo.toml b/ruma-events/Cargo.toml index 23c50476..2010fd50 100644 --- a/ruma-events/Cargo.toml +++ b/ruma-events/Cargo.toml @@ -27,3 +27,8 @@ maplit = "1.0.2" matches = "0.1.8" ruma-identifiers = { version = "0.16.2", path = "../ruma-identifiers", features = ["rand"] } trybuild = "1.0.28" +criterion = "0.3.2" + +[[bench]] +name = "event_deserialize" +harness = false diff --git a/ruma-events/benches/event_deserialize.rs b/ruma-events/benches/event_deserialize.rs new file mode 100644 index 00000000..47026ade --- /dev/null +++ b/ruma-events/benches/event_deserialize.rs @@ -0,0 +1,109 @@ +// `cargo bench` works, but if you use `cargo bench -- --save-baseline ` +// or pass any other args to it, it fails with the error +// `cargo bench unknown option --save-baseline`. +// To pass args to criterion, use this form +// `cargo bench --bench -- --save-baseline `. + +use criterion::{criterion_group, criterion_main, Criterion}; +use ruma_events::{ + room::power_levels::PowerLevelsEventContent, AnyEvent, AnyRoomEvent, AnyStateEvent, EventJson, + StateEvent, +}; +use serde_json::json; + +fn power_levels() -> serde_json::Value { + json!({ + "content": { + "ban": 50, + "events": { + "m.room.avatar": 50, + "m.room.canonical_alias": 50, + "m.room.history_visibility": 100, + "m.room.name": 50, + "m.room.power_levels": 100 + }, + "events_default": 0, + "invite": 0, + "kick": 50, + "redact": 50, + "state_default": 50, + "users": { + "@example:localhost": 100 + }, + "users_default": 0 + }, + "event_id": "$15139375512JaHAW:localhost", + "origin_server_ts": 45, + "sender": "@example:localhost", + "room_id": "!room:localhost", + "state_key": "", + "type": "m.room.power_levels", + "unsigned": { + "age": 45 + } + }) +} + +fn deserialize_any_event(c: &mut Criterion) { + let json_data = power_levels(); + + c.bench_function("deserialize to `AnyEvent`", |b| { + b.iter(|| { + let _ = serde_json::from_value::>(json_data.clone()) + .unwrap() + .deserialize() + .unwrap(); + }) + }); +} + +fn deserialize_any_room_event(c: &mut Criterion) { + let json_data = power_levels(); + + c.bench_function("deserialize to `AnyRoomEvent`", |b| { + b.iter(|| { + let _ = serde_json::from_value::>(json_data.clone()) + .unwrap() + .deserialize() + .unwrap(); + }) + }); +} + +fn deserialize_any_state_event(c: &mut Criterion) { + let json_data = power_levels(); + + c.bench_function("deserialize to `AnyStateEvent`", |b| { + b.iter(|| { + let _ = serde_json::from_value::>(json_data.clone()) + .unwrap() + .deserialize() + .unwrap(); + }) + }); +} + +fn deserialize_specific_event(c: &mut Criterion) { + let json_data = power_levels(); + + c.bench_function("deserialize to `StateEvent`", |b| { + b.iter(|| { + let _ = serde_json::from_value::>>( + json_data.clone(), + ) + .unwrap() + .deserialize() + .unwrap(); + }) + }); +} + +criterion_group!( + benches, + deserialize_any_event, + deserialize_any_room_event, + deserialize_any_state_event, + deserialize_specific_event +); + +criterion_main!(benches); diff --git a/ruma-events/src/enums.rs b/ruma-events/src/enums.rs index fb0fcabe..65dba072 100644 --- a/ruma-events/src/enums.rs +++ b/ruma-events/src/enums.rs @@ -1,5 +1,9 @@ use ruma_events_macros::event_content_enum; -use serde::{Deserialize, Serialize}; +use serde::{ + de::{self, Error as _}, + Serialize, +}; +use serde_json::{from_value as from_json_value, Value as JsonValue}; use crate::{ event_kinds::{ @@ -8,6 +12,7 @@ use crate::{ }, presence::PresenceEvent, room::redaction::{RedactionEvent, RedactionEventStub}, + util, }; event_content_enum! { @@ -116,7 +121,7 @@ pub type AnyStrippedStateEventStub = StrippedStateEventStub; /// Any event. -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Serialize)] #[serde(untagged)] pub enum AnyEvent { /// Any basic event. @@ -134,7 +139,7 @@ pub enum AnyEvent { } /// Any room event. -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Serialize)] #[serde(untagged)] pub enum AnyRoomEvent { /// Any message event. @@ -146,7 +151,7 @@ pub enum AnyRoomEvent { } /// Any room event stub (room event without a `room_id`, as returned in `/sync` responses) -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Serialize)] #[serde(untagged)] pub enum AnyRoomEventStub { /// Any message event stub @@ -154,5 +159,85 @@ pub enum AnyRoomEventStub { /// `"m.room.redaction"` stub Redaction(RedactionEventStub), /// Any state event stub - StateEvent(AnyStateEventStub), + State(AnyStateEventStub), +} + +// FIXME `#[serde(untagged)]` deserialization fails for these enums which +// is odd as we are doing basically the same thing here, investigate? +impl<'de> de::Deserialize<'de> for AnyEvent { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + let json = JsonValue::deserialize(deserializer)?; + let ev_type: String = util::get_field(&json, "type")?; + + match ev_type.as_str() { + "m.room.redaction" => { + Ok(AnyEvent::Redaction(from_json_value(json).map_err(D::Error::custom)?)) + } + "m.presence" => { + Ok(AnyEvent::Presence(from_json_value(json).map_err(D::Error::custom)?)) + } + ev_type if AnyBasicEventContent::is_compatible(ev_type) => { + Ok(AnyEvent::Basic(from_json_value(json).map_err(D::Error::custom)?)) + } + ev_type if AnyEphemeralRoomEventContent::is_compatible(ev_type) => { + Ok(AnyEvent::Ephemeral(from_json_value(json).map_err(D::Error::custom)?)) + } + ev_type if AnyMessageEventContent::is_compatible(ev_type) => { + Ok(AnyEvent::Message(from_json_value(json).map_err(D::Error::custom)?)) + } + ev_type if AnyStateEventContent::is_compatible(ev_type) => { + Ok(AnyEvent::State(from_json_value(json).map_err(D::Error::custom)?)) + } + _ => Err(D::Error::custom(format!("event type `{}` is not a valid event", ev_type))), + } + } +} + +impl<'de> de::Deserialize<'de> for AnyRoomEvent { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + let json = JsonValue::deserialize(deserializer)?; + let ev_type: String = util::get_field(&json, "type")?; + + match ev_type.as_str() { + "m.room.redaction" => { + Ok(AnyRoomEvent::Redaction(from_json_value(json).map_err(D::Error::custom)?)) + } + ev_type if AnyMessageEventContent::is_compatible(ev_type) => { + Ok(AnyRoomEvent::Message(from_json_value(json).map_err(D::Error::custom)?)) + } + ev_type if AnyStateEventContent::is_compatible(ev_type) => { + Ok(AnyRoomEvent::State(from_json_value(json).map_err(D::Error::custom)?)) + } + _ => Err(D::Error::custom(format!("event type `{}` is not a valid event", ev_type))), + } + } +} + +impl<'de> de::Deserialize<'de> for AnyRoomEventStub { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + let json = JsonValue::deserialize(deserializer)?; + let ev_type: String = util::get_field(&json, "type")?; + + match ev_type.as_str() { + "m.room.redaction" => { + Ok(AnyRoomEventStub::Redaction(from_json_value(json).map_err(D::Error::custom)?)) + } + ev_type if AnyMessageEventContent::is_compatible(ev_type) => { + Ok(AnyRoomEventStub::Message(from_json_value(json).map_err(D::Error::custom)?)) + } + ev_type if AnyStateEventContent::is_compatible(ev_type) => { + Ok(AnyRoomEventStub::State(from_json_value(json).map_err(D::Error::custom)?)) + } + _ => Err(D::Error::custom(format!("event type `{}` is not a valid event", ev_type))), + } + } } diff --git a/ruma-events/tests/enums.rs b/ruma-events/tests/enums.rs new file mode 100644 index 00000000..733c03ba --- /dev/null +++ b/ruma-events/tests/enums.rs @@ -0,0 +1,245 @@ +use std::convert::TryFrom; + +use matches::assert_matches; +use ruma_identifiers::RoomAliasId; +use serde_json::{from_value as from_json_value, json, Value as JsonValue}; + +use ruma_events::{ + room::{ + aliases::AliasesEventContent, + message::{MessageEventContent, TextMessageEventContent}, + power_levels::PowerLevelsEventContent, + }, + AnyEvent, AnyMessageEventContent, AnyRoomEvent, AnyRoomEventStub, AnyStateEventContent, + MessageEvent, MessageEventStub, StateEvent, StateEventStub, +}; + +fn message_event() -> JsonValue { + json!({ + "content": { + "body": "baba", + "format": "org.matrix.custom.html", + "formatted_body": "baba", + "msgtype": "m.text" + }, + "event_id": "$152037280074GZeOm:localhost", + "origin_server_ts": 1, + "sender": "@example:localhost", + "room_id": "!room:room.com", + "type": "m.room.message", + "unsigned": { + "age": 1 + } + }) +} + +fn message_event_stub() -> JsonValue { + json!({ + "content": { + "body": "baba", + "format": "org.matrix.custom.html", + "formatted_body": "baba", + "msgtype": "m.text" + }, + "event_id": "$152037280074GZeOm:localhost", + "origin_server_ts": 1, + "sender": "@example:localhost", + "type": "m.room.message", + "unsigned": { + "age": 1 + } + }) +} + +fn aliases_event() -> JsonValue { + json!({ + "content": { + "aliases": ["#somewhere:localhost"] + }, + "event_id": "$152037280074GZeOm:localhost", + "origin_server_ts": 1, + "sender": "@example:localhost", + "state_key": "", + "room_id": "!room:room.com", + "type": "m.room.aliases", + "unsigned": { + "age": 1 + } + }) +} + +fn aliases_event_stub() -> JsonValue { + json!({ + "content": { + "aliases": ["#somewhere:localhost"] + }, + "event_id": "$152037280074GZeOm:localhost", + "origin_server_ts": 1, + "sender": "@example:localhost", + "state_key": "", + "type": "m.room.aliases", + "unsigned": { + "age": 1 + } + }) +} + +#[test] +fn power_event_stub_deserialization() { + let json_data = json!({ + "content": { + "ban": 50, + "events": { + "m.room.avatar": 50, + "m.room.canonical_alias": 50, + "m.room.history_visibility": 100, + "m.room.name": 50, + "m.room.power_levels": 100 + }, + "events_default": 0, + "invite": 0, + "kick": 50, + "redact": 50, + "state_default": 50, + "users": { + "@example:localhost": 100 + }, + "users_default": 0 + }, + "event_id": "$15139375512JaHAW:localhost", + "origin_server_ts": 45, + "sender": "@example:localhost", + "state_key": "", + "type": "m.room.power_levels", + "unsigned": { + "age": 45 + } + }); + + assert_matches!( + from_json_value::(json_data), + Ok(AnyRoomEventStub::State( + StateEventStub { + content: AnyStateEventContent::RoomPowerLevels(PowerLevelsEventContent { + ban, .. + }), + .. + } + )) + if ban == js_int::Int::new(50).unwrap() + ); +} + +#[test] +fn message_event_stub_deserialization() { + let json_data = message_event_stub(); + + assert_matches!( + from_json_value::(json_data), + Ok(AnyRoomEventStub::Message( + MessageEventStub { + content: AnyMessageEventContent::RoomMessage(MessageEventContent::Text(TextMessageEventContent { + body, + formatted: Some(formatted), + relates_to: None, + })), + .. + } + )) + if body == "baba" && formatted.body == "baba" + ); +} + +#[test] +fn aliases_event_stub_deserialization() { + let json_data = aliases_event_stub(); + + assert_matches!( + from_json_value::(json_data), + Ok(AnyRoomEventStub::State( + StateEventStub { + content: AnyStateEventContent::RoomAliases(AliasesEventContent { + aliases, + }), + .. + } + )) + if aliases == vec![ RoomAliasId::try_from("#somewhere:localhost").unwrap() ] + ); +} + +#[test] +fn message_room_event_deserialization() { + let json_data = message_event(); + + assert_matches!( + from_json_value::(json_data), + Ok(AnyRoomEvent::Message( + MessageEvent { + content: AnyMessageEventContent::RoomMessage(MessageEventContent::Text(TextMessageEventContent { + body, + formatted: Some(formatted), + relates_to: None, + })), + .. + } + )) + if body == "baba" && formatted.body == "baba" + ); +} + +#[test] +fn alias_room_event_deserialization() { + let json_data = aliases_event(); + + assert_matches!( + from_json_value::(json_data), + Ok(AnyRoomEvent::State( + StateEvent { + content: AnyStateEventContent::RoomAliases(AliasesEventContent { + aliases, + }), + .. + } + )) + if aliases == vec![ RoomAliasId::try_from("#somewhere:localhost").unwrap() ] + ); +} + +#[test] +fn message_event_deserialization() { + let json_data = message_event(); + + assert_matches!( + from_json_value::(json_data), + Ok(AnyEvent::Message( + MessageEvent { + content: AnyMessageEventContent::RoomMessage(MessageEventContent::Text(TextMessageEventContent { + body, + formatted: Some(formatted), + relates_to: None, + })), + .. + } + )) + if body == "baba" && formatted.body == "baba" + ); +} + +#[test] +fn alias_event_deserialization() { + let json_data = aliases_event(); + + assert_matches!( + from_json_value::(json_data), + Ok(AnyEvent::State( + StateEvent { + content: AnyStateEventContent::RoomAliases(AliasesEventContent { + aliases, + }), + .. + } + )) + if aliases == vec![ RoomAliasId::try_from("#somewhere:localhost").unwrap() ] + ); +}