Repository: ruma/ruma
Pull request #68 (2020-06-18 19:21:37)

Revert " Remove event_enum! and only use event_content_enum"

ara4n: Multiple config formats will make comparing (and generating) config files a bit painful... I'd have thought having one that works well might be better than a hybid mix. Would be awesome if it supported roundtripping whilst preserving comments (unlike synapse's yaml) 

> On 12 Jun 2016, at 08:18, Jimmy Cuadra <notifications@github.com> wrote:
> 
> Users may have a preference for data serialization format for working with config files. Ruma should not force JSON on users that really don't want to use it.
> 
> The logic should be:
> 
> Use ruma.json if present.
> Use ruma.toml if present.
> Use ruma.yaml if present.
> Use ruma.yml if present.
> Crash with a useful error message.
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub, or mute the thread.
> 

jimmycuadra: Could you elaborate? When is a config file generated and why would supporting multiple formats make it hard? When are config files compared?
ara4n: I'd expect ruma to ship with a default config, and the sanest way to maintain that is probably to generate it (like synapse does), as well as being able to upgrade the config as new features are added (like synapse doesn't, as YAML doesn't roundtrip nicely).  I think it could be quite confusing and painful to support if Ruma supported multiple config formats, as users start pastebinning snippets in inconsistent formats, or if you need to diff between a working config on one system and a broken one on another system (which happens to be using a different format) - e.g. what I was having to do on Friday to diff Uniaika's synapse config with my config to try to work out why her YAML parser was choking.  You'd also have to ideally show config snippets in the doc in multiple formats to help folks who were trying to follow a tutorial etc.

This is all my personal preference, though - I just can't think of any projects which support arbitrary config formats where the benefit of the flexibility outweighs the additional complexity.  My hunch is that it might be much more useful to expose a config REST API of some kind to allow automatic reconfiguration (perhaps some subset of which could be standardised as part of a Matrix generic HS admin API), than get too hung up on how to persist the config...

</my two cents>
jimmycuadra: Thanks for the details. I'm not too worried about these points, because config files of any format will be deserialized into a static type in Rust, and if there were a need to do a diff, that could be done against the type instead of trying to compare raw config file syntax. Having a command to generating an initial config file for the user sounds like a good idea, but I doubt I would want to attempt updating an existing config file automatically.

Closing this, as it was implemented in #69.
ara4n: fair enough :)

On 13/06/2016 10:42, Jimmy Cuadra wrote:
> Closed #68 <https://github.com/ruma/ruma/issues/68>.
>
> — You are receiving this because you commented. Reply to this email
> directly, view it on GitHub
> <https://github.com/ruma/ruma/issues/68#event-690191437>, or mute
> the thread
> <https://github.com/notifications/unsubscribe/ABO_vcYlbLjeOzohytnzRblMEMODWexkks5qLSX-gaJpZM4IzudR>.
>


Patch:
diff --git a/ruma-client-api/src/lib.rs b/ruma-client-api/src/lib.rs
index cc57eb98..22b5a1c0 100644
--- a/ruma-client-api/src/lib.rs
+++ b/ruma-client-api/src/lib.rs
@@ -3,6 +3,7 @@
 //! shared by client and server code.
 
 #![deny(missing_copy_implementations, missing_debug_implementations, missing_docs)]
+#![allow(clippy::new_without_default)]
 
 pub mod error;
 pub mod r0;
diff --git a/ruma-client-api/src/unversioned/discover_homeserver.rs b/ruma-client-api/src/unversioned/discover_homeserver.rs
index b82ef0de..dee2c267 100644
--- a/ruma-client-api/src/unversioned/discover_homeserver.rs
+++ b/ruma-client-api/src/unversioned/discover_homeserver.rs
@@ -28,16 +28,46 @@ ruma_api! {
     error: crate::Error
 }
 
