Repository: ruma/ruma
Pull request #59 (2020-06-16 21:31:57)

Implement EventContent for CustomEventConent and include in Any*EventContent
If this looks good I should I remove the CustomEvent structs?

jimmycuadra: PR at #105.
DevinR528: We don't want to do this for the `Any*EventContent` enums right?
```rust
match event_type {
    "m.room.aliases" => {
        let content = if let Ok(aliases) = AliasesEventContent::from_parts(event_type, json) {
            aliases
        } else {
            let content = CustomEventContent::from_parts(event_type, json)?;
            return Ok(AnyStateEventContent::Custom(content));
        };
        Ok(AnyStateEventContent::RoomAliases(content))
    }
    // ... and so on
    ev_type => {
        let content = CustomEventContent::from_parts(event_type, json)?;
        Ok(AnyStateEventContent::Custom(content))
    }
}
```

Patch:
diff --git a/ruma-events-macros/src/content_enum.rs b/ruma-events-macros/src/content_enum.rs
index e54bb999..9fcb2cab 100644
--- a/ruma-events-macros/src/content_enum.rs
+++ b/ruma-events-macros/src/content_enum.rs
@@ -27,8 +27,10 @@ pub fn expand_content_enum(input: ContentEnumInput) -> syn::Result<TokenStream>
         pub enum #ident {
             #(
                 #[doc = #event_type_str]
-                #variants(#content)
-            ),*
+                #variants(#content),
+            )*
+            #[doc = "Represents any event not defined in the Matrix spec."]
+            Custom(::ruma_events::custom::CustomEventContent)
         }
     };
 
@@ -36,7 +38,8 @@ pub fn expand_content_enum(input: ContentEnumInput) -> syn::Result<TokenStream>
         impl ::ruma_events::EventContent for #ident {
             fn event_type(&self) -> &str {
                 match self {
-                    #( Self::#variants(content) => content.event_type() ),*
+                    #( Self::#variants(content) => content.event_type(), )*
+                    #ident::Custom(content) => content.event_type(),
                 }
             }
 
@@ -51,7 +54,10 @@ pub fn expand_content_enum(input: ContentEnumInput) -> syn::Result<TokenStream>
                             Ok(#ident::#variants(content))
                         },
                     )*
-                    ev => Err(::serde::de::Error::custom(format!("event not supported {}", ev))),
+                    ev_type => {
+                        let content = ::ruma_events::custom::CustomEventContent::from_parts(ev_type, input)?;
+                        Ok(#ident::Custom(content))
+                    },
                 }
             }
         }
diff --git a/ruma-events/src/custom.rs b/ruma-events/src/custom.rs
index 8f2e88e3..74dd8a9f 100644
--- a/ruma-events/src/custom.rs
+++ b/ruma-events/src/custom.rs
@@ -4,11 +4,9 @@ use std::time::SystemTime;
 
 use ruma_identifiers::{EventId, RoomId, UserId};
 use serde::Serialize;
-use serde_json::Value as JsonValue;
+use serde_json::{value::RawValue as RawJsonValue, Value as JsonValue};
 
-use crate::UnsignedData;
-
-// TODO: (De)serialization
+use crate::{EventContent, UnsignedData};
 
 /// A custom event's type and `content` JSON object.
 #[derive(Clone, Debug, Serialize)]
@@ -18,9 +16,21 @@ pub struct CustomEventContent {
     pub event_type: String,
 
     /// The actual `content` JSON object.
+    #[serde(flatten)]
     pub json: JsonValue,
 }
 
