events: Replace Captions with MessageContent

Use a custom serde implementation
This commit is contained in:
Kévin Commaille 2022-03-25 18:14:11 +01:00 committed by Kévin Commaille
parent 93b4114a82
commit 245bf75276
6 changed files with 58 additions and 89 deletions

View File

@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
use super::{ use super::{
file::{EncryptedContent, FileContent}, file::{EncryptedContent, FileContent},
message::{MessageContent, Text}, message::MessageContent,
room::message::Relation, room::message::Relation,
}; };
use crate::MxcUri; use crate::MxcUri;
@ -35,8 +35,13 @@ pub struct ImageEventContent {
pub thumbnail: Vec<ThumbnailContent>, pub thumbnail: Vec<ThumbnailContent>,
/// The captions of the message. /// The captions of the message.
#[serde(rename = "m.caption", default, skip_serializing_if = "Captions::is_empty")] #[serde(
pub caption: Captions, rename = "m.caption",
with = "super::message::content_serde::as_vec",
default,
skip_serializing_if = "Option::is_none"
)]
pub caption: Option<MessageContent>,
/// Information about related messages for [rich replies]. /// Information about related messages for [rich replies].
/// ///
@ -151,71 +156,6 @@ impl ThumbnailContent {
} }
} }
/// An array of captions.
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Captions(pub(crate) Vec<Text>);
impl Captions {
/// Creates a new `Captions` with the given captions.
///
/// The captions must be ordered by most preferred first.
pub fn new(captions: &[Text]) -> Self {
Self(captions.to_owned())
}
/// A convenience constructor to create a plain text caption.
pub fn plain(body: impl Into<String>) -> Self {
Self(vec![Text::plain(body)])
}
/// A convenience constructor to create an HTML caption.
pub fn html(body: impl Into<String>, html_body: impl Into<String>) -> Self {
Self(vec![Text::html(html_body), Text::plain(body)])
}
/// A convenience constructor to create a Markdown caption.
///
/// Returns an HTML caption if some Markdown formatting was detected, otherwise returns a plain
/// text caption.
#[cfg(feature = "markdown")]
pub fn markdown(body: impl AsRef<str> + Into<String>) -> Self {
let mut message = Vec::with_capacity(2);
if let Some(html_body) = Text::markdown(&body) {
message.push(html_body);
}
message.push(Text::plain(body));
Self(message)
}
/// Get the captions.
///
/// The captions are ordered by most preferred first.
pub fn captions(&self) -> &[Text] {
&self.0
}
/// Whether this is empty.
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
/// Get the plain text representation of this caption.
pub fn find_plain(&self) -> Option<&str> {
self.captions()
.iter()
.find(|content| content.mimetype == "text/plain")
.map(|content| content.body.as_ref())
}
/// Get the HTML representation of this caption.
pub fn find_html(&self) -> Option<&str> {
self.captions()
.iter()
.find(|content| content.mimetype == "text/html")
.map(|content| content.body.as_ref())
}
}
/// Image content. /// Image content.
#[derive(Default, Clone, Debug, Serialize, Deserialize)] #[derive(Default, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]

View File

@ -52,7 +52,7 @@ use std::ops::Deref;
use ruma_macros::EventContent; use ruma_macros::EventContent;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
mod content_serde; pub(crate) mod content_serde;
use content_serde::MessageContentSerDeHelper; use content_serde::MessageContentSerDeHelper;

View File

@ -67,3 +67,34 @@ impl Serialize for MessageContent {
st.end() st.end()
} }
} }
pub(crate) mod as_vec {
use serde::{ser::SerializeSeq, Deserialize, Deserializer, Serializer};
use crate::events::message::{MessageContent, Text};
/// Serializes a `Option<MessageContent>` as a `Vec<Text>`.
pub fn serialize<S>(content: &Option<MessageContent>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if let Some(content) = content {
let mut seq = serializer.serialize_seq(Some(content.len()))?;
for e in content.iter() {
seq.serialize_element(e)?;
}
seq.end()
} else {
serializer.serialize_seq(Some(0))?.end()
}
}
/// Deserializes a `Vec<Text>` to an `Option<MessageContent>`.
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<MessageContent>, D::Error>
where
D: Deserializer<'de>,
{
Option::<Vec<Text>>::deserialize(deserializer)
.map(|content| content.filter(|content| !content.is_empty()).map(Into::into))
}
}

