diff --git a/ruma-events-macros/src/event.rs b/ruma-events-macros/src/event.rs index 565ffb17..58703e2c 100644 --- a/ruma-events-macros/src/event.rs +++ b/ruma-events-macros/src/event.rs @@ -151,11 +151,18 @@ fn expand_deserialize_event( if name == "content" { if is_generic && ident.to_string().contains("Redacted") { quote! { - let content = if !C::has_deserialize_fields() { - C::empty(&event_type).map_err(A::Error::custom)? - } else { - let json = content.ok_or_else(|| ::serde::de::Error::missing_field("content"))?; - C::from_parts(&event_type, json).map_err(A::Error::custom)? + let content = match C::has_deserialize_fields() { + ::ruma_events::HasDeserializeFields::False => { + C::empty(&event_type).map_err(A::Error::custom)? + }, + ::ruma_events::HasDeserializeFields::True => { + let json = content.ok_or_else(|| ::serde::de::Error::missing_field("content"))?; + C::from_parts(&event_type, json).map_err(A::Error::custom)? + }, + ::ruma_events::HasDeserializeFields::Optional => { + let json = content.unwrap_or(::serde_json::value::RawValue::from_string("{}".to_string()).unwrap()); + C::from_parts(&event_type, json).map_err(A::Error::custom)? + }, }; } } else if is_generic { diff --git a/ruma-events-macros/src/event_content.rs b/ruma-events-macros/src/event_content.rs index 043dbe62..fc51ff07 100644 --- a/ruma-events-macros/src/event_content.rs +++ b/ruma-events-macros/src/event_content.rs @@ -135,7 +135,13 @@ pub fn expand_event_content(input: &DeriveInput, emit_redacted: bool) -> syn::Re ) }; - let has_fields = if kept_redacted_fields.is_empty() { + let has_deserialize_fields = if kept_redacted_fields.is_empty() { + quote! { ::ruma_events::HasDeserializeFields::False } + } else { + quote! { ::ruma_events::HasDeserializeFields::True } + }; + + let has_serialize_fields = if kept_redacted_fields.is_empty() { quote! { false } } else { quote! { true } @@ -170,11 +176,11 @@ pub fn expand_event_content(input: &DeriveInput, emit_redacted: bool) -> syn::Re } fn has_serialize_fields(&self) -> bool { - #has_fields + #has_serialize_fields } - fn has_deserialize_fields() -> bool { - #has_fields + fn has_deserialize_fields() -> ::ruma_events::HasDeserializeFields { + #has_deserialize_fields } } } diff --git a/ruma-events/src/custom.rs b/ruma-events/src/custom.rs index ac7d7e7e..b17e20bc 100644 --- a/ruma-events/src/custom.rs +++ b/ruma-events/src/custom.rs @@ -4,9 +4,9 @@ use serde::Serialize; use serde_json::{value::RawValue as RawJsonValue, Value as JsonValue}; use crate::{ - BasicEventContent, EphemeralRoomEventContent, EventContent, MessageEventContent, - RedactedEventContent, RedactedMessageEventContent, RedactedStateEventContent, RoomEventContent, - StateEventContent, + BasicEventContent, EphemeralRoomEventContent, EventContent, HasDeserializeFields, + MessageEventContent, RedactedEventContent, RedactedMessageEventContent, + RedactedStateEventContent, RoomEventContent, StateEventContent, }; /// A custom event's type and `content` JSON object. @@ -83,8 +83,8 @@ impl RedactedEventContent for RedactedCustomEventContent { false } - fn has_deserialize_fields() -> bool { - false + fn has_deserialize_fields() -> HasDeserializeFields { + HasDeserializeFields::False } } diff --git a/ruma-events/src/lib.rs b/ruma-events/src/lib.rs index 4bd43735..6357d390 100644 --- a/ruma-events/src/lib.rs +++ b/ruma-events/src/lib.rs @@ -243,20 +243,27 @@ pub trait StateEventContent: RoomEventContent {} /// The base trait that all redacted event content types implement. /// -/// Implementing this trait allows content types to be serialized as well as deserialized. +/// This trait's associated functions and methods should not be used to build +/// redacted events, prefer the `redact` method on `AnyStateEvent` and +/// `AnyMessageEvent` and their "sync" and "stripped" counterparts. The +/// `RedactedEventContent` trait is an implementation detail, ruma makes no +/// API guarantees. pub trait RedactedEventContent: EventContent { /// Constructs the redacted event content. /// /// If called for anything but "empty" redacted content this will error. + #[doc(hidden)] fn empty(_event_type: &str) -> Result { Err(serde::de::Error::custom("this event is not redacted")) } /// Determines if the redacted event content needs to serialize fields. + #[doc(hidden)] fn has_serialize_fields(&self) -> bool; /// Determines if the redacted event content needs to deserialize fields. - fn has_deserialize_fields() -> bool; + #[doc(hidden)] + fn has_deserialize_fields() -> HasDeserializeFields; } /// Marker trait for the content of a redacted message event. @@ -265,6 +272,21 @@ pub trait RedactedMessageEventContent: RedactedEventContent {} /// Marker trait for the content of a redacted state event. pub trait RedactedStateEventContent: RedactedEventContent {} +/// `HasDeserializeFields` is used in the code generated by the `Event` derive +/// to aid in deserializing redacted events. +#[doc(hidden)] +#[derive(Debug)] +pub enum HasDeserializeFields { + /// Deserialize the event's content, failing if invalid. + True, + /// Return the redacted version of this event's content. + False, + /// `Optional` is used for `RedactedAliasesEventContent` since it has + /// an empty version and one with content left after redaction that + /// must be supported together. + Optional, +} + /// Helper struct to determine if the event has been redacted. #[doc(hidden)] #[derive(Debug, Deserialize)] diff --git a/ruma-events/src/room/aliases.rs b/ruma-events/src/room/aliases.rs index cd1508aa..aa99f760 100644 --- a/ruma-events/src/room/aliases.rs +++ b/ruma-events/src/room/aliases.rs @@ -5,7 +5,9 @@ use ruma_identifiers::RoomAliasId; use serde::{Deserialize, Serialize}; use serde_json::value::RawValue as RawJsonValue; -use crate::{EventContent, RedactedEventContent, RedactedStateEventContent, StateEvent}; +use crate::{ + EventContent, HasDeserializeFields, RedactedEventContent, RedactedStateEventContent, StateEvent, +}; /// Informs the room about what room aliases it has been given. pub type AliasesEvent = StateEvent; @@ -53,8 +55,8 @@ impl RedactedEventContent for RedactedAliasesEventContent { self.aliases.is_some() } - fn has_deserialize_fields() -> bool { - true + fn has_deserialize_fields() -> HasDeserializeFields { + HasDeserializeFields::Optional } } diff --git a/ruma-events/tests/redacted.rs b/ruma-events/tests/redacted.rs index 908a25fb..5cafc87b 100644 --- a/ruma-events/tests/redacted.rs +++ b/ruma-events/tests/redacted.rs @@ -19,10 +19,6 @@ use ruma_events::{ use ruma_identifiers::{EventId, RoomId, UserId}; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; -fn is_zst(_: &T) -> bool { - std::mem::size_of::() == 0 -} - fn full_unsigned() -> UnsignedData { let mut unsigned = UnsignedData::default(); // The presence of `redacted_because` triggers the event enum to return early @@ -63,7 +59,7 @@ fn redacted_message_event_serialize() { } #[test] -fn redacted_aliases_event_serialize() { +fn redacted_aliases_event_serialize_no_content() { let redacted = RedactedSyncStateEvent { content: RedactedAliasesEventContent { aliases: None }, event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(), @@ -85,6 +81,32 @@ fn redacted_aliases_event_serialize() { assert_eq!(actual, expected); } +#[test] +fn redacted_aliases_event_serialize_with_content() { + let redacted = RedactedSyncStateEvent { + content: RedactedAliasesEventContent { aliases: Some(vec![]) }, + event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(), + state_key: "".to_string(), + origin_server_ts: UNIX_EPOCH + Duration::from_millis(1), + sender: UserId::try_from("@carl:example.com").unwrap(), + unsigned: UnsignedData::default(), + }; + + let expected = json!({ + "content": { + "aliases": [] + }, + "event_id": "$h29iv0s8:example.com", + "state_key": "", + "origin_server_ts": 1, + "sender": "@carl:example.com", + "type": "m.room.aliases" + }); + + let actual = to_json_value(&redacted).unwrap(); + assert_eq!(actual, expected); +} + #[test] fn redacted_aliases_deserialize() { let unsigned = full_unsigned(); @@ -101,14 +123,15 @@ fn redacted_aliases_deserialize() { let actual = to_json_value(&redacted).unwrap(); assert_matches!( - from_json_value::>(actual) + from_json_value::>(actual) .unwrap() .deserialize() .unwrap(), - AnyRoomEventStub::RedactedState(AnyRedactedStateEventStub::RoomAliases(RedactedStateEventStub { - event_id, content, .. + AnySyncRoomEvent::RedactedState(AnyRedactedSyncStateEvent::RoomAliases(RedactedSyncStateEvent { + content: RedactedAliasesEventContent { aliases }, + event_id, .. })) if event_id == EventId::try_from("$h29iv0s8:example.com").unwrap() - && is_zst(&content) + && aliases.is_none() ) } @@ -133,10 +156,10 @@ fn redacted_deserialize_any_room() { .deserialize() .unwrap(), AnyRoomEvent::RedactedMessage(AnyRedactedMessageEvent::RoomMessage(RedactedMessageEvent { - event_id, room_id, content, .. + content: RedactedMessageEventContent, + event_id, room_id, .. })) if event_id == EventId::try_from("$h29iv0s8:example.com").unwrap() && room_id == RoomId::try_from("!roomid:room.com").unwrap() - && is_zst(&content) ) } @@ -172,9 +195,9 @@ fn redacted_deserialize_any_room_sync() { .deserialize() .unwrap(), AnySyncRoomEvent::RedactedMessage(AnyRedactedSyncMessageEvent::RoomMessage(RedactedSyncMessageEvent { - event_id, content, .. + content: RedactedMessageEventContent, + event_id, .. })) if event_id == EventId::try_from("$h29iv0s8:example.com").unwrap() - && is_zst(&content) ) }