409 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			409 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| use std::{
 | |
|     borrow::Cow,
 | |
|     collections::btree_map::{BTreeMap, Entry},
 | |
|     fmt,
 | |
|     time::Duration,
 | |
| };
 | |
| 
 | |
| use js_int::UInt;
 | |
| use ruma_common::serde::{DeserializeFromCowStr, FromString};
 | |
| use serde::{
 | |
|     de::{self, Deserialize, Deserializer, MapAccess, Visitor},
 | |
|     ser::{self, Serialize, SerializeMap, Serializer},
 | |
| };
 | |
| use serde_json::from_value as from_json_value;
 | |
| 
 | |
| use super::{ErrorKind, Extra, RetryAfter};
 | |
| use crate::PrivOwnedStr;
 | |
| 
 | |
| enum Field<'de> {
 | |
|     ErrCode,
 | |
|     SoftLogout,
 | |
|     RetryAfterMs,
 | |
|     RoomVersion,
 | |
|     AdminContact,
 | |
|     Status,
 | |
|     Body,
 | |
|     CurrentVersion,
 | |
|     Other(Cow<'de, str>),
 | |
| }
 | |
| 
 | |
| impl<'de> Field<'de> {
 | |
|     fn new(s: Cow<'de, str>) -> Field<'de> {
 | |
|         match s.as_ref() {
 | |
|             "errcode" => Self::ErrCode,
 | |
|             "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,
 | |
|             _ => 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 extra = BTreeMap::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)
 | |
|                     }
 | |
|                     // if we already know we're deserializing a different variant to the one
 | |
|                     // containing this field, ignore its value.
 | |
|                     Some(_) => {
 | |
|                         let _ = map.next_value::<de::IgnoredAny>()?;
 | |
|                     },
 | |
|                 }
 | |
|             };
 | |
|             (@variant_containing soft_logout) => { ErrCode::UnknownToken };
 | |
|             (@variant_containing retry_after_ms) => { ErrCode::LimitExceeded };
 | |
|             (@variant_containing room_version) => { ErrCode::IncompatibleRoomVersion };
 | |
|             (@variant_containing admin_contact) => { ErrCode::ResourceLimitExceeded };
 | |
|             (@variant_containing status) => { ErrCode::BadStatus };
 | |
|             (@variant_containing body) => { ErrCode::BadStatus };
 | |
|             (@variant_containing current_version) => { ErrCode::WrongRoomKeysVersion };
 | |
