use std::{borrow::Cow, fmt, time::Duration};
use js_int::UInt;
use ruma_common::serde::JsonObject;
use serde::{
de::{self, Deserialize, Deserializer, MapAccess, Visitor},
ser::{self, Serialize, SerializeMap, Serializer},
};
use serde_json::{from_value as from_json_value, map::Entry};
use super::{
BadStatusErrorData, CustomErrorKind, ErrorCode, ErrorKind, IncompatibleRoomVersionErrorData,
LimitExceededErrorData, ResourceLimitExceededErrorData, RetryAfter, UnknownTokenErrorData,
UserLimitExceededErrorData, WrongRoomKeysVersionErrorData,
};
#[cfg(feature = "unstable-msc4406")]
use crate::{OwnedUserId, api::error::SenderIgnoredErrorData};
enum Field<'de> {
ErrorCode,
SoftLogout,
RetryAfterMs,
RoomVersion,
AdminContact,
Status,
Body,
CurrentVersion,
InfoUri,
CanUpgrade,
#[cfg(feature = "unstable-msc4406")]
Sender,
Other(Cow<'de, str>),
}
impl<'de> Field<'de> {
fn new(s: Cow<'de, str>) -> Field<'de> {
match s.as_ref() {
"errcode" => Self::ErrorCode,
"soft_logout" => Self::SoftLogout,
"retry_after_ms" => Self::RetryAfterMs,
"room_version" => Self::RoomVersion,
"admin_contact" => Self::AdminContact,
"status" => Self::Status,
"body" => Self::Body,
"current_version" => Self::CurrentVersion,
"info_uri" => Self::InfoUri,
"can_upgrade" => Self::CanUpgrade,
#[cfg(feature = "unstable-msc4406")]
"sender" => Self::Sender,
_ => Self::Other(s),
}
}
}
impl<'de> Deserialize<'de> for Field<'de> {
fn deserialize<D>(deserializer: D) -> Result<Field<'de>, D::Error>
where
D: Deserializer<'de>,
{
struct FieldVisitor;
impl<'de> Visitor<'de> for FieldVisitor {
type Value = Field<'de>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("any struct field")
}
fn visit_str<E>(self, value: &str) -> Result<Field<'de>, E>
where
E: de::Error,
{
Ok(Field::new(Cow::Owned(value.to_owned())))
}
fn visit_borrowed_str<E>(self, value: &'de str) -> Result<Field<'de>, E>
where
E: de::Error,
{
Ok(Field::new(Cow::Borrowed(value)))
}
fn visit_string<E>(self, value: String) -> Result<Field<'de>, E>
where
E: de::Error,
{
Ok(Field::new(Cow::Owned(value)))
}
}
deserializer.deserialize_identifier(FieldVisitor)
}
}
struct ErrorKindVisitor;
impl<'de> Visitor<'de> for ErrorKindVisitor {
type Value = ErrorKind;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("enum ErrorKind")
}
fn visit_map<V>(self, mut map: V) -> Result<ErrorKind, V::Error>
where
V: MapAccess<'de>,
{
let mut errcode = None;
let mut soft_logout = None;
let mut retry_after_ms = None;
let mut room_version = None;
let mut admin_contact = None;
let mut status = None;
let mut body = None;
let mut current_version = None;
let mut info_uri = None;
let mut can_upgrade = None;
#[cfg(feature = "unstable-msc4406")]
let mut sender = None;
let mut data = JsonObject::new();
macro_rules! set_field {
(errcode) => {
set_field!(@inner errcode)
};
($field:ident) => {
match errcode {
Some(set_field!(@variant_containing $field)) | None => {
set_field!(@inner $field)
}
Some(_) => {
let _ = map.next_value::<de::IgnoredAny>()?;
},
}
};
(@variant_containing soft_logout) => { ErrorCode::UnknownToken };
(@variant_containing retry_after_ms) => { ErrorCode::LimitExceeded };
(@variant_containing room_version) => { ErrorCode::IncompatibleRoomVersion };
(@variant_containing admin_contact) => { ErrorCode::ResourceLimitExceeded };
(@variant_containing status) => { ErrorCode::BadStatus };
(@variant_containing body) => { ErrorCode::BadStatus };
(@variant_containing current_version) => { ErrorCode::WrongRoomKeysVersion };
(@variant_containing info_uri) => { ErrorCode::UserLimitExceeded };
(@variant_containing can_upgrade) => { ErrorCode::UserLimitExceeded };
(@variant_containing sender) => { ErrorCode::SenderIgnored };
(@inner $field:ident) => {
{
if $field.is_some() {
return Err(de::Error::duplicate_field(stringify!($field)));
}
$field = Some(map.next_value()?);
}
};
}
while let Some(key) = map.next_key()? {
match key {
Field::ErrorCode => set_field!(errcode),
Field::SoftLogout => set_field!(soft_logout),
Field::RetryAfterMs => set_field!(retry_after_ms),
Field::RoomVersion => set_field!(room_version),
Field::AdminContact => set_field!(admin_contact),
Field::Status => set_field!(status),
Field::Body => set_field!(body),
Field::CurrentVersion => set_field!(current_version),
Field::InfoUri => set_field!(info_uri),
Field::CanUpgrade => set_field!(can_upgrade),
#[cfg(feature = "unstable-msc4406")]
Field::Sender => set_field!(sender),
Field::Other(other) => match data.entry(other.into_owned()) {
Entry::Vacant(v) => {
v.insert(map.next_value()?);
}
Entry::Occupied(o) => {
return Err(de::Error::custom(format!("duplicate field `{}`", o.key())));
}
},
}
}
let errcode = errcode.ok_or_else(|| de::Error::missing_field("errcode"))?;
Ok(match errcode {
ErrorCode::AppserviceLoginUnsupported => ErrorKind::AppserviceLoginUnsupported,
ErrorCode::BadAlias => ErrorKind::BadAlias,
ErrorCode::BadJson => ErrorKind::BadJson,
ErrorCode::BadState => ErrorKind::BadState,
ErrorCode::BadStatus => ErrorKind::BadStatus(BadStatusErrorData {
status: status
.map(|s| {
from_json_value::<u16>(s)
.map_err(de::Error::custom)?
.try_into()
.map_err(de::Error::custom)
})
.transpose()?,
body: body.map(from_json_value).transpose().map_err(de::Error::custom)?,
}),
ErrorCode::CannotLeaveServerNoticeRoom => ErrorKind::CannotLeaveServerNoticeRoom,
ErrorCode::CannotOverwriteMedia => ErrorKind::CannotOverwriteMedia,
ErrorCode::CaptchaInvalid => ErrorKind::CaptchaInvalid,
ErrorCode::CaptchaNeeded => ErrorKind::CaptchaNeeded,
#[cfg(feature = "unstable-msc4306")]
ErrorCode::ConflictingUnsubscription => ErrorKind::ConflictingUnsubscription,
ErrorCode::ConnectionFailed => ErrorKind::ConnectionFailed,
ErrorCode::ConnectionTimeout => ErrorKind::ConnectionTimeout,
ErrorCode::DuplicateAnnotation => ErrorKind::DuplicateAnnotation,
ErrorCode::Exclusive => ErrorKind::Exclusive,
ErrorCode::Forbidden => ErrorKind::Forbidden,
ErrorCode::GuestAccessForbidden => ErrorKind::GuestAccessForbidden,
ErrorCode::IncompatibleRoomVersion => {
ErrorKind::IncompatibleRoomVersion(IncompatibleRoomVersionErrorData {
room_version: from_json_value(
room_version.ok_or_else(|| de::Error::missing_field("room_version"))?,
)
.map_err(de::Error::custom)?,
})
}
ErrorCode::InvalidParam => ErrorKind::InvalidParam,
ErrorCode::InvalidRoomState => ErrorKind::InvalidRoomState,
ErrorCode::InvalidUsername => ErrorKind::InvalidUsername,
ErrorCode::InviteBlocked => ErrorKind::InviteBlocked,
ErrorCode::LimitExceeded => ErrorKind::LimitExceeded(LimitExceededErrorData {
retry_after: retry_after_ms
.map(from_json_value::<UInt>)
.transpose()
.map_err(de::Error::custom)?
.map(Into::into)
.map(Duration::from_millis)
.map(RetryAfter::Delay),
}),
ErrorCode::MissingParam => ErrorKind::MissingParam,
ErrorCode::MissingToken => ErrorKind::MissingToken,
ErrorCode::NotFound => ErrorKind::NotFound,
#[cfg(feature = "unstable-msc4306")]
ErrorCode::NotInThread => ErrorKind::NotInThread,
ErrorCode::NotJson => ErrorKind::NotJson,
ErrorCode::NotYetUploaded => ErrorKind::NotYetUploaded,
ErrorCode::ResourceLimitExceeded => {
ErrorKind::ResourceLimitExceeded(ResourceLimitExceededErrorData {
admin_contact: from_json_value(
admin_contact.ok_or_else(|| de::Error::missing_field("admin_contact"))?,
)
.map_err(de::Error::custom)?,
})
}
ErrorCode::RoomInUse => ErrorKind::RoomInUse,
#[cfg(feature = "unstable-msc4406")]
ErrorCode::SenderIgnored => ErrorKind::SenderIgnored(SenderIgnoredErrorData {
sender: sender
.map(from_json_value::<Option<OwnedUserId>>)
.transpose()
.map_err(de::Error::custom)?
.flatten(),
}),
ErrorCode::ServerNotTrusted => ErrorKind::ServerNotTrusted,
ErrorCode::ThreepidAuthFailed => ErrorKind::ThreepidAuthFailed,
ErrorCode::ThreepidDenied => ErrorKind::ThreepidDenied,
ErrorCode::ThreepidInUse => ErrorKind::ThreepidInUse,
ErrorCode::ThreepidMediumNotSupported => ErrorKind::ThreepidMediumNotSupported,
ErrorCode::ThreepidNotFound => ErrorKind::ThreepidNotFound,
ErrorCode::TokenIncorrect => ErrorKind::TokenIncorrect,
ErrorCode::TooLarge => ErrorKind::TooLarge,
ErrorCode::UnableToAuthorizeJoin => ErrorKind::UnableToAuthorizeJoin,
ErrorCode::UnableToGrantJoin => ErrorKind::UnableToGrantJoin,
#[cfg(feature = "unstable-msc3843")]
ErrorCode::Unactionable => ErrorKind::Unactionable,
ErrorCode::Unauthorized => ErrorKind::Unauthorized,
ErrorCode::Unknown => ErrorKind::Unknown,
#[cfg(feature = "unstable-msc4186")]
ErrorCode::UnknownPos => ErrorKind::UnknownPos,
ErrorCode::UnknownToken => ErrorKind::UnknownToken(UnknownTokenErrorData {
soft_logout: soft_logout
.map(from_json_value)
.transpose()
.map_err(de::Error::custom)?
.unwrap_or_default(),
}),
ErrorCode::Unrecognized => ErrorKind::Unrecognized,
ErrorCode::UnsupportedRoomVersion => ErrorKind::UnsupportedRoomVersion,
ErrorCode::UrlNotSet => ErrorKind::UrlNotSet,
ErrorCode::UserDeactivated => ErrorKind::UserDeactivated,
ErrorCode::UserInUse => ErrorKind::UserInUse,
ErrorCode::UserLimitExceeded => {
ErrorKind::UserLimitExceeded(UserLimitExceededErrorData {
info_uri: from_json_value(
info_uri.ok_or_else(|| de::Error::missing_field("info_uri"))?,
)
.map_err(de::Error::custom)?,
can_upgrade: can_upgrade
.map(from_json_value)
.transpose()
.map_err(de::Error::custom)?
.unwrap_or_default(),
})
}
ErrorCode::UserLocked => ErrorKind::UserLocked,
ErrorCode::UserSuspended => ErrorKind::UserSuspended,
ErrorCode::WeakPassword => ErrorKind::WeakPassword,
ErrorCode::WrongRoomKeysVersion => {
ErrorKind::WrongRoomKeysVersion(WrongRoomKeysVersionErrorData {
current_version: from_json_value(
current_version
.ok_or_else(|| de::Error::missing_field("current_version"))?,
)
.map_err(de::Error::custom)?,
})
}
ErrorCode::_Custom(errcode) => {
ErrorKind::_Custom(CustomErrorKind { errcode: errcode.0.into(), data })
}
})
}
}
impl<'de> Deserialize<'de> for ErrorKind {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_map(ErrorKindVisitor)
}
}
impl Serialize for ErrorKind {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut st = serializer.serialize_map(None)?;
st.serialize_entry("errcode", &self.errcode())?;
match self {
Self::BadStatus(BadStatusErrorData { status, body }) => {
if let Some(status) = status {
st.serialize_entry("status", &status.as_u16())?;
}
if let Some(body) = body {
st.serialize_entry("body", body)?;
}
}
Self::IncompatibleRoomVersion(IncompatibleRoomVersionErrorData { room_version }) => {
st.serialize_entry("room_version", room_version)?;
}
Self::LimitExceeded(LimitExceededErrorData {
retry_after: Some(RetryAfter::Delay(duration)),
}) => {
st.serialize_entry(
"retry_after_ms",
&UInt::try_from(duration.as_millis()).map_err(ser::Error::custom)?,
)?;
}
Self::ResourceLimitExceeded(ResourceLimitExceededErrorData { admin_contact }) => {
st.serialize_entry("admin_contact", admin_contact)?;
}
Self::UnknownToken(UnknownTokenErrorData { soft_logout: true }) | Self::UserLocked => {
st.serialize_entry("soft_logout", &true)?;
}
Self::UserLimitExceeded(UserLimitExceededErrorData { info_uri, can_upgrade }) => {
st.serialize_entry("info_uri", info_uri)?;
if *can_upgrade {
st.serialize_entry("can_upgrade", can_upgrade)?;
}
}
Self::WrongRoomKeysVersion(WrongRoomKeysVersionErrorData { current_version }) => {
st.serialize_entry("current_version", current_version)?;
}
#[cfg(feature = "unstable-msc4406")]
Self::SenderIgnored(SenderIgnoredErrorData { sender }) => {
if let Some(sender) = sender {
st.serialize_entry("sender", sender)?;
}
}
Self::_Custom(CustomErrorKind { data, .. }) => {
for (k, v) in data {
st.serialize_entry(k, v)?;
}
}
Self::AppserviceLoginUnsupported
| Self::BadAlias
| Self::BadJson
| Self::BadState
| Self::CannotLeaveServerNoticeRoom
| Self::CannotOverwriteMedia
| Self::CaptchaInvalid
| Self::CaptchaNeeded
| Self::ConnectionFailed
| Self::ConnectionTimeout
| Self::DuplicateAnnotation
| Self::Exclusive
| Self::Forbidden
| Self::GuestAccessForbidden
| Self::InvalidParam
| Self::InvalidRoomState
| Self::InvalidUsername
| Self::InviteBlocked
| Self::LimitExceeded(LimitExceededErrorData {
retry_after: None | Some(RetryAfter::DateTime(_)),
})
| Self::MissingParam
| Self::MissingToken
| Self::NotFound
| Self::NotJson
| Self::NotYetUploaded
| Self::RoomInUse
| Self::ServerNotTrusted
| Self::ThreepidAuthFailed
| Self::ThreepidDenied
| Self::ThreepidInUse
| Self::ThreepidMediumNotSupported
| Self::ThreepidNotFound
| Self::TokenIncorrect
| Self::TooLarge
| Self::UnableToAuthorizeJoin
| Self::UnableToGrantJoin
| Self::Unauthorized
| Self::Unknown
| Self::UnknownToken(UnknownTokenErrorData { soft_logout: false })
| Self::Unrecognized
| Self::UnsupportedRoomVersion
| Self::UrlNotSet
| Self::UserDeactivated
| Self::UserInUse
| Self::UserSuspended
| Self::WeakPassword => {}
#[cfg(feature = "unstable-msc4306")]
Self::ConflictingUnsubscription => {}
#[cfg(feature = "unstable-msc4306")]
Self::NotInThread => {}
#[cfg(feature = "unstable-msc3843")]
Self::Unactionable => {}
#[cfg(feature = "unstable-msc4186")]
Self::UnknownPos => {}
}
st.end()
}
}
#[cfg(test)]
mod tests {
use ruma_common::room_version_id;
use serde_json::{from_value as from_json_value, json};
use super::{ErrorKind, IncompatibleRoomVersionErrorData};
#[test]
fn deserialize_forbidden() {
let deserialized: ErrorKind = from_json_value(json!({ "errcode": "M_FORBIDDEN" })).unwrap();
assert_eq!(deserialized, ErrorKind::Forbidden);
}
#[test]
fn deserialize_forbidden_with_extra_fields() {
let deserialized: ErrorKind = from_json_value(json!({
"errcode": "M_FORBIDDEN",
"error": "…",
}))
.unwrap();
assert_eq!(deserialized, ErrorKind::Forbidden);
}
#[test]
fn deserialize_incompatible_room_version() {
let deserialized: ErrorKind = from_json_value(json!({
"errcode": "M_INCOMPATIBLE_ROOM_VERSION",
"room_version": "7",
}))
.unwrap();
assert_eq!(
deserialized,
ErrorKind::IncompatibleRoomVersion(IncompatibleRoomVersionErrorData {
room_version: room_version_id!("7")
})
);
}
}