View File

@ -9,10 +9,7 @@ use ruma_macros::EventContent;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::{ use super::{
file::FileContent, file::FileContent, image::ThumbnailContent, message::MessageContent, room::message::Relation,
image::{Captions, ThumbnailContent},
message::MessageContent,
room::message::Relation,
}; };
/// The payload for an extensible video message. /// The payload for an extensible video message.
@ -37,8 +34,13 @@ pub struct VideoEventContent {
pub thumbnail: Vec<ThumbnailContent>, pub thumbnail: Vec<ThumbnailContent>,
/// The captions of the message. /// The captions of the message.
#[serde(rename = "m.caption", default, skip_serializing_if = "Captions::is_empty")] #[serde(
pub caption: Captions, rename = "m.caption",
with = "super::message::content_serde::as_vec",
default,
skip_serializing_if = "Option::is_none"
)]
pub caption: Option<MessageContent>,
/// Information about related messages. /// Information about related messages.
#[serde(flatten, skip_serializing_if = "Option::is_none")] #[serde(flatten, skip_serializing_if = "Option::is_none")]

View File

@ -8,7 +8,7 @@ use ruma_common::{
events::{ events::{
file::{EncryptedContentInit, FileContent, FileContentInfo}, file::{EncryptedContentInit, FileContent, FileContentInfo},
image::{ image::{
Captions, ImageContent, ImageEventContent, ThumbnailContent, ThumbnailFileContent, ImageContent, ImageEventContent, ThumbnailContent, ThumbnailFileContent,
ThumbnailFileContentInfo, ThumbnailFileContentInfo,
}, },
message::MessageContent, message::MessageContent,
@ -128,7 +128,7 @@ fn image_event_serialization() {
), ),
None None
)], )],
caption: Captions::plain("This is my house"), caption: Some(MessageContent::plain("This is my house")),
relates_to: Some(Relation::Reply { relates_to: Some(Relation::Reply {
in_reply_to: InReplyTo::new(event_id!("$replyevent:example.com").to_owned()), in_reply_to: InReplyTo::new(event_id!("$replyevent:example.com").to_owned()),
}), }),
@ -207,7 +207,7 @@ fn plain_content_deserialization() {
assert_matches!( assert_matches!(
from_json_value::<ImageEventContent>(json_data) from_json_value::<ImageEventContent>(json_data)
.unwrap(), .unwrap(),
ImageEventContent { message, file, image, thumbnail, caption, .. } ImageEventContent { message, file, image, thumbnail, caption: Some(caption), .. }
if message.find_plain() == Some("Upload: my_cat.png") if message.find_plain() == Some("Upload: my_cat.png")
&& message.find_html().is_none() && message.find_html().is_none()
&& file.url == "mxc://notareal.hs/abcdef" && file.url == "mxc://notareal.hs/abcdef"
@ -248,7 +248,7 @@ fn encrypted_content_deserialization() {
assert_matches!( assert_matches!(
from_json_value::<ImageEventContent>(json_data) from_json_value::<ImageEventContent>(json_data)
.unwrap(), .unwrap(),
ImageEventContent { message, file, image, thumbnail, caption, .. } ImageEventContent { message, file, image, thumbnail, caption: None, .. }
if message.find_plain() == Some("Upload: my_file.txt") if message.find_plain() == Some("Upload: my_file.txt")
&& message.find_html().is_none() && message.find_html().is_none()
&& file.url == "mxc://notareal.hs/abcdef" && file.url == "mxc://notareal.hs/abcdef"
@ -256,7 +256,6 @@ fn encrypted_content_deserialization() {
&& image.width.is_none() && image.width.is_none()
&& image.height.is_none() && image.height.is_none()
&& thumbnail[0].file.url == "mxc://notareal.hs/thumbnail" && thumbnail[0].file.url == "mxc://notareal.hs/thumbnail"
&& caption.is_empty()
); );
} }
@ -295,7 +294,7 @@ fn message_event_deserialization() {
}, },
image, image,
thumbnail, thumbnail,
caption, caption: None,
.. ..
}, },
event_id, event_id,
@ -313,7 +312,6 @@ fn message_event_deserialization() {
&& image.width == Some(uint!(1300)) && image.width == Some(uint!(1300))
&& image.height == Some(uint!(837)) && image.height == Some(uint!(837))
&& thumbnail.is_empty() && thumbnail.is_empty()
&& caption.is_empty()
&& origin_server_ts == MilliSecondsSinceUnixEpoch(uint!(134_829_848)) && origin_server_ts == MilliSecondsSinceUnixEpoch(uint!(134_829_848))
&& room_id == room_id!("!roomid:notareal.hs") && room_id == room_id!("!roomid:notareal.hs")
&& sender == user_id!("@user:notareal.hs") && sender == user_id!("@user:notareal.hs")

View File

@ -9,7 +9,7 @@ use ruma_common::{
event_id, event_id,
events::{ events::{
file::{EncryptedContentInit, FileContent, FileContentInfo}, file::{EncryptedContentInit, FileContent, FileContentInfo},
image::{Captions, ThumbnailContent, ThumbnailFileContent, ThumbnailFileContentInfo}, image::{ThumbnailContent, ThumbnailFileContent, ThumbnailFileContentInfo},
message::MessageContent, message::MessageContent,
room::{ room::{
message::{InReplyTo, Relation}, message::{InReplyTo, Relation},
@ -135,7 +135,7 @@ fn event_serialization() {
), ),
None None
)], )],
caption: Captions::plain("This is my awesome vintage lava lamp"), caption: Some(MessageContent::plain("This is my awesome vintage lava lamp")),
relates_to: Some(Relation::Reply { relates_to: Some(Relation::Reply {
in_reply_to: InReplyTo::new(event_id!("$replyevent:example.com").to_owned()), in_reply_to: InReplyTo::new(event_id!("$replyevent:example.com").to_owned()),
}), }),
@ -215,7 +215,7 @@ fn plain_content_deserialization() {
assert_matches!( assert_matches!(
from_json_value::<VideoEventContent>(json_data) from_json_value::<VideoEventContent>(json_data)
.unwrap(), .unwrap(),
VideoEventContent { message, file, video, thumbnail, caption, .. } VideoEventContent { message, file, video, thumbnail, caption: Some(caption), .. }
if message.find_plain() == Some("Video: my_cat.mp4") if message.find_plain() == Some("Video: my_cat.mp4")
&& message.find_html().is_none() && message.find_html().is_none()
&& file.url == "mxc://notareal.hs/abcdef" && file.url == "mxc://notareal.hs/abcdef"
@ -257,7 +257,7 @@ fn encrypted_content_deserialization() {
assert_matches!( assert_matches!(
from_json_value::<VideoEventContent>(json_data) from_json_value::<VideoEventContent>(json_data)
.unwrap(), .unwrap(),
VideoEventContent { message, file, video, thumbnail, caption, .. } VideoEventContent { message, file, video, thumbnail, caption: None, .. }
if message.find_plain() == Some("Upload: my_cat.mp4") if message.find_plain() == Some("Upload: my_cat.mp4")
&& message.find_html().is_none() && message.find_html().is_none()
&& file.url == "mxc://notareal.hs/abcdef" && file.url == "mxc://notareal.hs/abcdef"
@ -266,7 +266,6 @@ fn encrypted_content_deserialization() {
&& video.height.is_none() && video.height.is_none()
&& video.duration.is_none() && video.duration.is_none()
&& thumbnail[0].file.url == "mxc://notareal.hs/thumbnail" && thumbnail[0].file.url == "mxc://notareal.hs/thumbnail"
&& caption.is_empty()
); );
} }
@ -305,7 +304,7 @@ fn message_event_deserialization() {
}, },
video, video,
thumbnail, thumbnail,
caption, caption: None,
.. ..
}, },
event_id, event_id,
@ -324,7 +323,6 @@ fn message_event_deserialization() {
&& video.height == Some(uint!(837)) && video.height == Some(uint!(837))
&& video.duration.is_none() && video.duration.is_none()
&& thumbnail.is_empty() && thumbnail.is_empty()
&& caption.is_empty()
&& origin_server_ts == MilliSecondsSinceUnixEpoch(uint!(134_829_848)) && origin_server_ts == MilliSecondsSinceUnixEpoch(uint!(134_829_848))
&& room_id == room_id!("!roomid:notareal.hs") && room_id == room_id!("!roomid:notareal.hs")
&& sender == user_id!("@user:notareal.hs") && sender == user_id!("@user:notareal.hs")