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