client-api: Make the ErrorKind enum future-compatible

This commit is contained in:
Jonas Platte 2020-10-25 00:36:17 +02:00
parent 3b3ef1cb75
commit 62d5108633
No known key found for this signature in database
GPG Key ID: 7D261D771D915378
2 changed files with 460 additions and 70 deletions

View File

@ -1,6 +1,7 @@
//! Errors that can be sent from the homeserver.
use std::{
collections::BTreeMap,
fmt::{self, Display, Formatter},
time::Duration,
};
@ -8,189 +9,174 @@ use std::{
use ruma_api::{error::ResponseDeserializationError, EndpointError};
use ruma_identifiers::RoomVersionId;
use serde::{Deserialize, Serialize};
use serde_json::{from_slice as from_json_slice, to_vec as to_json_vec};
use strum::{AsRefStr, Display};
use serde_json::{from_slice as from_json_slice, to_vec as to_json_vec, Value as JsonValue};
/// Deserialize and Serialize implementations for ErrorKind.
/// Separate module because it's a lot of code.
mod kind_serde;
/// An enum for the error kind. Items may contain additional information.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, AsRefStr, Display)]
#[serde(tag = "errcode")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ErrorKind {
/// M_FORBIDDEN
#[serde(rename = "M_FORBIDDEN")]
#[strum(to_string = "M_FORBIDDEN")]
Forbidden,
/// M_UNKNOWN_TOKEN
#[serde(rename = "M_UNKNOWN_TOKEN")]
#[strum(to_string = "M_UNKNOWN_TOKEN")]
UnknownToken {
/// If this is `true`, the client can acquire a new access token by specifying the device ID
/// it is already using to the login API. For more information, see [the spec].
///
/// [the spec]: https://matrix.org/docs/spec/client_server/r0.6.1#soft-logout
#[serde(default, skip_serializing_if = "ruma_serde::is_default")]
soft_logout: bool,
},
/// M_MISSING_TOKEN
#[serde(rename = "M_MISSING_TOKEN")]
#[strum(to_string = "M_MISSING_TOKEN")]
MissingToken,
/// M_BAD_JSON
#[serde(rename = "M_BAD_JSON")]
#[strum(to_string = "M_BAD_JSON")]
BadJson,
/// M_NOT_JSON
#[serde(rename = "M_NOT_JSON")]
#[strum(to_string = "M_NOT_JSON")]
NotJson,
/// M_NOT_FOUND
#[serde(rename = "M_NOT_FOUND")]
#[strum(to_string = "M_NOT_FOUND")]
NotFound,
/// M_LIMIT_EXCEEDED
#[serde(rename = "M_LIMIT_EXCEEDED")]
#[strum(to_string = "M_LIMIT_EXCEEDED")]
LimitExceeded {
/// How long a client should wait in milliseconds before they can try again.
#[serde(with = "ruma_serde::duration::opt_ms")]
retry_after_ms: Option<Duration>,
},
/// M_UNKNOWN
#[serde(rename = "M_UNKNOWN")]
#[strum(to_string = "M_UNKNOWN")]
Unknown,
/// M_UNRECOGNIZED
#[serde(rename = "M_UNRECOGNIZED")]
#[strum(to_string = "M_UNRECOGNIZED")]
Unrecognized,
/// M_UNAUTHORIZED
#[serde(rename = "M_UNAUTHORIZED")]
#[strum(to_string = "M_UNAUTHORIZED")]
Unauthorized,
/// M_USER_DEACTIVATED
#[serde(rename = "M_USER_DEACTIVATED")]
#[strum(to_string = "M_USER_DEACTIVATED")]
UserDeactivated,
/// M_USER_IN_USE
#[serde(rename = "M_USER_IN_USE")]
#[strum(to_string = "M_USER_IN_USE")]
UserInUse,
/// M_INVALID_USERNAME
#[serde(rename = "M_INVALID_USERNAME")]
#[strum(to_string = "M_INVALID_USERNAME")]
InvalidUsername,
/// M_ROOM_IN_USE
#[serde(rename = "M_ROOM_IN_USE")]
#[strum(to_string = "M_ROOM_IN_USE")]
RoomInUse,
/// M_INVALID_ROOM_STATE
#[serde(rename = "M_INVALID_ROOM_STATE")]
#[strum(to_string = "M_INVALID_ROOM_STATE")]
InvalidRoomState,
/// M_THREEPID_IN_USE
#[serde(rename = "M_THREEPID_IN_USE")]
#[strum(to_string = "M_THREEPID_IN_USE")]
ThreepidInUse,
/// M_THREEPID_NOT_FOUND
#[serde(rename = "M_THREEPID_NOT_FOUND")]
#[strum(to_string = "M_THREEPID_NOT_FOUND")]
ThreepidNotFound,
/// M_THREEPID_AUTH_FAILED
#[serde(rename = "M_THREEPID_AUTH_FAILED")]
#[strum(to_string = "M_THREEPID_AUTH_FAILED")]
ThreepidAuthFailed,
/// M_THREEPID_DENIED
#[serde(rename = "M_THREEPID_DENIED")]
#[strum(to_string = "M_THREEPID_DENIED")]
ThreepidDenied,
/// M_SERVER_NOT_TRUSTED
#[serde(rename = "M_SERVER_NOT_TRUSTED")]
#[strum(to_string = "M_SERVER_NOT_TRUSTED")]
ServerNotTrusted,
/// M_UNSUPPORTED_ROOM_VERSION
#[serde(rename = "M_UNSUPPORTED_ROOM_VERSION")]
#[strum(to_string = "M_UNSUPPORTED_ROOM_VERSION")]
UnsupportedRoomVersion,
/// M_INCOMPATIBLE_ROOM_VERSION
#[serde(rename = "M_INCOMPATIBLE_ROOM_VERSION")]
#[strum(to_string = "M_INCOMPATIBLE_ROOM_VERSION")]
IncompatibleRoomVersion {
/// The room's version.
room_version: RoomVersionId,
},
/// M_BAD_STATE
#[serde(rename = "M_BAD_STATE")]
#[strum(to_string = "M_BAD_STATE")]
BadState,
/// M_GUEST_ACCESS_FORBIDDEN
#[serde(rename = "M_GUEST_ACCESS_FORBIDDEN")]
#[strum(to_string = "M_GUEST_ACCESS_FORBIDDEN")]
GuestAccessForbidden,
/// M_CAPTCHA_NEEDED
#[serde(rename = "M_CAPTCHA_NEEDED")]
#[strum(to_string = "M_CAPTCHA_NEEDED")]
CaptchaNeeded,
/// M_CAPTCHA_INVALID
#[serde(rename = "M_CAPTCHA_INVALID")]
#[strum(to_string = "M_CAPTCHA_INVALID")]
CaptchaInvalid,
/// M_MISSING_PARAM
#[serde(rename = "M_MISSING_PARAM")]
#[strum(to_string = "M_MISSING_PARAM")]
MissingParam,
/// M_INVALID_PARAM
#[serde(rename = "M_INVALID_PARAM")]
#[strum(to_string = "M_INVALID_PARAM")]
InvalidParam,
/// M_TOO_LARGE
#[serde(rename = "M_TOO_LARGE")]
#[strum(to_string = "M_TOO_LARGE")]
TooLarge,
/// M_EXCLUSIVE
#[serde(rename = "M_EXCLUSIVE")]
#[strum(to_string = "M_EXCLUSIVE")]
Exclusive,
/// M_RESOURCE_LIMIT_EXCEEDED
#[serde(rename = "M_RESOURCE_LIMIT_EXCEEDED")]
#[strum(to_string = "M_RESOURCE_LIMIT_EXCEEDED")]
ResourceLimitExceeded {
/// A URI giving a contact method for the server administrator.
admin_contact: String,
},
/// M_CANNOT_LEAVE_SERVER_NOTICE_ROOM
#[serde(rename = "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM")]
#[strum(to_string = "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM")]
CannotLeaveServerNoticeRoom,
#[doc(hidden)]
_Custom { errcode: String, extra: BTreeMap<String, JsonValue> },
}
impl AsRef<str> for ErrorKind {
fn as_ref(&self) -> &str {
match self {
Self::Forbidden => "M_FORBIDDEN",
Self::UnknownToken { .. } => "M_UNKNOWN_TOKEN",
Self::MissingToken => "M_MISSING_TOKEN",
Self::BadJson => "M_BAD_JSON",
Self::NotJson => "M_NOT_JSON",
Self::NotFound => "M_NOT_FOUND",
Self::LimitExceeded { .. } => "M_LIMIT_EXCEEDED",
Self::Unknown => "M_UNKNOWN",
Self::Unrecognized => "M_UNRECOGNIZED",
Self::Unauthorized => "M_UNAUTHORIZED",
Self::UserDeactivated => "M_USER_DEACTIVATED",
Self::UserInUse => "M_USER_IN_USE",
Self::InvalidUsername => "M_INVALID_USERNAME",
Self::RoomInUse => "M_ROOM_IN_USE",
Self::InvalidRoomState => "M_INVALID_ROOM_STATE",
Self::ThreepidInUse => "M_THREEPID_IN_USE",
Self::ThreepidNotFound => "M_THREEPID_NOT_FOUND",
Self::ThreepidAuthFailed => "M_THREEPID_AUTH_FAILED",
Self::ThreepidDenied => "M_THREEPID_DENIED",
Self::ServerNotTrusted => "M_SERVER_NOT_TRUSTED",
Self::UnsupportedRoomVersion => "M_UNSUPPORTED_ROOM_VERSION",
Self::IncompatibleRoomVersion { .. } => "M_INCOMPATIBLE_ROOM_VERSION",
Self::BadState => "M_BAD_STATE",
Self::GuestAccessForbidden => "M_GUEST_ACCESS_FORBIDDEN",
Self::CaptchaNeeded => "M_CAPTCHA_NEEDED",
Self::CaptchaInvalid => "M_CAPTCHA_INVALID",
Self::MissingParam => "M_MISSING_PARAM",
Self::InvalidParam => "M_INVALID_PARAM",
Self::TooLarge => "M_TOO_LARGE",
Self::Exclusive => "M_EXCLUSIVE",
Self::ResourceLimitExceeded { .. } => "M_RESOURCE_LIMIT_EXCEEDED",
Self::CannotLeaveServerNoticeRoom => "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM",
Self::_Custom { errcode, .. } => &errcode,
}
}
}
impl Display for ErrorKind {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_ref())
}
}
/// A Matrix Error without a status code
@ -260,3 +246,27 @@ impl From<Error> for http::Response<Vec<u8>> {
.unwrap()
}
}
#[cfg(test)]
mod tests {
use serde_json::{from_value as from_json_value, json};
use super::{ErrorBody, ErrorKind};
#[test]
fn deserialize_forbidden() {
let deserialized: ErrorBody = from_json_value(json!({
"errcode": "M_FORBIDDEN",
"error": "You are not authorized to ban users in this room.",
}))
.unwrap();
assert_eq!(
deserialized,
ErrorBody {
kind: ErrorKind::Forbidden,
message: "You are not authorized to ban users in this room.".into(),
}
);
}
}

