mod get;
mod post;
use std::{io::Cursor, sync::Arc, time::Duration};
use const_format::formatcp;
use serde::{Serialize, de::DeserializeOwned};
use crate::{
API_BASE_URL, parser,
types::{
error::Result,
params::SearchParams,
response::{RateLimit, Response, SearchResult},
},
};
#[derive(Clone)]
pub struct Client {
reqwest: reqwest::Client,
api_key: Arc<String>,
system_id: u32,
}
impl Client {
pub fn new(api_key: &str, system_id: u32, user_agent: Option<&str>, timeout: Option<Duration>) -> Result<Self> {
const NAME: &str = env!("CARGO_PKG_NAME");
const VERSION: &str = env!("CARGO_PKG_VERSION");
const REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY");
Ok(Self {
reqwest: reqwest::ClientBuilder::new()
.user_agent(user_agent.unwrap_or(formatcp!("{NAME}/v{VERSION} +{REPOSITORY}")))
.timeout(timeout.unwrap_or(Duration::from_secs(20)))
.build()?,
api_key: Arc::new(api_key.to_string()),
system_id,
})
}
pub(crate) fn reader(response: &str) -> csv::Reader<Cursor<&str>> {
csv::ReaderBuilder::new().has_headers(false).flexible(true).from_reader(Cursor::new(response))
}
pub(crate) fn semicolon_reader(response: &str) -> csv::Reader<Cursor<&str>> {
csv::ReaderBuilder::new()
.has_headers(false)
.flexible(true)
.delimiter(b';')
.from_reader(Cursor::new(response))
}
pub(crate) fn writer() -> csv::Writer<Vec<u8>> {
csv::WriterBuilder::new().flexible(true).has_headers(false).from_writer(Vec::new())
}
fn headers(&self) -> reqwest::header::HeaderMap {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert("X-Pvoutput-Apikey", self.api_key.parse().unwrap());
headers.insert("X-Pvoutput-SystemId", self.system_id.to_string().parse().unwrap());
headers.insert("X-Rate-Limit", reqwest::header::HeaderValue::from_static("1"));
headers
}
async fn handle_response(response: reqwest::Response) -> Result<(RateLimit, String)> {
if response.status().is_success() {
Ok((RateLimit::try_from(response.headers())?, response.text().await?))
} else {
Err(parser::error(response).await)
}
}
async fn get(&self, url: &str, params: impl Serialize) -> Result<(RateLimit, String)> {
let response = self.reqwest.get(url).headers(self.headers()).query(¶ms).send().await?;
Self::handle_response(response).await
}
async fn post(&self, url: &str, body: impl Serialize) -> Result<(RateLimit, String)> {
let response = self.reqwest.post(url).headers(self.headers()).form(&body).send().await?;
Self::handle_response(response).await
}
async fn post_csv(&self, url: &str, body: Vec<impl Serialize>) -> Result<(RateLimit, String)> {
let mut writer = Self::writer();
for entry in body {
writer.serialize(entry)?;
}
let body = String::from_utf8_lossy(&writer.into_inner()?).replace('\n', ";");
self.post(url, &[("data", body)]).await
}
async fn get_multiple<T: DeserializeOwned>(&self, url: &str, params: impl Serialize) -> Result<Response<Vec<T>>> {
let (rate_limit, response) = self.get(url, params).await?;
let response = response.replace(';', "\n");
let mut reader = Self::reader(&response);
Ok(Response::new(reader.deserialize().collect::<csv::Result<Vec<T>>>()?, rate_limit))
}
async fn get_single<T: DeserializeOwned>(&self, url: &str, params: impl Serialize) -> Result<Response<T>> {
let (rate_limit, response) = self.get(url, params).await?;
let mut reader = Self::reader(&response);
Ok(Response::new(reader.deserialize().collect::<csv::Result<Vec<T>>>()?.remove(0), rate_limit))
}
async fn post_multiple<T: DeserializeOwned>(&self, url: &str, body: impl Serialize) -> Result<Response<Vec<T>>> {
let (rate_limit, response) = self.post(url, body).await?;
let response = response.replace(';', "\n");
let mut reader = Self::reader(&response);
Ok(Response::new(reader.deserialize().collect::<csv::Result<Vec<T>>>()?, rate_limit))
}
async fn post_empty_csv(&self, url: &str, body: Vec<impl Serialize>) -> Result<Response<()>> {
Ok(Response::new((), self.post_csv(url, body).await?.0))
}
async fn post_empty(&self, url: &str, body: impl Serialize) -> Result<Response<()>> {
Ok(Response::new((), self.post(url, body).await?.0))
}
pub async fn search(&self, params: SearchParams) -> Result<Response<Vec<SearchResult>>> {
self.get_multiple(formatcp!("{API_BASE_URL}/search.jsp"), params).await
}
}