1use std::collections::HashMap;
2
3use serde::{
4 Deserialize,
5 Serialize,
6};
7
8use crate::{
9 constants::{
10 TENDERLY_API_BASE_URL,
11 TENDERLY_SDK_VERSION,
12 },
13 errors::GeneralError,
14};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum ApiVersion {
18 V1,
19 V2,
20}
21
22impl ApiVersion {
23 fn as_str(&self) -> &'static str {
24 match self {
25 ApiVersion::V1 => "v1",
26 ApiVersion::V2 => "v2",
27 }
28 }
29}
30
31pub struct ApiClient {
32 client: reqwest::Client,
33 base_url: String,
34 api_key: String,
35}
36
37impl ApiClient {
38 pub fn new(api_key: String, version: ApiVersion) -> Result<Self, GeneralError> {
39 let client = reqwest::Client::builder()
40 .build()
41 .map_err(|e| GeneralError::Unknown(format!("Failed to create HTTP client: {}", e)))?;
42
43 let base_url = format!("{}/api/{}", TENDERLY_API_BASE_URL, version.as_str());
44
45 Ok(Self {
46 client,
47 base_url,
48 api_key,
49 })
50 }
51
52 pub async fn get<T>(
53 &self,
54 path: &str,
55 params: Option<&HashMap<String, Vec<String>>>,
56 ) -> Result<T, GeneralError>
57 where
58 T: for<'de> Deserialize<'de>,
59 {
60 let url = format!("{}{}", self.base_url, path.replace(' ', ""));
61
62 let mut request = self
63 .client
64 .get(&url)
65 .header("Content-Type", "application/json")
66 .header("X-Access-Key", &self.api_key)
67 .header(
68 "X-User-Agent",
69 format!("tenderly-rs/{}", TENDERLY_SDK_VERSION),
70 );
71
72 if let Some(params) = params {
73 for (key, values) in params {
74 for value in values {
75 request = request.query(&[(key, value)]);
76 }
77 }
78 }
79
80 let response = request.send().await?;
81 let status = response.status();
82
83 if !status.is_success() {
84 let error_data: serde_json::Value = response.json().await.unwrap_or_default();
85 return Err(GeneralError::ApiError(crate::errors::ApiError::new(
86 status.as_u16(),
87 crate::errors::TenderlyError {
88 id: error_data
89 .get("error")
90 .and_then(|e| e.get("id"))
91 .and_then(|v| v.as_str())
92 .map(|s| s.to_string()),
93 message: error_data
94 .get("error")
95 .and_then(|e| e.get("message"))
96 .and_then(|v| v.as_str())
97 .unwrap_or("Unknown error")
98 .to_string(),
99 slug: error_data
100 .get("error")
101 .and_then(|e| e.get("slug"))
102 .and_then(|v| v.as_str())
103 .unwrap_or("unknown")
104 .to_string(),
105 },
106 )));
107 }
108
109 let data: T = response.json().await?;
110 Ok(data)
111 }
112
113 pub async fn post<T, U>(&self, path: &str, data: Option<&T>) -> Result<U, GeneralError>
114 where
115 T: Serialize,
116 U: for<'de> Deserialize<'de>,
117 {
118 let url = format!("{}{}", self.base_url, path.replace(' ', ""));
119
120 let mut request = self
121 .client
122 .post(&url)
123 .header("Content-Type", "application/json")
124 .header("X-Access-Key", &self.api_key)
125 .header(
126 "X-User-Agent",
127 format!("tenderly-rs/{}", TENDERLY_SDK_VERSION),
128 );
129
130 if let Some(data) = data {
131 request = request.json(data);
132 }
133
134 let response = request.send().await?;
135 let status = response.status();
136
137 if !status.is_success() {
138 let error_data: serde_json::Value = response.json().await.unwrap_or_default();
139 return Err(GeneralError::ApiError(crate::errors::ApiError::new(
140 status.as_u16(),
141 crate::errors::TenderlyError {
142 id: error_data
143 .get("error")
144 .and_then(|e| e.get("id"))
145 .and_then(|v| v.as_str())
146 .map(|s| s.to_string()),
147 message: error_data
148 .get("error")
149 .and_then(|e| e.get("message"))
150 .and_then(|v| v.as_str())
151 .unwrap_or("Unknown error")
152 .to_string(),
153 slug: error_data
154 .get("error")
155 .and_then(|e| e.get("slug"))
156 .and_then(|v| v.as_str())
157 .unwrap_or("unknown")
158 .to_string(),
159 },
160 )));
161 }
162
163 let text = response.text().await?;
164 if text.is_empty() {
165 return Err(GeneralError::InvalidResponse(
166 crate::errors::InvalidResponseError::new("Empty response from API".to_string()),
167 ));
168 }
169
170 let mut json_value: serde_json::Value = serde_json::from_str(&text)
171 .map_err(|e| GeneralError::Unknown(format!("Error decoding response: {}", e)))?;
172
173 fn cleanup_balance_fields(value: &mut serde_json::Value) {
174 match value {
175 serde_json::Value::Object(map) => {
176 map.remove("balance");
177 for v in map.values_mut() {
178 cleanup_balance_fields(v);
179 }
180 }
181 serde_json::Value::Array(arr) => {
182 for item in arr.iter_mut() {
183 cleanup_balance_fields(item);
184 }
185 }
186 _ => {}
187 }
188 }
189
190 match serde_json::from_value::<U>(json_value.clone()) {
191 Ok(data) => Ok(data),
192 Err(e) => {
193 cleanup_balance_fields(&mut json_value);
194 serde_json::from_value(json_value)
195 .map_err(|_| GeneralError::Unknown(format!("Error decoding response: {}", e)))
196 }
197 }
198 }
199
200 pub async fn post_ignore_response<T>(
201 &self,
202 path: &str,
203 data: Option<&T>,
204 ) -> Result<(), GeneralError>
205 where
206 T: Serialize,
207 {
208 let url = format!("{}{}", self.base_url, path.replace(' ', ""));
209
210 let mut request = self
211 .client
212 .post(&url)
213 .header("Content-Type", "application/json")
214 .header("X-Access-Key", &self.api_key)
215 .header(
216 "X-User-Agent",
217 format!("tenderly-rs/{}", TENDERLY_SDK_VERSION),
218 );
219
220 if let Some(data) = data {
221 request = request.json(data);
222 }
223
224 let response = request.send().await?;
225 let status = response.status();
226
227 if !status.is_success() {
228 let error_data: serde_json::Value = response.json().await.unwrap_or_default();
229 return Err(GeneralError::ApiError(crate::errors::ApiError::new(
230 status.as_u16(),
231 crate::errors::TenderlyError {
232 id: error_data
233 .get("error")
234 .and_then(|e| e.get("id"))
235 .and_then(|v| v.as_str())
236 .map(|s| s.to_string()),
237 message: error_data
238 .get("error")
239 .and_then(|e| e.get("message"))
240 .and_then(|v| v.as_str())
241 .unwrap_or("Unknown error")
242 .to_string(),
243 slug: error_data
244 .get("error")
245 .and_then(|e| e.get("slug"))
246 .and_then(|v| v.as_str())
247 .unwrap_or("unknown")
248 .to_string(),
249 },
250 )));
251 }
252
253 let _ = response.text().await?;
254 Ok(())
255 }
256
257 pub async fn put<T, U>(
258 &self,
259 path: &str,
260 data: Option<&T>,
261 params: Option<&HashMap<String, String>>,
262 ) -> Result<U, GeneralError>
263 where
264 T: Serialize,
265 U: for<'de> Deserialize<'de>,
266 {
267 let url = format!("{}{}", self.base_url, path.replace(' ', ""));
268
269 let mut request = self
270 .client
271 .put(&url)
272 .header("Content-Type", "application/json")
273 .header("X-Access-Key", &self.api_key)
274 .header(
275 "X-User-Agent",
276 format!("tenderly-rs/{}", TENDERLY_SDK_VERSION),
277 );
278
279 if let Some(params) = params {
280 for (key, value) in params {
281 request = request.query(&[(key, value)]);
282 }
283 }
284
285 if let Some(data) = data {
286 request = request.json(data);
287 }
288
289 let response = request.send().await?;
290 let status = response.status();
291
292 if !status.is_success() {
293 let error_data: serde_json::Value = response.json().await.unwrap_or_default();
294 return Err(GeneralError::ApiError(crate::errors::ApiError::new(
295 status.as_u16(),
296 crate::errors::TenderlyError {
297 id: error_data
298 .get("error")
299 .and_then(|e| e.get("id"))
300 .and_then(|v| v.as_str())
301 .map(|s| s.to_string()),
302 message: error_data
303 .get("error")
304 .and_then(|e| e.get("message"))
305 .and_then(|v| v.as_str())
306 .unwrap_or("Unknown error")
307 .to_string(),
308 slug: error_data
309 .get("error")
310 .and_then(|e| e.get("slug"))
311 .and_then(|v| v.as_str())
312 .unwrap_or("unknown")
313 .to_string(),
314 },
315 )));
316 }
317
318 let data: U = response.json().await?;
319 Ok(data)
320 }
321
322 pub async fn delete<T>(&self, path: &str, data: Option<&T>) -> Result<(), GeneralError>
323 where
324 T: Serialize,
325 {
326 let url = format!("{}{}", self.base_url, path.replace(' ', ""));
327
328 let mut request = self
329 .client
330 .delete(&url)
331 .header("Content-Type", "application/json")
332 .header("X-Access-Key", &self.api_key)
333 .header(
334 "X-User-Agent",
335 format!("tenderly-rs/{}", TENDERLY_SDK_VERSION),
336 );
337
338 if let Some(data) = data {
339 request = request.json(data);
340 }
341
342 let response = request.send().await?;
343 let status = response.status();
344
345 if !status.is_success() {
346 let error_data: serde_json::Value = response.json().await.unwrap_or_default();
347 return Err(GeneralError::ApiError(crate::errors::ApiError::new(
348 status.as_u16(),
349 crate::errors::TenderlyError {
350 id: error_data
351 .get("error")
352 .and_then(|e| e.get("id"))
353 .and_then(|v| v.as_str())
354 .map(|s| s.to_string()),
355 message: error_data
356 .get("error")
357 .and_then(|e| e.get("message"))
358 .and_then(|v| v.as_str())
359 .unwrap_or("Unknown error")
360 .to_string(),
361 slug: error_data
362 .get("error")
363 .and_then(|e| e.get("slug"))
364 .and_then(|v| v.as_str())
365 .unwrap_or("unknown")
366 .to_string(),
367 },
368 )));
369 }
370
371 Ok(())
372 }
373}
374
375pub struct ApiClientProvider {
380 pub(crate) api_key: String,
381}
382
383impl ApiClientProvider {
384 pub fn new(api_key: String) -> Self {
386 Self { api_key }
387 }
388
389 pub fn get_api_client(&self, version: ApiVersion) -> Result<ApiClient, GeneralError> {
394 ApiClient::new(self.api_key.clone(), version)
395 }
396}