View File

@ -0,0 +1,380 @@
use std::{
borrow::Cow,
collections::btree_map::{BTreeMap, Entry},
convert::TryFrom,
fmt,
time::Duration,
};
use js_int::UInt;
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;
enum Field<'de> {
ErrCode,
SoftLogout,
RetryAfterMs,
RoomVersion,
AdminContact,
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,
_ => 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 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 };
(@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::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"))?;
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_ms: retry_after_ms
.map(from_json_value::<UInt>)
.transpose()
.map_err(de::Error::custom)?
.map(Into::into)
.map(Duration::from_millis),
},
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::_Custom(errcode) => ErrorKind::_Custom { errcode, extra },
})
}
}
// FIXME: Derive FromString once available
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,
_Custom(String),
}
impl<'de> Deserialize<'de> for ErrCode {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = Cow::<'de, str>::deserialize(deserializer)?;
Ok(s.into())
}
}
impl<T> From<T> for ErrCode
where
T: AsRef<str> + Into<String>,
{
fn from(s: T) -> Self {
match s.as_ref() {
"M_FORBIDDEN" => Self::Forbidden,
"M_UNKNOWN_TOKEN" => Self::UnknownToken,
"M_MISSING_TOKEN" => Self::MissingToken,
"M_BAD_JSON" => Self::BadJson,
"M_NOT_JSON" => Self::NotJson,
"M_NOT_FOUND" => Self::NotFound,
"M_LIMIT_EXCEEDED" => Self::LimitExceeded,
"M_UNKNOWN" => Self::Unknown,
"M_UNRECOGNIZED" => Self::Unrecognized,
"M_UNAUTHORIZED" => Self::Unauthorized,
"M_USER_DEACTIVATED" => Self::UserDeactivated,
"M_USER_IN_USE" => Self::UserInUse,
"M_INVALID_USERNAME" => Self::InvalidUsername,
"M_ROOM_IN_USE" => Self::RoomInUse,
"M_INVALID_ROOM_STATE" => Self::InvalidRoomState,
"M_THREEPID_IN_USE" => Self::ThreepidInUse,
"M_THREEPID_NOT_FOUND" => Self::ThreepidNotFound,
"M_THREEPID_AUTH_FAILED" => Self::ThreepidAuthFailed,
"M_THREEPID_DENIED" => Self::ThreepidDenied,
"M_SERVER_NOT_TRUSTED" => Self::ServerNotTrusted,
"M_UNSUPPORTED_ROOM_VERSION" => Self::UnsupportedRoomVersion,
"M_INCOMPATIBLE_ROOM_VERSION" => Self::IncompatibleRoomVersion,
"M_BAD_STATE" => Self::BadState,
"M_GUEST_ACCESS_FORBIDDEN" => Self::GuestAccessForbidden,
"M_CAPTCHA_NEEDED" => Self::CaptchaNeeded,
"M_CAPTCHA_INVALID" => Self::CaptchaInvalid,
"M_MISSING_PARAM" => Self::MissingParam,
"M_INVALID_PARAM" => Self::InvalidParam,
"M_TOO_LARGE" => Self::TooLarge,
"M_EXCLUSIVE" => Self::Exclusive,
"M_RESOURCE_LIMIT_EXCEEDED" => Self::ResourceLimitExceeded,
"M_CANNOT_LEAVE_SERVER_NOTICE_ROOM" => Self::CannotLeaveServerNoticeRoom,
_ => Self::_Custom(s.into()),
}
}
}
impl<'de> Deserialize<'de> for ErrorKind {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::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_ms: Some(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 {
st.serialize_entry(k, v)?;
}
}
_ => {}
}
st.end()
}
}
#[cfg(test)]
mod tests {
use ruma_identifiers::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);
}
#[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 { room_version: room_version_id!("7") }
);
}
}