shuttle_api_client/
util.rs

1use std::fmt::Debug;
2
3use anyhow::{Context, Result};
4use async_trait::async_trait;
5use bytes::Bytes;
6use http::StatusCode;
7use serde::de::DeserializeOwned;
8use shuttle_common::models::error::ApiError;
9
10/// Helpers for consuming and parsing response bodies and handling parsing of an ApiError if the response is 4xx/5xx
11#[async_trait]
12pub trait ToBodyContent {
13    async fn to_json<T: DeserializeOwned>(self) -> Result<ParsedJson<T>>;
14    async fn to_text(self) -> Result<String>;
15    async fn to_bytes(self) -> Result<Bytes>;
16    async fn to_empty(self) -> Result<()>;
17}
18
19fn into_api_error(body: &str, status_code: StatusCode) -> ApiError {
20    #[cfg(feature = "tracing")]
21    tracing::trace!("Parsing response as API error");
22
23    let res: ApiError = match serde_json::from_str(body) {
24        Ok(res) => res,
25        _ => ApiError::new(
26            format!("Failed to parse error response from the server:\n{}", body),
27            status_code,
28        ),
29    };
30
31    res
32}
33
34/// Tries to convert bytes to string. If not possible, returns a string symbolizing the bytes and the length
35fn bytes_to_string_with_fallback(bytes: Bytes) -> String {
36    String::from_utf8(bytes.to_vec()).unwrap_or_else(|_| format!("[{} bytes]", bytes.len()))
37}
38
39pub struct ParsedJson<T> {
40    inner: T,
41    pub raw_json: String,
42}
43
44impl<T> ParsedJson<T> {
45    pub fn into_inner(self) -> T {
46        self.inner
47    }
48    pub fn into_parts(self) -> (T, String) {
49        (self.inner, self.raw_json)
50    }
51}
52
53impl<T> AsRef<T> for ParsedJson<T> {
54    fn as_ref(&self) -> &T {
55        &self.inner
56    }
57}
58
59impl<T: Debug> Debug for ParsedJson<T> {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        self.inner.fmt(f)
62    }
63}
64impl<T: std::fmt::Display> std::fmt::Display for ParsedJson<T> {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        self.inner.fmt(f)
67    }
68}
69
70#[async_trait]
71impl ToBodyContent for reqwest::Response {
72    async fn to_json<T: DeserializeOwned>(self) -> Result<ParsedJson<T>> {
73        let status_code = self.status();
74        let bytes = self.bytes().await?;
75        let string = bytes_to_string_with_fallback(bytes);
76
77        #[cfg(feature = "tracing")]
78        tracing::trace!(response = %string, "Parsing response as JSON");
79
80        if status_code.is_client_error() || status_code.is_server_error() {
81            return Err(into_api_error(&string, status_code).into());
82        }
83
84        let t = serde_json::from_str(&string).context("failed to parse a successful response")?;
85
86        Ok(ParsedJson {
87            inner: t,
88            raw_json: string,
89        })
90    }
91
92    async fn to_text(self) -> Result<String> {
93        let status_code = self.status();
94        let bytes = self.bytes().await?;
95        let string = bytes_to_string_with_fallback(bytes);
96
97        #[cfg(feature = "tracing")]
98        tracing::trace!(response = %string, "Parsing response as text");
99
100        if status_code.is_client_error() || status_code.is_server_error() {
101            return Err(into_api_error(&string, status_code).into());
102        }
103
104        Ok(string)
105    }
106
107    async fn to_bytes(self) -> Result<Bytes> {
108        let status_code = self.status();
109        let bytes = self.bytes().await?;
110
111        #[cfg(feature = "tracing")]
112        tracing::trace!(response_length = bytes.len(), "Got response bytes");
113
114        if status_code.is_client_error() || status_code.is_server_error() {
115            let string = bytes_to_string_with_fallback(bytes);
116            return Err(into_api_error(&string, status_code).into());
117        }
118
119        Ok(bytes)
120    }
121
122    async fn to_empty(self) -> Result<()> {
123        let status_code = self.status();
124
125        if status_code.is_client_error() || status_code.is_server_error() {
126            let bytes = self.bytes().await?;
127            let string = bytes_to_string_with_fallback(bytes);
128            return Err(into_api_error(&string, status_code).into());
129        }
130
131        Ok(())
132    }
133}