Repository: ruma/ruma
Pull request #423 (2021-02-12 13:04:39)

Allow custom `msgtype`s for MessageEventContent 
Differences from #406:

* Cleaned up git history
* Removed `remaining` from `MessageDeHelper`
  * Gets rid of unnecessary deserialization work for the common case of known `msgtype`
  * ... at the cost of deserializing `msgtype` twice for custom `msgtype`s
* Removed some `#[doc(hidden)] pub` that weren't needed


Patch:
diff --git a/ruma-events/src/room/message.rs b/ruma-events/src/room/message.rs
index 51ace353..44ab325d 100644
--- a/ruma-events/src/room/message.rs
+++ b/ruma-events/src/room/message.rs
@@ -1,5 +1,7 @@
 //! Types for the *m.room.message* event.
 
+use std::collections::BTreeMap;
+
 use js_int::UInt;
 use ruma_events_macros::MessageEventContent;
 #[cfg(feature = "unstable-pre-spec")]
@@ -18,6 +20,7 @@ use super::{relationships::RelatesToJsonRepr, EncryptedFile, ImageInfo, Thumbnai
 // FIXME: Do we want to keep re-exporting this?
 pub use super::relationships::InReplyTo;
 
+mod content_serde;
 pub mod feedback;
 
 use crate::MessageEvent as OuterMessageEvent;
@@ -28,51 +31,45 @@ use crate::MessageEvent as OuterMessageEvent;
 pub type MessageEvent = OuterMessageEvent<MessageEventContent>;
 
 /// The payload for `MessageEvent`.
-#[derive(Clone, Debug, Deserialize, Serialize, MessageEventContent)]
+#[derive(Clone, Debug, Serialize, MessageEventContent)]
 #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
 #[ruma_event(type = "m.room.message")]
-#[serde(tag = "msgtype")]
+#[serde(untagged)]
 pub enum MessageEventContent {
     /// An audio message.
-    #[serde(rename = "m.audio")]
     Audio(AudioMessageEventContent),
 
     /// An emote message.
-    #[serde(rename = "m.emote")]
     Emote(EmoteMessageEventContent),
 
     /// A file message.
-    #[serde(rename = "m.file")]
     File(FileMessageEventContent),
 
     /// An image message.
-    #[serde(rename = "m.image")]
     Image(ImageMessageEventContent),
 
     /// A location message.
-    #[serde(rename = "m.location")]
     Location(LocationMessageEventContent),
 
     /// A notice message.
-    #[serde(rename = "m.notice")]
     Notice(NoticeMessageEventContent),
 
     /// A server notice message.
-    #[serde(rename = "m.server_notice")]
     ServerNotice(ServerNoticeMessageEventContent),
 
     /// A text message.
-    #[serde(rename = "m.text")]
     Text(TextMessageEventContent),
 
     /// A video message.
-    #[serde(rename = "m.video")]
     Video(VideoMessageEventContent),
 
     /// A request to initiate a key verification.
     #[cfg(feature = "unstable-pre-spec")]
-    #[serde(rename = "m.key.verification.request")]
     VerificationRequest(KeyVerificationRequestEventContent),
+
+    /// A custom message.
+    #[doc(hidden)]
+    _Custom(CustomEventContent),
 }
 
 /// Enum modeling the different ways relationships can be expressed in a
@@ -159,6 +156,7 @@ impl MessageEventContent {
 
 /// The payload for an audio message.
 #[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(tag = "msgtype", rename = "m.audio")]
 pub struct AudioMessageEventContent {
     /// The textual representation of this message.
     pub body: String,
@@ -195,6 +193,7 @@ pub struct AudioInfo {
 
 /// The payload for an emote message.
 #[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(tag = "msgtype", rename = "m.emote")]
 pub struct EmoteMessageEventContent {
     /// The emote action to perform.
     pub body: String,
@@ -206,6 +205,7 @@ pub struct EmoteMessageEventContent {
 
 /// The payload for a file message.
 #[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(tag = "msgtype", rename = "m.file")]
 pub struct FileMessageEventContent {
     /// A human-readable description of the file. This is recommended to be the filename of the
     /// original upload.
@@ -255,6 +255,7 @@ pub struct FileInfo {
 
 /// The payload for an image message.
 #[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(tag = "msgtype", rename = "m.image")]
 pub struct ImageMessageEventContent {
     /// A textual representation of the image. This could be the alt text of the image, the
     /// filename of the image, or some kind of content description for accessibility e.g.
@@ -277,6 +278,7 @@ pub struct ImageMessageEventContent {
 
 /// The payload for a location message.
 #[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(tag = "msgtype", rename = "m.location")]
 pub struct LocationMessageEventContent {
     /// A description of the location e.g. "Big Ben, London, UK,"or some kind of content
     /// description for accessibility, e.g. "location attachment."
@@ -311,6 +313,7 @@ pub struct LocationInfo {
 /// The payload for a notice message.
 #[derive(Clone, Debug, Deserialize, Serialize)]
 #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
+#[serde(tag = "msgtype", rename = "m.notice")]
 pub struct NoticeMessageEventContent {
     /// The notice text.
     pub body: String,
@@ -352,6 +355,7 @@ impl NoticeMessageEventContent {
 
 /// The payload for a server notice message.
 #[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(tag = "msgtype", rename = "m.server_notice")]
 pub struct ServerNoticeMessageEventContent {
     /// A human-readable description of the notice.
     pub body: String,
@@ -441,6 +445,7 @@ impl FormattedBody {
 /// The payload for a text message.
 #[derive(Clone, Debug, Deserialize, Serialize)]
 #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
+#[serde(tag = "msgtype", rename = "m.text")]
 pub struct TextMessageEventContent {
     /// The body of the message.
     pub body: String,
@@ -497,6 +502,7 @@ impl TextMessageEventContent {
 
 /// The payload for a video message.
 #[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(tag = "msgtype", rename = "m.video")]
 pub struct VideoMessageEventContent {
     /// A description of the video, e.g. "Gangnam Style," or some kind of content description for
     /// accessibility, e.g. "video attachment."
@@ -567,6 +573,7 @@ pub struct VideoInfo {
 /// The payload for a key verification request message.
 #[derive(Clone, Debug, Deserialize, Serialize)]
 #[cfg(feature = "unstable-pre-spec")]
+#[serde(tag = "msgtype", rename = "m.key.verification.request")]
 pub struct KeyVerificationRequestEventContent {
     /// A fallback message to alert users that their client does not support the key verification
     /// framework.
@@ -585,3 +592,15 @@ pub struct KeyVerificationRequestEventContent {
     /// events that have a m.reference relationship with this event.
     pub to: UserId,
 }
+
+/// The payload for a custom message event.
+#[doc(hidden)]
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct CustomEventContent {
+    /// A custom msgtype
+    pub msgtype: String,
+
+    /// Remaining event content
+    #[serde(flatten)]
+    pub data: BTreeMap<String, JsonValue>,
+}
diff --git a/ruma-events/src/room/message/content_serde.rs b/ruma-events/src/room/message/content_serde.rs
new file mode 100644
index 00000000..c618ed61
--- /dev/null
+++ b/ruma-events/src/room/message/content_serde.rs
@@ -0,0 +1,38 @@
+//! `Deserialize` implementation for MessageEventContent
+
+use serde::{de, Deserialize};
+use serde_json::value::RawValue as RawJsonValue;
+
+use crate::{from_raw_json_value, room::message::MessageEventContent};
+
+/// Helper struct to determine the msgtype from a `serde_json::value::RawValue`
+#[derive(Debug, Deserialize)]
+struct MessageDeHelper {
+    /// The message type field
+    msgtype: String,
+}
+
+impl<'de> de::Deserialize<'de> for MessageEventContent {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: de::Deserializer<'de>,
+    {
+        let json = Box::<RawJsonValue>::deserialize(deserializer)?;
+        let MessageDeHelper { msgtype } = from_raw_json_value(&json)?;
+
+        Ok(match msgtype.as_ref() {
+            "m.audio" => Self::Audio(from_raw_json_value(&json)?),
+            "m.emote" => Self::Emote(from_raw_json_value(&json)?),
+            "m.file" => Self::File(from_raw_json_value(&json)?),
+            "m.image" => Self::Image(from_raw_json_value(&json)?),
+            "m.location" => Self::Location(from_raw_json_value(&json)?),
+            "m.notice" => Self::Notice(from_raw_json_value(&json)?),
+            "m.server_notice" => Self::ServerNotice(from_raw_json_value(&json)?),
+            "m.text" => Self::Text(from_raw_json_value(&json)?),
+            "m.video" => Self::Video(from_raw_json_value(&json)?),
+            #[cfg(feature = "unstable-pre-spec")]
+            "m.key.verification.request" => Self::VerificationRequest(from_raw_json_value(&json)?),
+            _ => Self::_Custom(from_raw_json_value(&json)?),
+        })
+    }
+}


Test patch:
diff --git a/ruma-events/tests/room_message.rs b/ruma-events/tests/room_message.rs
index 36b5166d..4f155972 100644
--- a/ruma-events/tests/room_message.rs
+++ b/ruma-events/tests/room_message.rs
@@ -1,6 +1,7 @@
 use std::time::{Duration, UNIX_EPOCH};
 
 use assign::assign;
+use maplit::btreemap;
 use matches::assert_matches;
 #[cfg(feature = "unstable-pre-spec")]
 use ruma_events::{
@@ -9,7 +10,8 @@ use ruma_events::{
 use ruma_events::{
     room::{
         message::{
-            AudioMessageEventContent, MessageEventContent, Relation, TextMessageEventContent,
+            AudioMessageEventContent, CustomEventContent, MessageEventContent, Relation,
+            TextMessageEventContent,
         },
         relationships::InReplyTo,
     },
@@ -73,6 +75,53 @@ fn content_serialization() {
     );
 }
 
+#[test]
+fn custom_content_serialization() {
+    let json_data = btreemap! {
+        "custom_field".into() => json!("baba"),
+        "another_one".into() => json!("abab"),
+    };
+    let custom_event_content = MessageEventContent::_Custom(CustomEventContent {
+        msgtype: "my_custom_msgtype".into(),
+        data: json_data,
+    });
+
+    assert_eq!(
+        to_json_value(&custom_event_content).unwrap(),
+        json!({
+            "msgtype": "my_custom_msgtype",
+            "custom_field": "baba",
+            "another_one": "abab",
+        })
+    );
+}
+
+#[test]
+fn custom_content_deserialization() {
+    let json_data = json!({
+        "msgtype": "my_custom_msgtype",
+        "custom_field": "baba",
+        "another_one": "abab",
+    });
+
+    let expected_json_data = btreemap! {
+        "custom_field".into() => json!("baba"),
+        "another_one".into() => json!("abab"),
+    };
+
+    assert_matches!(
+        from_json_value::<Raw<MessageEventContent>>(json_data)
+            .unwrap()
+            .deserialize()
+            .unwrap(),
+        MessageEventContent::_Custom(CustomEventContent {
+            msgtype,
+            data
+        }) if msgtype == "my_custom_msgtype"
+            && data == expected_json_data
+    );
+}
+
 #[test]
 fn formatted_body_serialization() {
     let message_event_content = MessageEventContent::Text(TextMessageEventContent::html(