cronback_client/
api.rs

1use std::collections::BTreeMap;
2
3use http::StatusCode;
4use serde::de::DeserializeOwned;
5use serde::Deserialize;
6use tracing::log::warn;
7use url::Url;
8
9pub const REQUEST_ID_HEADER: &str = "x-cronback-request-id";
10pub const PROJECT_ID_HEADER: &str = "x-cronback-project-id";
11
12#[derive(Deserialize, Debug)]
13struct ApiErrorBody {
14    message: String,
15    params: Option<BTreeMap<String, Vec<String>>>,
16}
17
18#[derive(Debug, Clone)]
19pub struct ApiError {
20    status_code: StatusCode,
21    message: String,
22    params: Option<BTreeMap<String, Vec<String>>>,
23}
24
25impl std::fmt::Display for ApiError {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        writeln!(f, "({}) {}", self.status_code, self.message)?;
28        if let Some(ref params) = self.params {
29            for (key, errors) in params {
30                writeln!(f, "  [{}]:", key)?;
31                for error in errors {
32                    writeln!(f, "    - {}", error)?;
33                }
34            }
35        }
36        Ok(())
37    }
38}
39
40impl std::error::Error for ApiError {}
41
42#[derive(Debug, Clone)]
43pub struct Response<T> {
44    inner: Result<T, ApiError>,
45    url: Url,
46    request_id: Option<String>,
47    project_id: Option<String>,
48    status_code: StatusCode,
49    headers: http::HeaderMap,
50    raw_body: String,
51}
52
53impl<T> Response<T> {
54    pub fn into_inner(self) -> Result<T, ApiError> {
55        self.inner
56    }
57
58    pub fn inner(&self) -> &Result<T, ApiError> {
59        &self.inner
60    }
61
62    pub fn request_id(&self) -> &Option<String> {
63        &self.request_id
64    }
65
66    pub fn project_id(&self) -> &Option<String> {
67        &self.project_id
68    }
69
70    pub fn headers(&self) -> &http::HeaderMap {
71        &self.headers
72    }
73
74    pub fn status_code(&self) -> http::StatusCode {
75        self.status_code
76    }
77
78    pub fn raw_body(&self) -> &str {
79        &self.raw_body
80    }
81
82    pub fn url(&self) -> &Url {
83        &self.url
84    }
85
86    pub fn is_err(&self) -> bool {
87        self.inner.is_err()
88    }
89
90    pub fn is_ok(&self) -> bool {
91        self.inner.is_ok()
92    }
93}
94
95impl<T> Response<T>
96where
97    T: DeserializeOwned,
98{
99    pub(crate) async fn from_raw_response(
100        raw: reqwest::Response,
101    ) -> Result<Self, crate::Error> {
102        let url = raw.url().clone();
103        let status_code = raw.status();
104        let headers = raw.headers().clone();
105        let project_id = headers
106            .get(PROJECT_ID_HEADER)
107            .map(|v| v.to_str().unwrap().to_owned());
108        let request_id = headers
109            .get(REQUEST_ID_HEADER)
110            .map(|v| v.to_str().unwrap().to_owned());
111
112        let raw_body = raw.text().await?;
113
114        let inner = if status_code.is_success() {
115            if raw_body.is_empty() {
116                // Handles the case where the response is empty and the target
117                // type is ()
118                Ok(serde_json::from_value(serde_json::Value::Null)?)
119            } else {
120                Ok(serde_json::from_str(&raw_body)?)
121            }
122        } else {
123            // Attempt to parse the error as json
124            let error_body: Result<ApiErrorBody, serde_json::Error> =
125                serde_json::from_str(&raw_body);
126            match error_body {
127                | Ok(error_body) => {
128                    Err(ApiError {
129                        status_code,
130                        message: error_body.message,
131                        params: error_body.params,
132                    })
133                }
134                | Err(e) => {
135                    warn!(
136                        "Response error body is not json. Error: {}. Body: {}",
137                        e, raw_body
138                    );
139                    Err(ApiError {
140                        status_code,
141                        message: raw_body.clone(),
142                        params: None,
143                    })
144                }
145            }
146        };
147
148        Ok(Self {
149            inner,
150            url,
151            project_id,
152            request_id,
153            status_code,
154            headers,
155            raw_body,
156        })
157    }
158}