reqwest_response_ext/
lib.rs

1//! `TypedResponse` allows you to keep response body data, while remembering the
2//! desired shape of both success and failure variant. `TypedResponse` lets you
3//! extract raw body, or deserialize it into either `serde_json::Value` or required
4//! success/failure shape based on the original response HTTP status.
5//!
6
7#![cfg_attr(feature = "pedantic", warn(clippy::pedantic))]
8#![warn(clippy::use_self)]
9#![warn(clippy::map_flatten)]
10#![warn(clippy::map_unwrap_or)]
11#![warn(clippy::flat_map_option)]
12#![warn(deprecated_in_future)]
13#![warn(future_incompatible)]
14#![warn(noop_method_call)]
15#![warn(unreachable_pub)]
16#![warn(missing_debug_implementations)]
17#![warn(rust_2018_compatibility)]
18#![warn(rust_2021_compatibility)]
19#![warn(rust_2018_idioms)]
20#![warn(unused)]
21#![deny(warnings)]
22
23use std::borrow::Cow;
24use std::marker::PhantomData;
25
26use serde::de;
27use serde_json as json;
28
29#[cfg(feature = "blocking")]
30pub mod blocking;
31
32/// Holds raw response body, while remembering desired shape of the success (`T`)
33/// and failure (`E`) variants.
34///
35#[derive(Clone, Debug)]
36pub struct TypedResponse<T, E> {
37    body: bytes::Bytes,
38    result: Result<PhantomData<T>, PhantomData<E>>,
39}
40
41impl<T, E> TypedResponse<T, E>
42where
43    T: de::DeserializeOwned,
44    E: de::DeserializeOwned + From<json::Error>,
45{
46    /// Converts `reqwest::Response` into `TypedResponse<T, E>`
47    ///
48    pub async fn try_from_response(response: reqwest::Response) -> reqwest::Result<Self> {
49        let result = match response.status().is_success() {
50            false => Err(PhantomData),
51            true => Ok(PhantomData),
52        };
53
54        // Bail early on server error
55        if response.status().is_server_error() {
56            response.error_for_status_ref()?;
57        }
58
59        let body = response.bytes().await?;
60
61        Ok(Self { body, result })
62    }
63
64    /// Access the raw HTTP response as bytes
65    ///
66    pub fn bytes(&self) -> &bytes::Bytes {
67        &self.body
68    }
69
70    /// Access the raw HTTP response body as text
71    ///
72    pub fn text(&self) -> Cow<'_, str> {
73        String::from_utf8_lossy(&self.body)
74    }
75
76    /// Convert this response into `Result<serde_json::Value, serde_json::Value>`
77    /// where `Ok` and `Err` variants are based on the original HTTP Status
78    /// In case the body is not a valid JSON by itself it creates a JSON object
79    /// with deserialization error as a string content
80    ///
81    pub fn into_json(self) -> Result<json::Value, json::Value> {
82        let json_err = |e: json::Error| json::json! { e.to_string() };
83        match self.result {
84            Ok(_) => Ok(json::from_slice(&self.body).map_err(json_err)?),
85            Err(_) => Err(json::from_slice(&self.body).map_err(json_err)?),
86        }
87    }
88
89    /// Convert this response into `Result<T, E>` where `Ok` and `Err` variants
90    /// are based on the original HTTP Status and type parameters. In case of
91    /// JSON deserialization error it will be converted into `E`
92    pub fn into_result(self) -> Result<T, E> {
93        match self.result {
94            Ok(_) => Ok(json::from_slice(&self.body)?),
95            Err(_) => Err(json::from_slice(&self.body)?),
96        }
97    }
98}
99
100#[async_trait::async_trait]
101pub trait ResponseExt: Sized {
102    async fn try_from_response<T, E>(self) -> reqwest::Result<TypedResponse<T, E>>
103    where
104        T: de::DeserializeOwned + Send,
105        E: de::DeserializeOwned + From<json::Error> + Send;
106}
107
108#[async_trait::async_trait]
109impl ResponseExt for reqwest::Response {
110    async fn try_from_response<T, E>(self) -> reqwest::Result<TypedResponse<T, E>>
111    where
112        T: de::DeserializeOwned + Send,
113        E: de::DeserializeOwned + From<json::Error> + Send,
114    {
115        TypedResponse::try_from_response(self).await
116    }
117}