1mod get;
4mod post;
5
6use std::{
7 fmt::{Debug, Display},
8 io::Cursor,
9 sync::Arc,
10 time::Duration,
11};
12
13use const_format::formatcp;
14use serde::{Serialize, de::DeserializeOwned};
15
16use crate::{
17 API_BASE_URL, parser,
18 parser::parse_none,
19 types::{
20 error::{NoEndpointSpecificError, Result},
21 params::SearchParams,
22 response::{RateLimit, Response, SearchResult},
23 },
24};
25
26#[derive(Clone)]
43pub struct Client {
44 reqwest: reqwest::Client,
45 api_key: Arc<String>,
46 system_id: u32,
47}
48
49impl Client {
50 pub fn new(
72 api_key: &str,
73 system_id: u32,
74 user_agent: Option<&str>,
75 timeout: Option<Duration>,
76 ) -> Result<Self, NoEndpointSpecificError> {
77 Ok(Self {
78 reqwest: reqwest::ClientBuilder::new()
79 .user_agent(user_agent.unwrap_or(Self::default_user_agent()))
80 .timeout(timeout.unwrap_or(Duration::from_secs(20)))
81 .build()?,
82 api_key: Arc::new(api_key.to_string()),
83 system_id,
84 })
85 }
86
87 pub fn new_with_reqwest(
106 api_key: &str,
107 system_id: u32,
108 reqwest: reqwest::Client,
109 ) -> Result<Self, NoEndpointSpecificError> {
110 Ok(Self {
111 reqwest,
112 api_key: Arc::new(api_key.to_string()),
113 system_id,
114 })
115 }
116
117 #[must_use]
120 pub const fn default_user_agent() -> &'static str {
121 const NAME: &str = env!("CARGO_PKG_NAME");
122 const VERSION: &str = env!("CARGO_PKG_VERSION");
123 const REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY");
124
125 formatcp!("{NAME}/v{VERSION} +{REPOSITORY}")
126 }
127
128 pub(crate) fn reader(response: &str) -> csv::Reader<Cursor<&str>> {
131 csv::ReaderBuilder::new().has_headers(false).flexible(true).from_reader(Cursor::new(response))
132 }
133
134 pub(crate) fn semicolon_reader(response: &str) -> csv::Reader<Cursor<&str>> {
136 csv::ReaderBuilder::new()
137 .has_headers(false)
138 .flexible(true)
139 .delimiter(b';')
140 .from_reader(Cursor::new(response))
141 }
142
143 pub(crate) fn writer() -> csv::Writer<Vec<u8>> {
145 csv::WriterBuilder::new().flexible(true).has_headers(false).from_writer(Vec::new())
146 }
147
148 fn headers(&self) -> reqwest::header::HeaderMap {
150 let mut headers = reqwest::header::HeaderMap::new();
151 headers.insert("X-Pvoutput-Apikey", self.api_key.parse().unwrap());
152 headers.insert("X-Pvoutput-SystemId", self.system_id.to_string().parse().unwrap());
153 headers.insert("X-Rate-Limit", reqwest::header::HeaderValue::from_static("1"));
154 headers
155 }
156
157 async fn handle_response<Endpoint, F>(response: reqwest::Response, parser: F) -> Result<(RateLimit, String), Endpoint>
160 where
161 F: FnOnce(&str) -> Option<Endpoint>,
162 Endpoint: Debug + Display,
163 {
164 if response.status().is_success() {
165 Ok((RateLimit::try_from(response.headers())?, response.text().await?))
167 } else {
168 Err(parser::error(response, parser).await)
169 }
170 }
171
172 async fn get<Endpoint, F>(
175 &self,
176 url: &str,
177 params: impl Serialize,
178 parser: F,
179 ) -> Result<(RateLimit, String), Endpoint>
180 where
181 F: FnOnce(&str) -> Option<Endpoint>,
182 Endpoint: Debug + Display,
183 {
184 let response = self.reqwest.get(url).headers(self.headers()).query(¶ms).send().await?;
185
186 Self::handle_response(response, parser).await
187 }
188
189 async fn post<Endpoint, F>(&self, url: &str, body: impl Serialize, parser: F) -> Result<(RateLimit, String), Endpoint>
192 where
193 F: FnOnce(&str) -> Option<Endpoint>,
194 Endpoint: Debug + Display,
195 {
196 let response = self.reqwest.post(url).headers(self.headers()).form(&body).send().await?;
197
198 Self::handle_response(response, parser).await
199 }
200
201 async fn post_csv<Endpoint, F>(
203 &self,
204 url: &str,
205 body: Vec<impl Serialize>,
206 parser: F,
207 ) -> Result<(RateLimit, String), Endpoint>
208 where
209 F: FnOnce(&str) -> Option<Endpoint>,
210 Endpoint: Debug + Display,
211 {
212 let mut writer = Self::writer();
213 for entry in body {
214 writer.serialize(entry)?;
215 }
216 let body = String::from_utf8_lossy(&writer.into_inner()?).replace('\n', ";");
217
218 self.post(url, &[("data", body)], parser).await
219 }
220
221 async fn get_multiple<T: DeserializeOwned, Endpoint, F>(
223 &self,
224 url: &str,
225 params: impl Serialize,
226 parser: F,
227 ) -> Result<Response<Vec<T>>, Endpoint>
228 where
229 F: FnOnce(&str) -> Option<Endpoint>,
230 Endpoint: Debug + Display,
231 {
232 let (rate_limit, response) = self.get(url, params, parser).await?;
233 let response = response.replace(';', "\n");
234 let mut reader = Self::reader(&response);
235
236 Ok(Response::new(reader.deserialize().collect::<csv::Result<Vec<T>>>()?, rate_limit))
237 }
238
239 async fn get_single<T: DeserializeOwned, Endpoint, F>(
241 &self,
242 url: &str,
243 params: impl Serialize,
244 parser: F,
245 ) -> Result<Response<T>, Endpoint>
246 where
247 F: FnOnce(&str) -> Option<Endpoint>,
248 Endpoint: Debug + Display,
249 {
250 let (rate_limit, response) = self.get(url, params, parser).await?;
251 let mut reader = Self::reader(&response);
252
253 Ok(Response::new(reader.deserialize().collect::<csv::Result<Vec<T>>>()?.remove(0), rate_limit))
254 }
255
256 async fn post_multiple<T: DeserializeOwned, Endpoint, F>(
258 &self,
259 url: &str,
260 body: impl Serialize,
261 parser: F,
262 ) -> Result<Response<Vec<T>>, Endpoint>
263 where
264 F: FnOnce(&str) -> Option<Endpoint>,
265 Endpoint: Debug + Display,
266 {
267 let (rate_limit, response) = self.post(url, body, parser).await?;
268 let response = response.replace(';', "\n");
269 let mut reader = Self::reader(&response);
270
271 Ok(Response::new(reader.deserialize().collect::<csv::Result<Vec<T>>>()?, rate_limit))
272 }
273
274 async fn post_empty_csv<Endpoint, F>(
276 &self,
277 url: &str,
278 body: Vec<impl Serialize>,
279 parser: F,
280 ) -> Result<Response<()>, Endpoint>
281 where
282 F: FnOnce(&str) -> Option<Endpoint>,
283 Endpoint: Debug + Display,
284 {
285 Ok(Response::new((), self.post_csv(url, body, parser).await?.0))
286 }
287
288 async fn post_empty<Endpoint, F>(&self, url: &str, body: impl Serialize, parser: F) -> Result<Response<()>, Endpoint>
290 where
291 F: FnOnce(&str) -> Option<Endpoint>,
292 Endpoint: Debug + Display,
293 {
294 Ok(Response::new((), self.post(url, body, parser).await?.0))
295 }
296
297 pub async fn search(&self, params: SearchParams) -> Result<Response<Vec<SearchResult>>, NoEndpointSpecificError> {
302 self.get_multiple(formatcp!("{API_BASE_URL}/search.jsp"), params, parse_none).await
303 }
304}