Repository: ruma/ruma (rust)

Issue:
Generate event *type* enums
It would be good if our `event_enum!` macro would generate `*EventType` enums in addition to event enums and event content enums, for use in routes such as `send_message_event` and `create_message_event`.

cc @DevinR528 who wrote almost all of the events macro code

Pull request description:
Declare all event enums inside one macro invocation
I didn't remove the old `event_enum!` macro since I wanted to make sure this is moving in a good direction first.

```rust
test_event_enum! {
    /// Any global account data event.
    enum GlobalAccountData {
        events: [
            "m.direct",
            ...
        ]
    }

    /// blah blah
    enum State {
        events: [
            ...
        ]
    }
}
```

Resolves #520

Patch:
diff --git a/crates/ruma-events-macros/src/event_enum.rs b/crates/ruma-events-macros/src/event_enum.rs
index ba2516d9..3a93f9c0 100644
--- a/crates/ruma-events-macros/src/event_enum.rs
+++ b/crates/ruma-events-macros/src/event_enum.rs
@@ -4,7 +4,7 @@ use proc_macro2::{Span, TokenStream};
 use quote::{format_ident, quote, ToTokens};
 use syn::{Attribute, Ident, LitStr};
 
-use crate::event_parse::{EventEnumEntry, EventEnumInput, EventKind, EventKindVariation};
+use crate::event_parse::{EventEnumDecl, EventEnumEntry, EventKind, EventKindVariation};
 
 fn is_non_stripped_room_event(kind: &EventKind, var: &EventKindVariation) -> bool {
     matches!(kind, EventKind::Message | EventKind::State)
@@ -43,7 +43,7 @@ const EVENT_FIELDS: &[(&str, EventKindFn)] = &[
 ];
 
 /// Create a content enum from `EventEnumInput`.
-pub fn expand_event_enum(input: EventEnumInput) -> syn::Result<TokenStream> {
+pub fn expand_event_enum(input: &EventEnumDecl) -> syn::Result<TokenStream> {
     let ruma_events = crate::import_ruma_events();
 
     let name = &input.name;
@@ -969,13 +969,13 @@ fn field_return_type(
     }
 }
 
-struct EventEnumVariant {
+pub(crate) struct EventEnumVariant {
     pub attrs: Vec<Attribute>,
     pub ident: Ident,
 }
 
 impl EventEnumVariant {
-    fn to_tokens<T>(&self, prefix: Option<T>, with_attrs: bool) -> TokenStream
+    pub(crate) fn to_tokens<T>(&self, prefix: Option<T>, with_attrs: bool) -> TokenStream
     where
         T: ToTokens,
     {
@@ -993,21 +993,21 @@ impl EventEnumVariant {
         tokens
     }
 
-    fn decl(&self) -> TokenStream {
+    pub(crate) fn decl(&self) -> TokenStream {
         self.to_tokens::<TokenStream>(None, true)
     }
 
-    fn match_arm(&self, prefix: impl ToTokens) -> TokenStream {
+    pub(crate) fn match_arm(&self, prefix: impl ToTokens) -> TokenStream {
         self.to_tokens(Some(prefix), true)
     }
 
-    fn ctor(&self, prefix: impl ToTokens) -> TokenStream {
+    pub(crate) fn ctor(&self, prefix: impl ToTokens) -> TokenStream {
         self.to_tokens(Some(prefix), false)
     }
 }
 
 impl EventEnumEntry {
-    fn to_variant(&self) -> syn::Result<EventEnumVariant> {
+    pub(crate) fn to_variant(&self) -> syn::Result<EventEnumVariant> {
         let attrs = self.attrs.clone();
         let ident = to_camel_case(&self.ev_type)?;
         Ok(EventEnumVariant { attrs, ident })
diff --git a/crates/ruma-events-macros/src/event_parse.rs b/crates/ruma-events-macros/src/event_parse.rs
index 8097e096..41372cae 100644
--- a/crates/ruma-events-macros/src/event_parse.rs
+++ b/crates/ruma-events-macros/src/event_parse.rs
@@ -5,7 +5,7 @@ use std::fmt;
 use proc_macro2::Span;
 use quote::format_ident;
 use syn::{
-    bracketed,
+    braced,
     parse::{self, Parse, ParseStream},
     Attribute, Ident, LitStr, Token,
 };
@@ -140,8 +140,8 @@ impl Parse for EventKind {
             "State" => EventKind::State,
             "ToDevice" => EventKind::ToDevice,
             id => {
-                return Err(syn::Error::new(
-                    input.span(),
+                return Err(syn::Error::new_spanned(
+                    ident,
                     format!(
                         "valid event kinds are GlobalAccountData, RoomAccountData, EphemeralRoom, \
                         Message, State, ToDevice found `{}`",
@@ -184,8 +184,19 @@ pub fn to_kind_variation(ident: &Ident) -> Option<(EventKind, EventKindVariation
     }
 }
 
+pub struct EventEnumEntry {
+    pub attrs: Vec<Attribute>,
+    pub ev_type: LitStr,
+}
+
+impl Parse for EventEnumEntry {
+    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
+        Ok(Self { attrs: input.call(Attribute::parse_outer)?, ev_type: input.parse()? })
+    }
+}
+
 /// The entire `event_enum!` macro structure directly as it appears in the source code.
-pub struct EventEnumInput {
+pub struct EventEnumDecl {
     /// Outer attributes on the field, such as a docstring.
     pub attrs: Vec<Attribute>,
 
@@ -194,45 +205,33 @@ pub struct EventEnumInput {
 
     /// An array of valid matrix event types.
     ///
-    /// This will generate the variants of the event type "name". There needs to be a corresponding
+    /// This will generate the variants of the event type "kind". 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<EventEnumEntry>,
 }
 
-impl Parse for EventEnumInput {
-    fn parse(input: ParseStream<'_>) -> parse::Result<Self> {
-        let attrs = input.call(Attribute::parse_outer)?;
-        // "name" field
-        let _: kw::kind = input.parse()?;
-        let _: Token![:] = input.parse()?;
-
-        // the name of our event enum
-        let name: EventKind = input.parse()?;
-        let _: Token![,] = input.parse()?;
-
-        // "events" field
-        let _: kw::events = input.parse()?;
-        let _: Token![:] = input.parse()?;
-
-        // an array of event names `["m.room.whatever", ...]`
-        let content;
-        bracketed!(content in input);
-        let events = content.parse_terminated::<_, Token![,]>(EventEnumEntry::parse)?;
-        let events = events.into_iter().collect();
-
-        Ok(Self { attrs, name, events })
-    }
-}
-
-pub struct EventEnumEntry {
-    pub attrs: Vec<Attribute>,
-    pub ev_type: LitStr,
+/// The entire `event_enum!` macro structure directly as it appears in the source code.
+pub struct EventEnumInput {
+    pub(crate) enums: Vec<EventEnumDecl>,
 }
 
-impl Parse for EventEnumEntry {
-    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
-        Ok(Self { attrs: input.call(Attribute::parse_outer)?, ev_type: input.parse()? })
+impl Parse for EventEnumInput {
+    fn parse(input: ParseStream<'_>) -> parse::Result<Self> {
+        let mut enums = vec![];
+        while !input.is_empty() {
+            let attrs = input.call(Attribute::parse_outer)?;
+
+            let _: Token![enum] = input.parse()?;
+            let name: EventKind = input.parse()?;
+
+            let content;
+            braced!(content in input);
+            let events = content.parse_terminated::<_, Token![,]>(EventEnumEntry::parse)?;
+            let events = events.into_iter().collect();
+            enums.push(EventEnumDecl { attrs, name, events });
+        }
+        Ok(EventEnumInput { enums })
     }
 }
diff --git a/crates/ruma-events-macros/src/event_type.rs b/crates/ruma-events-macros/src/event_type.rs
new file mode 100644
index 00000000..028de31c
--- /dev/null
+++ b/crates/ruma-events-macros/src/event_type.rs
@@ -0,0 +1,176 @@
+use proc_macro2::{Span, TokenStream};
+use quote::{format_ident, quote};
+use syn::{Ident, LitStr};
+
+use crate::event_parse::{EventEnumEntry, EventEnumInput, EventKind};
+
+pub fn expand_event_type_enum(
+    input: EventEnumInput,
+    ruma_events: TokenStream,
+) -> syn::Result<TokenStream> {
+    let ruma_serde = quote! { #ruma_events::exports::ruma_serde };
+
+    let mut room: Vec<&Vec<EventEnumEntry>> = vec![];
+    let mut state: Vec<&Vec<EventEnumEntry>> = vec![];
+    let mut message: Vec<&Vec<EventEnumEntry>> = vec![];
+    let mut ephemeral: Vec<&Vec<EventEnumEntry>> = vec![];
+    let mut room_account: Vec<&Vec<EventEnumEntry>> = vec![];
+    let mut global_account: Vec<&Vec<EventEnumEntry>> = vec![];
+    let mut to_device: Vec<&Vec<EventEnumEntry>> = vec![];
+    for event in &input.enums {
+        match event.name {
+            EventKind::GlobalAccountData => global_account.push(&event.events),
+            EventKind::RoomAccountData => room_account.push(&event.events),
+            EventKind::Ephemeral => ephemeral.push(&event.events),
+            EventKind::Message => {
+                message.push(&event.events);
+                room.push(&event.events);
+            }
+            EventKind::State => {
+                state.push(&event.events);
+                room.push(&event.events);
+            }
+            EventKind::ToDevice => to_device.push(&event.events),
+            EventKind::Redaction => {}
+            EventKind::Presence => {}
+        }
+    }
+    let presence = vec![EventEnumEntry {
+        attrs: vec![],
+        ev_type: LitStr::new("m.presence", Span::call_site()),
+    }];
+    let mut all = input.enums.iter().map(|e| &e.events).collect::<Vec<_>>();
+    all.push(&presence);
+    let (all_event_types, all_str_ev_types) = generate_variants(&all)?;
+    let all =
+        generate_enum(format_ident!("EventType"), all_str_ev_types, all_event_types, &ruma_serde);
+
+    let (room_event_types, room_str_ev_types) = generate_variants(&room)?;
+    let room = generate_enum(
+        format_ident!("RoomEventType"),
+        room_str_ev_types,
+        room_event_types,
+        &ruma_serde,
+    );
+
+    let (state_event_types, state_str_ev_types) = generate_variants(&state)?;
+    let state = generate_enum(
+        format_ident!("StateEventType"),
+        state_str_ev_types,
+        state_event_types,
+        &ruma_serde,
+    );
+
+    let (message_event_types, message_str_ev_types) = generate_variants(&message)?;
+    let message = generate_enum(
+        format_ident!("MessageEventType"),
+        message_str_ev_types,
+        message_event_types,
+        &ruma_serde,
+    );
+
+    let (ephemeral_event_types, ephemeral_str_ev_types) = generate_variants(&ephemeral)?;
+    let ephemeral = generate_enum(
+        format_ident!("EphemeralRoomEventType"),
+        ephemeral_str_ev_types,
+        ephemeral_event_types,
+        &ruma_serde,
+    );
+
+    let (room_account_event_types, room_account_str_ev_types) = generate_variants(&room_account)?;
+    let room_account = generate_enum(
+        format_ident!("RoomAccountDataEventType"),
+        room_account_str_ev_types,
+        room_account_event_types,
+        &ruma_serde,
+    );
+
+    let (global_account_event_types, global_account_str_ev_types) =
+        generate_variants(&global_account)?;
+    let global_account = generate_enum(
+        format_ident!("GlobalAccountDataEventType"),
+        global_account_str_ev_types,
+        global_account_event_types,
+        &ruma_serde,
+    );
+
+    let (to_device_event_types, to_device_str_ev_types) = generate_variants(&to_device)?;
+    let to_device = generate_enum(
+        format_ident!("ToDeviceEventType"),
+        to_device_str_ev_types,
+        to_device_event_types,
+        &ruma_serde,
+    );
+
+    Ok(quote! {
+        #all
+        #room
+        #state
+        #message
+        #ephemeral
+        #room_account
+        #global_account
+        #to_device
+    })
+}
+
+fn generate_enum(
+    ident: Ident,
+    ev_type_strings: Vec<&LitStr>,
+    variants: Vec<TokenStream>,
+    ruma_serde: &TokenStream,
+) -> TokenStream {
+    let str_doc = format!("Creates a string slice from this `{}`.", ident);
+    let byte_doc = format!("Creates a byte slice from this `{}`.", ident);
+    let enum_doc = format!("The type of `{}` this is.", ident);
+    quote! {
+        #[doc = #enum_doc]
+        ///
+        /// This type can hold an arbitrary string. To check for events that are not available as a
+        /// documented variant here, use its string representation, obtained through `.as_str()`.
+        #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, #ruma_serde::StringEnum)]
+        pub enum #ident {
+            #(
+                #[doc = #ev_type_strings]
+                #[ruma_enum(rename = #ev_type_strings)]
+                #variants,
+            )*
+            #[doc(hidden)]
+            _Custom(String),
+        }
+
+        impl #ident {
+            #[doc = #str_doc]
+            pub fn as_str(&self) -> &str {
+                self.as_ref()
+            }
+
+            #[doc = #byte_doc]
+            pub fn as_bytes(&self) -> &[u8] {
+                self.as_str().as_bytes()
+            }
+        }
+    }
+}
+
+fn generate_variants<'a>(
+    input: &'a [&Vec<EventEnumEntry>],
+) -> syn::Result<(Vec<TokenStream>, Vec<&'a LitStr>)> {
+    let mut deduped: Vec<&EventEnumEntry> = vec![];
+    for item in input.iter().copied().flatten() {
+        if let Some(idx) = deduped.iter().position(|e| e.ev_type == item.ev_type) {
+            // If there is a variant without config attributes use that
+            if deduped[idx].attrs != item.attrs && item.attrs.is_empty() {
+                deduped[idx] = item;
+            }
+        } else {
+            deduped.push(item);
+        }
+    }
+    let event_types = deduped.iter().map(|e| &e.ev_type).collect();
+
+    let event_types_variants =
+        deduped.iter().map(|e| Ok(e.to_variant()?.decl())).collect::<syn::Result<Vec<_>>>()?;
+
+    Ok((event_types_variants, event_types))
+}
diff --git a/crates/ruma-events-macros/src/lib.rs b/crates/ruma-events-macros/src/lib.rs
index fc4fdf51..9fb87562 100644
--- a/crates/ruma-events-macros/src/lib.rs
+++ b/crates/ruma-events-macros/src/lib.rs
@@ -8,6 +8,7 @@
 
 #![warn(missing_docs)]
 
+use event_parse::EventEnumInput;
 use proc_macro::TokenStream;
 use proc_macro2 as pm2;
 use proc_macro_crate::{crate_name, FoundCrate};
@@ -16,13 +17,15 @@ use syn::{parse_macro_input, DeriveInput, Ident};
 
 use self::{
     event::expand_event, event_content::expand_event_content, event_enum::expand_event_enum,
-    event_parse::EventEnumInput,
+    event_type::expand_event_type_enum,
 };
 
 mod event;
 mod event_content;
 mod event_enum;
 mod event_parse;
+mod event_type;
+
 /// Generates an enum to represent the various Matrix event types.
 ///
 /// This macro also implements the necessary traits for the type to serialize and deserialize
@@ -35,20 +38,40 @@ mod event_parse;
 /// use ruma_events_macros::event_enum;
 ///
 /// event_enum! {
-///     kind: ToDevice,
-///     events: [
+///     enum ToDevice {
 ///         "m.any.event",
 ///         "m.other.event",
-///     ]
+///     }
+///
+///     enum State {
+///         "m.more.events",
+///         "m.different.event",
+///     }
 /// }
 /// ```
-/// (The argument to `kind` has to be a valid identifier for `<EventKind as Parse>::parse`)
+/// (The enum name has to be a valid identifier for `<EventKind as Parse>::parse`)
 //// TODO: Change above (`<EventKind as Parse>::parse`) to [] after fully qualified syntax is
 //// supported:  https://github.com/rust-lang/rust/issues/74563
 #[proc_macro]
 pub fn event_enum(input: TokenStream) -> TokenStream {
+    let ruma_events = import_ruma_events();
+
     let event_enum_input = syn::parse_macro_input!(input as EventEnumInput);
-    expand_event_enum(event_enum_input).unwrap_or_else(syn::Error::into_compile_error).into()
+    let enums = event_enum_input
+        .enums
+        .iter()
+        .map(expand_event_enum)
+        .collect::<syn::Result<pm2::TokenStream>>();
+    let event_types = expand_event_type_enum(event_enum_input, ruma_events);
+    event_types
+        .and_then(|types| {
+            enums.map(|mut enums| {
+                enums.extend(types);
+                enums
+            })
+        })
+        .unwrap_or_else(syn::Error::into_compile_error)
+        .into()
 }
 
 /// Generates an implementation of `ruma_events::EventContent`.
diff --git a/crates/ruma-events/src/enums.rs b/crates/ruma-events/src/enums.rs
index 8de18163..0db0f90c 100644
--- a/crates/ruma-events/src/enums.rs
+++ b/crates/ruma-events/src/enums.rs
@@ -6,36 +6,26 @@ use crate::{from_raw_json_value, EventDeHelper};
 
 event_enum! {
     /// Any global account data event.
-    kind: GlobalAccountData,
-    events: [
+    enum GlobalAccountData {
         "m.direct",
         "m.ignored_user_list",
         "m.push_rules",
-    ]
-}
+    }
 
-event_enum! {
     /// Any room account data event.
-    kind: RoomAccountData,
-    events: [
+    enum RoomAccountData {
         "m.fully_read",
         "m.tag",
-    ]
-}
+    }
 
-event_enum! {
     /// Any ephemeral room event.
-    kind: EphemeralRoom,
-    events: [
+    enum EphemeralRoom {
         "m.receipt",
         "m.typing",
-    ]
-}
+    }
 
-event_enum! {
     /// Any message event.
-    kind: Message,
-    events: [
+    enum Message {
         "m.call.answer",
         "m.call.invite",
         "m.call.hangup",
@@ -69,13 +59,11 @@ event_enum! {
         "m.room.message.feedback",
         "m.room.redaction",
         "m.sticker",
-    ]
-}
+    }
+
 
-event_enum! {
     /// Any state event.
-    kind: State,
-    events: [
+    enum State {
         "m.policy.rule.room",
         "m.policy.rule.server",
         "m.policy.rule.user",
@@ -95,13 +83,10 @@ event_enum! {
         "m.room.third_party_invite",
         "m.room.tombstone",
         "m.room.topic",
-    ]
-}
+    }
 
-event_enum! {
     /// Any to-device event.
-    kind: ToDevice,
-    events: [
+    enum ToDevice {
         "m.dummy",
         "m.room_key",
         "m.room_key_request",
@@ -119,7 +104,7 @@ event_enum! {
         #[cfg_attr(docsrs, doc(cfg(feature = "unstable-pre-spec")))]
         "m.key.verification.done",
         "m.room.encrypted",
-    ]
+    }
 }
 
 /// Any room event.
diff --git a/crates/ruma-events/src/event_type.rs b/crates/ruma-events/src/event_type.rs
deleted file mode 100644
index b2d75484..00000000
--- a/crates/ruma-events/src/event_type.rs
+++ /dev/null
@@ -1,278 +0,0 @@
-use ruma_serde::StringEnum;
-
-/// The type of an event.
-///
-/// This type can hold an arbitrary string. To check for events that are not available as a
-/// documented variant here, use its string representation, obtained through `.as_str()`.
-// FIXME: Add `m.foo.bar` or `m.foo_bar` as a naming scheme in StringEnum and remove most rename
-//        attributes.
-#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, StringEnum)]
-pub enum EventType {
-    /// m.call.answer
-    #[ruma_enum(rename = "m.call.answer")]
-    CallAnswer,
-
-    /// m.call.candidates
-    #[ruma_enum(rename = "m.call.candidates")]
-    CallCandidates,
-
-    /// m.call.hangup
-    #[ruma_enum(rename = "m.call.hangup")]
-    CallHangup,
-
-    /// m.call.invite
-    #[ruma_enum(rename = "m.call.invite")]
-    CallInvite,
-
-    /// m.direct
-    #[ruma_enum(rename = "m.direct")]
-    Direct,
-
-    /// m.dummy
-    #[ruma_enum(rename = "m.dummy")]
-    Dummy,
-
-    /// m.forwarded_room_key
-    #[ruma_enum(rename = "m.forwarded_room_key")]
-    ForwardedRoomKey,
-
-    /// m.fully_read
-    #[ruma_enum(rename = "m.fully_read")]
-    FullyRead,
-
-    /// m.key.verification.accept
-    #[ruma_enum(rename = "m.key.verification.accept")]
-    KeyVerificationAccept,
-
-    /// m.key.verification.cancel
-    #[ruma_enum(rename = "m.key.verification.cancel")]
-    KeyVerificationCancel,
-
-    /// m.key.verification.done
-    #[ruma_enum(rename = "m.key.verification.done")]
-    KeyVerificationDone,
-
-    /// m.key.verification.key
-    #[ruma_enum(rename = "m.key.verification.key")]
-    KeyVerificationKey,
-
-    /// m.key.verification.mac
-    #[ruma_enum(rename = "m.key.verification.mac")]
-    KeyVerificationMac,
-
-    /// m.key.verification.ready
-    #[ruma_enum(rename = "m.key.verification.ready")]
-    KeyVerificationReady,
-
-    /// m.key.verification.request
-    #[ruma_enum(rename = "m.key.verification.request")]
-    KeyVerificationRequest,
-
-    /// m.key.verification.start
-    #[ruma_enum(rename = "m.key.verification.start")]
-    KeyVerificationStart,
-
-    /// m.ignored_user_list
-    #[ruma_enum(rename = "m.ignored_user_list")]
-    IgnoredUserList,
-
-    /// m.policy.rule.room
-    #[ruma_enum(rename = "m.policy.rule.room")]
-    PolicyRuleRoom,
-
-    /// m.policy.rule.server
-    #[ruma_enum(rename = "m.policy.rule.server")]
-    PolicyRuleServer,
-
-    /// m.policy.rule.user
-    #[ruma_enum(rename = "m.policy.rule.user")]
-    PolicyRuleUser,
-
-    /// m.presence
-    #[ruma_enum(rename = "m.presence")]
-    Presence,
-
-    /// m.push_rules
-    #[ruma_enum(rename = "m.push_rules")]
-    PushRules,
-
-    /// m.receipt
-    #[ruma_enum(rename = "m.receipt")]
-    Receipt,
-
-    /// m.room.aliases
-    #[ruma_enum(rename = "m.room.aliases")]
-    RoomAliases,
-
-    /// m.room.avatar
-    #[ruma_enum(rename = "m.room.avatar")]
-    RoomAvatar,
-
-    /// m.room.canonical_alias
-    #[ruma_enum(rename = "m.room.canonical_alias")]
-    RoomCanonicalAlias,
-
-    /// m.room.create
-    #[ruma_enum(rename = "m.room.create")]
-    RoomCreate,
-
-    /// m.room.encrypted
-    #[ruma_enum(rename = "m.room.encrypted")]
-    RoomEncrypted,
-
-    /// m.room.encryption
-    #[ruma_enum(rename = "m.room.encryption")]
-    RoomEncryption,
-
-    /// m.room.guest_access
-    #[ruma_enum(rename = "m.room.guest_access")]
-    RoomGuestAccess,
-
-    /// m.room.history_visibility
-    #[ruma_enum(rename = "m.room.history_visibility")]
-    RoomHistoryVisibility,
-
-    /// m.room.join_rules
-    #[ruma_enum(rename = "m.room.join_rules")]
-    RoomJoinRules,
-
-    /// m.room.member
-    #[ruma_enum(rename = "m.room.member")]
-    RoomMember,
-
-    /// m.room.message
-    #[ruma_enum(rename = "m.room.message")]
-    RoomMessage,
-
-    /// m.room.message.feedback
-    #[ruma_enum(rename = "m.room.message.feedback")]
-    RoomMessageFeedback,
-
-    /// m.room.name
-    #[ruma_enum(rename = "m.room.name")]
-    RoomName,
-
-    /// m.room.pinned_events
-    #[ruma_enum(rename = "m.room.pinned_events")]
-    RoomPinnedEvents,
-
-    /// m.room.power_levels
-    #[ruma_enum(rename = "m.room.power_levels")]
-    RoomPowerLevels,
-
-    /// m.room.redaction
-    #[ruma_enum(rename = "m.room.redaction")]
-    RoomRedaction,
-
-    /// m.room.server_acl
-    #[ruma_enum(rename = "m.room.server_acl")]
-    RoomServerAcl,
-
-    /// m.room.third_party_invite
-    #[ruma_enum(rename = "m.room.third_party_invite")]
-    RoomThirdPartyInvite,
-
-    /// m.room.tombstone
-    #[ruma_enum(rename = "m.room.tombstone")]
-    RoomTombstone,
-
-    /// m.room.topic
-    #[ruma_enum(rename = "m.room.topic")]
-    RoomTopic,
-
-    /// m.room_key
-    #[ruma_enum(rename = "m.room_key")]
-    RoomKey,
-
-    /// m.room_key_request
-    #[ruma_enum(rename = "m.room_key_request")]
-    RoomKeyRequest,
-
-    /// m.sticker
-    #[ruma_enum(rename = "m.sticker")]
-    Sticker,
-
-    /// m.tag
-    #[ruma_enum(rename = "m.tag")]
-    Tag,
-
-    /// m.typing
-    #[ruma_enum(rename = "m.typing")]
-    Typing,
-
-    #[doc(hidden)]
-    _Custom(String),
-}
-
-impl EventType {
-    /// Creates a string slice from this `EventType`.
-    pub fn as_str(&self) -> &str {
-        self.as_ref()
-    }
-
-    /// Creates a byte slice from this `EventType`.
-    pub fn as_bytes(&self) -> &[u8] {
-        self.as_str().as_bytes()
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use ruma_serde::test::serde_json_eq;
-    use serde_json::json;
-
-    use super::EventType;
-
-    #[test]
-    fn serialize_and_deserialize_from_display_form() {
-        serde_json_eq(EventType::CallAnswer, json!("m.call.answer"));
-        serde_json_eq(EventType::CallCandidates, json!("m.call.candidates"));
-        serde_json_eq(EventType::CallHangup, json!("m.call.hangup"));
-        serde_json_eq(EventType::CallInvite, json!("m.call.invite"));
-        serde_json_eq(EventType::Direct, json!("m.direct"));
-        serde_json_eq(EventType::Dummy, json!("m.dummy"));
-        serde_json_eq(EventType::ForwardedRoomKey, json!("m.forwarded_room_key"));
-        serde_json_eq(EventType::FullyRead, json!("m.fully_read"));
-        serde_json_eq(EventType::KeyVerificationAccept, json!("m.key.verification.accept"));
-        serde_json_eq(EventType::KeyVerificationCancel, json!("m.key.verification.cancel"));
-        serde_json_eq(EventType::KeyVerificationDone, json!("m.key.verification.done"));
-        serde_json_eq(EventType::KeyVerificationKey, json!("m.key.verification.key"));
-        serde_json_eq(EventType::KeyVerificationMac, json!("m.key.verification.mac"));
-        serde_json_eq(EventType::KeyVerificationReady, json!("m.key.verification.ready"));
-        serde_json_eq(EventType::KeyVerificationRequest, json!("m.key.verification.request"));
-        serde_json_eq(EventType::KeyVerificationStart, json!("m.key.verification.start"));
-        serde_json_eq(EventType::IgnoredUserList, json!("m.ignored_user_list"));
-        serde_json_eq(EventType::PolicyRuleRoom, json!("m.policy.rule.room"));
-        serde_json_eq(EventType::PolicyRuleServer, json!("m.policy.rule.server"));
-        serde_json_eq(EventType::PolicyRuleUser, json!("m.policy.rule.user"));
-        serde_json_eq(EventType::Presence, json!("m.presence"));
-        serde_json_eq(EventType::PushRules, json!("m.push_rules"));
-        serde_json_eq(EventType::Receipt, json!("m.receipt"));
-        serde_json_eq(EventType::RoomAliases, json!("m.room.aliases"));
-        serde_json_eq(EventType::RoomAvatar, json!("m.room.avatar"));
-        serde_json_eq(EventType::RoomCanonicalAlias, json!("m.room.canonical_alias"));
-        serde_json_eq(EventType::RoomCreate, json!("m.room.create"));
-        serde_json_eq(EventType::RoomEncrypted, json!("m.room.encrypted"));
-        serde_json_eq(EventType::RoomEncryption, json!("m.room.encryption"));
-        serde_json_eq(EventType::RoomGuestAccess, json!("m.room.guest_access"));
-        serde_json_eq(EventType::RoomHistoryVisibility, json!("m.room.history_visibility"));
-        serde_json_eq(EventType::RoomJoinRules, json!("m.room.join_rules"));
-        serde_json_eq(EventType::RoomMember, json!("m.room.member"));
-        serde_json_eq(EventType::RoomMessage, json!("m.room.message"));
-        serde_json_eq(EventType::RoomMessageFeedback, json!("m.room.message.feedback"));
-        serde_json_eq(EventType::RoomName, json!("m.room.name"));
-        serde_json_eq(EventType::RoomPinnedEvents, json!("m.room.pinned_events"));
-        serde_json_eq(EventType::RoomPowerLevels, json!("m.room.power_levels"));
-        serde_json_eq(EventType::RoomRedaction, json!("m.room.redaction"));
-        serde_json_eq(EventType::RoomServerAcl, json!("m.room.server_acl"));
-        serde_json_eq(EventType::RoomThirdPartyInvite, json!("m.room.third_party_invite"));
-        serde_json_eq(EventType::RoomTombstone, json!("m.room.tombstone"));
-        serde_json_eq(EventType::RoomTopic, json!("m.room.topic"));
-        serde_json_eq(EventType::RoomKey, json!("m.room_key"));
-        serde_json_eq(EventType::RoomKeyRequest, json!("m.room_key_request"));
-        serde_json_eq(EventType::Sticker, json!("m.sticker"));
-        serde_json_eq(EventType::Tag, json!("m.tag"));
-        serde_json_eq(EventType::Typing, json!("m.typing"));
-        serde_json_eq(EventType::_Custom("io.ruma.test".into()), json!("io.ruma.test"));
-    }
-}
diff --git a/crates/ruma-events/src/lib.rs b/crates/ruma-events/src/lib.rs
index acbf8b8e..932e3a06 100644
--- a/crates/ruma-events/src/lib.rs
+++ b/crates/ruma-events/src/lib.rs
@@ -131,7 +131,6 @@ use self::room::redaction::{RedactionEvent, SyncRedactionEvent};
 mod enums;
 mod error;
 mod event_kinds;
-mod event_type;
 
 // Hack to allow both ruma-events itself and external crates (or tests) to use procedural macros
 // that expect `ruma_events` to exist in the prelude.
@@ -145,6 +144,7 @@ extern crate self as ruma_events;
 pub mod exports {
     pub use ruma_common;
     pub use ruma_identifiers;
+    pub use ruma_serde;
     pub use serde;
     pub use serde_json;
 }
@@ -194,7 +194,9 @@ pub use self::{
         AnyRoomAccountDataEvent, AnyRoomAccountDataEventContent, AnyRoomEvent, AnyStateEvent,
         AnyStateEventContent, AnyStrippedStateEvent, AnySyncEphemeralRoomEvent,
         AnySyncMessageEvent, AnySyncRoomEvent, AnySyncStateEvent, AnyToDeviceEvent,
-        AnyToDeviceEventContent,
+        AnyToDeviceEventContent, EphemeralRoomEventType, EventType, GlobalAccountDataEventType,
+        MessageEventType, RoomAccountDataEventType, RoomEventType, StateEventType,
+        ToDeviceEventType,
     },
     error::{FromStrError, InvalidInput},
     event_kinds::{
@@ -204,7 +206,6 @@ pub use self::{
         StrippedStateEvent, SyncEphemeralRoomEvent, SyncMessageEvent, SyncStateEvent,
         ToDeviceEvent,
     },
-    event_type::EventType,
 };
 
 /// Extra information about an event that is not incorporated into the event's


Test patch:
diff --git a/crates/ruma-events/tests/enums.rs b/crates/ruma-events/tests/enums.rs
index 494ccb03..94cc9eff 100644
--- a/crates/ruma-events/tests/enums.rs
+++ b/crates/ruma-events/tests/enums.rs
@@ -2,6 +2,7 @@ use js_int::uint;
 use matches::assert_matches;
 use ruma_common::MilliSecondsSinceUnixEpoch;
 use ruma_identifiers::{event_id, room_alias_id, room_id, user_id};
+use ruma_serde::test::serde_json_eq;
 use serde_json::{from_value as from_json_value, json, Value as JsonValue};
 
 use ruma_events::{
@@ -11,8 +12,9 @@ use ruma_events::{
         power_levels::PowerLevelsEventContent,
     },
     AnyEphemeralRoomEvent, AnyMessageEvent, AnyRoomEvent, AnyStateEvent, AnyStateEventContent,
-    AnySyncMessageEvent, AnySyncRoomEvent, AnySyncStateEvent, MessageEvent, StateEvent,
-    SyncMessageEvent, SyncStateEvent, Unsigned,
+    AnySyncMessageEvent, AnySyncRoomEvent, AnySyncStateEvent, EphemeralRoomEventType, EventType,
+    GlobalAccountDataEventType, MessageEvent, MessageEventType, RoomAccountDataEventType,
+    StateEvent, StateEventType, SyncMessageEvent, SyncStateEvent, ToDeviceEventType, Unsigned,
 };
 
 fn message_event() -> JsonValue {
@@ -316,3 +318,64 @@ fn ephemeral_event_deserialization() {
         if ephem.room_id() == &room_id!("!jEsUZKDJdhlrceRyVU:example.org")
     );
 }
+
+#[test]
+fn serialize_and_deserialize_from_display_form() {
+    serde_json_eq(EventType::CallAnswer, json!("m.call.answer"));
+    serde_json_eq(MessageEventType::CallAnswer, json!("m.call.answer"));
+    serde_json_eq(EventType::CallCandidates, json!("m.call.candidates"));
+    serde_json_eq(EventType::CallHangup, json!("m.call.hangup"));
+    serde_json_eq(EventType::CallInvite, json!("m.call.invite"));
+    serde_json_eq(EventType::Direct, json!("m.direct"));
+    serde_json_eq(GlobalAccountDataEventType::Direct, json!("m.direct"));
+    serde_json_eq(EventType::Dummy, json!("m.dummy"));
+    serde_json_eq(EventType::ForwardedRoomKey, json!("m.forwarded_room_key"));
+    serde_json_eq(EventType::FullyRead, json!("m.fully_read"));
+    serde_json_eq(RoomAccountDataEventType::FullyRead, json!("m.fully_read"));
+    serde_json_eq(EventType::KeyVerificationAccept, json!("m.key.verification.accept"));
+    serde_json_eq(EventType::KeyVerificationCancel, json!("m.key.verification.cancel"));
+    // m.key.verification.ready is unstable-pre-spec
+    // serde_json_eq(EventType::KeyVerificationDone, json!("m.key.verification.done"));
+    serde_json_eq(EventType::KeyVerificationKey, json!("m.key.verification.key"));
+    serde_json_eq(ToDeviceEventType::KeyVerificationKey, json!("m.key.verification.key"));
+    serde_json_eq(EventType::KeyVerificationMac, json!("m.key.verification.mac"));
+    // m.key.verification.ready is unstable-pre-spec
+    // serde_json_eq(EventType::KeyVerificationReady, json!("m.key.verification.ready"));
+    serde_json_eq(EventType::KeyVerificationRequest, json!("m.key.verification.request"));
+    serde_json_eq(EventType::KeyVerificationStart, json!("m.key.verification.start"));
+    serde_json_eq(EventType::IgnoredUserList, json!("m.ignored_user_list"));
+    serde_json_eq(EventType::PolicyRuleRoom, json!("m.policy.rule.room"));
+    serde_json_eq(EventType::PolicyRuleServer, json!("m.policy.rule.server"));
+    serde_json_eq(EventType::PolicyRuleUser, json!("m.policy.rule.user"));
+    serde_json_eq(EventType::Presence, json!("m.presence"));
+    serde_json_eq(EventType::PushRules, json!("m.push_rules"));
+    serde_json_eq(EventType::Receipt, json!("m.receipt"));
+    serde_json_eq(EventType::RoomAliases, json!("m.room.aliases"));
+    serde_json_eq(EventType::RoomAvatar, json!("m.room.avatar"));
+    serde_json_eq(EventType::RoomCanonicalAlias, json!("m.room.canonical_alias"));
+    serde_json_eq(EventType::RoomCreate, json!("m.room.create"));
+    serde_json_eq(StateEventType::RoomCreate, json!("m.room.create"));
+    serde_json_eq(EventType::RoomEncrypted, json!("m.room.encrypted"));
+    serde_json_eq(EventType::RoomEncryption, json!("m.room.encryption"));
+    serde_json_eq(EventType::RoomGuestAccess, json!("m.room.guest_access"));
+    serde_json_eq(EventType::RoomHistoryVisibility, json!("m.room.history_visibility"));
+    serde_json_eq(EventType::RoomJoinRules, json!("m.room.join_rules"));
+    serde_json_eq(EventType::RoomMember, json!("m.room.member"));
+    serde_json_eq(EventType::RoomMessage, json!("m.room.message"));
+    serde_json_eq(EventType::RoomMessageFeedback, json!("m.room.message.feedback"));
+    serde_json_eq(EventType::RoomName, json!("m.room.name"));
+    serde_json_eq(EventType::RoomPinnedEvents, json!("m.room.pinned_events"));
+    serde_json_eq(EventType::RoomPowerLevels, json!("m.room.power_levels"));
+    serde_json_eq(EventType::RoomRedaction, json!("m.room.redaction"));
+    serde_json_eq(EventType::RoomServerAcl, json!("m.room.server_acl"));
+    serde_json_eq(EventType::RoomThirdPartyInvite, json!("m.room.third_party_invite"));
+    serde_json_eq(EventType::RoomTombstone, json!("m.room.tombstone"));
+    serde_json_eq(EventType::RoomTopic, json!("m.room.topic"));
+    serde_json_eq(EventType::RoomKey, json!("m.room_key"));
+    serde_json_eq(EventType::RoomKeyRequest, json!("m.room_key_request"));
+    serde_json_eq(EventType::Sticker, json!("m.sticker"));
+    serde_json_eq(EventType::Tag, json!("m.tag"));
+    serde_json_eq(EventType::Typing, json!("m.typing"));
+    serde_json_eq(EphemeralRoomEventType::Typing, json!("m.typing"));
+    serde_json_eq(EventType::_Custom("io.ruma.test".into()), json!("io.ruma.test"));
+}
diff --git a/crates/ruma-events/tests/ui/07-enum-sanity-check.rs b/crates/ruma-events/tests/ui/07-enum-sanity-check.rs
index 83ac5a53..f32172fe 100644
--- a/crates/ruma-events/tests/ui/07-enum-sanity-check.rs
+++ b/crates/ruma-events/tests/ui/07-enum-sanity-check.rs
@@ -2,15 +2,14 @@ use ruma_events_macros::event_enum;
 
 event_enum! {
     /// Any global account data event.
-    kind: GlobalAccountData,
-    events: [
+    enum GlobalAccountData {
         "m.direct",
         #[cfg(test)]
         "m.ignored_user_list",
         "m.push_rules",
         #[cfg(any())]
         "m.ruma_test",
-    ]
+    }
 }
 
 fn main() {}
diff --git a/crates/ruma-events/tests/ui/08-enum-invalid-path.rs b/crates/ruma-events/tests/ui/08-enum-invalid-path.rs
index 2f6e1367..ff5d4829 100644
--- a/crates/ruma-events/tests/ui/08-enum-invalid-path.rs
+++ b/crates/ruma-events/tests/ui/08-enum-invalid-path.rs
@@ -1,17 +1,15 @@
 use ruma_events_macros::event_enum;
 
 event_enum! {
-    kind: State,
-    events: [
+    enum State {
         "m.not.a.path",
-    ]
+    }
 }
 
 event_enum! {
-    kind: State,
-    events: [
+    enum State {
         "not.a.path",
-    ]
+    }
 }
 
 fn main() {}
diff --git a/crates/ruma-events/tests/ui/08-enum-invalid-path.stderr b/crates/ruma-events/tests/ui/08-enum-invalid-path.stderr
index 48062907..6656a3d9 100644
--- a/crates/ruma-events/tests/ui/08-enum-invalid-path.stderr
+++ b/crates/ruma-events/tests/ui/08-enum-invalid-path.stderr
@@ -1,11 +1,11 @@
 error: well-known matrix events have to start with `m.` found `not.a.path`
-  --> $DIR/08-enum-invalid-path.rs:13:9
+  --> $DIR/08-enum-invalid-path.rs:11:9
    |
-13 |         "not.a.path",
+11 |         "not.a.path",
    |         ^^^^^^^^^^^^
 
 error[E0433]: failed to resolve: could not find `not` in `ruma_events`
- --> $DIR/08-enum-invalid-path.rs:6:9
+ --> $DIR/08-enum-invalid-path.rs:5:9
   |
-6 |         "m.not.a.path",
+5 |         "m.not.a.path",
   |         ^^^^^^^^^^^^^^ could not find `not` in `ruma_events`
diff --git a/crates/ruma-events/tests/ui/09-enum-invalid-kind.rs b/crates/ruma-events/tests/ui/09-enum-invalid-kind.rs
index f3597fe0..b21b75fe 100644
--- a/crates/ruma-events/tests/ui/09-enum-invalid-kind.rs
+++ b/crates/ruma-events/tests/ui/09-enum-invalid-kind.rs
@@ -1,14 +1,13 @@
 use ruma_events_macros::event_enum;
 
 event_enum! {
-    kind: NotReal,
-    events: [
+    enum NotReal {
         "m.direct",
         "m.dummy",
         "m.ignored_user_list",
         "m.push_rules",
         "m.room_key",
-    ]
+    }
 }
 
 fn main() {}
diff --git a/crates/ruma-events/tests/ui/09-enum-invalid-kind.stderr b/crates/ruma-events/tests/ui/09-enum-invalid-kind.stderr
index 4a1cc655..afeae5fa 100644
--- a/crates/ruma-events/tests/ui/09-enum-invalid-kind.stderr
+++ b/crates/ruma-events/tests/ui/09-enum-invalid-kind.stderr
@@ -1,5 +1,5 @@
 error: valid event kinds are GlobalAccountData, RoomAccountData, EphemeralRoom, Message, State, ToDevice found `NotReal`
- --> $DIR/09-enum-invalid-kind.rs:4:18
+ --> $DIR/09-enum-invalid-kind.rs:4:10
   |
-4 |     kind: NotReal,
-  |                  ^
+4 |     enum NotReal {
+  |          ^^^^^^^