1use std::{collections::BTreeMap, error::Error, fmt, str::FromStr};
6
7use palpo_macros::EventContent;
8use salvo::oapi::ToSchema;
9use serde::{Deserialize, Serialize};
10
11use crate::PrivOwnedStr;
12use crate::serde::deserialize_cow_str;
13
14pub type Tags = BTreeMap<TagName, TagInfo>;
16
17#[derive(ToSchema, Deserialize, Serialize, Clone, Debug, EventContent)]
21#[palpo_event(type = "m.tag", kind = RoomAccountData)]
22pub struct TagEventContent {
23 pub tags: Tags,
25}
26
27impl TagEventContent {
28 pub fn new(tags: Tags) -> Self {
30 Self { tags }
31 }
32}
33
34impl From<Tags> for TagEventContent {
35 fn from(tags: Tags) -> Self {
36 Self::new(tags)
37 }
38}
39
40#[derive(ToSchema, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
42pub struct UserTagName {
43 name: String,
44}
45
46impl AsRef<str> for UserTagName {
47 fn as_ref(&self) -> &str {
48 &self.name
49 }
50}
51
52impl FromStr for UserTagName {
53 type Err = InvalidUserTagName;
54
55 fn from_str(s: &str) -> Result<Self, Self::Err> {
56 if s.starts_with("u.") {
57 Ok(Self { name: s.into() })
58 } else {
59 Err(InvalidUserTagName)
60 }
61 }
62}
63
64#[derive(Debug)]
67#[allow(clippy::exhaustive_structs)]
68pub struct InvalidUserTagName;
69
70impl fmt::Display for InvalidUserTagName {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 write!(f, "missing 'u.' prefix in UserTagName")
73 }
74}
75
76impl Error for InvalidUserTagName {}
77
78#[derive(ToSchema, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
80pub enum TagName {
81 Favorite,
85
86 LowPriority,
88
89 ServerNotice,
92
93 User(UserTagName),
95
96 #[doc(hidden)]
98 _Custom(PrivOwnedStr),
99}
100
101impl TagName {
102 pub fn display_name(&self) -> &str {
107 match self {
108 Self::_Custom(s) => {
109 let start = s.0.rfind('.').map(|p| p + 1).unwrap_or(0);
110 &self.as_ref()[start..]
111 }
112 _ => &self.as_ref()[2..],
113 }
114 }
115}
116
117impl AsRef<str> for TagName {
118 fn as_ref(&self) -> &str {
119 match self {
120 Self::Favorite => "m.favourite",
121 Self::LowPriority => "m.lowpriority",
122 Self::ServerNotice => "m.server_notice",
123 Self::User(tag) => tag.as_ref(),
124 Self::_Custom(s) => &s.0,
125 }
126 }
127}
128
129impl<T> From<T> for TagName
130where
131 T: AsRef<str> + Into<String>,
132{
133 fn from(s: T) -> TagName {
134 match s.as_ref() {
135 "m.favourite" => Self::Favorite,
136 "m.lowpriority" => Self::LowPriority,
137 "m.server_notice" => Self::ServerNotice,
138 s if s.starts_with("u.") => Self::User(UserTagName { name: s.into() }),
139 s => Self::_Custom(PrivOwnedStr(s.into())),
140 }
141 }
142}
143
144impl fmt::Display for TagName {
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 self.as_ref().fmt(f)
147 }
148}
149
150impl<'de> Deserialize<'de> for TagName {
151 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
152 where
153 D: serde::Deserializer<'de>,
154 {
155 let cow = deserialize_cow_str(deserializer)?;
156 Ok(cow.into())
157 }
158}
159
160impl Serialize for TagName {
161 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
162 where
163 S: serde::Serializer,
164 {
165 serializer.serialize_str(self.as_ref())
166 }
167}
168
169#[derive(ToSchema, Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
171pub struct TagInfo {
172 #[serde(skip_serializing_if = "Option::is_none")]
177 pub order: Option<f64>,
178}
179
180impl TagInfo {
181 pub fn new() -> Self {
183 Default::default()
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use maplit::btreemap;
190 use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
191
192 use super::{TagEventContent, TagInfo, TagName};
193
194 #[test]
195 fn serialization() {
196 let tags = btreemap! {
197 TagName::Favorite => TagInfo::new(),
198 TagName::LowPriority => TagInfo::new(),
199 TagName::ServerNotice => TagInfo::new(),
200 "u.custom".to_owned().into() => TagInfo { order: Some(0.9) }
201 };
202
203 let content = TagEventContent { tags };
204
205 assert_eq!(
206 to_json_value(content).unwrap(),
207 json!({
208 "tags": {
209 "m.favourite": {},
210 "m.lowpriority": {},
211 "m.server_notice": {},
212 "u.custom": {
213 "order": 0.9
214 }
215 },
216 })
217 );
218 }
219
220 #[test]
221 fn deserialize_tag_info() {
222 let json = json!({});
223 assert_eq!(from_json_value::<TagInfo>(json).unwrap(), TagInfo::default());
224
225 let json = json!({ "order": null });
226 assert_eq!(from_json_value::<TagInfo>(json).unwrap(), TagInfo::default());
227
228 let json = json!({ "order": 0.42 });
229 assert_eq!(from_json_value::<TagInfo>(json).unwrap(), TagInfo { order: Some(0.42) });
230
231 #[cfg(feature = "compat-tag-info")]
232 {
233 let json = json!({ "order": "0.5" });
234 assert_eq!(from_json_value::<TagInfo>(json).unwrap(), TagInfo { order: Some(0.5) });
235
236 let json = json!({ "order": ".5" });
237 assert_eq!(from_json_value::<TagInfo>(json).unwrap(), TagInfo { order: Some(0.5) });
238 }
239
240 #[cfg(not(feature = "compat-tag-info"))]
241 {
242 let json = json!({ "order": "0.5" });
243 assert!(from_json_value::<TagInfo>(json).is_err());
244 }
245 }
246
247 #[test]
248 fn display_name() {
249 assert_eq!(TagName::Favorite.display_name(), "favourite");
250 assert_eq!(TagName::LowPriority.display_name(), "lowpriority");
251 assert_eq!(TagName::ServerNotice.display_name(), "server_notice");
252 assert_eq!(TagName::from("u.Work").display_name(), "Work");
253 assert_eq!(TagName::from("rs.palpo.rules").display_name(), "rules");
254 assert_eq!(TagName::from("Play").display_name(), "Play");
255 }
256}