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