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