//! Matrix room version identifiers. use std::{cmp::Ordering, convert::TryFrom, str::FromStr}; use ruma_serde_macros::DisplayAsRefStr; #[cfg(feature = "serde")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; /// 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().as_ref(), "1"); /// ``` /// /// Any string consisting of at minimum 1, at maximum 32 unicode codepoints is a room version ID. /// Custom room versions or ones that were introduced into the specification after this code was /// written are represented by a hidden enum variant. You can still construct them the same, and /// check for them using one of `RoomVersionId`s `PartialEq` implementations or through `.as_str()`. #[derive(Clone, Debug, PartialEq, Eq, Hash, DisplayAsRefStr)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub enum RoomVersionId { /// A version 1 room. V1, /// A version 2 room. V2, /// A version 3 room. V3, /// A version 4 room. V4, /// A version 5 room. V5, /// A version 6 room. V6, /// A version 7 room. V7, /// A version 8 room. V8, /// A version 9 room. V9, #[doc(hidden)] _Custom(CustomRoomVersion), } impl RoomVersionId { /// Creates a string slice from this `RoomVersionId`. pub fn as_str(&self) -> &str { // FIXME: Add support for non-`str`-deref'ing types for fallback to AsRefStr derive and // implement this function in terms of `AsRef` match &self { Self::V1 => "1", Self::V2 => "2", Self::V3 => "3", Self::V4 => "4", Self::V5 => "5", Self::V6 => "6", Self::V7 => "7", Self::V8 => "8", Self::V9 => "9", Self::_Custom(version) => version.as_str(), } } /// Creates a byte slice from this `RoomVersionId`. pub fn as_bytes(&self) -> &[u8] { self.as_str().as_bytes() } } impl From for String { fn from(id: RoomVersionId) -> Self { match id { RoomVersionId::V1 => "1".to_owned(), RoomVersionId::V2 => "2".to_owned(), RoomVersionId::V3 => "3".to_owned(), RoomVersionId::V4 => "4".to_owned(), RoomVersionId::V5 => "5".to_owned(), RoomVersionId::V6 => "6".to_owned(), RoomVersionId::V7 => "7".to_owned(), RoomVersionId::V8 => "8".to_owned(), RoomVersionId::V9 => "9".to_owned(), RoomVersionId::_Custom(version) => version.into(), } } } impl AsRef for RoomVersionId { fn as_ref(&self) -> &str { self.as_str() } } impl PartialOrd for RoomVersionId { /// Compare the two given room version IDs by comparing their string representations. /// /// Please be aware that room version IDs don't have a defined ordering in the Matrix /// specification. This implementation only exists to be able to use `RoomVersionId`s or /// types containing `RoomVersionId`s as `BTreeMap` keys. fn partial_cmp(&self, other: &RoomVersionId) -> Option { self.as_ref().partial_cmp(other.as_ref()) } } impl Ord for RoomVersionId { /// Compare the two given room version IDs by comparing their string representations. /// /// Please be aware that room version IDs don't have a defined ordering in the Matrix /// specification. This implementation only exists to be able to use `RoomVersionId`s or /// types containing `RoomVersionId`s as `BTreeMap` keys. fn cmp(&self, other: &Self) -> Ordering { self.as_ref().cmp(other.as_ref()) } } #[cfg(feature = "serde")] impl Serialize for RoomVersionId { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serializer.serialize_str(self.as_ref()) } } #[cfg(feature = "serde")] impl<'de> Deserialize<'de> for RoomVersionId { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { crate::deserialize_id(deserializer, "a Matrix room version ID as a string") } } /// Attempts to create a new Matrix room version ID from a string representation. fn try_from(room_version_id: S) -> Result where S: AsRef + Into>, { let version = match room_version_id.as_ref() { "1" => RoomVersionId::V1, "2" => RoomVersionId::V2, "3" => RoomVersionId::V3, "4" => RoomVersionId::V4, "5" => RoomVersionId::V5, "6" => RoomVersionId::V6, "7" => RoomVersionId::V7, "8" => RoomVersionId::V8, "9" => RoomVersionId::V9, custom => { ruma_identifiers_validation::room_version_id::validate(custom)?; RoomVersionId::_Custom(CustomRoomVersion(room_version_id.into())) } }; Ok(version) } impl FromStr for RoomVersionId { type Err = crate::Error; fn from_str(s: &str) -> Result { try_from(s) } } impl TryFrom<&str> for RoomVersionId { type Error = crate::Error; fn try_from(s: &str) -> Result { try_from(s) } } impl TryFrom for RoomVersionId { type Error = crate::Error; fn try_from(s: String) -> Result { try_from(s) } } impl PartialEq<&str> for RoomVersionId { fn eq(&self, other: &&str) -> bool { self.as_str() == *other } } impl PartialEq for &str { fn eq(&self, other: &RoomVersionId) -> bool { *self == other.as_str() } } impl PartialEq for RoomVersionId { fn eq(&self, other: &String) -> bool { self.as_str() == other } } impl PartialEq for String { fn eq(&self, other: &RoomVersionId) -> bool { self == other.as_str() } } #[derive(Clone, Debug, PartialEq, Eq, Hash)] #[doc(hidden)] pub struct CustomRoomVersion(Box); #[doc(hidden)] impl CustomRoomVersion { /// Creates a string slice from this `CustomRoomVersion` pub fn as_str(&self) -> &str { &self.0 } } #[doc(hidden)] impl From for String { fn from(v: CustomRoomVersion) -> Self { v.0.into() } } #[doc(hidden)] impl AsRef for CustomRoomVersion { fn as_ref(&self) -> &str { self.as_str() } } #[cfg(test)] mod tests { use std::convert::TryFrom; use super::RoomVersionId; use crate::Error; #[test] fn valid_version_1_room_version_id() { assert_eq!( RoomVersionId::try_from("1").expect("Failed to create RoomVersionId.").as_ref(), "1" ); } #[test] fn valid_version_2_room_version_id() { assert_eq!( RoomVersionId::try_from("2").expect("Failed to create RoomVersionId.").as_ref(), "2" ); } #[test] fn valid_version_3_room_version_id() { assert_eq!( RoomVersionId::try_from("3").expect("Failed to create RoomVersionId.").as_ref(), "3" ); } #[test] fn valid_version_4_room_version_id() { assert_eq!( RoomVersionId::try_from("4").expect("Failed to create RoomVersionId.").as_ref(), "4" ); } #[test] fn valid_version_5_room_version_id() { assert_eq!( RoomVersionId::try_from("5").expect("Failed to create RoomVersionId.").as_ref(), "5" ); } #[test] fn valid_version_6_room_version_id() { assert_eq!( RoomVersionId::try_from("6").expect("Failed to create RoomVersionId.").as_ref(), "6" ); } #[test] fn valid_custom_room_version_id() { assert_eq!( RoomVersionId::try_from("io.ruma.1").expect("Failed to create RoomVersionId.").as_ref(), "io.ruma.1" ); } #[test] fn empty_room_version_id() { assert_eq!(RoomVersionId::try_from(""), Err(Error::EmptyRoomVersionId)); } #[test] fn over_max_code_point_room_version_id() { assert_eq!( RoomVersionId::try_from("0123456789012345678901234567890123456789"), Err(Error::MaximumLengthExceeded) ); } #[cfg(feature = "serde")] #[test] fn serialize_official_room_id() { assert_eq!( serde_json::to_string( &RoomVersionId::try_from("1").expect("Failed to create RoomVersionId.") ) .expect("Failed to convert RoomVersionId to JSON."), r#""1""# ); } #[cfg(feature = "serde")] #[test] fn deserialize_official_room_id() { let deserialized = serde_json::from_str::(r#""1""#) .expect("Failed to convert RoomVersionId to JSON."); assert_eq!(deserialized, RoomVersionId::V1); assert_eq!( deserialized, RoomVersionId::try_from("1").expect("Failed to create RoomVersionId.") ); } #[cfg(feature = "serde")] #[test] fn serialize_custom_room_id() { assert_eq!( serde_json::to_string( &RoomVersionId::try_from("io.ruma.1").expect("Failed to create RoomVersionId.") ) .expect("Failed to convert RoomVersionId to JSON."), r#""io.ruma.1""# ); } #[cfg(feature = "serde")] #[test] fn deserialize_custom_room_id() { let deserialized = serde_json::from_str::(r#""io.ruma.1""#) .expect("Failed to convert RoomVersionId to JSON."); assert_eq!( deserialized, RoomVersionId::try_from("io.ruma.1").expect("Failed to create RoomVersionId.") ); } }