use std::collections::HashMap;
use reqwest::blocking::Client;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::error::TombaError;
use crate::DEFAULT_BASE_URL;
const SDK_VERSION: &str = "tomba:rust:v1.0.0";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RateLimit {
pub x_second_rate_limit: Option<String>,
pub x_minute_rate_limit: Option<String>,
pub x_daily_rate_limit: Option<String>,
pub x_minute_request_left: Option<String>,
pub x_daily_request_left: Option<String>,
pub x_minute_reset_seconds: Option<String>,
pub x_daily_reset_seconds: Option<String>,
pub retry_after: Option<String>,
pub rate_limit_policy: Option<String>,
pub rate_limit: Option<String>,
}
#[derive(Debug, Clone)]
pub struct TombaResponse {
pub data: Value,
pub rate_limit: RateLimit,
}
pub fn parse_rate_limit(headers: &reqwest::header::HeaderMap) -> RateLimit {
let get = |name: &str| -> Option<String> {
headers
.get(name)
.and_then(|v| v.to_str().ok())
.map(String::from)
};
RateLimit {
x_second_rate_limit: get("x-second-rate-limit"),
x_minute_rate_limit: get("x-minute-rate-limit"),
x_daily_rate_limit: get("x-daily-rate-limit"),
x_minute_request_left: get("x-minute-request-left"),
x_daily_request_left: get("x-daily-request-left"),
x_minute_reset_seconds: get("x-minute-reset-seconds"),
x_daily_reset_seconds: get("x-daily-reset-seconds"),
retry_after: get("retry-after"),
rate_limit_policy: get("ratelimit-policy"),
rate_limit: get("ratelimit"),
}
}
pub struct TombaConfig {
pub key: String,
pub secret: String,
}
pub struct Tomba {
url: String,
key: String,
secret: String,
client: Client,
}
impl Tomba {
pub fn init(config: TombaConfig) -> Result<Self, TombaError> {
let client = Client::builder()
.timeout(std::time::Duration::from_secs(120))
.build()?;
Ok(Self {
url: DEFAULT_BASE_URL.to_owned(),
key: config.key,
secret: config.secret,
client,
})
}
pub fn call(
&self,
method: &str,
path: &str,
params: &HashMap<String, String>,
) -> Result<TombaResponse, TombaError> {
let url = format!("{}{}", self.url, path);
let builder = match method {
"DELETE" => self.client.delete(&url),
_ => self.client.get(&url),
};
let resp = builder
.header("X-Tomba-Key", &self.key)
.header("X-Tomba-Secret", &self.secret)
.header("Content-Type", "application/json")
.header("x-Sdk-Version", SDK_VERSION)
.query(params)
.send()?;
self.handle_response(resp)
}
pub fn call_json(
&self,
method: &str,
path: &str,
body: &Value,
) -> Result<TombaResponse, TombaError> {
let url = format!("{}{}", self.url, path);
let builder = match method {
"PUT" => self.client.put(&url),
_ => self.client.post(&url),
};
let resp = builder
.header("X-Tomba-Key", &self.key)
.header("X-Tomba-Secret", &self.secret)
.header("Content-Type", "application/json")
.header("x-Sdk-Version", SDK_VERSION)
.json(body)
.send()?;
self.handle_response(resp)
}
fn handle_response(
&self,
resp: reqwest::blocking::Response,
) -> Result<TombaResponse, TombaError> {
let status = resp.status().as_u16();
let rate_limit = parse_rate_limit(resp.headers());
let body = resp.text()?;
if status >= 400 {
let message = serde_json::from_str::<Value>(&body)
.ok()
.and_then(|v| {
v.get("errors")
.and_then(|e| {
e.get(0)
.and_then(|e0| e0.get("message"))
.and_then(|m| m.as_str())
.map(String::from)
})
.or_else(|| {
v.get("message")
.and_then(|m| m.as_str())
.map(String::from)
})
})
.unwrap_or(body);
return Err(TombaError::Api {
message,
code: status,
});
}
let parsed: Value = serde_json::from_str(&body)?;
Ok(TombaResponse {
data: parsed,
rate_limit,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tomba_config() {
let config = TombaConfig {
key: "ta_key".to_string(),
secret: "ts_secret".to_string(),
};
assert_eq!(config.key, "ta_key");
assert_eq!(config.secret, "ts_secret");
}
#[test]
fn test_tomba_init() {
let config = TombaConfig {
key: "ta_key".to_string(),
secret: "ts_secret".to_string(),
};
let tomba = Tomba::init(config).expect("should construct");
assert_eq!(tomba.key, "ta_key");
assert_eq!(tomba.secret, "ts_secret");
assert_eq!(tomba.url, DEFAULT_BASE_URL);
}
}