Skip to main content

ruma_common/api/error/
kind_serde.rs

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                    // if we already know we're deserializing a different variant to the one
132                    // containing this field, ignore its value.
133                    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}