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 Ok(serde_json::from_value(serde_json::Value::Null)?)
119 } else {
120 Ok(serde_json::from_str(&raw_body)?)
121 }
122 } else {
123 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}