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