Enable deserialization of unsuccessful responses

Co-authored-by: Jonas Platte <jplatte@users.noreply.github.com>
This commit is contained in:
Ragotzy.devin 2020-03-16 20:07:12 -04:00 committed by GitHub
parent 6f5e25cb7d
commit 6a0ab987b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 105 additions and 41 deletions

View File

@ -7,7 +7,7 @@ use quote::{quote, ToTokens};
use syn::{ use syn::{
braced, braced,
parse::{Parse, ParseStream}, parse::{Parse, ParseStream},
Field, FieldValue, Ident, Token, Field, FieldValue, Ident, Token, Type,
}; };
mod attribute; mod attribute;
@ -32,6 +32,8 @@ pub struct Api {
request: Request, request: Request,
/// The `response` section of the macro. /// The `response` section of the macro.
response: Response, response: Response,
/// The `error` section of the macro.
error: Type,
} }
impl TryFrom<RawApi> for Api { impl TryFrom<RawApi> for Api {
@ -42,6 +44,9 @@ impl TryFrom<RawApi> for Api {
metadata: raw_api.metadata.try_into()?, metadata: raw_api.metadata.try_into()?,
request: raw_api.request.try_into()?, request: raw_api.request.try_into()?,
response: raw_api.response.try_into()?, response: raw_api.response.try_into()?,
error: raw_api
.error
.map_or(syn::parse_str::<Type>("ruma_api::error::Void").unwrap(), |err| err.ty),
}; };
let newtype_body_field = res.request.newtype_body_field(); 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 response_doc = format!("Data in the response from the `{}` API endpoint.", name);
let error = &self.error;
let api = quote! { let api = quote! {
use ruma_api::exports::serde::de::Error as _; use ruma_api::exports::serde::de::Error as _;
use ruma_api::exports::serde::Deserialize as _; use ruma_api::exports::serde::Deserialize as _;
@ -473,7 +480,7 @@ impl ToTokens for Api {
} }
impl std::convert::TryFrom<ruma_api::exports::http::Response<Vec<u8>>> for #response_try_from_type { impl std::convert::TryFrom<ruma_api::exports::http::Response<Vec<u8>>> for #response_try_from_type {
type Error = ruma_api::error::FromHttpResponseError; type Error = ruma_api::error::FromHttpResponseError<#error>;
#[allow(unused_variables)] #[allow(unused_variables)]
fn try_from( fn try_from(
@ -488,13 +495,17 @@ impl ToTokens for Api {
#response_init_fields #response_init_fields
}) })
} else { } 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 { impl ruma_api::Endpoint for Request {
type Response = Response; type Response = Response;
type ResponseError = #error;
/// Metadata for the `#name` endpoint. /// Metadata for the `#name` endpoint.
const METADATA: ruma_api::Metadata = ruma_api::Metadata { const METADATA: ruma_api::Metadata = ruma_api::Metadata {
@ -519,6 +530,7 @@ mod kw {
custom_keyword!(metadata); custom_keyword!(metadata);
custom_keyword!(request); custom_keyword!(request);
custom_keyword!(response); custom_keyword!(response);
custom_keyword!(error);
} }
/// The entire `ruma_api!` macro structure directly as it appears in the source code.. /// The entire `ruma_api!` macro structure directly as it appears in the source code..
@ -529,11 +541,18 @@ pub struct RawApi {
pub request: RawRequest, pub request: RawRequest,
/// The `response` section of the macro. /// The `response` section of the macro.
pub response: RawResponse, pub response: RawResponse,
/// The `error` section of the macro.
pub error: Option<RawErrorType>,
} }
impl Parse for RawApi { impl Parse for RawApi {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> { fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
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<Self> {
let error_kw = input.parse::<kw::error>()?;
input.parse::<Token![:]>()?;
let ty = input.parse()?;
Ok(Self { error_kw, ty })
}
}

View File

@ -4,6 +4,18 @@
use std::fmt::{self, Display, Formatter}; 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<Vec<u8>>,
) -> Result<Self, ResponseDeserializationError> {
Err(ResponseDeserializationError::from_response(response))
}
}
/// An error when converting one of ruma's endpoint-specific request or response /// An error when converting one of ruma's endpoint-specific request or response
/// types to the corresponding http type. /// types to the corresponding http type.
#[derive(Debug)] #[derive(Debug)]
@ -92,14 +104,14 @@ impl std::error::Error for RequestDeserializationError {}
/// response types. /// response types.
#[derive(Debug)] #[derive(Debug)]
#[non_exhaustive] #[non_exhaustive]
pub enum FromHttpResponseError { pub enum FromHttpResponseError<E> {
/// Deserialization failed /// Deserialization failed
Deserialization(ResponseDeserializationError), Deserialization(ResponseDeserializationError),
/// The server returned a non-success status /// The server returned a non-success status
Http(ServerError), Http(ServerError<E>),
} }
impl Display for FromHttpResponseError { impl<E: Display> Display for FromHttpResponseError<E> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self { match self {
Self::Deserialization(err) => write!(f, "deserialization failed: {}", err), Self::Deserialization(err) => write!(f, "deserialization failed: {}", err),
@ -108,13 +120,13 @@ impl Display for FromHttpResponseError {
} }
} }
impl From<ServerError> for FromHttpResponseError { impl<E> From<ServerError<E>> for FromHttpResponseError<E> {
fn from(err: ServerError) -> Self { fn from(err: ServerError<E>) -> Self {
Self::Http(err) Self::Http(err)
} }
} }
impl From<ResponseDeserializationError> for FromHttpResponseError { impl<E> From<ResponseDeserializationError> for FromHttpResponseError<E> {
fn from(err: ResponseDeserializationError) -> Self { fn from(err: ResponseDeserializationError) -> Self {
Self::Deserialization(err) Self::Deserialization(err)
} }
@ -123,7 +135,7 @@ impl From<ResponseDeserializationError> for FromHttpResponseError {
/// An error that occurred when trying to deserialize a response. /// An error that occurred when trying to deserialize a response.
#[derive(Debug)] #[derive(Debug)]
pub struct ResponseDeserializationError { pub struct ResponseDeserializationError {
inner: DeserializationError, inner: Option<DeserializationError>,
http_response: http::Response<Vec<u8>>, http_response: http::Response<Vec<u8>>,
} }
@ -135,13 +147,25 @@ impl ResponseDeserializationError {
inner: impl Into<DeserializationError>, inner: impl Into<DeserializationError>,
http_response: http::Response<Vec<u8>>, http_response: http::Response<Vec<u8>>,
) -> Self { ) -> 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<Vec<u8>>) -> Self {
Self { http_response, inner: None }
} }
} }
impl Display for ResponseDeserializationError { impl Display for ResponseDeserializationError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 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) /// An error was reported by the server (HTTP status code 4xx or 5xx)
#[derive(Debug)] #[derive(Debug)]
pub struct ServerError { pub enum ServerError<E> {
http_response: http::Response<Vec<u8>>, /// 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 { impl<E: Display> Display for ServerError<E> {
/// 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<Vec<u8>>) -> Self {
Self { http_response }
}
/// Get the HTTP response without parsing its contents.
pub fn into_raw_reponse(self) -> http::Response<Vec<u8>> {
self.http_response
}
}
impl Display for ServerError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self.http_response.status().canonical_reason() { match self {
Some(reason) => { ServerError::Known(e) => Display::fmt(e, f),
write!(f, "HTTP status {} {}", self.http_response.status().as_str(), reason) ServerError::Unknown(res_err) => Display::fmt(res_err, f),
}
None => write!(f, "HTTP status {}", self.http_response.status().as_str()),
} }
} }
} }
impl std::error::Error for ServerError {} impl<E: std::error::Error> std::error::Error for ServerError<E> {}
#[derive(Debug)] #[derive(Debug)]
enum SerializationError { enum SerializationError {

View File

@ -232,17 +232,32 @@ pub trait Outgoing {
type Incoming; 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<Vec<u8>>,
) -> Result<Self, error::ResponseDeserializationError>;
}
/// A Matrix API endpoint. /// A Matrix API endpoint.
/// ///
/// The type implementing this trait contains any data needed to make a request to the endpoint. /// The type implementing this trait contains any data needed to make a request to the endpoint.
pub trait Endpoint: Outgoing + TryInto<http::Request<Vec<u8>>, Error = IntoHttpError> pub trait Endpoint: Outgoing + TryInto<http::Request<Vec<u8>>, Error = IntoHttpError>
where where
<Self as Outgoing>::Incoming: TryFrom<http::Request<Vec<u8>>, Error = FromHttpRequestError>, <Self as Outgoing>::Incoming: TryFrom<http::Request<Vec<u8>>, Error = FromHttpRequestError>,
<Self::Response as Outgoing>::Incoming: <Self::Response as Outgoing>::Incoming: TryFrom<
TryFrom<http::Response<Vec<u8>>, Error = FromHttpResponseError>, http::Response<Vec<u8>>,
Error = FromHttpResponseError<<Self as Endpoint>::ResponseError>,
>,
{ {
/// Data returned in a successful response from the endpoint. /// Data returned in a successful response from the endpoint.
type Response: Outgoing + TryInto<http::Response<Vec<u8>>, Error = IntoHttpError>; type Response: Outgoing + TryInto<http::Response<Vec<u8>>, Error = IntoHttpError>;
/// Error type returned when response from endpoint fails.
type ResponseError: EndpointError;
/// Metadata about the endpoint. /// Metadata about the endpoint.
const METADATA: Metadata; const METADATA: Metadata;
@ -284,7 +299,7 @@ mod tests {
use crate::{ use crate::{
error::{ error::{
FromHttpRequestError, FromHttpResponseError, IntoHttpError, FromHttpRequestError, FromHttpResponseError, IntoHttpError,
RequestDeserializationError, ServerError, RequestDeserializationError, ServerError, Void,
}, },
Endpoint, Metadata, Outgoing, Endpoint, Metadata, Outgoing,
}; };
@ -302,6 +317,7 @@ mod tests {
impl Endpoint for Request { impl Endpoint for Request {
type Response = Response; type Response = Response;
type ResponseError = Void;
const METADATA: Metadata = Metadata { const METADATA: Metadata = Metadata {
description: "Add an alias to a room.", description: "Add an alias to a room.",
@ -384,13 +400,15 @@ mod tests {
} }
impl TryFrom<http::Response<Vec<u8>>> for Response { impl TryFrom<http::Response<Vec<u8>>> for Response {
type Error = FromHttpResponseError; type Error = FromHttpResponseError<Void>;
fn try_from(http_response: http::Response<Vec<u8>>) -> Result<Response, Self::Error> { fn try_from(http_response: http::Response<Vec<u8>>) -> Result<Response, Self::Error> {
if http_response.status().as_u16() < 400 { if http_response.status().as_u16() < 400 {
Ok(Response) Ok(Response)
} else { } else {
Err(FromHttpResponseError::Http(ServerError::new(http_response))) Err(FromHttpResponseError::Http(ServerError::Unknown(
crate::error::ResponseDeserializationError::from_response(http_response),
)))
} }
} }
} }