ruma_common/identifiers/
room_version_id.rs

1//! Matrix room version identifiers.
2
3use std::{cmp::Ordering, str::FromStr};
4
5use ruma_macros::DisplayAsRefStr;
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7
8use super::IdParseError;
9
10/// A Matrix [room version] ID.
11///
12/// A `RoomVersionId` can be or converted or deserialized from a string slice, and can be converted
13/// or serialized back into a string as needed.
14///
15/// ```
16/// # use ruma_common::RoomVersionId;
17/// assert_eq!(RoomVersionId::try_from("1").unwrap().as_str(), "1");
18/// ```
19///
20/// Any string consisting of at minimum 1, at maximum 32 unicode codepoints is a room version ID.
21/// Custom room versions or ones that were introduced into the specification after this code was
22/// written are represented by a hidden enum variant. You can still construct them the same, and
23/// check for them using one of `RoomVersionId`s `PartialEq` implementations or through `.as_str()`.
24///
25/// The `PartialOrd` and `Ord` implementations of this type sort the variants by comparing their
26/// string representations, which have no special meaning. To check the compatibility between
27/// room versions, one should use the [`RoomVersionRules`] instead.
28///
29/// [room version]: https://spec.matrix.org/latest/rooms/
30#[derive(Clone, Debug, PartialEq, Eq, Hash, DisplayAsRefStr)]
31#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
32pub enum RoomVersionId {
33    /// A version 1 room.
34    V1,
35
36    /// A version 2 room.
37    V2,
38
39    /// A version 3 room.
40    V3,
41
42    /// A version 4 room.
43    V4,
44
45    /// A version 5 room.
46    V5,
47
48    /// A version 6 room.
49    V6,
50
51    /// A version 7 room.
52    V7,
53
54    /// A version 8 room.
55    V8,
56
57    /// A version 9 room.
58    V9,
59
60    /// A version 10 room.
61    V10,
62
63    /// A version 11 room.
64    V11,
65
66    #[doc(hidden)]
67    _Custom(CustomRoomVersion),
68}
69
70impl RoomVersionId {
71    /// Creates a string slice from this `RoomVersionId`.
72    pub fn as_str(&self) -> &str {
73        // FIXME: Add support for non-`str`-deref'ing types for fallback to AsRefStr derive and
74        //        implement this function in terms of `AsRef<str>`
75        match &self {
76            Self::V1 => "1",
77            Self::V2 => "2",
78            Self::V3 => "3",
79            Self::V4 => "4",
80            Self::V5 => "5",
81            Self::V6 => "6",
82            Self::V7 => "7",
83            Self::V8 => "8",
84            Self::V9 => "9",
85            Self::V10 => "10",
86            Self::V11 => "11",
87            Self::_Custom(version) => version.as_str(),
88        }
89    }
90
91    /// Creates a byte slice from this `RoomVersionId`.
92    pub fn as_bytes(&self) -> &[u8] {
93        self.as_str().as_bytes()
94    }
95}
96
97impl From<RoomVersionId> for String {
98    fn from(id: RoomVersionId) -> Self {
99        match id {
100            RoomVersionId::V1 => "1".to_owned(),
101            RoomVersionId::V2 => "2".to_owned(),
102            RoomVersionId::V3 => "3".to_owned(),
103            RoomVersionId::V4 => "4".to_owned(),
104            RoomVersionId::V5 => "5".to_owned(),
105            RoomVersionId::V6 => "6".to_owned(),
106            RoomVersionId::V7 => "7".to_owned(),
107            RoomVersionId::V8 => "8".to_owned(),
108            RoomVersionId::V9 => "9".to_owned(),
109            RoomVersionId::V10 => "10".to_owned(),
110            RoomVersionId::V11 => "11".to_owned(),
111            RoomVersionId::_Custom(version) => version.into(),
112        }
113    }
114}
115
116impl AsRef<str> for RoomVersionId {
117    fn as_ref(&self) -> &str {
118        self.as_str()
119    }
120}
121
122impl AsRef<[u8]> for RoomVersionId {
123    fn as_ref(&self) -> &[u8] {
124        self.as_bytes()
125    }
126}
127
128impl PartialOrd for RoomVersionId {
129    /// Compare the two given room version IDs by comparing their string representations.
130    ///
131    /// Please be aware that room version IDs don't have a defined ordering in the Matrix
132    /// specification. This implementation only exists to be able to use `RoomVersionId`s or
133    /// types containing `RoomVersionId`s as `BTreeMap` keys.
134    fn partial_cmp(&self, other: &RoomVersionId) -> Option<Ordering> {
135        Some(self.cmp(other))
136    }
137}
138
139impl Ord for RoomVersionId {
140    /// Compare the two given room version IDs by comparing their string representations.
141    ///
142    /// Please be aware that room version IDs don't have a defined ordering in the Matrix
143    /// specification. This implementation only exists to be able to use `RoomVersionId`s or
144    /// types containing `RoomVersionId`s as `BTreeMap` keys.
145    fn cmp(&self, other: &Self) -> Ordering {
146        self.as_str().cmp(other.as_str())
147    }
148}
149
150impl Serialize for RoomVersionId {
151    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
152    where
153        S: Serializer,
154    {
155        serializer.serialize_str(self.as_str())
156    }
157}
158
159impl<'de> Deserialize<'de> for RoomVersionId {
160    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
161    where
162        D: Deserializer<'de>,
163    {
164        super::deserialize_id(deserializer, "a Matrix room version ID as a string")
165    }
166}
167
168/// Attempts to create a new Matrix room version ID from a string representation.
169fn try_from<S>(room_version_id: S) -> Result<RoomVersionId, IdParseError>
170where
171    S: AsRef<str> + Into<Box<str>>,
172{
173    let version = match room_version_id.as_ref() {
174        "1" => RoomVersionId::V1,
175        "2" => RoomVersionId::V2,
176        "3" => RoomVersionId::V3,
177        "4" => RoomVersionId::V4,
178        "5" => RoomVersionId::V5,
179        "6" => RoomVersionId::V6,
180        "7" => RoomVersionId::V7,
181        "8" => RoomVersionId::V8,
182        "9" => RoomVersionId::V9,
183        "10" => RoomVersionId::V10,
184        "11" => RoomVersionId::V11,
185        custom => {
186            ruma_identifiers_validation::room_version_id::validate(custom)?;
187            RoomVersionId::_Custom(CustomRoomVersion(room_version_id.into()))
188        }
189    };
190
191    Ok(version)
192}
193
194impl FromStr for RoomVersionId {
195    type Err = IdParseError;
196
197    fn from_str(s: &str) -> Result<Self, Self::Err> {
198        try_from(s)
199    }
200}
201
202impl TryFrom<&str> for RoomVersionId {
203    type Error = IdParseError;
204
205    fn try_from(s: &str) -> Result<Self, Self::Error> {
206        try_from(s)
207    }
208}
209
210impl TryFrom<String> for RoomVersionId {
211    type Error = IdParseError;
212
213    fn try_from(s: String) -> Result<Self, Self::Error> {
214        try_from(s)
215    }
216}
217
218impl PartialEq<&str> for RoomVersionId {
219    fn eq(&self, other: &&str) -> bool {
220        self.as_str() == *other
221    }
222}
223
224impl PartialEq<RoomVersionId> for &str {
225    fn eq(&self, other: &RoomVersionId) -> bool {
226        *self == other.as_str()
227    }
228}
229
230impl PartialEq<String> for RoomVersionId {
231    fn eq(&self, other: &String) -> bool {
232        self.as_str() == other
233    }
234}
235
236impl PartialEq<RoomVersionId> for String {
237    fn eq(&self, other: &RoomVersionId) -> bool {
238        self == other.as_str()
239    }
240}
241
242#[derive(Clone, Debug, PartialEq, Eq, Hash)]
243#[doc(hidden)]
244#[allow(unknown_lints, unnameable_types)]
245pub struct CustomRoomVersion(Box<str>);
246
247#[doc(hidden)]
248impl CustomRoomVersion {
249    /// Creates a string slice from this `CustomRoomVersion`
250    pub fn as_str(&self) -> &str {
251        &self.0
252    }
253}
254
255#[doc(hidden)]
256impl From<CustomRoomVersion> for String {
257    fn from(v: CustomRoomVersion) -> Self {
258        v.0.into()
259    }
260}
261
262#[doc(hidden)]
263impl AsRef<str> for CustomRoomVersion {
264    fn as_ref(&self) -> &str {
265        self.as_str()
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::RoomVersionId;
272    use crate::IdParseError;
273
274    #[test]
275    fn valid_version_1_room_version_id() {
276        assert_eq!(
277            RoomVersionId::try_from("1").expect("Failed to create RoomVersionId.").as_str(),
278            "1"
279        );
280    }
281
282    #[test]
283    fn valid_version_2_room_version_id() {
284        assert_eq!(
285            RoomVersionId::try_from("2").expect("Failed to create RoomVersionId.").as_str(),
286            "2"
287        );
288    }
289
290    #[test]
291    fn valid_version_3_room_version_id() {
292        assert_eq!(
293            RoomVersionId::try_from("3").expect("Failed to create RoomVersionId.").as_str(),
294            "3"
295        );
296    }
297
298    #[test]
299    fn valid_version_4_room_version_id() {
300        assert_eq!(
301            RoomVersionId::try_from("4").expect("Failed to create RoomVersionId.").as_str(),
302            "4"
303        );
304    }
305
306    #[test]
307    fn valid_version_5_room_version_id() {
308        assert_eq!(
309            RoomVersionId::try_from("5").expect("Failed to create RoomVersionId.").as_str(),
310            "5"
311        );
312    }
313
314    #[test]
315    fn valid_version_6_room_version_id() {
316        assert_eq!(
317            RoomVersionId::try_from("6").expect("Failed to create RoomVersionId.").as_str(),
318            "6"
319        );
320    }
321
322    #[test]
323    fn valid_custom_room_version_id() {
324        assert_eq!(
325            RoomVersionId::try_from("io.ruma.1").expect("Failed to create RoomVersionId.").as_str(),
326            "io.ruma.1"
327        );
328    }
329
330    #[test]
331    fn empty_room_version_id() {
332        assert_eq!(RoomVersionId::try_from(""), Err(IdParseError::Empty));
333    }
334
335    #[test]
336    fn over_max_code_point_room_version_id() {
337        assert_eq!(
338            RoomVersionId::try_from("0123456789012345678901234567890123456789"),
339            Err(IdParseError::MaximumLengthExceeded)
340        );
341    }
342
343    #[test]
344    fn serialize_official_room_id() {
345        assert_eq!(
346            serde_json::to_string(
347                &RoomVersionId::try_from("1").expect("Failed to create RoomVersionId.")
348            )
349            .expect("Failed to convert RoomVersionId to JSON."),
350            r#""1""#
351        );
352    }
353
354    #[test]
355    fn deserialize_official_room_id() {
356        let deserialized = serde_json::from_str::<RoomVersionId>(r#""1""#)
357            .expect("Failed to convert RoomVersionId to JSON.");
358
359        assert_eq!(deserialized, RoomVersionId::V1);
360
361        assert_eq!(
362            deserialized,
363            RoomVersionId::try_from("1").expect("Failed to create RoomVersionId.")
364        );
365    }
366
367    #[test]
368    fn serialize_custom_room_id() {
369        assert_eq!(
370            serde_json::to_string(
371                &RoomVersionId::try_from("io.ruma.1").expect("Failed to create RoomVersionId.")
372            )
373            .expect("Failed to convert RoomVersionId to JSON."),
374            r#""io.ruma.1""#
375        );
376    }
377
378    #[test]
379    fn deserialize_custom_room_id() {
380        let deserialized = serde_json::from_str::<RoomVersionId>(r#""io.ruma.1""#)
381            .expect("Failed to convert RoomVersionId to JSON.");
382
383        assert_eq!(
384            deserialized,
385            RoomVersionId::try_from("io.ruma.1").expect("Failed to create RoomVersionId.")
386        );
387    }
388
389    #[test]
390    fn custom_room_id_invalid_character() {
391        assert!(serde_json::from_str::<RoomVersionId>(r#""io_ruma_1""#).is_err());
392        assert!(serde_json::from_str::<RoomVersionId>(r#""=""#).is_err());
393        assert!(serde_json::from_str::<RoomVersionId>(r#""/""#).is_err());
394        assert!(serde_json::from_str::<RoomVersionId>(r#"".""#).is_ok());
395        assert!(serde_json::from_str::<RoomVersionId>(r#""-""#).is_ok());
396        assert_eq!(
397            RoomVersionId::try_from("io_ruma_1").unwrap_err(),
398            IdParseError::InvalidCharacters
399        );
400    }
401}