Skip to main content

tomba/
tomba.rs

1// Copyright 2021 Tomba technology web service LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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/// Rate-limit information extracted from response headers.
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct RateLimit {
29    /// Maximum requests allowed per second.
30    pub x_second_rate_limit: Option<String>,
31    /// Maximum requests allowed per minute.
32    pub x_minute_rate_limit: Option<String>,
33    /// Maximum requests allowed per day.
34    pub x_daily_rate_limit: Option<String>,
35    /// Remaining requests in the current minute window.
36    pub x_minute_request_left: Option<String>,
37    /// Remaining requests in the current daily window.
38    pub x_daily_request_left: Option<String>,
39    /// Seconds until the per-minute limit resets.
40    pub x_minute_reset_seconds: Option<String>,
41    /// Seconds until the daily limit resets.
42    pub x_daily_reset_seconds: Option<String>,
43    /// Standard `Retry-After` header value (seconds).
44    pub retry_after: Option<String>,
45    /// Standard `RateLimit-Policy` header value.
46    pub rate_limit_policy: Option<String>,
47    /// Standard `RateLimit` header value.
48    pub rate_limit: Option<String>,
49}
50
51/// A response from the Tomba API containing the parsed JSON body
52/// and rate-limit metadata from the response headers.
53#[derive(Debug, Clone)]
54pub struct TombaResponse {
55    /// The parsed JSON response body.
56    pub data: Value,
57    /// Rate-limit information extracted from response headers.
58    pub rate_limit: RateLimit,
59}
60
61/// Parse rate-limit headers from an HTTP response.
62pub 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
83/// Configuration for the Tomba client.
84pub struct TombaConfig {
85    /// Tomba API key (starts with `ta_`).
86    pub key: String,
87    /// Tomba secret key (starts with `ts_`).
88    pub secret: String,
89}
90
91/// The Tomba API client.
92///
93/// Create an instance with [`Tomba::init`], then call the endpoint
94/// methods such as [`Tomba::account`], [`Tomba::domain_search`], etc.
95pub struct Tomba {
96    url: String,
97    key: String,
98    secret: String,
99    client: Client,
100}
101
102impl Tomba {
103    /// Create a new Tomba client.
104    ///
105    /// # Examples
106    ///
107    /// ```no_run
108    /// use tomba::{Tomba, TombaConfig};
109    ///
110    /// let config = TombaConfig {
111    ///     key: "ta_xxxx".to_string(),
112    ///     secret: "ts_xxxx".to_string(),
113    /// };
114    /// let mut tomba = Tomba::init(config).expect("should construct");
115    /// ```
116    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    // ------------------------------------------------------------------
129    // HTTP helpers
130    // ------------------------------------------------------------------
131
132    /// Send a request with query parameters (GET / DELETE).
133    ///
134    /// * `method` -- `"GET"` or `"DELETE"`
135    /// * `path`   -- path relative to the base URL, e.g. `"me"`
136    /// * `params` -- query-string key/value pairs
137    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    /// Send a request with a JSON body (POST / PUT).
162    ///
163    /// * `method` -- `"POST"` or `"PUT"`
164    /// * `path`   -- path relative to the base URL
165    /// * `body`   -- JSON value to send as the request body
166    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    /// Interpret the HTTP response, returning the parsed JSON body
191    /// along with rate-limit headers, or a [`TombaError`].
192    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}