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