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