1use std::collections::HashMap;
16
17use reqwest::blocking::Client;
18use serde::{Deserialize, Serialize};
19use serde_json::Value;
20
21use crate::error::TombaError;
22use crate::DEFAULT_BASE_URL;
23
24const SDK_VERSION: &str = "tomba:rust:v1.0.0";
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct RateLimit {
29 pub x_second_rate_limit: Option<String>,
31 pub x_minute_rate_limit: Option<String>,
33 pub x_daily_rate_limit: Option<String>,
35 pub x_minute_request_left: Option<String>,
37 pub x_daily_request_left: Option<String>,
39 pub x_minute_reset_seconds: Option<String>,
41 pub x_daily_reset_seconds: Option<String>,
43 pub retry_after: Option<String>,
45 pub rate_limit_policy: Option<String>,
47 pub rate_limit: Option<String>,
49}
50
51#[derive(Debug, Clone)]
54pub struct TombaResponse {
55 pub data: Value,
57 pub rate_limit: RateLimit,
59}
60
61pub fn parse_rate_limit(headers: &reqwest::header::HeaderMap) -> RateLimit {
63 let get = |name: &str| -> Option<String> {
64 headers
65 .get(name)
66 .and_then(|v| v.to_str().ok())
67 .map(String::from)
68 };
69 RateLimit {
70 x_second_rate_limit: get("x-second-rate-limit"),
71 x_minute_rate_limit: get("x-minute-rate-limit"),
72 x_daily_rate_limit: get("x-daily-rate-limit"),
73 x_minute_request_left: get("x-minute-request-left"),
74 x_daily_request_left: get("x-daily-request-left"),
75 x_minute_reset_seconds: get("x-minute-reset-seconds"),
76 x_daily_reset_seconds: get("x-daily-reset-seconds"),
77 retry_after: get("retry-after"),
78 rate_limit_policy: get("ratelimit-policy"),
79 rate_limit: get("ratelimit"),
80 }
81}
82
83pub struct TombaConfig {
85 pub key: String,
87 pub secret: String,
89}
90
91pub struct Tomba {
96 url: String,
97 key: String,
98 secret: String,
99 client: Client,
100}
101
102impl Tomba {
103 pub fn init(config: TombaConfig) -> Result<Self, TombaError> {
117 let client = Client::builder()
118 .timeout(std::time::Duration::from_secs(120))
119 .build()?;
120 Ok(Self {
121 url: DEFAULT_BASE_URL.to_owned(),
122 key: config.key,
123 secret: config.secret,
124 client,
125 })
126 }
127
128 pub fn call(
138 &self,
139 method: &str,
140 path: &str,
141 params: &HashMap<String, String>,
142 ) -> Result<TombaResponse, TombaError> {
143 let url = format!("{}{}", self.url, path);
144
145 let builder = match method {
146 "DELETE" => self.client.delete(&url),
147 _ => self.client.get(&url),
148 };
149
150 let resp = builder
151 .header("X-Tomba-Key", &self.key)
152 .header("X-Tomba-Secret", &self.secret)
153 .header("Content-Type", "application/json")
154 .header("x-Sdk-Version", SDK_VERSION)
155 .query(params)
156 .send()?;
157
158 self.handle_response(resp)
159 }
160
161 pub fn call_json(
167 &self,
168 method: &str,
169 path: &str,
170 body: &Value,
171 ) -> Result<TombaResponse, TombaError> {
172 let url = format!("{}{}", self.url, path);
173
174 let builder = match method {
175 "PUT" => self.client.put(&url),
176 _ => self.client.post(&url),
177 };
178
179 let resp = builder
180 .header("X-Tomba-Key", &self.key)
181 .header("X-Tomba-Secret", &self.secret)
182 .header("Content-Type", "application/json")
183 .header("x-Sdk-Version", SDK_VERSION)
184 .json(body)
185 .send()?;
186
187 self.handle_response(resp)
188 }
189
190 fn handle_response(
193 &self,
194 resp: reqwest::blocking::Response,
195 ) -> Result<TombaResponse, TombaError> {
196 let status = resp.status().as_u16();
197 let rate_limit = parse_rate_limit(resp.headers());
198 let body = resp.text()?;
199
200 if status >= 400 {
201 let message = serde_json::from_str::<Value>(&body)
202 .ok()
203 .and_then(|v| {
204 v.get("errors")
205 .and_then(|e| {
206 e.get(0)
207 .and_then(|e0| e0.get("message"))
208 .and_then(|m| m.as_str())
209 .map(String::from)
210 })
211 .or_else(|| {
212 v.get("message")
213 .and_then(|m| m.as_str())
214 .map(String::from)
215 })
216 })
217 .unwrap_or(body);
218
219 return Err(TombaError::Api {
220 message,
221 code: status,
222 });
223 }
224
225 let parsed: Value = serde_json::from_str(&body)?;
226 Ok(TombaResponse {
227 data: parsed,
228 rate_limit,
229 })
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn test_tomba_config() {
239 let config = TombaConfig {
240 key: "ta_key".to_string(),
241 secret: "ts_secret".to_string(),
242 };
243
244 assert_eq!(config.key, "ta_key");
245 assert_eq!(config.secret, "ts_secret");
246 }
247
248 #[test]
249 fn test_tomba_init() {
250 let config = TombaConfig {
251 key: "ta_key".to_string(),
252 secret: "ts_secret".to_string(),
253 };
254 let tomba = Tomba::init(config).expect("should construct");
255
256 assert_eq!(tomba.key, "ta_key");
257 assert_eq!(tomba.secret, "ts_secret");
258 assert_eq!(tomba.url, DEFAULT_BASE_URL);
259 }
260}