|             (@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::ErrCode => 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::Other(other) => match extra.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"))?;
 | |
|         let extra = Extra(extra);
 | |
| 
 | |
|         Ok(match errcode {
 | |
|             ErrCode::Forbidden => ErrorKind::forbidden(),
 | |
|             ErrCode::UnknownToken => ErrorKind::UnknownToken {
 | |
|                 soft_logout: soft_logout
 | |
|                     .map(from_json_value)
 | |
|                     .transpose()
 | |
|                     .map_err(de::Error::custom)?
 | |
|                     .unwrap_or_default(),
 | |
|             },
 | |
|             ErrCode::MissingToken => ErrorKind::MissingToken,
 | |
|             ErrCode::BadJson => ErrorKind::BadJson,
 | |
|             ErrCode::NotJson => ErrorKind::NotJson,
 | |
|             ErrCode::NotFound => ErrorKind::NotFound,
 | |
|             ErrCode::LimitExceeded => ErrorKind::LimitExceeded {
 | |
|                 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),
 | |
|             },
 | |
|             ErrCode::Unknown => ErrorKind::Unknown,
 | |
|             ErrCode::Unrecognized => ErrorKind::Unrecognized,
 | |
|             ErrCode::Unauthorized => ErrorKind::Unauthorized,
 | |
|             ErrCode::UserDeactivated => ErrorKind::UserDeactivated,
 | |
|             ErrCode::UserInUse => ErrorKind::UserInUse,
 | |
|             ErrCode::InvalidUsername => ErrorKind::InvalidUsername,
 | |
|             ErrCode::RoomInUse => ErrorKind::RoomInUse,
 | |
|             ErrCode::InvalidRoomState => ErrorKind::InvalidRoomState,
 | |
|             ErrCode::ThreepidInUse => ErrorKind::ThreepidInUse,
 | |
|             ErrCode::ThreepidNotFound => ErrorKind::ThreepidNotFound,
 | |
|             ErrCode::ThreepidAuthFailed => ErrorKind::ThreepidAuthFailed,
 | |
|             ErrCode::ThreepidDenied => ErrorKind::ThreepidDenied,
 | |
|             ErrCode::ServerNotTrusted => ErrorKind::ServerNotTrusted,
 | |
|             ErrCode::UnsupportedRoomVersion => ErrorKind::UnsupportedRoomVersion,
 | |
|             ErrCode::IncompatibleRoomVersion => ErrorKind::IncompatibleRoomVersion {
 | |
|                 room_version: from_json_value(
 | |
|                     room_version.ok_or_else(|| de::Error::missing_field("room_version"))?,
 | |
|                 )
 | |
|                 .map_err(de::Error::custom)?,
 | |
|             },
 | |
|             ErrCode::BadState => ErrorKind::BadState,
 | |
|             ErrCode::GuestAccessForbidden => ErrorKind::GuestAccessForbidden,
 | |
|             ErrCode::CaptchaNeeded => ErrorKind::CaptchaNeeded,
 | |
|             ErrCode::CaptchaInvalid => ErrorKind::CaptchaInvalid,
 | |
|             ErrCode::MissingParam => ErrorKind::MissingParam,
 | |
|             ErrCode::InvalidParam => ErrorKind::InvalidParam,
 | |
|             ErrCode::TooLarge => ErrorKind::TooLarge,
 | |
|             ErrCode::Exclusive => ErrorKind::Exclusive,
 | |
|             ErrCode::ResourceLimitExceeded => ErrorKind::ResourceLimitExceeded {
 | |
|                 admin_contact: from_json_value(
 | |
|                     admin_contact.ok_or_else(|| de::Error::missing_field("admin_contact"))?,
 | |
|                 )
 | |
|                 .map_err(de::Error::custom)?,
 | |
|             },
 | |
|             ErrCode::CannotLeaveServerNoticeRoom => ErrorKind::CannotLeaveServerNoticeRoom,
 | |
|             ErrCode::WeakPassword => ErrorKind::WeakPassword,
 | |
|             ErrCode::UnableToAuthorizeJoin => ErrorKind::UnableToAuthorizeJoin,
 | |
|             ErrCode::UnableToGrantJoin => ErrorKind::UnableToGrantJoin,
 | |
|             ErrCode::BadAlias => ErrorKind::BadAlias,
 | |
|             ErrCode::DuplicateAnnotation => ErrorKind::DuplicateAnnotation,
 | |
|             ErrCode::NotYetUploaded => ErrorKind::NotYetUploaded,
 | |
|             ErrCode::CannotOverwriteMedia => ErrorKind::CannotOverwriteMedia,
 | |
|             #[cfg(any(feature = "unstable-msc3575", feature = "unstable-msc4186"))]
 | |
|             ErrCode::UnknownPos => ErrorKind::UnknownPos,
 | |
|             ErrCode::UrlNotSet => ErrorKind::UrlNotSet,
 | |
|             ErrCode::BadStatus => ErrorKind::BadStatus {
 | |
|                 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)?,
 | |
|             },
 | |
|             ErrCode::ConnectionFailed => ErrorKind::ConnectionFailed,
 | |
|             ErrCode::ConnectionTimeout => ErrorKind::ConnectionTimeout,
 | |
|             ErrCode::WrongRoomKeysVersion => ErrorKind::WrongRoomKeysVersion {
 | |
|                 current_version: from_json_value(
 | |
|                     current_version.ok_or_else(|| de::Error::missing_field("current_version"))?,
 | |
|                 )
 | |
|                 .map_err(de::Error::custom)?,
 | |
|             },
 | |
|             #[cfg(feature = "unstable-msc3843")]
 | |
|             ErrCode::Unactionable => ErrorKind::Unactionable,
 | |
|             ErrCode::_Custom(errcode) => ErrorKind::_Custom { errcode, extra },
 | |
|         })
 | |
|     }
 | |
| }
 | |
| 
 | |
| #[derive(FromString, DeserializeFromCowStr)]
 | |
| #[ruma_enum(rename_all = "M_MATRIX_ERROR_CASE")]
 | |
