diff --git a/ruma-events-macros/src/event.rs b/ruma-events-macros/src/event.rs index f1e84adf..9f631681 100644 --- a/ruma-events-macros/src/event.rs +++ b/ruma-events-macros/src/event.rs @@ -4,13 +4,13 @@ use proc_macro2::{Span, TokenStream}; use quote::quote; use syn::{Data, DataStruct, DeriveInput, Field, Fields, FieldsNamed, Ident}; -use crate::event_parse::{to_kind_variation, EventKindVariation}; +use crate::event_parse::{to_kind_variation, EventKind, EventKindVariation}; /// Derive `Event` macro code generation. pub fn expand_event(input: DeriveInput) -> syn::Result { let ident = &input.ident; - let (_kind, var) = to_kind_variation(ident).ok_or_else(|| { + let (kind, var) = to_kind_variation(ident).ok_or_else(|| { syn::Error::new(Span::call_site(), "not a valid ruma event struct identifier") })?; @@ -99,9 +99,13 @@ pub fn expand_event(input: DeriveInput) -> syn::Result { } }; - let deserialize_impl = expand_deserialize_event(input, &var, fields, is_generic)?; + let deserialize_impl = expand_deserialize_event(&input, &var, &fields, is_generic)?; + + let conversion_impl = expand_from_into(&input, &kind, &var, &fields); Ok(quote! { + #conversion_impl + #serialize_impl #deserialize_impl @@ -109,9 +113,9 @@ pub fn expand_event(input: DeriveInput) -> syn::Result { } fn expand_deserialize_event( - input: DeriveInput, + input: &DeriveInput, var: &EventKindVariation, - fields: Vec, + fields: &[Field], is_generic: bool, ) -> syn::Result { let ident = &input.ident; @@ -309,6 +313,58 @@ fn expand_deserialize_event( }) } +fn expand_from_into( + input: &DeriveInput, + kind: &EventKind, + var: &EventKindVariation, + fields: &[Field], +) -> Option { + let ident = &input.ident; + + let (impl_generics, ty_gen, where_clause) = input.generics.split_for_impl(); + + let fields = fields.iter().flat_map(|f| &f.ident).collect::>(); + + let fields_without_unsigned = + fields.iter().filter(|id| id.to_string().as_str() != "unsigned").collect::>(); + + let (into, into_full_event) = if var.is_redacted() { + (quote! { unsigned: unsigned.into(), }, quote! { unsigned: unsigned.into_full(room_id), }) + } else if kind == &EventKind::Ephemeral { + (TokenStream::new(), TokenStream::new()) + } else { + (quote! { unsigned, }, quote! { unsigned, }) + }; + + if let EventKindVariation::Sync | EventKindVariation::RedactedSync = var { + let full_struct = kind.to_event_ident(&var.to_full_variation()); + Some(quote! { + impl #impl_generics From<#full_struct #ty_gen> for #ident #ty_gen #where_clause { + fn from(event: #full_struct #ty_gen) -> Self { + let #full_struct { + #( #fields, )* .. + } = event; + Self { #( #fields_without_unsigned, )* #into } + } + } + + impl #impl_generics #ident #ty_gen #where_clause { + /// Convert this sync event into a full event, one with a room_id field. + pub fn into_full_event(self, room_id: ::ruma_identifiers::RoomId) -> #full_struct #ty_gen { + let Self { #( #fields, )* } = self; + #full_struct { + #( #fields_without_unsigned, )* + room_id: room_id.clone(), + #into_full_event + } + } + } + }) + } else { + None + } +} + /// CamelCase's a field ident like "foo_bar" to "FooBar". fn to_camel_case(name: &Ident) -> Ident { let span = name.span(); diff --git a/ruma-events-macros/src/event_enum.rs b/ruma-events-macros/src/event_enum.rs index 4eb5718f..ba76ceac 100644 --- a/ruma-events-macros/src/event_enum.rs +++ b/ruma-events-macros/src/event_enum.rs @@ -122,6 +122,8 @@ fn expand_any_with_deser( } }; + let event_enum_to_from_sync = expand_conversion_impl(kind, var, &variants); + let redacted_enum = expand_redacted_enum(kind, var); let field_accessor_impl = accessor_methods(kind, var, &variants); @@ -131,6 +133,8 @@ fn expand_any_with_deser( Some(quote! { #any_enum + #event_enum_to_from_sync + #field_accessor_impl #redact_impl @@ -141,6 +145,104 @@ fn expand_any_with_deser( }) } +fn expand_conversion_impl( + kind: &EventKind, + var: &EventKindVariation, + variants: &[Ident], +) -> Option { + let ident = kind.to_event_enum_ident(var)?; + let variants = &variants + .iter() + .filter(|id| { + // We filter this variant out only for non redacted events. + // The type of the struct held in the enum variant is different in this case + // so we construct the variant manually. + !(id.to_string().as_str() == "RoomRedaction" + && matches!(var, EventKindVariation::Full | EventKindVariation::Sync)) + }) + .collect::>(); + + match var { + EventKindVariation::Full | EventKindVariation::Redacted => { + // the opposite event variation full -> sync, redacted -> redacted sync + let variation = if var == &EventKindVariation::Full { + EventKindVariation::Sync + } else { + EventKindVariation::RedactedSync + }; + + let sync = kind.to_event_enum_ident(&variation)?; + let sync_struct = kind.to_event_ident(&variation)?; + + let redaction = if let (EventKind::Message, EventKindVariation::Full) = (kind, var) { + quote! { + #ident::RoomRedaction(event) => { + Self::RoomRedaction(::ruma_events::room::redaction::SyncRedactionEvent::from(event)) + }, + } + } else { + TokenStream::new() + }; + + Some(quote! { + impl From<#ident> for #sync { + fn from(event: #ident) -> Self { + match event { + #( + #ident::#variants(event) => { + Self::#variants(::ruma_events::#sync_struct::from(event)) + }, + )* + #redaction + #ident::Custom(event) => { + Self::Custom(::ruma_events::#sync_struct::from(event)) + }, + } + } + } + }) + } + EventKindVariation::Sync | EventKindVariation::RedactedSync => { + let variation = if var == &EventKindVariation::Sync { + EventKindVariation::Full + } else { + EventKindVariation::Redacted + }; + let full = kind.to_event_enum_ident(&variation)?; + + let redaction = if let (EventKind::Message, EventKindVariation::Sync) = (kind, var) { + quote! { + Self::RoomRedaction(event) => { + #full::RoomRedaction(event.into_full_event(room_id)) + }, + } + } else { + TokenStream::new() + }; + + Some(quote! { + impl #ident { + /// Convert this sync event into a full event, one with a room_id field. + pub fn into_full_event(self, room_id: ::ruma_identifiers::RoomId) -> #full { + match self { + #( + Self::#variants(event) => { + #full::#variants(event.into_full_event(room_id)) + }, + )* + #redaction + Self::Custom(event) => { + #full::Custom(event.into_full_event(room_id)) + }, + } + } + } + }) + } + _ => None, + } +} + /// Generates the 3 redacted state enums, 2 redacted message enums, /// and `Deserialize` implementations. /// diff --git a/ruma-events-macros/src/event_parse.rs b/ruma-events-macros/src/event_parse.rs index 5f7babc0..408cc929 100644 --- a/ruma-events-macros/src/event_parse.rs +++ b/ruma-events-macros/src/event_parse.rs @@ -2,7 +2,6 @@ use std::fmt; -use matches::matches; use proc_macro2::Span; use quote::format_ident; use syn::{ @@ -44,9 +43,21 @@ impl EventKindVariation { pub fn is_redacted(&self) -> bool { matches!(self, Self::Redacted | Self::RedactedSync | Self::RedactedStripped) } + + pub fn to_full_variation(&self) -> Self { + match self { + EventKindVariation::Redacted + | EventKindVariation::RedactedSync + | EventKindVariation::RedactedStripped => EventKindVariation::Redacted, + EventKindVariation::Full | EventKindVariation::Sync | EventKindVariation::Stripped => { + EventKindVariation::Full + } + } + } } // If the variants of this enum change `to_event_path` needs to be updated as well. +#[derive(Debug, Eq, PartialEq)] pub enum EventKind { Basic, Ephemeral, diff --git a/ruma-events/src/lib.rs b/ruma-events/src/lib.rs index a65fd4dc..01e2cb26 100644 --- a/ruma-events/src/lib.rs +++ b/ruma-events/src/lib.rs @@ -121,6 +121,7 @@ use std::fmt::Debug; use js_int::Int; use ruma_common::Raw; +use ruma_identifiers::RoomId; use serde::{ de::{self, IgnoredAny}, Deserialize, Serialize, @@ -246,6 +247,32 @@ pub struct RedactedSyncUnsigned { pub redacted_because: Option>, } +impl From for RedactedSyncUnsigned { + fn from(redacted: RedactedUnsigned) -> Self { + match redacted.redacted_because.map(|b| *b) { + Some(RedactionEvent { + sender, + event_id, + origin_server_ts, + redacts, + unsigned, + content, + .. + }) => Self { + redacted_because: Some(Box::new(SyncRedactionEvent { + sender, + event_id, + origin_server_ts, + redacts, + unsigned, + content, + })), + }, + _ => Self { redacted_because: None }, + } + } +} + impl RedactedSyncUnsigned { /// Whether this unsigned data is empty (`redacted_because` is `None`). /// @@ -256,6 +283,32 @@ impl RedactedSyncUnsigned { pub fn is_empty(&self) -> bool { self.redacted_because.is_none() } + + /// Convert a `RedactedSyncUnsigned` into `RedactedUnsigned`, converting the + /// underlying sync redaction event to a full redaction event (with room_id). + pub fn into_full(self, room_id: RoomId) -> RedactedUnsigned { + match self.redacted_because.map(|b| *b) { + Some(SyncRedactionEvent { + sender, + event_id, + origin_server_ts, + redacts, + unsigned, + content, + }) => RedactedUnsigned { + redacted_because: Some(Box::new(RedactionEvent { + room_id, + sender, + event_id, + origin_server_ts, + redacts, + unsigned, + content, + })), + }, + _ => RedactedUnsigned { redacted_because: None }, + } + } } /// The base trait that all event content types implement. diff --git a/ruma-events/tests/message_event.rs b/ruma-events/tests/message_event.rs index 760fce2a..c1113514 100644 --- a/ruma-events/tests/message_event.rs +++ b/ruma-events/tests/message_event.rs @@ -3,14 +3,14 @@ use std::{ time::{Duration, UNIX_EPOCH}, }; -use js_int::UInt; +use js_int::{uint, UInt}; use matches::assert_matches; use ruma_common::Raw; use ruma_events::{ call::{answer::AnswerEventContent, SessionDescription, SessionDescriptionType}, room::{ImageInfo, ThumbnailInfo}, sticker::StickerEventContent, - AnyMessageEventContent, MessageEvent, RawExt, Unsigned, + AnyMessageEventContent, AnySyncMessageEvent, MessageEvent, RawExt, Unsigned, }; use ruma_identifiers::{EventId, RoomId, UserId}; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; @@ -222,3 +222,58 @@ fn deserialize_message_sticker() { && unsigned.is_empty() ); } + +#[test] +fn deserialize_message_then_convert_to_full() { + let rid = RoomId::try_from("!roomid:room.com").unwrap(); + let json_data = json!({ + "content": { + "answer": { + "type": "answer", + "sdp": "Hello" + }, + "call_id": "foofoo", + "version": 1 + }, + "event_id": "$h29iv0s8:example.com", + "origin_server_ts": 1, + "sender": "@carl:example.com", + "type": "m.call.answer" + }); + + let sync_ev = + from_json_value::>(json_data).unwrap().deserialize().unwrap(); + + // Test conversion method + let full = sync_ev.into_full_event(rid); + let full_json = to_json_value(full).unwrap(); + + assert_matches!( + from_json_value::>>(full_json) + .unwrap() + .deserialize() + .unwrap(), + MessageEvent { + content: AnyMessageEventContent::CallAnswer(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!(1) + && event_id == "$h29iv0s8:example.com" + && origin_server_ts == UNIX_EPOCH + Duration::from_millis(1) + && room_id == "!roomid:room.com" + && sender == "@carl:example.com" + && unsigned.is_empty() + ); +} diff --git a/ruma-events/tests/state_event.rs b/ruma-events/tests/state_event.rs index 2858155b..7fce4db7 100644 --- a/ruma-events/tests/state_event.rs +++ b/ruma-events/tests/state_event.rs @@ -8,8 +8,8 @@ use matches::assert_matches; use ruma_common::Raw; use ruma_events::{ room::{aliases::AliasesEventContent, avatar::AvatarEventContent, ImageInfo, ThumbnailInfo}, - AnyRoomEvent, AnyStateEvent, AnyStateEventContent, RawExt, StateEvent, SyncStateEvent, - Unsigned, + AnyRoomEvent, AnyStateEvent, AnyStateEventContent, AnySyncStateEvent, RawExt, StateEvent, + SyncStateEvent, Unsigned, }; use ruma_identifiers::{EventId, RoomAliasId, RoomId, UserId}; use serde_json::{ @@ -134,6 +134,7 @@ fn deserialize_aliases_with_prev_content() { #[test] fn deserialize_aliases_sync_with_room_id() { + // The same JSON can be used to create a sync event, it just ignores the `room_id` field let json_data = aliases_event_with_prev_content(); assert_matches!( @@ -284,3 +285,36 @@ fn deserialize_member_event_with_top_level_membership_field() { && content.displayname == Some("example".into()) ); } + +#[test] +fn deserialize_full_event_convert_to_sync() { + let json_data = aliases_event_with_prev_content(); + + let full_ev = from_json_value::>(json_data).unwrap().deserialize().unwrap(); + + // Test conversion to sync event (without room_id field) + let sync: AnySyncStateEvent = full_ev.into(); + let sync_json = to_json_value(sync).unwrap(); + + assert_matches!( + from_json_value::>(sync_json) + .unwrap() + .deserialize() + .unwrap(), + AnySyncStateEvent::RoomAliases(SyncStateEvent { + content, + event_id, + origin_server_ts, + prev_content: Some(prev_content), + sender, + state_key, + unsigned, + }) if content.aliases == vec![RoomAliasId::try_from("#somewhere:localhost").unwrap()] + && event_id == "$h29iv0s8:example.com" + && origin_server_ts == UNIX_EPOCH + Duration::from_millis(1) + && prev_content.aliases == vec![RoomAliasId::try_from("#inner:localhost").unwrap()] + && sender == "@carl:example.com" + && state_key == "" + && unsigned.is_empty() + ); +} diff --git a/ruma-identifiers/src/room_version_id.rs b/ruma-identifiers/src/room_version_id.rs index 4a75c1f1..290996b5 100644 --- a/ruma-identifiers/src/room_version_id.rs +++ b/ruma-identifiers/src/room_version_id.rs @@ -385,8 +385,6 @@ mod tests { #[cfg(feature = "serde")] #[test] fn deserialize_official_room_id() { - use matches::assert_matches; - let deserialized = from_str::(r#""1""#).expect("Failed to convert RoomVersionId to JSON.");