From 6a0ab987b546f5e6389a7af4834e71ab515b42fc Mon Sep 17 00:00:00 2001 From: "Ragotzy.devin" Date: Mon, 16 Mar 2020 20:07:12 -0400 Subject: [PATCH] Enable deserialization of unsuccessful responses Co-authored-by: Jonas Platte --- ruma-api-macros/src/api.rs | 42 +++++++++++++++++++-- src/error.rs | 76 ++++++++++++++++++++++---------------- src/lib.rs | 28 +++++++++++--- 3 files changed, 105 insertions(+), 41 deletions(-) diff --git a/ruma-api-macros/src/api.rs b/ruma-api-macros/src/api.rs index d7741437..ab5cc91b 100644 --- a/ruma-api-macros/src/api.rs +++ b/ruma-api-macros/src/api.rs @@ -7,7 +7,7 @@ use quote::{quote, ToTokens}; use syn::{ braced, parse::{Parse, ParseStream}, - Field, FieldValue, Ident, Token, + Field, FieldValue, Ident, Token, Type, }; mod attribute; @@ -32,6 +32,8 @@ pub struct Api { request: Request, /// The `response` section of the macro. response: Response, + /// The `error` section of the macro. + error: Type, } impl TryFrom for Api { @@ -42,6 +44,9 @@ impl TryFrom for Api { metadata: raw_api.metadata.try_into()?, request: raw_api.request.try_into()?, response: raw_api.response.try_into()?, + error: raw_api + .error + .map_or(syn::parse_str::("ruma_api::error::Void").unwrap(), |err| err.ty), }; let newtype_body_field = res.request.newtype_body_field(); @@ -398,6 +403,8 @@ impl ToTokens for Api { ); let response_doc = format!("Data in the response from the `{}` API endpoint.", name); + let error = &self.error; + let api = quote! { use ruma_api::exports::serde::de::Error as _; use ruma_api::exports::serde::Deserialize as _; @@ -473,7 +480,7 @@ impl ToTokens for Api { } impl std::convert::TryFrom>> for #response_try_from_type { - type Error = ruma_api::error::FromHttpResponseError; + type Error = ruma_api::error::FromHttpResponseError<#error>; #[allow(unused_variables)] fn try_from( @@ -488,13 +495,17 @@ impl ToTokens for Api { #response_init_fields }) } else { - Err(ruma_api::error::ServerError::new(response).into()) + match <#error as ruma_api::EndpointError>::try_from_response(response) { + Ok(err) => Err(ruma_api::error::ServerError::Known(err).into()), + Err(response_err) => Err(ruma_api::error::ServerError::Unknown(response_err).into()) + } } } } impl ruma_api::Endpoint for Request { type Response = Response; + type ResponseError = #error; /// Metadata for the `#name` endpoint. const METADATA: ruma_api::Metadata = ruma_api::Metadata { @@ -519,6 +530,7 @@ mod kw { custom_keyword!(metadata); custom_keyword!(request); custom_keyword!(response); + custom_keyword!(error); } /// The entire `ruma_api!` macro structure directly as it appears in the source code.. @@ -529,11 +541,18 @@ pub struct RawApi { pub request: RawRequest, /// The `response` section of the macro. pub response: RawResponse, + /// The `error` section of the macro. + pub error: Option, } impl Parse for RawApi { fn parse(input: ParseStream<'_>) -> syn::Result { - Ok(Self { metadata: input.parse()?, request: input.parse()?, response: input.parse()? }) + Ok(Self { + metadata: input.parse()?, + request: input.parse()?, + response: input.parse()?, + error: input.parse().ok(), + }) } } @@ -599,3 +618,18 @@ impl Parse for RawResponse { }) } } + +pub struct RawErrorType { + pub error_kw: kw::error, + pub ty: Type, +} + +impl Parse for RawErrorType { + fn parse(input: ParseStream<'_>) -> syn::Result { + let error_kw = input.parse::()?; + input.parse::()?; + let ty = input.parse()?; + + Ok(Self { error_kw, ty }) + } +} diff --git a/src/error.rs b/src/error.rs index b3d17c9d..69e265f9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -4,6 +4,18 @@ use std::fmt::{self, Display, Formatter}; +// FIXME when `!` becomes stable use it +/// Default `ResponseError` for `ruma_api!` macro +#[derive(Clone, Copy, Debug)] +pub struct Void; + +impl crate::EndpointError for Void { + fn try_from_response( + response: http::Response>, + ) -> Result { + Err(ResponseDeserializationError::from_response(response)) + } +} /// An error when converting one of ruma's endpoint-specific request or response /// types to the corresponding http type. #[derive(Debug)] @@ -92,14 +104,14 @@ impl std::error::Error for RequestDeserializationError {} /// response types. #[derive(Debug)] #[non_exhaustive] -pub enum FromHttpResponseError { +pub enum FromHttpResponseError { /// Deserialization failed Deserialization(ResponseDeserializationError), /// The server returned a non-success status - Http(ServerError), + Http(ServerError), } -impl Display for FromHttpResponseError { +impl Display for FromHttpResponseError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { Self::Deserialization(err) => write!(f, "deserialization failed: {}", err), @@ -108,13 +120,13 @@ impl Display for FromHttpResponseError { } } -impl From for FromHttpResponseError { - fn from(err: ServerError) -> Self { +impl From> for FromHttpResponseError { + fn from(err: ServerError) -> Self { Self::Http(err) } } -impl From for FromHttpResponseError { +impl From for FromHttpResponseError { fn from(err: ResponseDeserializationError) -> Self { Self::Deserialization(err) } @@ -123,7 +135,7 @@ impl From for FromHttpResponseError { /// An error that occurred when trying to deserialize a response. #[derive(Debug)] pub struct ResponseDeserializationError { - inner: DeserializationError, + inner: Option, http_response: http::Response>, } @@ -135,13 +147,25 @@ impl ResponseDeserializationError { inner: impl Into, http_response: http::Response>, ) -> Self { - Self { inner: inner.into(), http_response } + Self { inner: Some(inner.into()), http_response } + } + + /// This method is public so it is accessible from `ruma_api!` generated + /// code. It is not considered part of ruma-api's public API. + /// Creates an Error from a `http::Response`. + #[doc(hidden)] + pub fn from_response(http_response: http::Response>) -> Self { + Self { http_response, inner: None } } } impl Display for ResponseDeserializationError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - Display::fmt(&self.inner, f) + if let Some(ref inner) = self.inner { + Display::fmt(inner, f) + } else { + Display::fmt("deserialization error, no error specified", f) + } } } @@ -149,36 +173,24 @@ impl std::error::Error for ResponseDeserializationError {} /// An error was reported by the server (HTTP status code 4xx or 5xx) #[derive(Debug)] -pub struct ServerError { - http_response: http::Response>, +pub enum ServerError { + /// An error that is expected to happen under certain circumstances and + /// that has a well-defined structure + Known(E), + /// An error of unexpected type of structure + Unknown(ResponseDeserializationError), } -impl ServerError { - /// This method is public so it is accessible from `ruma_api!` generated - /// code. It is not considered part of ruma-api's public API. - #[doc(hidden)] - pub fn new(http_response: http::Response>) -> Self { - Self { http_response } - } - - /// Get the HTTP response without parsing its contents. - pub fn into_raw_reponse(self) -> http::Response> { - self.http_response - } -} - -impl Display for ServerError { +impl Display for ServerError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match self.http_response.status().canonical_reason() { - Some(reason) => { - write!(f, "HTTP status {} {}", self.http_response.status().as_str(), reason) - } - None => write!(f, "HTTP status {}", self.http_response.status().as_str()), + match self { + ServerError::Known(e) => Display::fmt(e, f), + ServerError::Unknown(res_err) => Display::fmt(res_err, f), } } } -impl std::error::Error for ServerError {} +impl std::error::Error for ServerError {} #[derive(Debug)] enum SerializationError { diff --git a/src/lib.rs b/src/lib.rs index fd9abb37..286cde4a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -232,17 +232,32 @@ pub trait Outgoing { type Incoming; } +/// Gives users the ability to define their own serializable/deserializable errors. +pub trait EndpointError: Sized { + /// Tries to construct `Self` from an `http::Response`. + /// + /// This will always return `Err` variant when no `error` field is defined in + /// the `ruma_api` macro. + fn try_from_response( + response: http::Response>, + ) -> Result; +} + /// A Matrix API endpoint. /// /// The type implementing this trait contains any data needed to make a request to the endpoint. pub trait Endpoint: Outgoing + TryInto>, Error = IntoHttpError> where ::Incoming: TryFrom>, Error = FromHttpRequestError>, - ::Incoming: - TryFrom>, Error = FromHttpResponseError>, + ::Incoming: TryFrom< + http::Response>, + Error = FromHttpResponseError<::ResponseError>, + >, { /// Data returned in a successful response from the endpoint. type Response: Outgoing + TryInto>, Error = IntoHttpError>; + /// Error type returned when response from endpoint fails. + type ResponseError: EndpointError; /// Metadata about the endpoint. const METADATA: Metadata; @@ -284,7 +299,7 @@ mod tests { use crate::{ error::{ FromHttpRequestError, FromHttpResponseError, IntoHttpError, - RequestDeserializationError, ServerError, + RequestDeserializationError, ServerError, Void, }, Endpoint, Metadata, Outgoing, }; @@ -302,6 +317,7 @@ mod tests { impl Endpoint for Request { type Response = Response; + type ResponseError = Void; const METADATA: Metadata = Metadata { description: "Add an alias to a room.", @@ -384,13 +400,15 @@ mod tests { } impl TryFrom>> for Response { - type Error = FromHttpResponseError; + type Error = FromHttpResponseError; fn try_from(http_response: http::Response>) -> Result { if http_response.status().as_u16() < 400 { Ok(Response) } else { - Err(FromHttpResponseError::Http(ServerError::new(http_response))) + Err(FromHttpResponseError::Http(ServerError::Unknown( + crate::error::ResponseDeserializationError::from_response(http_response), + ))) } } }