| enum ErrCode {
 | |
|     Forbidden,
 | |
|     UnknownToken,
 | |
|     MissingToken,
 | |
|     BadJson,
 | |
|     NotJson,
 | |
|     NotFound,
 | |
|     LimitExceeded,
 | |
|     Unknown,
 | |
|     Unrecognized,
 | |
|     Unauthorized,
 | |
|     UserDeactivated,
 | |
|     UserInUse,
 | |
|     InvalidUsername,
 | |
|     RoomInUse,
 | |
|     InvalidRoomState,
 | |
|     ThreepidInUse,
 | |
|     ThreepidNotFound,
 | |
|     ThreepidAuthFailed,
 | |
|     ThreepidDenied,
 | |
|     ServerNotTrusted,
 | |
|     UnsupportedRoomVersion,
 | |
|     IncompatibleRoomVersion,
 | |
|     BadState,
 | |
|     GuestAccessForbidden,
 | |
|     CaptchaNeeded,
 | |
|     CaptchaInvalid,
 | |
|     MissingParam,
 | |
|     InvalidParam,
 | |
|     TooLarge,
 | |
|     Exclusive,
 | |
|     ResourceLimitExceeded,
 | |
|     CannotLeaveServerNoticeRoom,
 | |
|     WeakPassword,
 | |
|     UnableToAuthorizeJoin,
 | |
|     UnableToGrantJoin,
 | |
|     BadAlias,
 | |
|     DuplicateAnnotation,
 | |
|     #[ruma_enum(alias = "FI.MAU.MSC2246_NOT_YET_UPLOADED")]
 | |
|     NotYetUploaded,
 | |
|     #[ruma_enum(alias = "FI.MAU.MSC2246_CANNOT_OVERWRITE_MEDIA")]
 | |
|     CannotOverwriteMedia,
 | |
|     #[cfg(any(feature = "unstable-msc3575", feature = "unstable-msc4186"))]
 | |
|     UnknownPos,
 | |
|     UrlNotSet,
 | |
|     BadStatus,
 | |
|     ConnectionFailed,
 | |
|     ConnectionTimeout,
 | |
|     WrongRoomKeysVersion,
 | |
|     #[cfg(feature = "unstable-msc3843")]
 | |
|     Unactionable,
 | |
|     _Custom(PrivOwnedStr),
 | |
| }
 | |
| 
 | |
| 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.as_ref())?;
 | |
|         match self {
 | |
|             Self::UnknownToken { soft_logout: true } => {
 | |
|                 st.serialize_entry("soft_logout", &true)?;
 | |
|             }
 | |
|             Self::LimitExceeded { retry_after: Some(RetryAfter::Delay(duration)) } => {
 | |
|                 st.serialize_entry(
 | |
|                     "retry_after_ms",
 | |
|                     &UInt::try_from(duration.as_millis()).map_err(ser::Error::custom)?,
 | |
|                 )?;
 | |
|             }
 | |
|             Self::IncompatibleRoomVersion { room_version } => {
 | |
|                 st.serialize_entry("room_version", room_version)?;
 | |
|             }
 | |
|             Self::ResourceLimitExceeded { admin_contact } => {
 | |
|                 st.serialize_entry("admin_contact", admin_contact)?;
 | |
|             }
 | |
|             Self::_Custom { extra, .. } => {
 | |
|                 for (k, v) in &extra.0 {
 | |
|                     st.serialize_entry(k, v)?;
 | |
|                 }
 | |
|             }
 | |
|             _ => {}
 | |
|         }
 | |
|         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;
 | |
| 
 | |
|     #[test]
 | |
|     fn deserialize_forbidden() {
 | |
|         let deserialized: ErrorKind = from_json_value(json!({ "errcode": "M_FORBIDDEN" })).unwrap();
 | |
|         assert_eq!(
 | |
|             deserialized,
 | |
|             ErrorKind::Forbidden {
 | |
|                 #[cfg(feature = "unstable-msc2967")]
 | |
|                 authenticate: None
 | |
|             }
 | |
|         );
 | |
|     }
 | |
| 
 | |
|     #[test]
 | |
|     fn deserialize_forbidden_with_extra_fields() {
 | |
|         let deserialized: ErrorKind = from_json_value(json!({
 | |
|             "errcode": "M_FORBIDDEN",
 | |
|             "error": "…",
 | |
|         }))
 | |
|         .unwrap();
 | |
| 
 | |
|         assert_eq!(
 | |
|             deserialized,
 | |
|             ErrorKind::Forbidden {
 | |
|                 #[cfg(feature = "unstable-msc2967")]
 | |
|                 authenticate: None
 | |
|             }
 | |
|         );
 | |
|     }
 | |
| 
 | |
|     #[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 { room_version: room_version_id!("7") }
 | |
|         );
 | |
|     }
 | |
| }
 |