1use std::{borrow::Cow, fmt, time::Duration};
2
3use js_int::UInt;
4use ruma_common::serde::JsonObject;
5use serde::{
6 de::{self, Deserialize, Deserializer, MapAccess, Visitor},
7 ser::{self, Serialize, SerializeMap, Serializer},
8};
9use serde_json::{from_value as from_json_value, map::Entry};
10
11use super::{
12 BadStatusErrorData, CustomErrorKind, ErrorCode, ErrorKind, IncompatibleRoomVersionErrorData,
13 LimitExceededErrorData, ResourceLimitExceededErrorData, RetryAfter, UnknownTokenErrorData,
14 UserLimitExceededErrorData, WrongRoomKeysVersionErrorData,
15};
16
17enum Field<'de> {
18 ErrorCode,
19 SoftLogout,
20 RetryAfterMs,
21 RoomVersion,
22 AdminContact,
23 Status,
24 Body,
25 CurrentVersion,
26 InfoUri,
27 CanUpgrade,
28 Other(Cow<'de, str>),
29}
30
31impl<'de> Field<'de> {
32 fn new(s: Cow<'de, str>) -> Field<'de> {
33 match s.as_ref() {
34 "errcode" => Self::ErrorCode,
35 "soft_logout" => Self::SoftLogout,
36 "retry_after_ms" => Self::RetryAfterMs,
37 "room_version" => Self::RoomVersion,
38 "admin_contact" => Self::AdminContact,
39 "status" => Self::Status,
40 "body" => Self::Body,
41 "current_version" => Self::CurrentVersion,
42 "info_uri" => Self::InfoUri,
43 "can_upgrade" => Self::CanUpgrade,
44 _ => Self::Other(s),
45 }
46 }
47}
48
49impl<'de> Deserialize<'de> for Field<'de> {
50 fn deserialize<D>(deserializer: D) -> Result<Field<'de>, D::Error>
51 where
52 D: Deserializer<'de>,
53 {
54 struct FieldVisitor;
55
56 impl<'de> Visitor<'de> for FieldVisitor {
57 type Value = Field<'de>;
58
59 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
60 formatter.write_str("any struct field")
61 }
62
63 fn visit_str<E>(self, value: &str) -> Result<Field<'de>, E>
64 where
65 E: de::Error,
66 {
67 Ok(Field::new(Cow::Owned(value.to_owned())))
68 }
69
70 fn visit_borrowed_str<E>(self, value: &'de str) -> Result<Field<'de>, E>
71 where
72 E: de::Error,
73 {
74 Ok(Field::new(Cow::Borrowed(value)))
75 }
76
77 fn visit_string<E>(self, value: String) -> Result<Field<'de>, E>
78 where
79 E: de::Error,
80 {
81 Ok(Field::new(Cow::Owned(value)))
82 }
83 }
84
85 deserializer.deserialize_identifier(FieldVisitor)
86 }
87}
88
89struct ErrorKindVisitor;
90
91impl<'de> Visitor<'de> for ErrorKindVisitor {
92 type Value = ErrorKind;
93
94 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
95 formatter.write_str("enum ErrorKind")
96 }
97
98 fn visit_map<V>(self, mut map: V) -> Result<ErrorKind, V::Error>
99 where
100 V: MapAccess<'de>,
101 {
102 let mut errcode = None;
103 let mut soft_logout = None;
104 let mut retry_after_ms = None;
105 let mut room_version = None;
106 let mut admin_contact = None;
107 let mut status = None;
108 let mut body = None;
109 let mut current_version = None;
110 let mut info_uri = None;
111 let mut can_upgrade = None;
112 let mut data = JsonObject::new();
113
114 macro_rules! set_field {
115 (errcode) => {
116 set_field!(@inner errcode)
117 };
118 ($field:ident) => {
119 match errcode {
120 Some(set_field!(@variant_containing $field)) | None => {
121 set_field!(@inner $field)
122 }
123 Some(_) => {
126 let _ = map.next_value::<de::IgnoredAny>()?;
127 },
128 }
129 };
130 (@variant_containing soft_logout) => { ErrorCode::UnknownToken };
131 (@variant_containing retry_after_ms) => { ErrorCode::LimitExceeded };
132 (@variant_containing room_version) => { ErrorCode::IncompatibleRoomVersion };
133 (@variant_containing admin_contact) => { ErrorCode::ResourceLimitExceeded };
134 (@variant_containing status) => { ErrorCode::BadStatus };
135 (@variant_containing body) => { ErrorCode::BadStatus };
136 (@variant_containing current_version) => { ErrorCode::WrongRoomKeysVersion };
137 (@variant_containing info_uri) => { ErrorCode::UserLimitExceeded };
138 (@variant_containing can_upgrade) => { ErrorCode::UserLimitExceeded };
139 (@inner $field:ident) => {
140 {
141 if $field.is_some() {
142 return Err(de::Error::duplicate_field(stringify!($field)));
143 }
144 $field = Some(map.next_value()?);
145 }
146 };
147 }
148
149 while let Some(key) = map.next_key()? {
150 match key {
151 Field::ErrorCode => set_field!(errcode),
152 Field::SoftLogout => set_field!(soft_logout),
153 Field::RetryAfterMs => set_field!(retry_after_ms),
154 Field::RoomVersion => set_field!(room_version),
155 Field::AdminContact => set_field!(admin_contact),
156 Field::Status => set_field!(status),
157 Field::Body => set_field!(body),
158 Field::CurrentVersion => set_field!(current_version),
159 Field::InfoUri => set_field!(info_uri),
160 Field::CanUpgrade => set_field!(can_upgrade),
161 Field::Other(other) => match data.entry(other.into_owned()) {
162 Entry::Vacant(v) => {
163 v.insert(map.next_value()?);
164 }
165 Entry::Occupied(o) => {
166 return Err(de::Error::custom(format!("duplicate field `{}`", o.key())));
167 }
168 },
169 }
170 }
171
172 let errcode = errcode.ok_or_else(|| de::Error::missing_field("errcode"))?;
173
174 Ok(match errcode {
175 ErrorCode::AppserviceLoginUnsupported => ErrorKind::AppserviceLoginUnsupported,
176 ErrorCode::BadAlias => ErrorKind::BadAlias,
177 ErrorCode::BadJson => ErrorKind::BadJson,
178 ErrorCode::BadState => ErrorKind::BadState,
179 ErrorCode::BadStatus => ErrorKind::BadStatus(BadStatusErrorData {
180 status: status
181 .map(|s| {
182 from_json_value::<u16>(s)
183 .map_err(de::Error::custom)?
184 .try_into()
185 .map_err(de::Error::custom)
186 })
187 .transpose()?,
188 body: body.map(from_json_value).transpose().map_err(de::Error::custom)?,
189 }),
190 ErrorCode::CannotLeaveServerNoticeRoom => ErrorKind::CannotLeaveServerNoticeRoom,
191 ErrorCode::CannotOverwriteMedia => ErrorKind::CannotOverwriteMedia,
192 ErrorCode::CaptchaInvalid => ErrorKind::CaptchaInvalid,
193 ErrorCode::CaptchaNeeded => ErrorKind::CaptchaNeeded,
194 #[cfg(feature = "unstable-msc4306")]
195 ErrorCode::ConflictingUnsubscription => ErrorKind::ConflictingUnsubscription,
196 ErrorCode::ConnectionFailed => ErrorKind::ConnectionFailed,
197 ErrorCode::ConnectionTimeout => ErrorKind::ConnectionTimeout,
198 ErrorCode::DuplicateAnnotation => ErrorKind::DuplicateAnnotation,
199 ErrorCode::Exclusive => ErrorKind::Exclusive,
200 ErrorCode::Forbidden => ErrorKind::Forbidden,
201 ErrorCode::GuestAccessForbidden => ErrorKind::GuestAccessForbidden,
202 ErrorCode::IncompatibleRoomVersion => {
203 ErrorKind::IncompatibleRoomVersion(IncompatibleRoomVersionErrorData {
204 room_version: from_json_value(
205 room_version.ok_or_else(|| de::Error::missing_field("room_version"))?,
206 )
207 .map_err(de::Error::custom)?,
208 })
209 }
210 ErrorCode::InvalidParam => ErrorKind::InvalidParam,
211 ErrorCode::InvalidRoomState => ErrorKind::InvalidRoomState,
212 ErrorCode::InvalidUsername => ErrorKind::InvalidUsername,
213 ErrorCode::InviteBlocked => ErrorKind::InviteBlocked,
214 ErrorCode::LimitExceeded => ErrorKind::LimitExceeded(LimitExceededErrorData {
215 retry_after: retry_after_ms
216 .map(from_json_value::<UInt>)
217 .transpose()
218 .map_err(de::Error::custom)?
219 .map(Into::into)
220 .map(Duration::from_millis)
221 .map(RetryAfter::Delay),
222 }),
223 ErrorCode::MissingParam => ErrorKind::MissingParam,
224 ErrorCode::MissingToken => ErrorKind::MissingToken,
225 ErrorCode::NotFound => ErrorKind::NotFound,
226 #[cfg(feature = "unstable-msc4306")]
227 ErrorCode::NotInThread => ErrorKind::NotInThread,
228 ErrorCode::NotJson => ErrorKind::NotJson,
229 ErrorCode::NotYetUploaded => ErrorKind::NotYetUploaded,
230 ErrorCode::ResourceLimitExceeded => {
231 ErrorKind::ResourceLimitExceeded(ResourceLimitExceededErrorData {
232 admin_contact: from_json_value(
233 admin_contact.ok_or_else(|| de::Error::missing_field("admin_contact"))?,
234 )
235 .map_err(de::Error::custom)?,
236 })
237 }
238 ErrorCode::RoomInUse => ErrorKind::RoomInUse,
239 ErrorCode::ServerNotTrusted => ErrorKind::ServerNotTrusted,
240 ErrorCode::ThreepidAuthFailed => ErrorKind::ThreepidAuthFailed,
241 ErrorCode::ThreepidDenied => ErrorKind::ThreepidDenied,
242 ErrorCode::ThreepidInUse => ErrorKind::ThreepidInUse,
243 ErrorCode::ThreepidMediumNotSupported => ErrorKind::ThreepidMediumNotSupported,
244 ErrorCode::ThreepidNotFound => ErrorKind::ThreepidNotFound,
245 ErrorCode::TokenIncorrect => ErrorKind::TokenIncorrect,
246 ErrorCode::TooLarge => ErrorKind::TooLarge,
247 ErrorCode::UnableToAuthorizeJoin => ErrorKind::UnableToAuthorizeJoin,
248 ErrorCode::UnableToGrantJoin => ErrorKind::UnableToGrantJoin,
249 #[cfg(feature = "unstable-msc3843")]
250 ErrorCode::Unactionable => ErrorKind::Unactionable,
251 ErrorCode::Unauthorized => ErrorKind::Unauthorized,
252 ErrorCode::Unknown => ErrorKind::Unknown,
253 #[cfg(feature = "unstable-msc4186")]
254 ErrorCode::UnknownPos => ErrorKind::UnknownPos,
255 ErrorCode::UnknownToken => ErrorKind::UnknownToken(UnknownTokenErrorData {
256 soft_logout: soft_logout
257 .map(from_json_value)
258 .transpose()
259 .map_err(de::Error::custom)?
260 .unwrap_or_default(),
261 }),
262 ErrorCode::Unrecognized => ErrorKind::Unrecognized,
263 ErrorCode::UnsupportedRoomVersion => ErrorKind::UnsupportedRoomVersion,
264 ErrorCode::UrlNotSet => ErrorKind::UrlNotSet,
265 ErrorCode::UserDeactivated => ErrorKind::UserDeactivated,
266 ErrorCode::UserInUse => ErrorKind::UserInUse,
267 ErrorCode::UserLimitExceeded => {
268 ErrorKind::UserLimitExceeded(UserLimitExceededErrorData {
269 info_uri: from_json_value(
270 info_uri.ok_or_else(|| de::Error::missing_field("info_uri"))?,
271 )
272 .map_err(de::Error::custom)?,
273 can_upgrade: can_upgrade
274 .map(from_json_value)
275 .transpose()
276 .map_err(de::Error::custom)?
277 .unwrap_or_default(),
278 })
279 }
280 ErrorCode::UserLocked => ErrorKind::UserLocked,
281 ErrorCode::UserSuspended => ErrorKind::UserSuspended,
282 ErrorCode::WeakPassword => ErrorKind::WeakPassword,
283 ErrorCode::WrongRoomKeysVersion => {
284 ErrorKind::WrongRoomKeysVersion(WrongRoomKeysVersionErrorData {
285 current_version: from_json_value(
286 current_version
287 .ok_or_else(|| de::Error::missing_field("current_version"))?,
288 )
289 .map_err(de::Error::custom)?,
290 })
291 }
292 ErrorCode::_Custom(errcode) => {
293 ErrorKind::_Custom(CustomErrorKind { errcode: errcode.0.into(), data })
294 }
295 })
296 }
297}
298
299impl<'de> Deserialize<'de> for ErrorKind {
300 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
301 where
302 D: Deserializer<'de>,
303 {
304 deserializer.deserialize_map(ErrorKindVisitor)
305 }
306}
307
308impl Serialize for ErrorKind {
309 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
310 where
311 S: Serializer,
312 {
313 let mut st = serializer.serialize_map(None)?;
314 st.serialize_entry("errcode", &self.errcode())?;
315 match self {
316 Self::BadStatus(BadStatusErrorData { status, body }) => {
317 if let Some(status) = status {
318 st.serialize_entry("status", &status.as_u16())?;
319 }
320 if let Some(body) = body {
321 st.serialize_entry("body", body)?;
322 }
323 }
324 Self::IncompatibleRoomVersion(IncompatibleRoomVersionErrorData { room_version }) => {
325 st.serialize_entry("room_version", room_version)?;
326 }
327 Self::LimitExceeded(LimitExceededErrorData {
328 retry_after: Some(RetryAfter::Delay(duration)),
329 }) => {
330 st.serialize_entry(
331 "retry_after_ms",
332 &UInt::try_from(duration.as_millis()).map_err(ser::Error::custom)?,
333 )?;
334 }
335 Self::ResourceLimitExceeded(ResourceLimitExceededErrorData { admin_contact }) => {
336 st.serialize_entry("admin_contact", admin_contact)?;
337 }
338 Self::UnknownToken(UnknownTokenErrorData { soft_logout: true }) | Self::UserLocked => {
339 st.serialize_entry("soft_logout", &true)?;
340 }
341 Self::UserLimitExceeded(UserLimitExceededErrorData { info_uri, can_upgrade }) => {
342 st.serialize_entry("info_uri", info_uri)?;
343
344 if *can_upgrade {
345 st.serialize_entry("can_upgrade", can_upgrade)?;
346 }
347 }
348 Self::WrongRoomKeysVersion(WrongRoomKeysVersionErrorData { current_version }) => {
349 st.serialize_entry("current_version", current_version)?;
350 }
351 Self::_Custom(CustomErrorKind { data, .. }) => {
352 for (k, v) in data {
353 st.serialize_entry(k, v)?;
354 }
355 }
356 Self::AppserviceLoginUnsupported
357 | Self::BadAlias
358 | Self::BadJson
359 | Self::BadState
360 | Self::CannotLeaveServerNoticeRoom
361 | Self::CannotOverwriteMedia
362 | Self::CaptchaInvalid
363 | Self::CaptchaNeeded
364 | Self::ConnectionFailed
365 | Self::ConnectionTimeout
366 | Self::DuplicateAnnotation
367 | Self::Exclusive
368 | Self::Forbidden
369 | Self::GuestAccessForbidden
370 | Self::InvalidParam
371 | Self::InvalidRoomState
372 | Self::InvalidUsername
373 | Self::InviteBlocked
374 | Self::LimitExceeded(LimitExceededErrorData {
375 retry_after: None | Some(RetryAfter::DateTime(_)),
376 })
377 | Self::MissingParam
378 | Self::MissingToken
379 | Self::NotFound
380 | Self::NotJson
381 | Self::NotYetUploaded
382 | Self::RoomInUse
383 | Self::ServerNotTrusted
384 | Self::ThreepidAuthFailed
385 | Self::ThreepidDenied
386 | Self::ThreepidInUse
387 | Self::ThreepidMediumNotSupported
388 | Self::ThreepidNotFound
389 | Self::TokenIncorrect
390 | Self::TooLarge
391 | Self::UnableToAuthorizeJoin
392 | Self::UnableToGrantJoin
393 | Self::Unauthorized
394 | Self::Unknown
395 | Self::UnknownToken(UnknownTokenErrorData { soft_logout: false })
396 | Self::Unrecognized
397 | Self::UnsupportedRoomVersion
398 | Self::UrlNotSet
399 | Self::UserDeactivated
400 | Self::UserInUse
401 | Self::UserSuspended
402 | Self::WeakPassword => {}
403 #[cfg(feature = "unstable-msc4306")]
404 Self::ConflictingUnsubscription => {}
405 #[cfg(feature = "unstable-msc4306")]
406 Self::NotInThread => {}
407 #[cfg(feature = "unstable-msc3843")]
408 Self::Unactionable => {}
409 #[cfg(feature = "unstable-msc4186")]
410 Self::UnknownPos => {}
411 }
412 st.end()
413 }
414}
415
416#[cfg(test)]
417mod tests {
418 use ruma_common::room_version_id;
419 use serde_json::{from_value as from_json_value, json};
420
421 use super::{ErrorKind, IncompatibleRoomVersionErrorData};
422
423 #[test]
424 fn deserialize_forbidden() {
425 let deserialized: ErrorKind = from_json_value(json!({ "errcode": "M_FORBIDDEN" })).unwrap();
426 assert_eq!(deserialized, ErrorKind::Forbidden);
427 }
428
429 #[test]
430 fn deserialize_forbidden_with_extra_fields() {
431 let deserialized: ErrorKind = from_json_value(json!({
432 "errcode": "M_FORBIDDEN",
433 "error": "…",
434 }))
435 .unwrap();
436
437 assert_eq!(deserialized, ErrorKind::Forbidden);
438 }
439
440 #[test]
441 fn deserialize_incompatible_room_version() {
442 let deserialized: ErrorKind = from_json_value(json!({
443 "errcode": "M_INCOMPATIBLE_ROOM_VERSION",
444 "room_version": "7",
445 }))
446 .unwrap();
447
448 assert_eq!(
449 deserialized,
450 ErrorKind::IncompatibleRoomVersion(IncompatibleRoomVersionErrorData {
451 room_version: room_version_id!("7")
452 })
453 );
454 }
455}