+impl Request {
+    /// Creates an empty `Request`.
+    pub fn new() -> Self {
+        Self
+    }
+}
+
+impl Response {
+    /// Creates a `Response` with the given `HomeserverInfo`.
+    pub fn new(homeserver: HomeserverInfo) -> Self {
+        Self { homeserver, identity_server: None }
+    }
+}
+
 /// Information about a discovered homeserver.
 #[derive(Clone, Debug, Deserialize, Hash, PartialEq, PartialOrd, Serialize)]
+#[non_exhaustive]
 pub struct HomeserverInfo {
     /// The base URL for the homeserver for client-server connections.
     pub base_url: String,
 }
 
+impl HomeserverInfo {
+    /// Creates a `HomeserverInfo` with the given `base_url`.
+    pub fn new(base_url: String) -> Self {
+        Self { base_url }
+    }
+}
+
 /// Information about a discovered identity server.
 #[derive(Clone, Debug, Deserialize, Hash, PartialEq, PartialOrd, Serialize)]
+#[non_exhaustive]
 pub struct IdentityServerInfo {
     /// The base URL for the identity server for client-server connections.
     pub base_url: String,
 }
+
+impl IdentityServerInfo {
+    /// Creates an `IdentityServerInfo` with the given `base_url`.
+    pub fn new(base_url: String) -> Self {
+        Self { base_url }
+    }
+}
diff --git a/ruma-client-api/src/unversioned/get_supported_versions.rs b/ruma-client-api/src/unversioned/get_supported_versions.rs
index f248c17e..9f9f692b 100644
--- a/ruma-client-api/src/unversioned/get_supported_versions.rs
+++ b/ruma-client-api/src/unversioned/get_supported_versions.rs
@@ -27,3 +27,17 @@ ruma_api! {
 
     error: crate::Error
 }
