Skip to main content

nextcloud_talk/
room_type.rs

1//! Conversation room type enumeration for Nextcloud Talk.
2
3use std::fmt;
4
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6
7/// The type of a Nextcloud Talk conversation.
8///
9/// Nextcloud Talk identifies conversation types by numeric codes:
10/// - One-to-one conversation (`1`)
11/// - Group conversation (`2`)
12/// - Public link conversation (`3`)
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
14#[repr(u8)]
15pub enum RoomType {
16    /// One-to-one direct conversation between two users (`1`).
17    OneToOne = 1,
18    /// Group conversation (`2`).
19    Group = 2,
20    /// Public conversation accessible via public link (`3`).
21    Public = 3,
22}
23
24impl RoomType {
25    /// Numeric code sent over the wire.
26    pub const fn as_u8(self) -> u8 {
27        self as u8
28    }
29
30    /// Maps a wire code to a [`RoomType`], returning `None` for unknown codes.
31    pub const fn from_u8(code: u8) -> Option<Self> {
32        match code {
33            1 => Some(Self::OneToOne),
34            2 => Some(Self::Group),
35            3 => Some(Self::Public),
36            _ => None,
37        }
38    }
39
40    /// Human-readable label for this room type.
41    pub const fn as_str(self) -> &'static str {
42        match self {
43            Self::OneToOne => "one-to-one",
44            Self::Group => "group",
45            Self::Public => "public",
46        }
47    }
48}
49
50impl Serialize for RoomType {
51    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
52        serializer.serialize_u8(self.as_u8())
53    }
54}
55
56impl<'de> Deserialize<'de> for RoomType {
57    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
58        struct RoomTypeVisitor;
59
60        impl serde::de::Visitor<'_> for RoomTypeVisitor {
61            type Value = RoomType;
62
63            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64                f.write_str("a valid Nextcloud Talk room type code (1, 2, or 3)")
65            }
66
67            fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<RoomType, E> {
68                let code = u8::try_from(v)
69                    .map_err(|_| E::custom(format!("room type code {v} out of range")))?;
70                RoomType::from_u8(code)
71                    .ok_or_else(|| E::custom(format!("unknown room type code {code}")))
72            }
73
74            fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<RoomType, E> {
75                let code = u8::try_from(v)
76                    .map_err(|_| E::custom(format!("room type code {v} out of range")))?;
77                RoomType::from_u8(code)
78                    .ok_or_else(|| E::custom(format!("unknown room type code {code}")))
79            }
80
81            fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<RoomType, E> {
82                let code: u8 = v
83                    .trim()
84                    .parse()
85                    .map_err(|_| E::custom(format!("invalid room type string '{v}'")))?;
86                RoomType::from_u8(code)
87                    .ok_or_else(|| E::custom(format!("unknown room type code {code}")))
88            }
89        }
90
91        deserializer.deserialize_any(RoomTypeVisitor)
92    }
93}
94
95impl fmt::Display for RoomType {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        f.write_str(self.as_str())
98    }
99}
100
101impl TryFrom<u8> for RoomType {
102    type Error = UnknownRoomTypeError;
103
104    fn try_from(value: u8) -> Result<Self, Self::Error> {
105        Self::from_u8(value).ok_or(UnknownRoomTypeError(value))
106    }
107}
108
109impl From<RoomType> for u8 {
110    fn from(room_type: RoomType) -> Self {
111        room_type.as_u8()
112    }
113}
114
115/// Error returned when converting an unrecognized integer into a [`RoomType`].
116#[derive(Clone, Copy, Debug, PartialEq, Eq)]
117pub struct UnknownRoomTypeError(u8);
118
119impl UnknownRoomTypeError {
120    /// The unrecognised numeric code.
121    pub const fn code(self) -> u8 {
122        self.0
123    }
124}
125
126impl fmt::Display for UnknownRoomTypeError {
127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128        write!(f, "unknown room type code: {}", self.0)
129    }
130}
131
132impl std::error::Error for UnknownRoomTypeError {}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn room_type_display_and_as_str() {
140        assert_eq!(RoomType::OneToOne.as_str(), "one-to-one");
141        assert_eq!(RoomType::Group.as_str(), "group");
142        assert_eq!(RoomType::Public.as_str(), "public");
143        assert_eq!(RoomType::OneToOne.to_string(), "one-to-one");
144    }
145
146    #[test]
147    fn room_type_conversion_u8() {
148        assert_eq!(RoomType::from_u8(1), Some(RoomType::OneToOne));
149        assert_eq!(RoomType::from_u8(2), Some(RoomType::Group));
150        assert_eq!(RoomType::from_u8(3), Some(RoomType::Public));
151        assert_eq!(RoomType::from_u8(99), None);
152
153        assert_eq!(RoomType::try_from(1), Ok(RoomType::OneToOne));
154        assert_eq!(RoomType::try_from(2), Ok(RoomType::Group));
155        assert_eq!(RoomType::try_from(3), Ok(RoomType::Public));
156        let err = RoomType::try_from(99).unwrap_err();
157        assert_eq!(err.code(), 99);
158        assert_eq!(err.to_string(), "unknown room type code: 99");
159
160        assert_eq!(u8::from(RoomType::OneToOne), 1);
161        assert_eq!(u8::from(RoomType::Group), 2);
162        assert_eq!(u8::from(RoomType::Public), 3);
163    }
164
165    #[test]
166    fn room_type_serde_round_trip() {
167        let serialized = serde_json::to_string(&RoomType::Group).expect("serialize");
168        assert_eq!(serialized, "2");
169        let deserialized: RoomType = serde_json::from_str(&serialized).expect("deserialize");
170        assert_eq!(deserialized, RoomType::Group);
171
172        let from_str: RoomType = serde_json::from_str("\"3\"").expect("deserialize from string");
173        assert_eq!(from_str, RoomType::Public);
174    }
175}