Repository: ruma/ruma (rust)

Issue:
Add support for unstable types in EventContent
Currently it's not possible to have a `type` that can have both a stable and unstable value.

Me need similar attributes to serde:
- `rename` to the unstable name while the feature is unstable.
- `alias` to the unstable name once the feature is stable.

An example that needs this are polls ([MSC3381](https://github.com/matrix-org/matrix-spec-proposals/pull/3381)).


Pull request:
events: Add support for type aliases
Allows to use unstable types

Closes #1000

Patch:
diff --git a/.clippy.toml b/.clippy.toml
index 88520209..a1d46bbd 100644
--- a/.clippy.toml
+++ b/.clippy.toml
@@ -1,4 +1,4 @@
-msrv = "1.55"
+msrv = "1.59"
 avoid-breaking-exported-api = false
 disallowed-methods = [
     # https://github.com/serde-rs/json/issues/160
diff --git a/.github/workflows/msrv.yml b/.github/workflows/msrv.yml
index a049c9a5..502735f6 100644
--- a/.github/workflows/msrv.yml
+++ b/.github/workflows/msrv.yml
@@ -1,4 +1,4 @@
-name: Rust 1.55
+name: Rust 1.59
 
 env:
   CARGO_TERM_COLOR: always
@@ -35,11 +35,11 @@ jobs:
       - name: Checkout repo
         uses: actions/checkout@v2
 
-      - name: Install rust 1.55 toolchain
+      - name: Install rust 1.59 toolchain
         uses: actions-rs/toolchain@v1
         with:
           profile: minimal
-          toolchain: "1.55"
+          toolchain: "1.59"
 
       # Used to compile xtask
       - name: Install rust stable toolchain
@@ -54,7 +54,7 @@ jobs:
           # A stable compiler update should automatically not reuse old caches.
           # Add the MSRV as a stable cache key too so bumping it also gets us a
           # fresh cache.
-          sharedKey: msrv1.55
+          sharedKey: msrv1.59
 
       - name: Run checks
         uses: actions-rs/cargo@v1
diff --git a/README.md b/README.md
index 6583abe7..8530e0b1 100644
--- a/README.md
+++ b/README.md
@@ -53,7 +53,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).
 
 ## Minimum Rust version
 
-Ruma currently requires Rust 1.55. In general, we will never require beta or
+Ruma currently requires Rust 1.59. In general, we will never require beta or
 nightly for crates.io releases of our crates, and we will try to avoid releasing
 crates that depend on features that were only just stabilized.
 
diff --git a/crates/ruma-macros/src/events/event_content.rs b/crates/ruma-macros/src/events/event_content.rs
index 6370d5ec..2249762f 100644
--- a/crates/ruma-macros/src/events/event_content.rs
+++ b/crates/ruma-macros/src/events/event_content.rs
@@ -23,6 +23,8 @@ mod kw {
     syn::custom_keyword!(type_fragment);
     // The type to use for a state events' `state_key` field.
     syn::custom_keyword!(state_key_type);
+    // Another type string accepted for deserialization.
+    syn::custom_keyword!(alias);
 }
 
 /// Parses attributes for `*EventContent` derives.
@@ -47,6 +49,9 @@ enum EventMeta {
     TypeFragment,
 
     StateKeyType(Box<Type>),
+
+    /// Variant that holds alternate event type accepted for deserialization.
+    Alias(LitStr),
 }
 
 impl EventMeta {
@@ -70,6 +75,13 @@ impl EventMeta {
             _ => None,
         }
     }
+
+    fn get_alias(&self) -> Option<&LitStr> {
+        match self {
+            Self::Alias(t) => Some(t),
+            _ => None,
+        }
+    }
 }
 
 impl Parse for EventMeta {
@@ -96,6 +108,10 @@ impl Parse for EventMeta {
             let _: kw::state_key_type = input.parse()?;
             let _: Token![=] = input.parse()?;
             input.parse().map(EventMeta::StateKeyType)
+        } else if lookahead.peek(kw::alias) {
+            let _: kw::alias = input.parse()?;
+            let _: Token![=] = input.parse()?;
+            input.parse().map(EventMeta::Alias)
         } else {
             Err(lookahead.error())
         }
@@ -120,6 +136,10 @@ impl MetaAttrs {
     fn get_state_key_type(&self) -> Option<&Type> {
         self.0.iter().find_map(|a| a.get_state_key_type())
     }
+
+    fn get_aliases(&self) -> impl Iterator<Item = &LitStr> {
+        self.0.iter().filter_map(|a| a.get_alias())
+    }
 }
 
 impl Parse for MetaAttrs {
@@ -227,6 +247,16 @@ pub fn expand_event_content(
         ));
     }
 
+    let aliases: Vec<_> = content_attr.iter().flat_map(|attrs| attrs.get_aliases()).collect();
+    for alias in &aliases {
+        if alias.value().ends_with(".*") != prefix.is_some() {
+            return Err(syn::Error::new_spanned(
+                event_type,
+                "aliases should have the same `.*` suffix, or lack thereof, as the main event type",
+            ));
+        }
+    }
+
     // We only generate redacted content structs for state and message-like events
     let redacted_event_content = needs_redacted(&content_attr, event_kind).then(|| {
         generate_redacted_event_content(
@@ -235,6 +265,7 @@ pub fn expand_event_content(
             event_type,
             event_kind,
             state_key_type.as_ref(),
+            &aliases,
             ruma_common,
         )
         .unwrap_or_else(syn::Error::into_compile_error)
@@ -246,6 +277,7 @@ pub fn expand_event_content(
         event_type,
         event_kind,
         state_key_type.as_ref(),
+        &aliases,
         ruma_common,
     )
     .unwrap_or_else(syn::Error::into_compile_error);
@@ -270,6 +302,7 @@ fn generate_redacted_event_content<'a>(
     event_type: &LitStr,
     event_kind: Option<EventKind>,
     state_key_type: Option<&TokenStream>,
+    aliases: &[&LitStr],
     ruma_common: &TokenStream,
 ) -> syn::Result<TokenStream> {
     assert!(
@@ -354,6 +387,7 @@ fn generate_redacted_event_content<'a>(
         event_type,
         event_kind,
         state_key_type,
+        aliases,
         ruma_common,
     )
     .unwrap_or_else(syn::Error::into_compile_error);
@@ -362,6 +396,12 @@ fn generate_redacted_event_content<'a>(
         generate_static_event_content_impl(&redacted_ident, k, true, event_type, ruma_common)
     });
 
+    let mut event_types = aliases.to_owned();
+    event_types.push(event_type);
+    let event_types = quote! {
+        [#(#event_types,)*]
+    };
+
     Ok(quote! {
         // this is the non redacted event content's impl
         #[automatically_derived]
@@ -387,9 +427,9 @@ fn generate_redacted_event_content<'a>(
         #[automatically_derived]
         impl #ruma_common::events::RedactedEventContent for #redacted_ident {
             fn empty(ev_type: &str) -> #serde_json::Result<Self> {
-                if ev_type != #event_type {
+                if !#event_types.contains(&ev_type) {
                     return Err(#serde::de::Error::custom(
-                        format!("expected event type `{}`, found `{}`", #event_type, ev_type)
+                        format!("expected event type as one of `{:?}`, found `{}`", #event_types, ev_type)
                     ));
                 }
 
@@ -476,6 +516,7 @@ fn generate_event_content_impl<'a>(
     event_type: &LitStr,
     event_kind: Option<EventKind>,
     state_key_type: Option<&TokenStream>,
+    aliases: &[&'a LitStr],
     ruma_common: &TokenStream,
 ) -> syn::Result<TokenStream> {
     let serde = quote! { #ruma_common::exports::serde };
@@ -565,24 +606,41 @@ fn generate_event_content_impl<'a>(
         }
     });
 
-    let from_parts_fn_impl = if let Some((type_prefix, type_fragment_field)) = &type_suffix_data {
+    let event_types = aliases.iter().chain([&event_type]);
+
+    let from_parts_fn_impl = if let Some((_, type_fragment_field)) = &type_suffix_data {
+        let type_prefixes = event_types.map(|ev_type| {
+            ev_type
+                .value()
+                .strip_suffix('*')
+                .expect("aliases have already been checked to have the same suffix")
+                .to_owned()
+        });
+        let type_prefixes = quote! {
+            [#(#type_prefixes,)*]
+        };
+
         quote! {
-            if let Some(type_fragment) = ev_type.strip_prefix(#type_prefix) {
+            if let Some(type_fragment) = #type_prefixes.iter().find_map(|prefix| ev_type.strip_prefix(prefix)) {
                 let mut content: Self = #serde_json::from_str(content.get())?;
                 content.#type_fragment_field = type_fragment.to_owned();
 
                 ::std::result::Result::Ok(content)
             } else {
                 ::std::result::Result::Err(#serde::de::Error::custom(
-                    ::std::format!("expected event type starting with `{}`, found `{}`", #type_prefix, ev_type)
+                    ::std::format!("expected event type starting with one of `{:?}`, found `{}`", #type_prefixes, ev_type)
                 ))
             }
         }
     } else {
+        let event_types = quote! {
+            [#(#event_types,)*]
+        };
+
         quote! {
-            if ev_type != #event_type {
+            if !#event_types.contains(&ev_type) {
                 return ::std::result::Result::Err(#serde::de::Error::custom(
-                    ::std::format!("expected event type `{}`, found `{}`", #event_type, ev_type)
+                    ::std::format!("expected event type as one of `{:?}`, found `{}`", #event_types, ev_type)
                 ));
             }
 
diff --git a/crates/ruma-macros/src/events/event_enum.rs b/crates/ruma-macros/src/events/event_enum.rs
index 65dcb15e..37c3c48b 100644
--- a/crates/ruma-macros/src/events/event_enum.rs
+++ b/crates/ruma-macros/src/events/event_enum.rs
@@ -1,6 +1,6 @@
 //! Implementation of event enum and event content enum macros.
 
-use std::fmt;
+use std::{fmt, iter::zip};
 
 use proc_macro2::{Span, TokenStream};
 use quote::{format_ident, quote, IdentFragment, ToTokens};
@@ -49,24 +49,24 @@ pub fn expand_event_enums(input: &EventEnumDecl) -> syn::Result<TokenStream> {
 
     let kind = input.kind;
     let attrs = &input.attrs;
-    let events: Vec<_> =
-        input.events.iter().map(|entry| (entry.ev_type.clone(), entry.ev_path.clone())).collect();
+    let docs: Vec<_> = input.events.iter().map(EventEnumEntry::docs).collect::<syn::Result<_>>()?;
     let variants: Vec<_> =
         input.events.iter().map(EventEnumEntry::to_variant).collect::<syn::Result<_>>()?;
 
-    let events = &events;
+    let events = &input.events;
+    let docs = &docs;
     let variants = &variants;
     let ruma_common = &ruma_common;
 
-    res.extend(expand_content_enum(kind, events, attrs, variants, ruma_common));
+    res.extend(expand_content_enum(kind, events, docs, attrs, variants, ruma_common));
     res.extend(
-        expand_event_enum(kind, V::None, events, attrs, variants, ruma_common)
+        expand_event_enum(kind, V::None, events, docs, attrs, variants, ruma_common)
             .unwrap_or_else(syn::Error::into_compile_error),
     );
 
     if matches!(kind, EventKind::MessageLike | EventKind::State) {
         res.extend(
-            expand_event_enum(kind, V::Sync, events, attrs, variants, ruma_common)
+            expand_event_enum(kind, V::Sync, events, docs, attrs, variants, ruma_common)
                 .unwrap_or_else(syn::Error::into_compile_error),
         );
         res.extend(
@@ -89,18 +89,18 @@ pub fn expand_event_enums(input: &EventEnumDecl) -> syn::Result<TokenStream> {
 
     if matches!(kind, EventKind::Ephemeral) {
         res.extend(
-            expand_event_enum(kind, V::Sync, events, attrs, variants, ruma_common)
+            expand_event_enum(kind, V::Sync, events, docs, attrs, variants, ruma_common)
                 .unwrap_or_else(syn::Error::into_compile_error),
         );
     }
 
     if matches!(kind, EventKind::State) {
         res.extend(
-            expand_event_enum(kind, V::Stripped, events, attrs, variants, ruma_common)
+            expand_event_enum(kind, V::Stripped, events, docs, attrs, variants, ruma_common)
                 .unwrap_or_else(syn::Error::into_compile_error),
         );
         res.extend(
-            expand_event_enum(kind, V::Initial, events, attrs, variants, ruma_common)
+            expand_event_enum(kind, V::Initial, events, docs, attrs, variants, ruma_common)
                 .unwrap_or_else(syn::Error::into_compile_error),
         );
     }
@@ -111,7 +111,8 @@ pub fn expand_event_enums(input: &EventEnumDecl) -> syn::Result<TokenStream> {
 fn expand_event_enum(
     kind: EventKind,
     var: EventEnumVariation,
-    events: &[(LitStr, Path)],
+    events: &[EventEnumEntry],
+    docs: &[TokenStream],
     attrs: &[Attribute],
     variants: &[EventEnumVariant],
     ruma_common: &TokenStream,
@@ -120,13 +121,18 @@ fn expand_event_enum(
     let ident = kind.to_event_enum_ident(var.into())?;
 
     let variant_decls = variants.iter().map(|v| v.decl());
-    let content: Vec<_> =
-        events.iter().map(|(name, path)| to_event_path(name, path, kind, var)).collect();
-    let event_types: Vec<_> = events.iter().map(|(name, _)| name).collect();
+    let content: Vec<_> = events
+        .iter()
+        .map(|event| {
+            event
+                .stable_name()
+                .map(|stable_name| to_event_path(stable_name, &event.ev_path, kind, var))
+        })
+        .collect::<syn::Result<_>>()?;
 
     let custom_ty = format_ident!("Custom{}Content", kind);
 
-    let deserialize_impl = expand_deserialize_impl(kind, var, events, variants, ruma_common)?;
+    let deserialize_impl = expand_deserialize_impl(kind, var, events, ruma_common)?;
     let field_accessor_impl = expand_accessor_methods(kind, var, variants, ruma_common)?;
     let from_impl = expand_from_impl(&ident, &content, variants);
 
@@ -137,7 +143,7 @@ fn expand_event_enum(
         #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
         pub enum #ident {
             #(
-                #[doc = #event_types]
+                #docs
                 #variant_decls(#content),
             )*
             /// An event not defined by the Matrix specification
@@ -156,8 +162,7 @@ fn expand_event_enum(
 fn expand_deserialize_impl(
     kind: EventKind,
     var: EventEnumVariation,
-    events: &[(LitStr, Path)],
-    variants: &[EventEnumVariant],
+    events: &[EventEnumEntry],
     ruma_common: &TokenStream,
 ) -> syn::Result<TokenStream> {
     let serde = quote! { #ruma_common::exports::serde };
@@ -165,13 +170,27 @@ fn expand_deserialize_impl(
 
     let ident = kind.to_event_enum_ident(var.into())?;
 
-    let variant_attrs = variants.iter().map(|v| {
-        let attrs = &v.attrs;
-        quote! { #(#attrs)* }
-    });
-    let self_variants = variants.iter().map(|v| v.ctor(quote! { Self }));
-    let content = events.iter().map(|(name, path)| to_event_path(name, path, kind, var));
-    let event_types: Vec<_> = events.iter().map(|(name, _)| name).collect();
+    let match_arms: TokenStream = events
+        .iter()
+        .map(|event| {
+            let variant = event.to_variant()?;
+            let variant_attrs = {
+                let attrs = &variant.attrs;
+                quote! { #(#attrs)* }
+            };
+            let self_variant = variant.ctor(quote! { Self });
+            let content = to_event_path(event.stable_name()?, &event.ev_path, kind, var);
+            let ev_types = event.aliases.iter().chain([&event.ev_type]);
+
+            Ok(quote! {
+                #variant_attrs #(#ev_types)|* => {
+                    let event = #serde_json::from_str::<#content>(json.get())
+                        .map_err(D::Error::custom)?;
+                    Ok(#self_variant(event))
+                },
+            })
+        })
+        .collect::<syn::Result<_>>()?;
 
     Ok(quote! {
         #[allow(unused_qualifications)]
@@ -187,13 +206,7 @@ fn expand_deserialize_impl(
                     #ruma_common::serde::from_raw_json_value(&json)?;
 
                 match &*ev_type {
-                    #(
-                        #variant_attrs #event_types => {
-                            let event = #serde_json::from_str::<#content>(json.get())
-                                .map_err(D::Error::custom)?;
-                            Ok(#self_variants(event))
-                        },
-                    )*
+                    #match_arms
                     _ => {
                         let event = #serde_json::from_str(json.get()).map_err(D::Error::custom)?;
                         Ok(Self::_Custom(event))
@@ -293,11 +306,12 @@ fn expand_into_full_event(
 /// Create a content enum from `EventEnumInput`.
 fn expand_content_enum(
     kind: EventKind,
-    events: &[(LitStr, Path)],
+    events: &[EventEnumEntry],
+    docs: &[TokenStream],
     attrs: &[Attribute],
     variants: &[EventEnumVariant],
     ruma_common: &TokenStream,
-) -> TokenStream {
+) -> syn::Result<TokenStream> {
     let serde = quote! { #ruma_common::exports::serde };
     let serde_json = quote! { #ruma_common::exports::serde_json };
 
@@ -305,31 +319,53 @@ fn expand_content_enum(
 
     let event_type_enum = kind.to_event_type_enum();
 
-    let content: Vec<_> =
-        events.iter().map(|(name, path)| to_event_content_path(kind, name, path, None)).collect();
-    let event_type_match_arms = events.iter().map(|(s, _)| {
-        if let Some(prefix) = s.value().strip_suffix(".*") {
-            quote! { _s if _s.starts_with(#prefix) }
-        } else {
-            quote! { #s }
-        }
-    });
-    let event_types: Vec<_> = events.iter().map(|(name, _)| name).collect();
+    let content: Vec<_> = events
+        .iter()
+        .map(|event| {
+            let stable_name = event.stable_name()?;
+            Ok(to_event_content_path(kind, stable_name, &event.ev_path, None))
+        })
+        .collect::<syn::Result<_>>()?;
+    let event_type_match_arms: TokenStream = zip(zip(events, variants), &content)
+        .map(|((event, variant), ev_content)| {
+            let variant_attrs = {
+                let attrs = &variant.attrs;
+                quote! { #(#attrs)* }
+            };
+            let variant_ctor = variant.ctor(quote! { Self });
+
+            let ev_types = event.aliases.iter().chain([&event.ev_type]);
+            let ev_types = if event.ev_type.value().ends_with(".*") {
+                let ev_types = ev_types.map(|ev_type| {
+                    ev_type
+                        .value()
+                        .strip_suffix(".*")
+                        .expect("aliases have already been checked to have the same suffix")
+                        .to_owned()
+                });
+                quote! { _s if #(_s.starts_with(#ev_types))||* }
+            } else {
+                quote! { #(#ev_types)|* }
+            };
+
+            Ok(quote! {
+                #variant_attrs #ev_types => {
+                    let content = #ev_content::from_parts(event_type, input)?;
+                    ::std::result::Result::Ok(#variant_ctor(content))
+                },
+            })
+        })
+        .collect::<syn::Result<_>>()?;
 
     let variant_decls = variants.iter().map(|v| v.decl()).collect::<Vec<_>>();
-    let variant_attrs = variants.iter().map(|v| {
-        let attrs = &v.attrs;
-        quote! { #(#attrs)* }
-    });
     let variant_arms = variants.iter().map(|v| v.match_arm(quote! { Self })).collect::<Vec<_>>();
-    let variant_ctors = variants.iter().map(|v| v.ctor(quote! { Self }));
 
     let from_impl = expand_from_impl(&ident, &content, variants);
 
     let serialize_custom_event_error_path =
         quote! { #ruma_common::events::serialize_custom_event_error }.to_string();
 
-    quote! {
+    Ok(quote! {
         #( #attrs )*
         #[derive(Clone, Debug, #serde::Serialize)]
         #[serde(untagged)]
@@ -337,7 +373,7 @@ fn expand_content_enum(
         #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
         pub enum #ident {
             #(
-                #[doc = #event_types]
+                #docs
                 #variant_decls(#content),
             )*
             #[doc(hidden)]
@@ -363,12 +399,7 @@ fn expand_content_enum(
                 input: &#serde_json::value::RawValue,
             ) -> #serde_json::Result<Self> {
                 match event_type {
-                    #(
-                        #variant_attrs #event_type_match_arms => {
-                            let content = #content::from_parts(event_type, input)?;
-                            ::std::result::Result::Ok(#variant_ctors(content))
-                        }
-                    )*
+                    #event_type_match_arms
                     ty => {
                         ::std::result::Result::Ok(Self::_Custom {
                             event_type: crate::PrivOwnedStr(ty.into()),
@@ -379,7 +410,7 @@ fn expand_content_enum(
         }
 
         #from_impl
-    }
+    })
 }
 
 fn expand_redact(
@@ -654,10 +685,75 @@ impl EventEnumEntry {
 
     pub(crate) fn to_variant(&self) -> syn::Result<EventEnumVariant> {
         let attrs = self.attrs.clone();
-        let ident = m_prefix_name_to_type_name(&self.ev_type)?;
+        let ident = m_prefix_name_to_type_name(self.stable_name()?)?;
 
         Ok(EventEnumVariant { attrs, ident })
     }
+
+    pub(crate) fn stable_name(&self) -> syn::Result<&LitStr> {
+        if self.ev_type.value().starts_with("m.") {
+            Ok(&self.ev_type)
+        } else {
+            self.aliases.iter().find(|alias| alias.value().starts_with("m.")).ok_or_else(|| {
+                syn::Error::new(
+                    Span::call_site(),
+                    format!(
+                        "A matrix event must declare a well-known type that starts with `m.` \
+                        either as the main type or as an alias, found `{}`",
+                        self.ev_type.value()
+                    ),
+                )
+            })
+        }
+    }
+
+    pub(crate) fn docs(&self) -> syn::Result<TokenStream> {
+        let stable_name = self.stable_name()?;
+
+        let mut doc = quote! {
+            #[doc = #stable_name]
+        };
+
+        if self.ev_type != *stable_name {
+            let unstable_name =
+                format!("This variant uses the unstable type `{}`.", self.ev_type.value());
+
+            doc.extend(quote! {
+                #[doc = ""]
+                #[doc = #unstable_name]
+            });
+        }
+
+        match self.aliases.len() {
+            0 => {}
+            1 => {
+                let alias = format!(
+                    "This variant can also be deserialized from the `{}` type.",
+                    self.aliases[0].value()
+                );
+                doc.extend(quote! {
+                    #[doc = ""]
+                    #[doc = #alias]
+                });
+            }
+            _ => {
+                let aliases = format!(
+                    "This variant can also be deserialized from the following types: {}.",
+                    self.aliases
+                        .iter()
+                        .map(|alias| format!("`{}`", alias.value()))
+                        .collect::<Vec<_>>()
+                        .join(", ")
+                );
+                doc.extend(quote! {
+                    #[doc = ""]
+                    #[doc = #aliases]
+                });
+            }
+        }
+
+        Ok(doc)
+    }
 }
 
 pub(crate) fn expand_from_impls_derived(input: DeriveInput) -> TokenStream {
diff --git a/crates/ruma-macros/src/events/event_parse.rs b/crates/ruma-macros/src/events/event_parse.rs
index 4304219e..3ce278a0 100644
--- a/crates/ruma-macros/src/events/event_parse.rs
+++ b/crates/ruma-macros/src/events/event_parse.rs
@@ -7,6 +7,7 @@ use quote::{format_ident, IdentFragment};
 use syn::{
     braced,
     parse::{self, Parse, ParseStream},
+    punctuated::Punctuated,
     Attribute, Ident, LitStr, Path, Token,
 };
 
@@ -14,6 +15,7 @@ use syn::{
 mod kw {
     syn::custom_keyword!(kind);
     syn::custom_keyword!(events);
+    syn::custom_keyword!(alias);
 }
 
 // If the variants of this enum change `to_event_path` needs to be updated as well.
@@ -230,18 +232,41 @@ pub fn to_kind_variation(ident: &Ident) -> Option<(EventKind, EventKindVariation
 
 pub struct EventEnumEntry {
     pub attrs: Vec<Attribute>,
+    pub aliases: Vec<LitStr>,
     pub ev_type: LitStr,
     pub ev_path: Path,
 }
 
 impl Parse for EventEnumEntry {
     fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
-        let attrs = input.call(Attribute::parse_outer)?;
+        let (ruma_enum_attrs, attrs) = input
+            .call(Attribute::parse_outer)?
+            .into_iter()
+            .partition::<Vec<_>, _>(|attr| attr.path.is_ident("ruma_enum"));
         let ev_type: LitStr = input.parse()?;
         let _: Token![=>] = input.parse()?;
         let ev_path = input.call(Path::parse_mod_style)?;
+        let has_suffix = ev_type.value().ends_with(".*");
+
+        let mut aliases = Vec::with_capacity(ruma_enum_attrs.len());
+        for attr_list in ruma_enum_attrs {
+            for alias_attr in attr_list
+                .parse_args_with(Punctuated::<EventEnumAliasAttr, Token![,]>::parse_terminated)?
+            {
+                let alias = alias_attr.into_inner();
+
+                if alias.value().ends_with(".*") == has_suffix {
+                    aliases.push(alias)
+                } else {
+                    return Err(syn::Error::new_spanned(
+                        &attr_list,
+                        "aliases should have the same `.*` suffix, or lack thereof, as the main event type",
+                    ));
+                }
+            }
+        }
 
-        Ok(Self { attrs, ev_type, ev_path })
+        Ok(Self { attrs, aliases, ev_type, ev_path })
     }
 }
 
@@ -285,3 +310,20 @@ impl Parse for EventEnumInput {
         Ok(EventEnumInput { enums })
     }
 }
+
+pub struct EventEnumAliasAttr(LitStr);
+
+impl EventEnumAliasAttr {
+    pub fn into_inner(self) -> LitStr {
+        self.0
+    }
+}
+
+impl Parse for EventEnumAliasAttr {
+    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
+        let _: kw::alias = input.parse()?;
+        let _: Token![=] = input.parse()?;
+        let s: LitStr = input.parse()?;
+        Ok(Self(s))
+    }
+}
diff --git a/crates/ruma-macros/src/events/event_type.rs b/crates/ruma-macros/src/events/event_type.rs
index 056aed7c..baf81126 100644
--- a/crates/ruma-macros/src/events/event_type.rs
+++ b/crates/ruma-macros/src/events/event_type.rs
@@ -37,6 +37,7 @@ pub fn expand_event_type_enum(
     }
     let presence = vec![EventEnumEntry {
         attrs: vec![],
+        aliases: vec![],
         ev_type: LitStr::new("m.presence", Span::call_site()),
         ev_path: parse_quote! { #ruma_common::events::presence },
     }];
@@ -139,28 +140,32 @@ fn generate_enum(
         })
         .collect::<syn::Result<_>>()?;
 
-    let from_str_match_arms: Vec<_> = deduped
-        .iter()
-        .map(|e| {
-            let v = e.to_variant()?;
-            let ctor = v.ctor(quote! { Self });
+    let mut from_str_match_arms = TokenStream::new();
+    for event in &deduped {
+        let v = event.to_variant()?;
+        let ctor = v.ctor(quote! { Self });
+        let ev_types = event.aliases.iter().chain([&event.ev_type]);
+        let attrs = &event.attrs;
+
+        if event.ev_type.value().ends_with(".*") {
+            for ev_type in ev_types {
+                let name = ev_type.value();
+                let prefix = name
+                    .strip_suffix('*')
+                    .expect("aliases have already been checked to have the same suffix");
 
-            let match_arm = if let Some(prefix) = e.ev_type.value().strip_suffix('*') {
-                quote! {
+                from_str_match_arms.extend(quote! {
+                    #(#attrs)*
                     // Use if-let guard once available
                     _s if _s.starts_with(#prefix) => {
                         #ctor(::std::convert::From::from(_s.strip_prefix(#prefix).unwrap()))
                     }
-                }
-            } else {
-                let t = &e.ev_type;
-                quote! { #t => #ctor }
-            };
-
-            let attrs = &e.attrs;
-            Ok(quote! { #(#attrs)* #match_arm })
-        })
-        .collect::<syn::Result<_>>()?;
+                })
+            }
+        } else {
+            from_str_match_arms.extend(quote! { #(#attrs)* #(#ev_types)|* => #ctor, })
+        }
+    }
 
     let from_ident_for_room = if ident == "StateEventType" || ident == "MessageLikeEventType" {
         let match_arms: Vec<_> = deduped
@@ -232,7 +237,7 @@ fn generate_enum(
         impl ::std::convert::From<&::std::primitive::str> for #ident {
             fn from(s: &::std::primitive::str) -> Self {
                 match s {
-                    #(#from_str_match_arms,)*
+                    #from_str_match_arms
                     _ => Self::_Custom(crate::PrivOwnedStr(::std::convert::From::from(s))),
                 }
             }
diff --git a/crates/ruma-state-res/.clippy.toml b/crates/ruma-state-res/.clippy.toml
index 962cf70b..807a7017 100644
--- a/crates/ruma-state-res/.clippy.toml
+++ b/crates/ruma-state-res/.clippy.toml
@@ -1,4 +1,4 @@
-msrv = "1.55"
+msrv = "1.59"
 avoid-breaking-exported-api = false
 disallowed-methods = [
     # https://github.com/serde-rs/json/issues/160
diff --git a/xtask/src/ci.rs b/xtask/src/ci.rs
index e17bc0f9..37908682 100644
--- a/xtask/src/ci.rs
+++ b/xtask/src/ci.rs
@@ -9,7 +9,7 @@ mod spec_links;
 
 use spec_links::check_spec_links;
 
-const MSRV: &str = "1.55";
+const MSRV: &str = "1.59";
 const NIGHTLY: &str = "nightly";
 
 #[derive(Args)]


Test patch:
diff --git a/crates/ruma-common/tests/events/ui/03-invalid-event-type.stderr b/crates/ruma-common/tests/events/ui/03-invalid-event-type.stderr
index d8fc4543..af86cee6 100644
--- a/crates/ruma-common/tests/events/ui/03-invalid-event-type.stderr
+++ b/crates/ruma-common/tests/events/ui/03-invalid-event-type.stderr
@@ -6,7 +6,7 @@ error: no event type attribute found, add `#[ruma_event(type = "any.room.event",
   |
   = note: this error originates in the derive macro `EventContent` (in Nightly builds, run with -Z macro-backtrace for more info)
 
-error: expected one of: `type`, `kind`, `skip_redaction`, `custom_redacted`, `type_fragment`, `state_key_type`
+error: expected one of: `type`, `kind`, `skip_redaction`, `custom_redacted`, `type_fragment`, `state_key_type`, `alias`
   --> tests/events/ui/03-invalid-event-type.rs:11:14
    |
 11 | #[ruma_event(event = "m.macro.test", kind = State)]
diff --git a/crates/ruma-common/tests/events/ui/07-enum-sanity-check.rs b/crates/ruma-common/tests/events/ui/07-enum-sanity-check.rs
index ef939cac..31e63c9a 100644
--- a/crates/ruma-common/tests/events/ui/07-enum-sanity-check.rs
+++ b/crates/ruma-common/tests/events/ui/07-enum-sanity-check.rs
@@ -4,7 +4,10 @@ use ruma_macros::event_enum;
 event_enum! {
     /// Any global account data event.
     enum GlobalAccountData {
+        #[ruma_enum(alias = "io.ruma.direct")]
         "m.direct" => events::direct,
+        #[ruma_enum(alias = "m.identity_server")]
+        "io.ruma.identity_server" => events::identity_server,
         #[cfg(test)]
         "m.ignored_user_list" => events::ignored_user_list,
         // Doesn't actually have a wildcard, but this should work as a wildcard test
@@ -14,7 +17,24 @@ event_enum! {
     }
 }
 
-fn main() {}
+fn main() {
+    assert_eq!(GlobalAccountDataEventType::from("m.direct"), GlobalAccountDataEventType::Direct);
+    assert_eq!(
+        GlobalAccountDataEventType::from("io.ruma.direct"),
+        GlobalAccountDataEventType::Direct
+    );
+    assert_eq!(GlobalAccountDataEventType::Direct.to_cow_str(), "m.direct");
+
+    assert_eq!(
+        GlobalAccountDataEventType::from("m.identity_server"),
+        GlobalAccountDataEventType::IdentityServer
+    );
+    assert_eq!(
+        GlobalAccountDataEventType::from("io.ruma.identity_server"),
+        GlobalAccountDataEventType::IdentityServer
+    );
+    assert_eq!(GlobalAccountDataEventType::IdentityServer.to_cow_str(), "io.ruma.identity_server");
+}
 
 #[doc(hidden)]
 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]