mod get;
mod post;
use std::{
fmt::{Debug, Display},
io::Cursor,
sync::Arc,
time::Duration,
};
use const_format::formatcp;
use serde::{Serialize, de::DeserializeOwned};
use crate::{
API_BASE_URL, parser,
parser::parse_none,
types::{
error::{NoEndpointSpecificError, 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, NoEndpointSpecificError> {
Ok(Self {
reqwest: reqwest::ClientBuilder::new()
.user_agent(user_agent.unwrap_or(Self::default_user_agent()))
.timeout(timeout.unwrap_or(Duration::from_secs(20)))
.build()?,
api_key: Arc::new(api_key.to_string()),
system_id,
})
}
pub fn new_with_reqwest(
api_key: &str,
system_id: u32,
reqwest: reqwest::Client,
) -> Result<Self, NoEndpointSpecificError> {
Ok(Self {
reqwest,
api_key: Arc::new(api_key.to_string()),
system_id,
})
}
#[must_use]
pub const fn default_user_agent() -> &'static str {
const NAME: &str = env!("CARGO_PKG_NAME");
const VERSION: &str = env!("CARGO_PKG_VERSION");
const REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY");
formatcp!("{NAME}/v{VERSION} +{REPOSITORY}")
}
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<Endpoint, F>(response: reqwest::Response, parser: F) -> Result<(RateLimit, String), Endpoint>
where
F: FnOnce(&str) -> Option<Endpoint>,
Endpoint: Debug + Display,
{
if response.status().is_success() {
Ok((RateLimit::try_from(response.headers())?, response.text().await?))
} else {
Err(parser::error(response, parser).await)
}
}
async fn get<Endpoint, F>(
&self,
url: &str,
params: impl Serialize,
parser: F,
) -> Result<(RateLimit, String), Endpoint>
where
F: FnOnce(&str) -> Option<Endpoint>,
Endpoint: Debug + Display,
{
let response = self.reqwest.get(url).headers(self.headers()).query(¶ms).send().await?;
Self::handle_response(response, parser).await
}
async fn post<Endpoint, F>(&self, url: &str, body: impl Serialize, parser: F) -> Result<(RateLimit, String), Endpoint>
where
F: FnOnce(&str) -> Option<Endpoint>,
Endpoint: Debug + Display,
{
let response = self.reqwest.post(url).headers(self.headers()).form(&body).send().await?;
Self::handle_response(response, parser).await
}
async fn post_csv<Endpoint, F>(
&self,
url: &str,
body: Vec<impl Serialize>,
parser: F,
) -> Result<(RateLimit, String), Endpoint>
where
F: FnOnce(&str) -> Option<Endpoint>,
Endpoint: Debug + Display,
{
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)], parser).await
}
async fn get_multiple<T: DeserializeOwned, Endpoint, F>(
&self,
url: &str,
params: impl Serialize,
parser: F,
) -> Result<Response<Vec<T>>, Endpoint>
where
F: FnOnce(&str) -> Option<Endpoint>,
Endpoint: Debug + Display,
{
let (rate_limit, response) = self.get(url, params, parser).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, Endpoint, F>(
&self,
url: &str,
params: impl Serialize,
parser: F,
) -> Result<Response<T>, Endpoint>
where
F: FnOnce(&str) -> Option<Endpoint>,
Endpoint: Debug + Display,
{
let (rate_limit, response) = self.get(url, params, parser).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, Endpoint, F>(
&self,
url: &str,
body: impl Serialize,
parser: F,
) -> Result<Response<Vec<T>>, Endpoint>
where
F: FnOnce(&str) -> Option<Endpoint>,
Endpoint: Debug + Display,
{
let (rate_limit, response) = self.post(url, body, parser).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<Endpoint, F>(
&self,
url: &str,
body: Vec<impl Serialize>,
parser: F,
) -> Result<Response<()>, Endpoint>
where
F: FnOnce(&str) -> Option<Endpoint>,
Endpoint: Debug + Display,
{
Ok(Response::new((), self.post_csv(url, body, parser).await?.0))
}
async fn post_empty<Endpoint, F>(&self, url: &str, body: impl Serialize, parser: F) -> Result<Response<()>, Endpoint>
where
F: FnOnce(&str) -> Option<Endpoint>,
Endpoint: Debug + Display,
{
Ok(Response::new((), self.post(url, body, parser).await?.0))
}
pub async fn search(&self, params: SearchParams) -> Result<Response<Vec<SearchResult>>, NoEndpointSpecificError> {
self.get_multiple(formatcp!("{API_BASE_URL}/search.jsp"), params, parse_none).await
}
}