From 800fba7c3217f95bd628e15e229291012cb4d748 Mon Sep 17 00:00:00 2001 From: "Ragotzy.devin" Date: Sun, 7 Jun 2020 16:56:43 -0400 Subject: [PATCH] Implement PresenceEvent and EphemeralEvent * Add derive for PresenceEventContent and create struct AnyPresenceEventContent since there is only one content type * Add derive for Ephemeral event and create enum AnyEphemeralEventContent, convert receipt and typing to use derive(EphemeralEventContent) over ruma_api! --- ruma-events-macros/src/content_enum.rs | 3 + ruma-events-macros/src/event.rs | 133 ++++++++++++++---------- ruma-events-macros/src/event_content.rs | 43 ++++++-- ruma-events-macros/src/lib.rs | 23 +++- src/content_enums.rs | 6 ++ src/event_kinds.rs | 23 ++-- src/lib.rs | 9 +- src/presence.rs | 81 ++++++++------- src/receipt.rs | 32 ++---- src/typing.rs | 26 ++--- tests/ephemeral_event.rs | 133 ++++++++++++++++++++++++ 11 files changed, 362 insertions(+), 150 deletions(-) create mode 100644 tests/ephemeral_event.rs diff --git a/ruma-events-macros/src/content_enum.rs b/ruma-events-macros/src/content_enum.rs index 38fa50a7..eb4312e4 100644 --- a/ruma-events-macros/src/content_enum.rs +++ b/ruma-events-macros/src/content_enum.rs @@ -16,6 +16,9 @@ fn marker_traits(ident: &Ident) -> TokenStream { impl ::ruma_events::RoomEventContent for #ident {} impl ::ruma_events::MessageEventContent for #ident {} }, + "AnyEphemeralRoomEventContent" => quote! { + impl ::ruma_events::EphemeralRoomEventContent for #ident {} + }, _ => TokenStream::new(), } } diff --git a/ruma-events-macros/src/event.rs b/ruma-events-macros/src/event.rs index cea46d25..007ab319 100644 --- a/ruma-events-macros/src/event.rs +++ b/ruma-events-macros/src/event.rs @@ -7,6 +7,9 @@ use syn::{Data, DataStruct, DeriveInput, Field, Fields, FieldsNamed, Ident}; /// Derive `Event` macro code generation. pub fn expand_event(input: DeriveInput) -> syn::Result { let ident = &input.ident; + let (impl_gen, ty_gen, where_clause) = input.generics.split_for_impl(); + let is_presence_event = ident == "PresenceEvent"; + let fields = if let Data::Struct(DataStruct { fields, .. }) = input.data.clone() { if let Fields::Named(FieldsNamed { named, .. }) = fields { if !named.iter().any(|f| f.ident.as_ref().unwrap() == "content") { @@ -30,8 +33,6 @@ pub fn expand_event(input: DeriveInput) -> syn::Result { )); }; - let content_trait = Ident::new(&format!("{}Content", ident), input.ident.span()); - let serialize_fields = fields .iter() .map(|field| { @@ -66,20 +67,25 @@ pub fn expand_event(input: DeriveInput) -> syn::Result { }) .collect::>(); + let event_ty = if is_presence_event { + quote! { + "m.presence"; + } + } else { + quote! { self.content.event_type(); } + }; + let serialize_impl = quote! { - impl ::serde::ser::Serialize for #ident - where - C: ::ruma_events::#content_trait, - { + impl #impl_gen ::serde::ser::Serialize for #ident #ty_gen #where_clause { fn serialize(&self, serializer: S) -> Result where S: ::serde::ser::Serializer, { use ::serde::ser::SerializeStruct as _; - let event_type = self.content.event_type(); + let event_type = #event_ty; - let mut state = serializer.serialize_struct("StateEvent", 7)?; + let mut state = serializer.serialize_struct(stringify!(#ident), 7)?; state.serialize_field("type", event_type)?; #( #serialize_fields )* @@ -88,7 +94,7 @@ pub fn expand_event(input: DeriveInput) -> syn::Result { } }; - let deserialize_impl = expand_deserialize_event(&input, fields)?; + let deserialize_impl = expand_deserialize_event(is_presence_event, input, fields)?; Ok(quote! { #serialize_impl @@ -97,9 +103,14 @@ pub fn expand_event(input: DeriveInput) -> syn::Result { }) } -fn expand_deserialize_event(input: &DeriveInput, fields: Vec) -> syn::Result { +fn expand_deserialize_event( + is_presence_event: bool, + input: DeriveInput, + fields: Vec, +) -> syn::Result { let ident = &input.ident; let content_ident = Ident::new(&format!("{}Content", ident), input.ident.span()); + let (impl_generics, ty_gen, where_clause) = input.generics.split_for_impl(); let enum_variants = fields .iter() @@ -115,7 +126,11 @@ fn expand_deserialize_event(input: &DeriveInput, fields: Vec) -> syn::Res let name = field.ident.as_ref().unwrap(); let ty = &field.ty; if name == "content" || name == "prev_content" { - quote! { Box<::serde_json::value::RawValue> } + if is_presence_event { + quote! { #content_ident } + } else { + quote! { Box<::serde_json::value::RawValue> } + } } else if name == "origin_server_ts" { quote! { ::js_int::UInt } } else { @@ -125,50 +140,65 @@ fn expand_deserialize_event(input: &DeriveInput, fields: Vec) -> syn::Res .collect::>(); let ok_or_else_fields = fields - .iter() - .map(|field| { - let name = field.ident.as_ref().unwrap(); - if name == "content" { + .iter() + .map(|field| { + let name = field.ident.as_ref().unwrap(); + if name == "content" { + if is_presence_event { + quote! { + let content = content.ok_or_else(|| ::serde::de::Error::missing_field("content"))?; + } + } else { quote! { let json = content.ok_or_else(|| ::serde::de::Error::missing_field("content"))?; let content = C::from_parts(&event_type, json).map_err(A::Error::custom)?; } - } else if name == "prev_content" { - quote! { - let prev_content = if let Some(json) = prev_content { - Some(C::from_parts(&event_type, json).map_err(A::Error::custom)?) - } else { - None - }; - } - } else if name == "origin_server_ts" { - quote! { - let origin_server_ts = origin_server_ts - .map(|time| { - let t = time.into(); - ::std::time::UNIX_EPOCH + ::std::time::Duration::from_millis(t) - }) - .ok_or_else(|| ::serde::de::Error::missing_field("origin_server_ts"))?; - } - } else if name == "unsigned" { - quote! { let unsigned = unsigned.unwrap_or_default(); } - } else { - quote! { - let #name = #name.ok_or_else(|| { - ::serde::de::Error::missing_field(stringify!(#name)) - })?; - } } - }) - .collect::>(); + } else if name == "prev_content" { + quote! { + let prev_content = if let Some(json) = prev_content { + Some(C::from_parts(&event_type, json).map_err(A::Error::custom)?) + } else { + None + }; + } + } else if name == "origin_server_ts" { + quote! { + let origin_server_ts = origin_server_ts + .map(|time| { + let t = time.into(); + ::std::time::UNIX_EPOCH + ::std::time::Duration::from_millis(t) + }) + .ok_or_else(|| ::serde::de::Error::missing_field("origin_server_ts"))?; + } + } else if name == "unsigned" { + quote! { let unsigned = unsigned.unwrap_or_default(); } + } else { + quote! { + let #name = #name.ok_or_else(|| { + ::serde::de::Error::missing_field(stringify!(#name)) + })?; + } + } + }) + .collect::>(); let field_names = fields.iter().flat_map(|f| &f.ident).collect::>(); + let deserialize_impl_gen = if is_presence_event { + quote! { <'de> } + } else { + let gen = &input.generics.params; + quote! { <'de, #gen> } + }; + let deserialize_phantom_type = if is_presence_event { + quote! {} + } else { + quote! { ::std::marker::PhantomData } + }; + Ok(quote! { - impl<'de, C> ::serde::de::Deserialize<'de> for #ident - where - C: ::ruma_events::#content_ident, - { + impl #deserialize_impl_gen ::serde::de::Deserialize<'de> for #ident #ty_gen #where_clause { fn deserialize(deserializer: D) -> Result where D: ::serde::de::Deserializer<'de>, @@ -183,13 +213,10 @@ fn expand_deserialize_event(input: &DeriveInput, fields: Vec) -> syn::Res /// Visits the fields of an event struct to handle deserialization of /// the `content` and `prev_content` fields. - struct EventVisitor(::std::marker::PhantomData); + struct EventVisitor #impl_generics (#deserialize_phantom_type #ty_gen); - impl<'de, C> ::serde::de::Visitor<'de> for EventVisitor - where - C: ::ruma_events::#content_ident, - { - type Value = #ident; + impl #deserialize_impl_gen ::serde::de::Visitor<'de> for EventVisitor #ty_gen #where_clause { + type Value = #ident #ty_gen; fn expecting(&self, formatter: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { write!(formatter, "struct implementing {}", stringify!(#content_ident)) @@ -232,7 +259,7 @@ fn expand_deserialize_event(input: &DeriveInput, fields: Vec) -> syn::Res } } - deserializer.deserialize_map(EventVisitor(::std::marker::PhantomData)) + deserializer.deserialize_map(EventVisitor(#deserialize_phantom_type)) } } }) diff --git a/ruma-events-macros/src/event_content.rs b/ruma-events-macros/src/event_content.rs index 9c261fe3..f48405e9 100644 --- a/ruma-events-macros/src/event_content.rs +++ b/ruma-events-macros/src/event_content.rs @@ -23,10 +23,7 @@ impl Parse for EventMeta { } } -/// Create a `RoomEventContent` implementation for a struct. -/// -/// This is used internally for code sharing as `RoomEventContent` is not derivable. -fn expand_room_event_content(input: DeriveInput) -> syn::Result { +fn expand_event_content(input: DeriveInput) -> syn::Result { let ident = &input.ident; let event_type_attr = input @@ -47,7 +44,7 @@ fn expand_room_event_content(input: DeriveInput) -> syn::Result { lit }; - let event_content_impl = quote! { + Ok(quote! { impl ::ruma_events::EventContent for #ident { fn event_type(&self) -> &str { #event_type @@ -64,7 +61,15 @@ fn expand_room_event_content(input: DeriveInput) -> syn::Result { ::serde_json::from_str(content.get()).map_err(|e| e.to_string()) } } - }; + }) +} + +/// Create a `RoomEventContent` implementation for a struct. +/// +/// This is used internally for code sharing as `RoomEventContent` is not derivable. +fn expand_room_event_content(input: DeriveInput) -> syn::Result { + let ident = input.ident.clone(); + let event_content_impl = expand_event_content(input)?; Ok(quote! { #event_content_impl @@ -85,7 +90,7 @@ pub fn expand_message_event_content(input: DeriveInput) -> syn::Result syn::Result { let ident = input.ident.clone(); let room_ev_content = expand_room_event_content(input)?; @@ -96,3 +101,27 @@ pub fn expand_state_event_content(input: DeriveInput) -> syn::Result syn::Result { + let ident = input.ident.clone(); + let event_content_impl = expand_event_content(input)?; + + Ok(quote! { + #event_content_impl + + impl ::ruma_events::PresenceEventContent for #ident { } + }) +} + +/// Create a `EphemeralRoomEventContent` implementation for a struct +pub fn expand_ephemeral_event_content(input: DeriveInput) -> syn::Result { + let ident = input.ident.clone(); + let event_content_impl = expand_event_content(input)?; + + Ok(quote! { + #event_content_impl + + impl ::ruma_events::EphemeralRoomEventContent for #ident { } + }) +} diff --git a/ruma-events-macros/src/lib.rs b/ruma-events-macros/src/lib.rs index ede5d4c0..5df6c8d4 100644 --- a/ruma-events-macros/src/lib.rs +++ b/ruma-events-macros/src/lib.rs @@ -17,7 +17,10 @@ use syn::{parse_macro_input, DeriveInput}; use self::{ content_enum::{expand_content_enum, parse::ContentEnumInput}, event::expand_event, - event_content::{expand_message_event_content, expand_state_event_content}, + event_content::{ + expand_ephemeral_event_content, expand_message_event_content, + expand_presence_event_content, expand_state_event_content, + }, gen::RumaEvent, parse::RumaEventInput, }; @@ -152,6 +155,24 @@ pub fn derive_state_event_content(input: TokenStream) -> TokenStream { .into() } +/// Generates an implementation of `ruma_events::PresenceEventContent` and it's super traits. +#[proc_macro_derive(PresenceEventContent, attributes(ruma_event))] +pub fn derive_presence_event_content(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + expand_presence_event_content(input) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +/// Generates an implementation of `ruma_events::EphemeralRoomEventContent` and it's super traits. +#[proc_macro_derive(EphemeralRoomEventContent, attributes(ruma_event))] +pub fn derive_ephemeral_event_content(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + expand_ephemeral_event_content(input) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + /// Generates implementations needed to serialize and deserialize Matrix events. #[proc_macro_derive(Event, attributes(ruma_event))] pub fn derive_state_event(input: TokenStream) -> TokenStream { diff --git a/src/content_enums.rs b/src/content_enums.rs index 9197ef44..8181962c 100644 --- a/src/content_enums.rs +++ b/src/content_enums.rs @@ -34,3 +34,9 @@ event_content_enum! { "m.room.topic", ] } + +event_content_enum! { + /// An ephemeral room event. + name: AnyEphemeralRoomEventContent, + events: [ "m.typing", "m.receipt" ] +} diff --git a/src/event_kinds.rs b/src/event_kinds.rs index 17362874..dc93294e 100644 --- a/src/event_kinds.rs +++ b/src/event_kinds.rs @@ -1,18 +1,11 @@ -use std::{ - convert::TryFrom, - time::{SystemTime, UNIX_EPOCH}, -}; +use std::{convert::TryFrom, time::SystemTime}; -use js_int::UInt; use ruma_events_macros::Event; use ruma_identifiers::{EventId, RoomId, UserId}; -use serde::{ - ser::{Error, SerializeStruct}, - Serialize, Serializer, -}; +use serde::ser::Error; use crate::{ - BasicEventContent, MessageEventContent, RoomEventContent, StateEventContent, + BasicEventContent, EphemeralRoomEventContent, MessageEventContent, StateEventContent, ToDeviceEventContent, UnsignedData, }; @@ -22,6 +15,16 @@ pub struct BasicEvent { pub content: C, } +/// Ephemeral room event. +#[derive(Clone, Debug, Event)] +pub struct EphemeralRoomEvent { + /// Data specific to the event type. + pub content: C, + + /// The ID of the room associated with this event. + pub room_id: RoomId, +} + /// Message event. #[derive(Clone, Debug, Event)] pub struct MessageEvent { diff --git a/src/lib.rs b/src/lib.rs index 587d914c..bd91867d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -151,7 +151,7 @@ pub mod forwarded_room_key; pub mod fully_read; // pub mod ignored_user_list; pub mod key; -// pub mod presence; +pub mod presence; // pub mod push_rules; pub mod receipt; pub mod room; @@ -165,10 +165,10 @@ pub mod typing; pub use self::{ algorithm::Algorithm, - content_enums::{AnyMessageEventContent, AnyStateEventContent}, + content_enums::{AnyEphemeralRoomEventContent, AnyMessageEventContent, AnyStateEventContent}, error::{FromStrError, InvalidEvent, InvalidInput}, event_enums::AnyStateEvent, - event_kinds::{MessageEvent, StateEvent}, + event_kinds::{EphemeralRoomEvent, MessageEvent, StateEvent}, event_type::EventType, json::EventJson, }; @@ -217,6 +217,9 @@ pub trait EventContent: Sized + Serialize { fn from_parts(event_type: &str, content: Box) -> Result; } +/// Marker trait for the content of an ephemeral room event. +pub trait EphemeralRoomEventContent: EventContent {} + /// Marker trait for the content of a basic event. pub trait BasicEventContent: EventContent {} diff --git a/src/presence.rs b/src/presence.rs index ab4f1ee6..0256c537 100644 --- a/src/presence.rs +++ b/src/presence.rs @@ -1,46 +1,53 @@ -//! Types for the *m.presence* event. +//! A presence event is represented by a parameterized struct. +//! +//! There is only one type that will satisfy the bounds of `PresenceEventContent` +//! as this event has only one possible content value according to Matrix spec. use js_int::UInt; -use ruma_events_macros::ruma_event; +pub use ruma_common::presence::PresenceState; +use ruma_events_macros::Event; use ruma_identifiers::UserId; +use serde::{Deserialize, Serialize}; -ruma_event! { - /// Informs the client of a user's presence state change. - PresenceEvent { - kind: Event, - event_type: "m.presence", - fields: { - /// The unique identifier for the user associated with this event. - pub sender: UserId, - }, - content: { - /// The current avatar URL for this user. - #[serde(skip_serializing_if = "Option::is_none")] - pub avatar_url: Option, +/// Presence event. +#[derive(Clone, Debug, Event)] +pub struct PresenceEvent { + /// Data specific to the event type. + pub content: PresenceEventContent, - /// Whether or not the user is currently active. - #[serde(skip_serializing_if = "Option::is_none")] - pub currently_active: Option, - - /// The current display name for this user. - #[serde(skip_serializing_if = "Option::is_none")] - pub displayname: Option, - - /// The last time since this user performed some action, in milliseconds. - #[serde(skip_serializing_if = "Option::is_none")] - pub last_active_ago: Option, - - /// The presence state for this user. - pub presence: PresenceState, - - /// An optional description to accompany the presence. - #[serde(skip_serializing_if = "Option::is_none")] - pub status_msg: Option, - }, - } + /// Contains the fully-qualified ID of the user who sent this event. + pub sender: UserId, } -pub use ruma_common::presence::PresenceState; +/// Informs the room of members presence. +/// +/// This is the only event content a `PresenceEvent` can contain as it's +/// `content` field. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct PresenceEventContent { + /// The current avatar URL for this user. + #[serde(skip_serializing_if = "Option::is_none")] + pub avatar_url: Option, + + /// Whether or not the user is currently active. + #[serde(skip_serializing_if = "Option::is_none")] + pub currently_active: Option, + + /// The current display name for this user. + #[serde(skip_serializing_if = "Option::is_none")] + pub displayname: Option, + + /// The last time since this user performed some action, in milliseconds. + #[serde(skip_serializing_if = "Option::is_none")] + pub last_active_ago: Option, + + /// The presence state for this user. + pub presence: PresenceState, + + /// An optional description to accompany the presence. + #[serde(skip_serializing_if = "Option::is_none")] + pub status_msg: Option, +} #[cfg(test)] mod tests { @@ -51,7 +58,7 @@ mod tests { use ruma_identifiers::UserId; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; - use super::{PresenceEventContent, PresenceState}; + use super::{PresenceEvent, PresenceEventContent, PresenceState}; use crate::EventJson; #[test] diff --git a/src/receipt.rs b/src/receipt.rs index f18cd8cd..a4d81507 100644 --- a/src/receipt.rs +++ b/src/receipt.rs @@ -2,30 +2,20 @@ use std::{collections::BTreeMap, time::SystemTime}; -use ruma_events_macros::ruma_event; +use ruma_events_macros::EphemeralRoomEventContent; use ruma_identifiers::{EventId, RoomId, UserId}; use serde::{Deserialize, Serialize}; -ruma_event! { - /// Informs the client of new receipts. - ReceiptEvent { - kind: Event, - event_type: "m.receipt", - fields: { - /// The unique identifier for the room associated with this event. - /// - /// `None` if the room is known through other means (such as this even being part of an - /// event list scoped to a room in a `/sync` response) - pub room_id: Option, - }, - content_type_alias: { - /// The payload for `ReceiptEvent`. - /// - /// A mapping of event ID to a collection of receipts for this event ID. The event ID is the ID of - /// the event being acknowledged and *not* an ID for the receipt itself. - BTreeMap - }, - } +/// Informs the client who has read a message specified by it's event id. +#[derive(Clone, Debug, Deserialize, Serialize, EphemeralRoomEventContent)] +#[ruma_event(type = "m.receipt")] +#[serde(transparent)] +pub struct ReceiptEventContent { + /// The payload for `ReceiptEvent`. + /// + /// A mapping of event ID to a collection of receipts for this event ID. The event ID is the ID of + /// the event being acknowledged and *not* an ID for the receipt itself. + pub receipts: BTreeMap, } /// A collection of receipts. diff --git a/src/typing.rs b/src/typing.rs index 7cd627d8..455c0c4c 100644 --- a/src/typing.rs +++ b/src/typing.rs @@ -1,23 +1,13 @@ //! Types for the *m.typing* event. -use ruma_events_macros::ruma_event; +use ruma_events_macros::EphemeralRoomEventContent; use ruma_identifiers::{RoomId, UserId}; +use serde::{Deserialize, Serialize}; -ruma_event! { - /// Informs the client of the list of users currently typing. - TypingEvent { - kind: Event, - event_type: "m.typing", - fields: { - /// The unique identifier for the room associated with this event. - /// - /// `None` if the room is known through other means (such as this even being part of an - /// event list scoped to a room in a `/sync` response) - pub room_id: Option, - }, - content: { - /// The list of user IDs typing in this room, if any. - pub user_ids: Vec, - }, - } +/// Informs the client who is currently typing in a given room. +#[derive(Clone, Debug, Deserialize, Serialize, EphemeralRoomEventContent)] +#[ruma_event(type = "m.typing")] +pub struct TypingEventContent { + /// The list of user IDs typing in this room, if any. + pub user_ids: Vec, } diff --git a/tests/ephemeral_event.rs b/tests/ephemeral_event.rs new file mode 100644 index 00000000..73a2cf8a --- /dev/null +++ b/tests/ephemeral_event.rs @@ -0,0 +1,133 @@ +use std::{ + convert::TryFrom, + time::{Duration, UNIX_EPOCH}, +}; + +use maplit::btreemap; +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::{ + receipt::{Receipt, ReceiptEventContent, Receipts}, + typing::TypingEventContent, + AnyEphemeralRoomEventContent, EphemeralRoomEvent, EventJson, +}; + +#[test] +fn ephemeral_serialize_typing() { + let aliases_event = EphemeralRoomEvent { + content: AnyEphemeralRoomEventContent::Typing(TypingEventContent { + user_ids: vec![UserId::try_from("@carl:example.com").unwrap()], + }), + room_id: RoomId::try_from("!roomid:room.com").unwrap(), + }; + + let actual = to_json_value(&aliases_event).unwrap(); + let expected = json!({ + "content": { + "user_ids": [ "@carl:example.com" ] + }, + "room_id": "!roomid:room.com", + "type": "m.typing", + }); + + assert_eq!(actual, expected); +} + +#[test] +fn deserialize_ephemeral_typing() { + let json_data = json!({ + "content": { + "user_ids": [ "@carl:example.com" ] + }, + "room_id": "!roomid:room.com", + "type": "m.typing" + }); + + assert_matches!( + from_json_value::>>(json_data) + .unwrap() + .deserialize() + .unwrap(), + EphemeralRoomEvent { + content: AnyEphemeralRoomEventContent::Typing(TypingEventContent { + user_ids, + }), + room_id, + } if user_ids[0] == UserId::try_from("@carl:example.com").unwrap() + && room_id == RoomId::try_from("!roomid:room.com").unwrap() + ); +} + +#[test] +fn ephemeral_serialize_receipt() { + let event_id = EventId::try_from("$h29iv0s8:example.com").unwrap(); + let user_id = UserId::try_from("@carl:example.com").unwrap(); + + let aliases_event = EphemeralRoomEvent { + content: AnyEphemeralRoomEventContent::Receipt(ReceiptEventContent { + receipts: btreemap! { + event_id => Receipts { + read: Some(btreemap! { + user_id => Receipt { ts: Some(UNIX_EPOCH + Duration::from_millis(1)) }, + }), + }, + }, + }), + room_id: RoomId::try_from("!roomid:room.com").unwrap(), + }; + + let actual = to_json_value(&aliases_event).unwrap(); + let expected = json!({ + "content": { + "$h29iv0s8:example.com": { + "m.read": { + "@carl:example.com": { "ts": 1 } + } + } + }, + "room_id": "!roomid:room.com", + "type": "m.receipt" + }); + + assert_eq!(actual, expected); +} + +#[test] +fn deserialize_ephemeral_receipt() { + let event_id = EventId::try_from("$h29iv0s8:example.com").unwrap(); + let user_id = UserId::try_from("@carl:example.com").unwrap(); + + let json_data = json!({ + "content": { + "$h29iv0s8:example.com": { + "m.read": { + "@carl:example.com": { "ts": 1 } + } + } + }, + "room_id": "!roomid:room.com", + "type": "m.receipt" + }); + + assert_matches!( + from_json_value::>>(json_data) + .unwrap() + .deserialize() + .unwrap(), + EphemeralRoomEvent { + content: AnyEphemeralRoomEventContent::Receipt(ReceiptEventContent { + receipts, + }), + room_id, + } if !receipts.is_empty() && receipts.contains_key(&event_id) + && room_id == RoomId::try_from("!roomid:room.com").unwrap() + && receipts + .get(&event_id) + .map(|r| r.read.as_ref().unwrap().get(&user_id).unwrap().clone()) + .map(|r| r.ts) + .unwrap() + == Some(UNIX_EPOCH + Duration::from_millis(1)) + ); +}