use std::sync::{Arc, Mutex};
use std::time::Duration;
use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, USER_AGENT};
use crate::error::{from_response, Error, Result};
use crate::resources::cards::CardsResource;
use crate::resources::games::GamesResource;
use crate::resources::sets::SetsResource;
const DEFAULT_BASE_URL: &str = "https://api.tcgpricelookup.com/v1";
const DEFAULT_USER_AGENT: &str = concat!("tcglookup-rs/", env!("CARGO_PKG_VERSION"));
const DEFAULT_TIMEOUT_SECS: u64 = 30;
#[derive(Debug, Clone, Copy, Default)]
pub struct RateLimitInfo {
pub limit: Option<u32>,
pub remaining: Option<u32>,
}
#[derive(Clone)]
pub struct Client {
inner: Arc<ClientInner>,
}
struct ClientInner {
api_key: String,
base_url: String,
user_agent: String,
http: reqwest::Client,
rate_limit: Mutex<RateLimitInfo>,
}
impl Client {
pub fn new(api_key: impl Into<String>) -> Self {
ClientBuilder::new(api_key).build()
}
pub fn builder(api_key: impl Into<String>) -> ClientBuilder {
ClientBuilder::new(api_key)
}
pub fn cards(&self) -> CardsResource<'_> {
CardsResource::new(self)
}
pub fn sets(&self) -> SetsResource<'_> {
SetsResource::new(self)
}
pub fn games(&self) -> GamesResource<'_> {
GamesResource::new(self)
}
pub fn rate_limit(&self) -> RateLimitInfo {
*self.inner.rate_limit.lock().unwrap()
}
pub(crate) async fn get<T>(&self, path: &str, query: &[(&str, String)]) -> Result<T>
where
T: serde::de::DeserializeOwned,
{
let url = format!("{}{}", self.inner.base_url, path);
let req = self
.inner
.http
.get(&url)
.header("X-API-Key", &self.inner.api_key)
.header(ACCEPT, "application/json")
.header(USER_AGENT, &self.inner.user_agent)
.query(query);
let res = req.send().await?;
let status = res.status();
self.capture_rate_limit(res.headers());
let body = res.text().await?;
if !status.is_success() {
return Err(from_response(status.as_u16(), body));
}
if body.is_empty() {
return Err(Error::Api {
status: status.as_u16(),
message: "empty response body".to_string(),
body: String::new(),
});
}
serde_json::from_str(&body).map_err(Error::from)
}
fn capture_rate_limit(&self, headers: &HeaderMap) {
let parse = |k: &str| {
headers
.get(k)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u32>().ok())
};
let mut rl = self.inner.rate_limit.lock().unwrap();
rl.limit = parse("x-ratelimit-limit");
rl.remaining = parse("x-ratelimit-remaining");
}
}
pub struct ClientBuilder {
api_key: String,
base_url: String,
user_agent: String,
timeout: Duration,
}
impl ClientBuilder {
pub fn new(api_key: impl Into<String>) -> Self {
Self {
api_key: api_key.into(),
base_url: DEFAULT_BASE_URL.to_string(),
user_agent: DEFAULT_USER_AGENT.to_string(),
timeout: Duration::from_secs(DEFAULT_TIMEOUT_SECS),
}
}
pub fn base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = url.into().trim_end_matches('/').to_string();
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
self.user_agent = ua.into();
self
}
pub fn build(self) -> Client {
let mut headers = HeaderMap::new();
headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
let http = reqwest::Client::builder()
.timeout(self.timeout)
.default_headers(headers)
.build()
.expect("reqwest client construction should never fail");
Client {
inner: Arc::new(ClientInner {
api_key: self.api_key,
base_url: self.base_url,
user_agent: self.user_agent,
http,
rate_limit: Mutex::new(RateLimitInfo::default()),
}),
}
}
}