+
+impl Request {
+    /// Creates an empty `Request`.
+    pub fn new() -> Self {
+        Self
+    }
+}
+
+impl Response {
+    /// Creates a `Response` with the given `versions`.
+    pub fn new(versions: Vec<String>) -> Self {
+        Self { versions, unstable_features: BTreeMap::new() }
+    }
+}
diff --git a/ruma-events-macros/src/event.rs b/ruma-events-macros/src/event.rs
index 32532529..40bb3ae7 100644
--- a/ruma-events-macros/src/event.rs
+++ b/ruma-events-macros/src/event.rs
@@ -244,7 +244,9 @@ fn expand_deserialize_event(
 
                         while let Some(key) = map.next_key()? {
                             match key {
-                                Field::Unknown => continue,
+                                Field::Unknown => {
+                                    let _: ::serde::de::IgnoredAny = map.next_value()?;
+                                },
                                 Field::Type => {
                                     if event_type.is_some() {
                                         return Err(::serde::de::Error::duplicate_field("type"));
diff --git a/ruma-events-macros/src/content_enum.rs b/ruma-events-macros/src/event_enum.rs
similarity index 58%
rename from ruma-events-macros/src/content_enum.rs
rename to ruma-events-macros/src/event_enum.rs
index 9fcb2cab..8ea4049e 100644
--- a/ruma-events-macros/src/content_enum.rs
+++ b/ruma-events-macros/src/event_enum.rs
@@ -1,4 +1,4 @@
-//! Implementation of the content_enum type macro.
+//! Implementation of event enum and event content enum macros.
 
 use proc_macro2::TokenStream;
 use quote::quote;
@@ -7,16 +7,69 @@ use syn::{
     Attribute, Expr, ExprLit, Ident, Lit, LitStr, Token,
 };
 
-/// 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<TokenStream> {
+/// Create a content enum from `EventEnumInput`.
+pub fn expand_event_enum(input: EventEnumInput) -> syn::Result<TokenStream> {
     let attrs = &input.attrs;
     let ident = &input.name;
+    let event_type_str = &input.events;
+
+    let variants = input.events.iter().map(to_camel_case).collect::<syn::Result<Vec<_>>>()?;
+    let content = input.events.iter().map(to_event_path).collect::<Vec<_>>();
+
+    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<D>(deserializer: D) -> Result<Self, D::Error>
+            where
+                D: ::serde::de::Deserializer<'de>,
+            {
+                use ::serde::de::Error as _;
 
+                let json = Box::<::serde_json::value::RawValue>::deserialize(deserializer)?;
+                let ::ruma_events::EventDeHelper { ev_type } = ::ruma_events::from_raw_json_value(&json)?;
+                match ev_type.as_str() {
+                    #(
+                        #event_type_str => {
+                            let event = ::serde_json::from_str::<#content>(json.get()).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<TokenStream> {
+    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::<Vec<_>>();
+    let variants = input.events.iter().map(to_camel_case).collect::<syn::Result<Vec<_>>>()?;
     let content = input.events.iter().map(to_event_content_path).collect::<Vec<_>>();
 
     let content_enum = quote! {
@@ -29,8 +82,8 @@ pub fn expand_content_enum(input: ContentEnumInput) -> syn::Result<TokenStream>
                 #[doc = #event_type_str]
                 #variants(#content),
             )*
-            #[doc = "Represents any event not defined in the Matrix spec."]
-            Custom(::ruma_events::custom::CustomEventContent)
+            /// Any custom event.
+            Custom(::ruma_events::custom::CustomEventContent),
         }
     };
 
@@ -39,14 +92,11 @@ pub fn expand_content_enum(input: ContentEnumInput) -> syn::Result<TokenStream>
             fn event_type(&self) -> &str {
                 match self {
                     #( Self::#variants(content) => content.event_type(), )*
-                    #ident::Custom(content) => content.event_type(),
+                    Self::Custom(content) => content.event_type(),
                 }
             }
 
-            fn from_parts(
-                event_type: &str,
-                input: Box<::serde_json::value::RawValue>,
-            ) -> Result<Self, ::serde_json::Error> {
+            fn from_parts(event_type: &str, input: Box<::serde_json::value::RawValue>) -> Result<Self, ::serde_json::Error> {
                 match event_type {
                     #(
                         #event_type_str => {
@@ -74,7 +124,7 @@ pub fn expand_content_enum(input: ContentEnumInput) -> syn::Result<TokenStream>
         }
     };
 
-    let marker_trait_impls = marker_traits(ident);
+    let marker_trait_impls = marker_traits(&ident);
 
     Ok(quote! {
         #content_enum
@@ -107,9 +157,28 @@ fn marker_traits(ident: &Ident) -> TokenStream {
     }
 }
 
-fn to_event_content_path(
-    name: &LitStr,
-) -> syn::punctuated::Punctuated<syn::Token![::], syn::PathSegment> {
+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::<Vec<_>>();
+
+    let event_str = path.last().unwrap();
+    let event = event_str
+        .split('_')
+        .map(|s| s.chars().next().unwrap().to_uppercase().to_string() + &s[1..])
+        .collect::<String>();
+
+    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();
 
@@ -125,56 +194,60 @@ fn to_event_content_path(
 
     let content_str = Ident::new(&format!("{}EventContent", event), span);
     let path = path.iter().map(|s| Ident::new(s, span));
-    syn::parse_quote! {
+    quote! {
         ::ruma_events::#( #path )::*::#content_str
     }
 }
 
 /// 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 {
+/// camel casing to give the `Event` struct name.
+pub(crate) fn to_camel_case(name: &LitStr) -> syn::Result<Ident> {
     let span = name.span();
     let name = name.value();
 
     if &name[..2] != "m." {
-        panic!("well-known matrix events have to start with `m.` found `{}`", name,)
+        return Err(syn::Error::new(
+            span,
+            format!("well-known matrix events have to start with `m.` found `{}`", name),
+        ));
     }
 
     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)
+
+    Ok(Ident::new(&s, span))
 }
 
-/// Custom keywords for the `event_content_content_enum!` macro
+/// Custom keywords for the `event_enum!` macro
 mod kw {
     syn::custom_keyword!(name);
     syn::custom_keyword!(events);
 }
 
-/// The entire `event_content_content_enum!` macro structure directly as it appears in the source code..
-pub struct ContentEnumInput {
+/// 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<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".
+    /// An array of valid matrix event types. This will generate the variants of the event 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 ContentEnumInput {
+impl Parse for EventEnumInput {
     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 content_enum enum
+        // the name of our event enum
         let name: Ident = input.parse()?;
         input.parse::<Token![,]>()?;
 
diff --git a/ruma-events-macros/src/lib.rs b/ruma-events-macros/src/lib.rs
index 84b4a5e1..0e5b9866 100644
--- a/ruma-events-macros/src/lib.rs
+++ b/ruma-events-macros/src/lib.rs
@@ -11,26 +11,27 @@ use proc_macro::TokenStream;
 use syn::{parse_macro_input, DeriveInput};
 
 use self::{
-    content_enum::{expand_content_enum, ContentEnumInput},
     event::expand_event,
     event_content::{
         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},
 };
 
-mod content_enum;
 mod event;
 mod event_content;
+mod event_enum;
 
-/// Generates a content enum to represent the various Matrix event types.
+/// 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_content_enum(input: TokenStream) -> TokenStream {
-    let content_enum_input = syn::parse_macro_input!(input as ContentEnumInput);
-    expand_content_enum(content_enum_input).unwrap_or_else(|err| err.to_compile_error()).into()
+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 an implementation of `ruma_events::EventContent`.
diff --git a/ruma-events/src/enums.rs b/ruma-events/src/enums.rs
index 333f8d65..2476cbe2 100644
--- a/ruma-events/src/enums.rs
+++ b/ruma-events/src/enums.rs
@@ -1,35 +1,32 @@
-use ruma_events_macros::event_content_enum;
+use ruma_events_macros::event_enum;
 use serde::{
-    de::{self, DeserializeOwned, Error as _},
-    Deserialize, Serialize,
+    de::{self, Error},
+    Serialize,
 };
 use serde_json::value::RawValue as RawJsonValue;
 
 use crate::{
-    event_kinds::{
-        BasicEvent, EphemeralRoomEvent, MessageEvent, MessageEventStub, StateEvent, StateEventStub,
-        StrippedStateEventStub, ToDeviceEvent,
-    },
-    presence::PresenceEvent,
-    room::redaction::{RedactionEvent, RedactionEventStub},
+    event_kinds::{MessageEventStub, StateEventStub, StrippedStateEventStub},
+    from_raw_json_value, EventDeHelper,
 };
 
-event_content_enum! {
+event_enum! {
     /// Any basic event.
-    name: AnyBasicEventContent,
+    name: AnyBasicEvent,
     events: [
         "m.direct",
         "m.dummy",
         "m.ignored_user_list",
+        "m.presence",
         "m.push_rules",
         "m.room_key",
         "m.tag",
     ]
 }
 
-event_content_enum! {
+event_enum! {
     /// Any ephemeral room event.
-    name: AnyEphemeralRoomEventContent,
+    name: AnyEphemeralRoomEvent,
     events: [
         "m.fully_read",
         "m.receipt",
@@ -37,24 +34,25 @@ event_content_enum! {
     ]
 }
 
-event_content_enum! {
+event_enum! {
     /// Any message event.
-    name: AnyMessageEventContent,
+    name: AnyMessageEvent,
     events: [
         "m.call.answer",
         "m.call.invite",
         "m.call.hangup",
         "m.call.candidates",
-        "m.room.encrypted",
         "m.room.message",
         "m.room.message.feedback",
+        "m.room.redaction",
         "m.sticker",
+
     ]
 }
 
-event_content_enum! {
+event_enum! {
     /// Any state event.
-    name: AnyStateEventContent,
+    name: AnyStateEvent,
     events: [
         "m.room.aliases",
         "m.room.avatar",
@@ -76,9 +74,9 @@ event_content_enum! {
     ]
 }
 
-event_content_enum! {
+event_enum! {
     /// Any to-device event.
-    name: AnyToDeviceEventContent,
+    name: AnyToDeviceEvent,
     events: [
         "m.dummy",
         "m.room_key",
@@ -94,21 +92,9 @@ event_content_enum! {
     ]
 }
 
-/// Any basic event, one that has no (well-known) fields outside of `content`.
-pub type AnyBasicEvent = BasicEvent<AnyBasicEventContent>;
-
-/// Any ephemeral room event.
-pub type AnyEphemeralRoomEvent = EphemeralRoomEvent<AnyEphemeralRoomEventContent>;
-
-/// Any message event.
-pub type AnyMessageEvent = MessageEvent<AnyMessageEventContent>;
-
 /// Any message event stub (message event without a `room_id`, as returned in `/sync` responses)
 pub type AnyMessageEventStub = MessageEventStub<AnyMessageEventContent>;
 
-/// Any state event.
-pub type AnyStateEvent = StateEvent<AnyStateEventContent>;
-
 /// Any state event stub (state event without a `room_id`, as returned in `/sync` responses)
 pub type AnyStateEventStub = StateEventStub<AnyStateEventContent>;
 
@@ -116,23 +102,16 @@ pub type AnyStateEventStub = StateEventStub<AnyStateEventContent>;
 /// been invited to in `/sync` responses)
 pub type AnyStrippedStateEventStub = StrippedStateEventStub<AnyStateEventContent>;
 
-/// Any to-device event.
-pub type AnyToDeviceEvent = ToDeviceEvent<AnyToDeviceEventContent>;
-
 /// Any event.
 #[derive(Clone, Debug, Serialize)]
 #[serde(untagged)]
 pub enum AnyEvent {
     /// Any basic event.
     Basic(AnyBasicEvent),
-    /// `"m.presence"`, the only non-room event with a `sender` field.
-    Presence(PresenceEvent),
     /// Any ephemeral room event.
     Ephemeral(AnyEphemeralRoomEvent),
     /// Any message event.
     Message(AnyMessageEvent),
-    /// `"m.room.redaction"`, the only room event with a `redacts` field.
-    Redaction(RedactionEvent),
     /// Any state event.
     State(AnyStateEvent),
 }
@@ -143,8 +122,6 @@ pub enum AnyEvent {
 pub enum AnyRoomEvent {
     /// Any message event.
     Message(AnyMessageEvent),
-    /// `"m.room.redaction"`, the only room event with a `redacts` field.
-    Redaction(RedactionEvent),
     /// Any state event.
     State(AnyStateEvent),
 }
@@ -155,26 +132,10 @@ pub enum AnyRoomEvent {
 pub enum AnyRoomEventStub {
     /// Any message event stub
     Message(AnyMessageEventStub),
-    /// `"m.room.redaction"` stub
-    Redaction(RedactionEventStub),
     /// Any state event stub
     State(AnyStateEventStub),
 }
 
-#[derive(Deserialize)]
-struct EventDeHelper {
-    #[serde(rename = "type")]
-    ev_type: String,
-}
-
-fn from_raw_json_value<T, E>(val: &RawJsonValue) -> Result<T, E>
-where
-    T: DeserializeOwned,
-    E: de::Error,
-{
-    serde_json::from_str(val.get()).map_err(E::custom)
-}
-
 // 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 {
@@ -186,8 +147,6 @@ impl<'de> de::Deserialize<'de> for AnyEvent {
         let EventDeHelper { ev_type } = from_raw_json_value(&json)?;
 
         match ev_type.as_str() {
-            "m.room.redaction" => Ok(AnyEvent::Redaction(from_raw_json_value(&json)?)),
-            "m.presence" => Ok(AnyEvent::Presence(from_raw_json_value(&json)?)),
             ev_type if AnyBasicEventContent::is_compatible(ev_type) => {
                 Ok(AnyEvent::Basic(from_raw_json_value(&json)?))
             }
@@ -214,7 +173,6 @@ impl<'de> de::Deserialize<'de> for AnyRoomEvent {
         let EventDeHelper { ev_type } = from_raw_json_value(&json)?;
 
         match ev_type.as_str() {
-            "m.room.redaction" => Ok(AnyRoomEvent::Redaction(from_raw_json_value(&json)?)),
             ev_type if AnyMessageEventContent::is_compatible(ev_type) => {
                 Ok(AnyRoomEvent::Message(from_raw_json_value(&json)?))
             }
@@ -235,7 +193,6 @@ impl<'de> de::Deserialize<'de> for AnyRoomEventStub {
         let EventDeHelper { ev_type } = from_raw_json_value(&json)?;
 
         match ev_type.as_str() {
-            "m.room.redaction" => Ok(AnyRoomEventStub::Redaction(from_raw_json_value(&json)?)),
             ev_type if AnyMessageEventContent::is_compatible(ev_type) => {
                 Ok(AnyRoomEventStub::Message(from_raw_json_value(&json)?))
             }
diff --git a/ruma-events/src/lib.rs b/ruma-events/src/lib.rs
index 91810ed7..8d3fcec5 100644
--- a/ruma-events/src/lib.rs
+++ b/ruma-events/src/lib.rs
@@ -120,7 +120,7 @@
 use std::fmt::Debug;
 
 use js_int::Int;
-use serde::{Deserialize, Serialize};
+use serde::{de, Deserialize, Serialize};
 use serde_json::value::RawValue as RawJsonValue;
 
 use self::room::redaction::RedactionEvent;
@@ -234,3 +234,21 @@ pub trait MessageEventContent: RoomEventContent {}
 
 /// Marker trait for the content of a state event.
 pub trait StateEventContent: RoomEventContent {}
+
+/// Helper struct to obtain the event type from a serde_json::value::RawValue.
+#[doc(hidden)]
+#[derive(Debug, Deserialize)]
+pub struct EventDeHelper {
+    #[serde(rename = "type")]
+    pub ev_type: String,
+}
+
+/// Helper function for serde_json::value::RawValue deserialization.
+#[doc(hidden)]
+pub fn from_raw_json_value<T, E>(val: &RawJsonValue) -> Result<T, E>
+where
+    T: de::DeserializeOwned,
+    E: de::Error,
+{
+    serde_json::from_str(val.get()).map_err(E::custom)
+}


Test patch:
diff --git a/ruma-events/tests/enums.rs b/ruma-events/tests/enums.rs
index 733c03ba..50ce88c3 100644
--- a/ruma-events/tests/enums.rs
+++ b/ruma-events/tests/enums.rs
@@ -10,8 +10,9 @@ use ruma_events::{
         message::{MessageEventContent, TextMessageEventContent},
         power_levels::PowerLevelsEventContent,
     },
-    AnyEvent, AnyMessageEventContent, AnyRoomEvent, AnyRoomEventStub, AnyStateEventContent,
-    MessageEvent, MessageEventStub, StateEvent, StateEventStub,
+    AnyEvent, AnyMessageEvent, AnyMessageEventContent, AnyRoomEvent, AnyRoomEventStub,
+    AnyStateEvent, AnyStateEventContent, MessageEvent, MessageEventStub, StateEvent,
+    StateEventStub,
 };
 
 fn message_event() -> JsonValue {
@@ -175,14 +176,14 @@ fn message_room_event_deserialization() {
     assert_matches!(
         from_json_value::<AnyRoomEvent>(json_data),
         Ok(AnyRoomEvent::Message(
-            MessageEvent {
-                content: AnyMessageEventContent::RoomMessage(MessageEventContent::Text(TextMessageEventContent {
+            AnyMessageEvent::RoomMessage(MessageEvent {
+                content: MessageEventContent::Text(TextMessageEventContent {
                     body,
                     formatted: Some(formatted),
                     relates_to: None,
-                })),
+                }),
                 ..
-            }
+            })
         ))
         if body == "baba" && formatted.body == "<strong>baba</strong>"
     );
@@ -195,12 +196,12 @@ fn alias_room_event_deserialization() {
     assert_matches!(
         from_json_value::<AnyRoomEvent>(json_data),
         Ok(AnyRoomEvent::State(
-            StateEvent {
-                content: AnyStateEventContent::RoomAliases(AliasesEventContent {
+            AnyStateEvent::RoomAliases(StateEvent {
+                content: AliasesEventContent {
                     aliases,
-                }),
+                },
                 ..
-            }
+            })
         ))
         if aliases == vec![ RoomAliasId::try_from("#somewhere:localhost").unwrap() ]
     );
@@ -213,14 +214,14 @@ fn message_event_deserialization() {
     assert_matches!(
         from_json_value::<AnyEvent>(json_data),
         Ok(AnyEvent::Message(
-            MessageEvent {
-                content: AnyMessageEventContent::RoomMessage(MessageEventContent::Text(TextMessageEventContent {
+            AnyMessageEvent::RoomMessage(MessageEvent {
+                content: MessageEventContent::Text(TextMessageEventContent {
                     body,
                     formatted: Some(formatted),
                     relates_to: None,
-                })),
+                }),
                 ..
-            }
+            })
         ))
         if body == "baba" && formatted.body == "<strong>baba</strong>"
     );
@@ -233,12 +234,12 @@ fn alias_event_deserialization() {
     assert_matches!(
         from_json_value::<AnyEvent>(json_data),
         Ok(AnyEvent::State(
-            StateEvent {
-                content: AnyStateEventContent::RoomAliases(AliasesEventContent {
+            AnyStateEvent::RoomAliases(StateEvent {
+                content: AliasesEventContent {
                     aliases,
-                }),
+                },
                 ..
-            }
+            })
         ))
         if aliases == vec![ RoomAliasId::try_from("#somewhere:localhost").unwrap() ]
     );
diff --git a/ruma-events/tests/event_enums.rs b/ruma-events/tests/event_enums.rs
new file mode 100644
index 00000000..c1791016
--- /dev/null
+++ b/ruma-events/tests/event_enums.rs
@@ -0,0 +1,124 @@
+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 ui() {
+    let t = trybuild::TestCases::new();
+    t.pass("tests/ui/07-enum-sanity-check.rs");
+    t.compile_fail("tests/ui/08-enum-invalid-path.rs");
+}
+
+#[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::<AnyMessageEvent>(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);
+}
diff --git a/ruma-events/tests/state_event.rs b/ruma-events/tests/state_event.rs
index a303fad7..e18db062 100644
--- a/ruma-events/tests/state_event.rs
+++ b/ruma-events/tests/state_event.rs
@@ -7,7 +7,8 @@ use js_int::UInt;
 use matches::assert_matches;
 use ruma_events::{
     room::{aliases::AliasesEventContent, avatar::AvatarEventContent, ImageInfo, ThumbnailInfo},
-    AnyStateEventContent, EventJson, StateEvent, StateEventStub, UnsignedData,
+    AnyRoomEvent, AnyStateEvent, AnyStateEventContent, EventJson, StateEvent, StateEventStub,
+    UnsignedData,
 };
 use ruma_identifiers::{EventId, RoomAliasId, RoomId, UserId};
 use serde_json::{
@@ -236,3 +237,48 @@ fn deserialize_avatar_without_prev_content() {
             && unsigned.is_empty()
     );
 }
+
+#[test]
+fn deserialize_member_event_with_top_level_membership_field() {
+    let json_data = json!({
+        "content": {
+            "avatar_url": null,
+            "displayname": "example",
+            "membership": "join"
+        },
+        "event_id": "$h29iv0s8:example.com",
+        "membership": "join",
+        "room_id": "!room:localhost",
+        "origin_server_ts": 1,
+        "sender": "@example:localhost",
+        "state_key": "@example:localhost",
+        "type": "m.room.member",
+        "unsigned": {
+            "age": 1,
+            "replaces_state": "$151800111315tsynI:localhost",
+            "prev_content": {
+                "avatar_url": null,
+                "displayname": "example",
+                "membership": "invite"
+            }
+        }
+    });
+
+    assert_matches!(
+        from_json_value::<AnyRoomEvent>(json_data)
+            .unwrap(),
+        AnyRoomEvent::State(
+            AnyStateEvent::RoomMember(StateEvent {
+                content,
+                event_id,
+                origin_server_ts,
+                prev_content: None,
+                sender,
+                ..
+            }
+        )) if event_id == EventId::try_from("$h29iv0s8:example.com").unwrap()
+            && origin_server_ts == UNIX_EPOCH + Duration::from_millis(1)
+            && sender == UserId::try_from("@example:localhost").unwrap()
+            && content.displayname == Some("example".to_string())
+    );
+}
diff --git a/ruma-events/tests/ui/07-enum-sanity-check.rs b/ruma-events/tests/ui/07-enum-sanity-check.rs
index 916d7570..633589ff 100644
--- a/ruma-events/tests/ui/07-enum-sanity-check.rs
+++ b/ruma-events/tests/ui/07-enum-sanity-check.rs
@@ -1,8 +1,8 @@
-use ruma_events_macros::event_content_enum;
+use ruma_events_macros::event_enum;
 
-event_content_enum! {
+event_enum! {
     /// Any basic event.
-    name: AnyBasicEventContent,
+    name: AnyBasicEvent,
     events: [
         "m.direct",
         "m.dummy",
diff --git a/ruma-events/tests/ui/08-enum-invalid-path.rs b/ruma-events/tests/ui/08-enum-invalid-path.rs
index 8d9a6bf1..410b7efc 100644
--- a/ruma-events/tests/ui/08-enum-invalid-path.rs
+++ b/ruma-events/tests/ui/08-enum-invalid-path.rs
@@ -1,13 +1,13 @@
-use ruma_events_macros::event_content_enum;
+use ruma_events_macros::event_enum;
 
-event_content_enum! {
+event_enum! {
     name: InvalidEvent,
     events: [
         "m.not.a.path",
     ]
 }
 
-event_content_enum! {
+event_enum! {
     name: InvalidEvent,
     events: [
         "not.a.path",
diff --git a/ruma-events/tests/ui/08-enum-invalid-path.stderr b/ruma-events/tests/ui/08-enum-invalid-path.stderr
index fe3b34c6..48062907 100644
--- a/ruma-events/tests/ui/08-enum-invalid-path.stderr
+++ b/ruma-events/tests/ui/08-enum-invalid-path.stderr
@@ -1,15 +1,8 @@
-error: proc macro panicked
-  --> $DIR/08-enum-invalid-path.rs:10:1
+error: well-known matrix events have to start with `m.` found `not.a.path`
+  --> $DIR/08-enum-invalid-path.rs:13:9
    |
-10 | / event_content_enum! {
-11 | |     name: InvalidEvent,
-12 | |     events: [
-13 | |         "not.a.path",
-14 | |     ]
-15 | | }
-   | |_^
-   |
-   = help: message: well-known matrix events have to start with `m.` found `not.a.path`
+13 |         "not.a.path",
+   |         ^^^^^^^^^^^^
 
 error[E0433]: failed to resolve: could not find `not` in `ruma_events`
  --> $DIR/08-enum-invalid-path.rs:6:9