+impl EventContent for CustomEventContent {
+    fn event_type(&self) -> &str {
+        &self.event_type
+    }
+
+    fn from_parts(event_type: &str, content: Box<RawJsonValue>) -> Result<Self, serde_json::Error> {
+        let json = serde_json::from_str(content.get())?;
+        Ok(Self { event_type: event_type.to_string(), json })
+    }
+}
+
 /// A custom event not covered by the Matrix specification.
 #[derive(Clone, Debug)]
 pub struct CustomBasicEvent {


Test patch:
diff --git a/ruma-events/tests/custom.rs b/ruma-events/tests/custom.rs
new file mode 100644
index 00000000..251764f9
--- /dev/null
+++ b/ruma-events/tests/custom.rs
@@ -0,0 +1,190 @@
+use std::{
+    convert::TryFrom,
+    time::{Duration, UNIX_EPOCH},
+};
+
+use matches::assert_matches;
+use ruma_events::{
+    custom::CustomEventContent, AnyMessageEventContent, AnyStateEventContent, EventJson,
+    MessageEvent, StateEvent, StateEventStub, UnsignedData,
+};
+use ruma_identifiers::{EventId, RoomId, UserId};
+use serde_json::{
+    from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue,
+};
+
+fn custom_state_event() -> JsonValue {
+    json!({
+        "content": {
+            "m.relates_to": {
+                "event_id": "$MDitXXXXXX",
+                "key": "👍",
+                "rel_type": "m.annotation"
+            }
+        },
+        "event_id": "$h29iv0s8:example.com",
+        "origin_server_ts": 10,
+        "room_id": "!room:room.com",
+        "sender": "@carl:example.com",
+        "state_key": "",
+        "type": "m.reaction",
+        "unsigned": {
+            "age": 85
+        }
+    })
+}
+
+#[test]
+fn serialize_custom_message_event() {
+    let aliases_event = MessageEvent {
+        content: AnyMessageEventContent::Custom(CustomEventContent {
+            json: json!({
+                "body": " * edited message",
+                "m.new_content": {
+                    "body": "edited message",
+                    "msgtype": "m.text"
+                },
+                "m.relates_to": {
+                    "event_id": "some event id",
+                    "rel_type": "m.replace"
+                },
+                "msgtype": "m.text"
+            }),
+            event_type: "m.room.message".to_string(),
+        }),
+        event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(),
+        origin_server_ts: UNIX_EPOCH + Duration::from_millis(10),
+        room_id: RoomId::try_from("!room:room.com").unwrap(),
+        sender: UserId::try_from("@carl:example.com").unwrap(),
+        unsigned: UnsignedData::default(),
+    };
+
+    let actual = to_json_value(&aliases_event).unwrap();
+    let expected = json!({
+        "content": {
+            "body": " * edited message",
+            "m.new_content": {
+                "body": "edited message",
+                "msgtype": "m.text"
+            },
+            "m.relates_to": {
+                "event_id": "some event id",
+                "rel_type": "m.replace"
+            },
+            "msgtype": "m.text"
+        },
+        "event_id": "$h29iv0s8:example.com",
+        "origin_server_ts": 10,
+        "sender": "@carl:example.com",
+        "room_id": "!room:room.com",
+        "type": "m.room.message",
+    });
+
+    assert_eq!(actual, expected);
+}
+
+#[test]
+fn serialize_custom_state_event() {
+    let aliases_event = StateEvent {
+        content: AnyStateEventContent::Custom(CustomEventContent {
+            json: json!({
+                "custom": 10
+            }),
+            event_type: "m.made.up".to_string(),
+        }),
+        event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(),
+        origin_server_ts: UNIX_EPOCH + Duration::from_millis(10),
+        prev_content: None,
+        room_id: RoomId::try_from("!roomid:room.com").unwrap(),
+        sender: UserId::try_from("@carl:example.com").unwrap(),
+        state_key: "".to_string(),
+        unsigned: UnsignedData::default(),
+    };
+
+    let actual = to_json_value(&aliases_event).unwrap();
+    let expected = json!({
+        "content": {
+            "custom": 10
+        },
+        "event_id": "$h29iv0s8:example.com",
+        "origin_server_ts": 10,
+        "room_id": "!roomid:room.com",
+        "sender": "@carl:example.com",
+        "state_key": "",
+        "type": "m.made.up",
+    });
+
+    assert_eq!(actual, expected);
+}
+
+#[test]
+fn deserialize_custom_state_event() {
+    let json_data = custom_state_event();
+
+    let expected_content = json!({
+        "m.relates_to": {
+            "event_id": "$MDitXXXXXX",
+            "key": "👍",
+            "rel_type": "m.annotation"
+        }
+    });
+
+    assert_matches!(
+        from_json_value::<EventJson<StateEvent<AnyStateEventContent>>>(json_data)
+            .unwrap()
+            .deserialize()
+            .unwrap(),
+        StateEvent {
+            content: AnyStateEventContent::Custom(CustomEventContent {
+                json, event_type,
+            }),
+            event_id,
+            origin_server_ts,
+            sender,
+            room_id,
+            prev_content: None,
+            state_key,
+            unsigned,
+        } if json == expected_content && event_type == "m.reaction"
+            && event_id == EventId::try_from("$h29iv0s8:example.com").unwrap()
+            && origin_server_ts == UNIX_EPOCH + Duration::from_millis(10)
+            && sender == UserId::try_from("@carl:example.com").unwrap()
+            && room_id == RoomId::try_from("!room:room.com").unwrap()
+            && state_key == ""
+            && !unsigned.is_empty()
+    );
+}
+
+#[test]
+fn deserialize_custom_state_stub_event() {
+    let json_data = custom_state_event();
+
+    let expected_content = json!({
+        "m.relates_to": {
+            "event_id": "$MDitXXXXXX",
+            "key": "👍",
+            "rel_type": "m.annotation"
+        }
+    });
+
+    assert_matches!(
+        from_json_value::<StateEventStub<AnyStateEventContent>>(json_data)
+            .unwrap(),
+        StateEventStub {
+            content: AnyStateEventContent::Custom(CustomEventContent {
+                json, event_type,
+            }),
+            event_id,
+            origin_server_ts,
+            sender,
+            prev_content: None,
+            state_key,
+            unsigned,
+        } if json == expected_content && event_type == "m.reaction"
+            && event_id == EventId::try_from("$h29iv0s8:example.com").unwrap()
+            && origin_server_ts == UNIX_EPOCH + Duration::from_millis(10)
+            && sender == UserId::try_from("@carl:example.com").unwrap()
+            && state_key == ""
+            && !unsigned.is_empty()
+    );
+}