diff --git a/crates/ruma-events/src/room/name.rs b/crates/ruma-events/src/room/name.rs index e2dc0960..dd2530aa 100644 --- a/crates/ruma-events/src/room/name.rs +++ b/crates/ruma-events/src/room/name.rs @@ -1,5 +1,7 @@ //! Types for the *m.room.name* event. +use std::convert::TryFrom; + use ruma_events_macros::EventContent; use serde::{Deserialize, Serialize}; @@ -11,51 +13,70 @@ pub type NameEvent = StateEvent; /// The payload for `NameEvent`. #[derive(Clone, Debug, Deserialize, Serialize, EventContent)] #[ruma_event(type = "m.room.name", kind = State)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct NameEventContent { - /// The name of the room. This MUST NOT exceed 255 bytes. - #[serde(default, deserialize_with = "room_name")] - pub(crate) name: Option, + /// The name of the room. + #[serde(default, deserialize_with = "ruma_serde::empty_string_as_none")] + pub name: Option, } impl NameEventContent { /// Create a new `NameEventContent` with the given name. - /// - /// # Errors - /// - /// `InvalidInput` will be returned if the name is more than 255 bytes. - pub fn new(name: String) -> Result { - match name.len() { - 0 => Ok(Self { name: None }), - 1..=255 => Ok(Self { name: Some(name) }), - _ => Err(InvalidInput("a room name cannot be more than 255 bytes".into())), - } + pub fn new(name: Option) -> Self { + Self { name } } /// The name of the room, if any. - pub fn name(&self) -> Option<&str> { - self.name.as_deref() + pub fn name(&self) -> Option<&RoomName> { + self.name.as_ref() } } -fn room_name<'de, D>(deserializer: D) -> Result, D::Error> -where - D: serde::de::Deserializer<'de>, -{ - use serde::de::Error; +/// The name of a room. +/// +/// It should not exceed 255 characters and should not be empty. +#[derive(Clone, Debug, Serialize, PartialEq, Eq)] +#[serde(transparent)] +pub struct RoomName(String); - // this handles the null case and the empty string or nothing case - match Option::::deserialize(deserializer)? { - Some(name) => match name.len() { - 0 => Ok(None), - 1..=255 => Ok(Some(name)), - _ => Err(D::Error::custom("a room name cannot be more than 255 bytes")), - }, - None => Ok(None), +impl TryFrom for RoomName { + type Error = InvalidInput; + + fn try_from(value: String) -> Result { + match value.len() { + 0 => Err(InvalidInput("a room name cannot be empty.".into())), + 1..=255 => Ok(RoomName(value)), + _ => Err(InvalidInput("a room name cannot be more than 255 bytes.".into())), + } + } +} + +impl From for String { + fn from(name: RoomName) -> Self { + name.0 + } +} + +impl<'de> Deserialize<'de> for RoomName { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error; + + let str_name = String::deserialize(deserializer)?; + + match RoomName::try_from(str_name) { + Ok(name) => Ok(name), + Err(e) => Err(D::Error::custom(e.to_string())), + } } } #[cfg(test)] mod tests { + use std::convert::TryFrom; + use js_int::{int, uint}; use matches::assert_matches; use ruma_common::MilliSecondsSinceUnixEpoch; @@ -64,12 +85,12 @@ mod tests { use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; use super::NameEventContent; - use crate::{StateEvent, Unsigned}; + use crate::{room::name::RoomName, StateEvent, Unsigned}; #[test] fn serialization_with_optional_fields_as_none() { let name_event = StateEvent { - content: NameEventContent { name: Some("The room name".into()) }, + content: NameEventContent { name: RoomName::try_from("The room name".to_owned()).ok() }, event_id: event_id!("$h29iv0s8:example.com"), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(1)), prev_content: None, @@ -98,10 +119,12 @@ mod tests { #[test] fn serialization_with_all_fields() { let name_event = StateEvent { - content: NameEventContent { name: Some("The room name".into()) }, + content: NameEventContent { name: RoomName::try_from("The room name".to_owned()).ok() }, event_id: event_id!("$h29iv0s8:example.com"), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(1)), - prev_content: Some(NameEventContent { name: Some("The old name".into()) }), + prev_content: Some(NameEventContent { + name: RoomName::try_from("The old name".to_owned()).ok(), + }), room_id: room_id!("!n8f893n9:example.com"), sender: user_id!("@carl:example.com"), state_key: "".into(), @@ -173,7 +196,7 @@ mod tests { #[test] fn new_with_empty_name_creates_content_as_none() { assert_matches!( - NameEventContent::new(String::new()).unwrap(), + NameEventContent::new(RoomName::try_from(String::new()).ok()), NameEventContent { name: None } ); } @@ -228,7 +251,7 @@ mod tests { #[test] fn nonempty_field_as_some() { - let name = Some("The room name".into()); + let name = RoomName::try_from("The room name".to_owned()).ok(); let json_data = json!({ "content": { "name": "The room name" diff --git a/crates/ruma-events/tests/initial_state.rs b/crates/ruma-events/tests/initial_state.rs index 56bc3921..0d28d185 100644 --- a/crates/ruma-events/tests/initial_state.rs +++ b/crates/ruma-events/tests/initial_state.rs @@ -1,5 +1,7 @@ +use std::convert::TryFrom; + use matches::assert_matches; -use ruma_events::{AnyInitialStateEvent, InitialStateEvent}; +use ruma_events::{room::name::RoomName, AnyInitialStateEvent, InitialStateEvent}; use serde_json::json; #[test] @@ -11,6 +13,6 @@ fn deserialize_initial_state_event() { })) .unwrap(), AnyInitialStateEvent::RoomName(InitialStateEvent { content, state_key}) - if content.name() == Some("foo") && state_key.is_empty() + if content.name() == RoomName::try_from("foo".to_owned()).ok().as_ref() && state_key.is_empty() ); } diff --git a/crates/ruma-events/tests/stripped.rs b/crates/ruma-events/tests/stripped.rs index eff66dce..fa98d225 100644 --- a/crates/ruma-events/tests/stripped.rs +++ b/crates/ruma-events/tests/stripped.rs @@ -1,6 +1,8 @@ +use std::convert::TryFrom; + use js_int::uint; use ruma_events::{ - room::{join_rules::JoinRule, topic::TopicEventContent}, + room::{join_rules::JoinRule, name::RoomName, topic::TopicEventContent}, AnyStateEventContent, AnyStrippedStateEvent, StrippedStateEvent, }; use ruma_identifiers::{mxc_uri, user_id}; @@ -94,7 +96,7 @@ fn deserialize_stripped_state_events() { let event = from_json_value::(name_event).unwrap(); match event { AnyStrippedStateEvent::RoomName(event) => { - assert_eq!(event.content.name(), Some("Ruma")); + assert_eq!(event.content.name(), RoomName::try_from("Ruma".to_owned()).ok().as_ref()); assert_eq!(event.state_key, ""); assert_eq!(event.sender.to_string(), "@example:localhost"); }