tenderly_rs/core/
api_client.rs

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
375/// API Client Provider
376///
377/// Provides API clients for different API versions.
378/// Clients are created on-demand and cached for reuse.
379pub struct ApiClientProvider {
380    pub(crate) api_key: String,
381}
382
383impl ApiClientProvider {
384    /// Create a new ApiClientProvider
385    pub fn new(api_key: String) -> Self {
386        Self { api_key }
387    }
388
389    /// Get an API client for the specified version
390    ///
391    /// Creates a new client each time. For better performance,
392    /// clients should be stored and reused (e.g., in services).
393    pub fn get_api_client(&self, version: ApiVersion) -> Result<ApiClient, GeneralError> {
394        ApiClient::new(self.api_key.clone(), version)
395    }
396}