diff --git a/src/diesel_integration.rs b/src/diesel_integration.rs index 8ae777e6..65e5ae63 100644 --- a/src/diesel_integration.rs +++ b/src/diesel_integration.rs @@ -38,4 +38,5 @@ diesel_impl!(EventId); diesel_impl!(RoomAliasId); diesel_impl!(RoomId); diesel_impl!(RoomIdOrAliasId); +diesel_impl!(RoomVersionId); diesel_impl!(UserId); diff --git a/src/error.rs b/src/error.rs index a23ece09..397bd7be 100644 --- a/src/error.rs +++ b/src/error.rs @@ -16,9 +16,9 @@ pub enum Error { InvalidCharacters, /// The domain part of the the ID string is not a valid IP address or DNS name. InvalidHost, - /// The ID exceeds 255 bytes. + /// The ID exceeds 255 bytes (or 32 codepoints for a room version ID.) MaximumLengthExceeded, - /// The ID is less than 4 characters. + /// The ID is less than 4 characters (or is an empty room version ID.) MinimumLengthNotSatisfied, /// The ID is missing the colon delimiter between localpart and server name. MissingDelimiter, diff --git a/src/lib.rs b/src/lib.rs index 4672f296..a02bd9b8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ //! Crate **ruma_identifiers** contains types for [Matrix](https://matrix.org/) identifiers -//! for events, rooms, room aliases, and users. +//! for events, rooms, room aliases, room versions, and users. #![deny( missing_copy_implementations, @@ -40,7 +40,7 @@ pub use url::Host; pub use crate::{ error::Error, event_id::EventId, room_alias_id::RoomAliasId, room_id::RoomId, - room_id_or_room_alias_id::RoomIdOrAliasId, user_id::UserId, + room_id_or_room_alias_id::RoomIdOrAliasId, room_version_id::RoomVersionId, user_id::UserId, }; #[cfg(feature = "diesel")] @@ -50,9 +50,10 @@ mod event_id; mod room_alias_id; mod room_id; mod room_id_or_room_alias_id; +mod room_version_id; mod user_id; -/// All events must be 255 bytes or less. +/// All identifiers must be 255 bytes or less. const MAX_BYTES: usize = 255; /// The minimum number of characters an ID can be. /// diff --git a/src/room_version_id.rs b/src/room_version_id.rs new file mode 100644 index 00000000..caa374e0 --- /dev/null +++ b/src/room_version_id.rs @@ -0,0 +1,351 @@ +//! Matrix room version identifiers. + +use std::{ + convert::TryFrom, + fmt::{Display, Formatter, Result as FmtResult}, +}; + +#[cfg(feature = "diesel")] +use diesel::sql_types::Text; +use serde::{ + de::{Error as SerdeError, Unexpected, Visitor}, + Deserialize, Deserializer, Serialize, Serializer, +}; + +use crate::error::Error; + +/// Room version identifiers cannot be more than 32 code points. +const MAX_CODE_POINTS: usize = 32; + +/// A Matrix room version ID. +/// +/// A `RoomVersionId` can be or converted or deserialized from a string slice, and can be converted +/// or serialized back into a string as needed. +/// +/// ``` +/// # use std::convert::TryFrom; +/// # use ruma_identifiers::RoomVersionId; +/// assert_eq!(RoomVersionId::try_from("1").unwrap().to_string(), "1"); +/// ``` +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] +#[cfg_attr(feature = "diesel", sql_type = "Text")] +pub struct RoomVersionId(InnerRoomVersionId); + +/// Possibile values for room version, distinguishing between official Matrix versions and custom +/// versions. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +enum InnerRoomVersionId { + /// A version 1 room. + Version1, + /// A version 2 room. + Version2, + /// A version 3 room. + Version3, + /// A version 4 room. + Version4, + /// A custom room version. + Custom(String), +} + +/// A serde visitor for `RoomVersionId`. +struct RoomVersionIdVisitor; + +impl RoomVersionId { + /// Creates a version 1 room ID. + pub fn version_1() -> Self { + Self(InnerRoomVersionId::Version1) + } + + /// Creates a version 2 room ID. + pub fn version_2() -> Self { + Self(InnerRoomVersionId::Version2) + } + + /// Creates a version 3 room ID. + pub fn version_3() -> Self { + Self(InnerRoomVersionId::Version3) + } + + /// Creates a version 4 room ID. + pub fn version_4() -> Self { + Self(InnerRoomVersionId::Version4) + } + + /// Creates a custom room version ID from the given string slice. + pub fn custom(id: &str) -> Self { + Self(InnerRoomVersionId::Custom(id.to_string())) + } + + /// Whether or not this room version is an official one specified by the Matrix protocol. + pub fn is_official(&self) -> bool { + !self.is_custom() + } + + /// Whether or not this is a custom room version. + pub fn is_custom(&self) -> bool { + match self.0 { + InnerRoomVersionId::Custom(_) => true, + _ => false, + } + } + + /// Whether or not this is a version 1 room. + pub fn is_version_1(&self) -> bool { + self.0 == InnerRoomVersionId::Version1 + } + + /// Whether or not this is a version 2 room. + pub fn is_version_2(&self) -> bool { + self.0 == InnerRoomVersionId::Version2 + } + + /// Whether or not this is a version 3 room. + pub fn is_version_3(&self) -> bool { + self.0 == InnerRoomVersionId::Version3 + } + + /// Whether or not this is a version 4 room. + pub fn is_version_4(&self) -> bool { + self.0 == InnerRoomVersionId::Version4 + } +} + +impl Display for RoomVersionId { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + let message = match self.0 { + InnerRoomVersionId::Version1 => "1", + InnerRoomVersionId::Version2 => "2", + InnerRoomVersionId::Version3 => "3", + InnerRoomVersionId::Version4 => "4", + InnerRoomVersionId::Custom(ref version) => version, + }; + + write!(f, "{}", message) + } +} + +impl Serialize for RoomVersionId { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for RoomVersionId { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(RoomVersionIdVisitor) + } +} + +impl<'a> TryFrom<&'a str> for RoomVersionId { + type Error = Error; + + /// Attempts to create a new Matrix room version ID from a string representation. + fn try_from(room_version_id: &'a str) -> Result { + let version = match room_version_id { + "1" => Self(InnerRoomVersionId::Version1), + "2" => Self(InnerRoomVersionId::Version2), + "3" => Self(InnerRoomVersionId::Version3), + "4" => Self(InnerRoomVersionId::Version4), + custom => { + if custom.is_empty() { + return Err(Error::MinimumLengthNotSatisfied); + } else if custom.chars().count() > MAX_CODE_POINTS { + return Err(Error::MaximumLengthExceeded); + } else { + Self(InnerRoomVersionId::Custom(custom.to_string())) + } + } + }; + + Ok(version) + } +} + +impl<'de> Visitor<'de> for RoomVersionIdVisitor { + type Value = RoomVersionId; + + fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { + write!(formatter, "a Matrix room version ID as a string") + } + + fn visit_str(self, v: &str) -> Result + where + E: SerdeError, + { + match RoomVersionId::try_from(v) { + Ok(room_id) => Ok(room_id), + Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), + } + } +} + +#[cfg(test)] +mod tests { + use std::convert::TryFrom; + + use serde_json::{from_str, to_string}; + + use super::RoomVersionId; + use crate::error::Error; + + #[test] + fn valid_version_1_room_version_id() { + assert_eq!( + RoomVersionId::try_from("1") + .expect("Failed to create RoomVersionId.") + .to_string(), + "1" + ); + } + #[test] + fn valid_version_2_room_version_id() { + assert_eq!( + RoomVersionId::try_from("2") + .expect("Failed to create RoomVersionId.") + .to_string(), + "2" + ); + } + #[test] + fn valid_version_3_room_version_id() { + assert_eq!( + RoomVersionId::try_from("3") + .expect("Failed to create RoomVersionId.") + .to_string(), + "3" + ); + } + #[test] + fn valid_version_4_room_version_id() { + assert_eq!( + RoomVersionId::try_from("4") + .expect("Failed to create RoomVersionId.") + .to_string(), + "4" + ); + } + + #[test] + fn valid_custom_room_version_id() { + assert_eq!( + RoomVersionId::try_from("io.ruma.1") + .expect("Failed to create RoomVersionId.") + .to_string(), + "io.ruma.1" + ); + } + + #[test] + fn empty_room_version_id() { + assert_eq!( + RoomVersionId::try_from(""), + Err(Error::MinimumLengthNotSatisfied) + ); + } + + #[test] + fn over_max_code_point_room_version_id() { + assert_eq!( + RoomVersionId::try_from("0123456789012345678901234567890123456789"), + Err(Error::MaximumLengthExceeded) + ); + } + + #[test] + fn serialize_official_room_id() { + assert_eq!( + to_string(&RoomVersionId::try_from("1").expect("Failed to create RoomVersionId.")) + .expect("Failed to convert RoomVersionId to JSON."), + r#""1""# + ); + } + + #[test] + fn deserialize_official_room_id() { + let deserialized = + from_str::(r#""1""#).expect("Failed to convert RoomVersionId to JSON."); + + assert!(deserialized.is_version_1()); + assert!(deserialized.is_official()); + + assert_eq!( + deserialized, + RoomVersionId::try_from("1").expect("Failed to create RoomVersionId.") + ); + } + + #[test] + fn serialize_custom_room_id() { + assert_eq!( + to_string( + &RoomVersionId::try_from("io.ruma.1").expect("Failed to create RoomVersionId.") + ) + .expect("Failed to convert RoomVersionId to JSON."), + r#""io.ruma.1""# + ); + } + + #[test] + fn deserialize_custom_room_id() { + let deserialized = from_str::(r#""io.ruma.1""#) + .expect("Failed to convert RoomVersionId to JSON."); + + assert!(deserialized.is_custom()); + + assert_eq!( + deserialized, + RoomVersionId::try_from("io.ruma.1").expect("Failed to create RoomVersionId.") + ); + } + + #[test] + fn constructors() { + assert!(RoomVersionId::version_1().is_version_1()); + assert!(RoomVersionId::version_2().is_version_2()); + assert!(RoomVersionId::version_3().is_version_3()); + assert!(RoomVersionId::version_4().is_version_4()); + assert!(RoomVersionId::custom("foo").is_custom()); + } + + #[test] + fn predicate_methods() { + let version_1 = RoomVersionId::try_from("1").expect("Failed to create RoomVersionId."); + let version_2 = RoomVersionId::try_from("2").expect("Failed to create RoomVersionId."); + let version_3 = RoomVersionId::try_from("3").expect("Failed to create RoomVersionId."); + let version_4 = RoomVersionId::try_from("4").expect("Failed to create RoomVersionId."); + let custom = RoomVersionId::try_from("io.ruma.1").expect("Failed to create RoomVersionId."); + + assert!(version_1.is_version_1()); + assert!(version_2.is_version_2()); + assert!(version_3.is_version_3()); + assert!(version_4.is_version_4()); + + assert!(!version_1.is_version_2()); + assert!(!version_1.is_version_3()); + assert!(!version_1.is_version_4()); + + assert!(version_1.is_official()); + assert!(version_2.is_official()); + assert!(version_3.is_official()); + assert!(version_4.is_official()); + + assert!(!version_1.is_custom()); + assert!(!version_2.is_custom()); + assert!(!version_3.is_custom()); + assert!(!version_4.is_custom()); + + assert!(custom.is_custom()); + assert!(!custom.is_official()); + assert!(!custom.is_version_1()); + assert!(!custom.is_version_2()); + assert!(!custom.is_version_3()); + assert!(!custom.is_version_4()); + } +}