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-msc4388")]
206 ErrorCode::ConcurrentWrite => ErrorKind::ConcurrentWrite,
207 #[cfg(feature = "unstable-msc4306")]
208 ErrorCode::ConflictingUnsubscription => ErrorKind::ConflictingUnsubscription,
209 ErrorCode::ConnectionFailed => ErrorKind::ConnectionFailed,
210 ErrorCode::ConnectionTimeout => ErrorKind::ConnectionTimeout,
211 ErrorCode::DuplicateAnnotation => ErrorKind::DuplicateAnnotation,
212 ErrorCode::Exclusive => ErrorKind::Exclusive,
213 ErrorCode::Forbidden => ErrorKind::Forbidden,
214 ErrorCode::GuestAccessForbidden => ErrorKind::GuestAccessForbidden,
215 ErrorCode::IncompatibleRoomVersion => {
216 ErrorKind::IncompatibleRoomVersion(IncompatibleRoomVersionErrorData {
217 room_version: from_json_value(
218 room_version.ok_or_else(|| de::Error::missing_field("room_version"))?,
219 )
220 .map_err(de::Error::custom)?,
221 })
222 }
223 ErrorCode::InvalidParam => ErrorKind::InvalidParam,
224 ErrorCode::InvalidRoomState => ErrorKind::InvalidRoomState,
225 ErrorCode::InvalidUsername => ErrorKind::InvalidUsername,
226 ErrorCode::InviteBlocked => ErrorKind::InviteBlocked,
227 ErrorCode::LimitExceeded => ErrorKind::LimitExceeded(LimitExceededErrorData {
228 retry_after: retry_after_ms
229 .map(from_json_value::<UInt>)
230 .transpose()
231 .map_err(de::Error::custom)?
232 .map(Into::into)
233 .map(Duration::from_millis)
234 .map(RetryAfter::Delay),
235 }),
236 ErrorCode::MissingParam => ErrorKind::MissingParam,
237 ErrorCode::MissingToken => ErrorKind::MissingToken,
238 ErrorCode::NotFound => ErrorKind::NotFound,
239 #[cfg(feature = "unstable-msc4306")]
240 ErrorCode::NotInThread => ErrorKind::NotInThread,
241 ErrorCode::NotJson => ErrorKind::NotJson,
242 ErrorCode::NotYetUploaded => ErrorKind::NotYetUploaded,
243 ErrorCode::ResourceLimitExceeded => {
244 ErrorKind::ResourceLimitExceeded(ResourceLimitExceededErrorData {
245 admin_contact: from_json_value(
246 admin_contact.ok_or_else(|| de::Error::missing_field("admin_contact"))?,
247 )
248 .map_err(de::Error::custom)?,
249 })
250 }
251 ErrorCode::RoomInUse => ErrorKind::RoomInUse,
252 #[cfg(feature = "unstable-msc4406")]
253 ErrorCode::SenderIgnored => ErrorKind::SenderIgnored(SenderIgnoredErrorData {
254 sender: sender
255 .map(from_json_value::<Option<OwnedUserId>>)
256 .transpose()
257 .map_err(de::Error::custom)?
258 .flatten(),
259 }),
260 ErrorCode::ServerNotTrusted => ErrorKind::ServerNotTrusted,
261 ErrorCode::ThreepidAuthFailed => ErrorKind::ThreepidAuthFailed,
262 ErrorCode::ThreepidDenied => ErrorKind::ThreepidDenied,
263 ErrorCode::ThreepidInUse => ErrorKind::ThreepidInUse,
264 ErrorCode::ThreepidMediumNotSupported => ErrorKind::ThreepidMediumNotSupported,
265 ErrorCode::ThreepidNotFound => ErrorKind::ThreepidNotFound,
266 ErrorCode::TokenIncorrect => ErrorKind::TokenIncorrect,
267 ErrorCode::TooLarge => ErrorKind::TooLarge,
268 ErrorCode::UnableToAuthorizeJoin => ErrorKind::UnableToAuthorizeJoin,
269 ErrorCode::UnableToGrantJoin => ErrorKind::UnableToGrantJoin,
270 #[cfg(feature = "unstable-msc3843")]
271 ErrorCode::Unactionable => ErrorKind::Unactionable,
272 ErrorCode::Unauthorized => ErrorKind::Unauthorized,
273 ErrorCode::Unknown => ErrorKind::Unknown,
274 #[cfg(feature = "unstable-msc4186")]
275 ErrorCode::UnknownPos => ErrorKind::UnknownPos,
276 ErrorCode::UnknownToken => ErrorKind::UnknownToken(UnknownTokenErrorData {
277 soft_logout: soft_logout
278 .map(from_json_value)
279 .transpose()
280 .map_err(de::Error::custom)?
281 .unwrap_or_default(),
282 }),
283 ErrorCode::Unrecognized => ErrorKind::Unrecognized,
284 ErrorCode::UnsupportedRoomVersion => ErrorKind::UnsupportedRoomVersion,
285 ErrorCode::UrlNotSet => ErrorKind::UrlNotSet,
286 ErrorCode::UserDeactivated => ErrorKind::UserDeactivated,
287 ErrorCode::UserInUse => ErrorKind::UserInUse,
288 ErrorCode::UserLimitExceeded => {
289 ErrorKind::UserLimitExceeded(UserLimitExceededErrorData {
290 info_uri: from_json_value(
291 info_uri.ok_or_else(|| de::Error::missing_field("info_uri"))?,
292 )
293 .map_err(de::Error::custom)?,
294 can_upgrade: can_upgrade
295 .map(from_json_value)
296 .transpose()
297 .map_err(de::Error::custom)?
298 .unwrap_or_default(),
299 })
300 }
301 ErrorCode::UserLocked => ErrorKind::UserLocked,
302 ErrorCode::UserSuspended => ErrorKind::UserSuspended,
303 ErrorCode::WeakPassword => ErrorKind::WeakPassword,
304 ErrorCode::WrongRoomKeysVersion => {
305 ErrorKind::WrongRoomKeysVersion(WrongRoomKeysVersionErrorData {
306 current_version: from_json_value(
307 current_version
308 .ok_or_else(|| de::Error::missing_field("current_version"))?,
309 )
310 .map_err(de::Error::custom)?,
311 })
312 }
313 ErrorCode::_Custom(errcode) => {
314 ErrorKind::_Custom(CustomErrorKind { errcode: errcode.0.into(), data })
315 }
316 })
317 }
318}
319
320impl<'de> Deserialize<'de> for ErrorKind {
321 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
322 where
323 D: Deserializer<'de>,
324 {
325 deserializer.deserialize_map(ErrorKindVisitor)
326 }
327}
328
329impl Serialize for ErrorKind {
330 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
331 where
332 S: Serializer,
333 {
334 let mut st = serializer.serialize_map(None)?;
335 st.serialize_entry("errcode", &self.errcode())?;
336 match self {
337 Self::BadStatus(BadStatusErrorData { status, body }) => {
338 if let Some(status) = status {
339 st.serialize_entry("status", &status.as_u16())?;
340 }
341 if let Some(body) = body {
342 st.serialize_entry("body", body)?;
343 }
344 }
345 Self::IncompatibleRoomVersion(IncompatibleRoomVersionErrorData { room_version }) => {
346 st.serialize_entry("room_version", room_version)?;
347 }
348 Self::LimitExceeded(LimitExceededErrorData {
349 retry_after: Some(RetryAfter::Delay(duration)),
350 }) => {
351 st.serialize_entry(
352 "retry_after_ms",
353 &UInt::try_from(duration.as_millis()).map_err(ser::Error::custom)?,
354 )?;
355 }
356 Self::ResourceLimitExceeded(ResourceLimitExceededErrorData { admin_contact }) => {
357 st.serialize_entry("admin_contact", admin_contact)?;
358 }
359 Self::UnknownToken(UnknownTokenErrorData { soft_logout: true }) | Self::UserLocked => {
360 st.serialize_entry("soft_logout", &true)?;
361 }
362 Self::UserLimitExceeded(UserLimitExceededErrorData { info_uri, can_upgrade }) => {
363 st.serialize_entry("info_uri", info_uri)?;
364
365 if *can_upgrade {
366 st.serialize_entry("can_upgrade", can_upgrade)?;
367 }
368 }
369 Self::WrongRoomKeysVersion(WrongRoomKeysVersionErrorData { current_version }) => {
370 st.serialize_entry("current_version", current_version)?;
371 }
372 #[cfg(feature = "unstable-msc4406")]
373 Self::SenderIgnored(SenderIgnoredErrorData { sender }) => {
374 if let Some(sender) = sender {
375 st.serialize_entry("sender", sender)?;
376 }
377 }
378 Self::_Custom(CustomErrorKind { data, .. }) => {
379 for (k, v) in data {
380 st.serialize_entry(k, v)?;
381 }
382 }
383 Self::AppserviceLoginUnsupported
384 | Self::BadAlias
385 | Self::BadJson
386 | Self::BadState
387 | Self::CannotLeaveServerNoticeRoom
388 | Self::CannotOverwriteMedia
389 | Self::CaptchaInvalid
390 | Self::CaptchaNeeded
391 | Self::ConnectionFailed
392 | Self::ConnectionTimeout
393 | Self::DuplicateAnnotation
394 | Self::Exclusive
395 | Self::Forbidden
396 | Self::GuestAccessForbidden
397 | Self::InvalidParam
398 | Self::InvalidRoomState
399 | Self::InvalidUsername
400 | Self::InviteBlocked
401 | Self::LimitExceeded(LimitExceededErrorData {
402 retry_after: None | Some(RetryAfter::DateTime(_)),
403 })
404 | Self::MissingParam
405 | Self::MissingToken
406 | Self::NotFound
407 | Self::NotJson
408 | Self::NotYetUploaded
409 | Self::RoomInUse
410 | Self::ServerNotTrusted
411 | Self::ThreepidAuthFailed
412 | Self::ThreepidDenied
413 | Self::ThreepidInUse
414 | Self::ThreepidMediumNotSupported
415 | Self::ThreepidNotFound
416 | Self::TokenIncorrect
417 | Self::TooLarge
418 | Self::UnableToAuthorizeJoin
419 | Self::UnableToGrantJoin
420 | Self::Unauthorized
421 | Self::Unknown
422 | Self::UnknownToken(UnknownTokenErrorData { soft_logout: false })
423 | Self::Unrecognized
424 | Self::UnsupportedRoomVersion
425 | Self::UrlNotSet
426 | Self::UserDeactivated
427 | Self::UserInUse
428 | Self::UserSuspended
429 | Self::WeakPassword => {}
430 #[cfg(feature = "unstable-msc4306")]
431 Self::ConflictingUnsubscription => {}
432 #[cfg(feature = "unstable-msc4306")]
433 Self::NotInThread => {}
434 #[cfg(feature = "unstable-msc3843")]
435 Self::Unactionable => {}
436 #[cfg(feature = "unstable-msc4186")]
437 Self::UnknownPos => {}
438 #[cfg(feature = "unstable-msc4388")]
439 Self::ConcurrentWrite => {}
440 }
441 st.end()
442 }
443}
444
445#[cfg(test)]
446mod tests {
447 use ruma_common::room_version_id;
448 use serde_json::{from_value as from_json_value, json};
449
450 use super::{ErrorKind, IncompatibleRoomVersionErrorData};
451
452 #[test]
453 fn deserialize_forbidden() {
454 let deserialized: ErrorKind = from_json_value(json!({ "errcode": "M_FORBIDDEN" })).unwrap();
455 assert_eq!(deserialized, ErrorKind::Forbidden);
456 }
457
458 #[test]
459 fn deserialize_forbidden_with_extra_fields() {
460 let deserialized: ErrorKind = from_json_value(json!({
461 "errcode": "M_FORBIDDEN",
462 "error": "…",
463 }))
464 .unwrap();
465
466 assert_eq!(deserialized, ErrorKind::Forbidden);
467 }
468
469 #[test]
470 fn deserialize_incompatible_room_version() {
471 let deserialized: ErrorKind = from_json_value(json!({
472 "errcode": "M_INCOMPATIBLE_ROOM_VERSION",
473 "room_version": "7",
474 }))
475 .unwrap();
476
477 assert_eq!(
478 deserialized,
479 ErrorKind::IncompatibleRoomVersion(IncompatibleRoomVersionErrorData {
480 room_version: room_version_id!("7")
481 })
482 );
483 }
484}