events: Enforce MessageContent to not be empty

This commit is contained in:
Kévin Commaille 2022-03-26 09:39:29 +01:00 committed by Kévin Commaille
parent f9390c7c35
commit 685bd34fd4
3 changed files with 51 additions and 14 deletions

View File

@ -47,7 +47,7 @@
//! [MSC3245]: https://github.com/matrix-org/matrix-spec-proposals/pull/3245 //! [MSC3245]: https://github.com/matrix-org/matrix-spec-proposals/pull/3245
//! [MSC3381]: https://github.com/matrix-org/matrix-spec-proposals/pull/3381 //! [MSC3381]: https://github.com/matrix-org/matrix-spec-proposals/pull/3381
//! [`RoomMessageEventContent`]: super::room::message::RoomMessageEventContent //! [`RoomMessageEventContent`]: super::room::message::RoomMessageEventContent
use std::ops::Deref; use std::{convert::TryFrom, ops::Deref};
use ruma_macros::EventContent; use ruma_macros::EventContent;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -123,11 +123,25 @@ impl MessageEventContent {
} }
/// Text message content. /// Text message content.
///
/// A `MessageContent` must contain at least one message to be used as a fallback text
/// representation.
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
#[serde(try_from = "MessageContentSerDeHelper")] #[serde(try_from = "MessageContentSerDeHelper")]
pub struct MessageContent(pub(crate) Vec<Text>); pub struct MessageContent(pub(crate) Vec<Text>);
impl MessageContent { impl MessageContent {
/// Create a `MessageContent` from an array of messages.
///
/// Returns `None` if the array is empty.
pub fn new(messages: Vec<Text>) -> Option<Self> {
if messages.is_empty() {
None
} else {
Some(Self(messages))
}
}
/// A convenience constructor to create a plain text message. /// A convenience constructor to create a plain text message.
pub fn plain(body: impl Into<String>) -> Self { pub fn plain(body: impl Into<String>) -> Self {
Self(vec![Text::plain(body)]) Self(vec![Text::plain(body)])
@ -178,9 +192,17 @@ impl MessageContent {
} }
} }
impl From<Vec<Text>> for MessageContent { /// The error type returned when trying to construct an empty `MessageContent`.
fn from(variants: Vec<Text>) -> Self { #[derive(Debug, Error)]
Self(variants) #[non_exhaustive]
#[error("MessageContent cannot be empty")]
pub struct EmptyMessageContentError;
impl TryFrom<Vec<Text>> for MessageContent {
type Error = EmptyMessageContentError;
fn try_from(messages: Vec<Text>) -> Result<Self, Self::Error> {
Self::new(messages).ok_or(EmptyMessageContentError)
} }
} }

View File

@ -62,7 +62,7 @@ impl Serialize for MessageContent {
} }
pub(crate) mod as_vec { pub(crate) mod as_vec {
use serde::{ser::SerializeSeq, Deserialize, Deserializer, Serializer}; use serde::{de, ser::SerializeSeq, Deserialize, Deserializer, Serializer};
use crate::events::message::{MessageContent, Text}; use crate::events::message::{MessageContent, Text};
@ -87,7 +87,10 @@ pub(crate) mod as_vec {
where where
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
Option::<Vec<Text>>::deserialize(deserializer) Option::<Vec<Text>>::deserialize(deserializer).and_then(|content| {
.map(|content| content.filter(|content| !content.is_empty()).map(Into::into)) content.map(MessageContent::new).ok_or_else(|| {
de::Error::invalid_value(de::Unexpected::Other("empty array"), &"a non-empty array")
})
})
} }
} }

View File

@ -1,5 +1,7 @@
#![cfg(feature = "unstable-msc1767")] #![cfg(feature = "unstable-msc1767")]
use std::convert::TryFrom;
use assign::assign; use assign::assign;
use js_int::uint; use js_int::uint;
use matches::assert_matches; use matches::assert_matches;
@ -7,7 +9,7 @@ use ruma_common::{
event_id, event_id,
events::{ events::{
emote::EmoteEventContent, emote::EmoteEventContent,
message::MessageEventContent, message::{MessageContent, MessageEventContent, Text},
notice::NoticeEventContent, notice::NoticeEventContent,
room::message::{ room::message::{
EmoteMessageEventContent, InReplyTo, MessageType, NoticeMessageEventContent, Relation, EmoteMessageEventContent, InReplyTo, MessageType, NoticeMessageEventContent, Relation,
@ -19,6 +21,19 @@ use ruma_common::{
}; };
use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
#[test]
fn try_from_valid() {
assert_matches!(
MessageContent::try_from(vec![Text::plain("A message")]),
Ok(message) if message.len() == 1
);
}
#[test]
fn try_from_invalid() {
assert_matches!(MessageContent::try_from(vec![]), Err(_));
}
#[test] #[test]
fn html_content_serialization() { fn html_content_serialization() {
let message_event_content = let message_event_content =
@ -712,13 +727,12 @@ fn room_message_emote_unstable_deserialization() {
#[test] #[test]
#[cfg(feature = "unstable-msc3554")] #[cfg(feature = "unstable-msc3554")]
fn lang_serialization() { fn lang_serialization() {
use ruma_common::events::message::{MessageContent, Text}; let content = MessageContent::try_from(vec![
let content = MessageContent::from(vec![
assign!(Text::plain("Bonjour le monde !"), { lang: Some("fr".into()) }), assign!(Text::plain("Bonjour le monde !"), { lang: Some("fr".into()) }),
assign!(Text::plain("Hallo Welt!"), { lang: Some("de".into()) }), assign!(Text::plain("Hallo Welt!"), { lang: Some("de".into()) }),
assign!(Text::plain("Hello World!"), { lang: Some("en".into()) }), assign!(Text::plain("Hello World!"), { lang: Some("en".into()) }),
]); ])
.unwrap();
assert_eq!( assert_eq!(
to_json_value(&content).unwrap(), to_json_value(&content).unwrap(),
@ -735,8 +749,6 @@ fn lang_serialization() {
#[test] #[test]
#[cfg(feature = "unstable-msc3554")] #[cfg(feature = "unstable-msc3554")]
fn lang_deserialization() { fn lang_deserialization() {
use ruma_common::events::message::MessageContent;
let json_data = json!({ let json_data = json!({
"org.matrix.msc1767.message": [ "org.matrix.msc1767.message": [
{ "body": "Bonjour le monde !", "mimetype": "text/plain", "lang": "fr"}, { "body": "Bonjour le monde !", "mimetype": "text/plain", "lang": "fr"},