1use std::fmt;
2use std::sync::Arc;
3use std::time::Duration;
4
5use serde::de::DeserializeOwned;
6use serde::Deserialize;
7use ureq::RequestExt;
8
9use crate::error::{ApiError, Error};
10use crate::multipart::Form;
11use crate::version::{DEFAULT_BASE_URL, VERSION};
12
13pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18#[non_exhaustive]
19pub struct HttpRequest {
20 pub method: String,
22 pub url: String,
24 pub headers: Vec<(String, String)>,
26 pub body: Option<Vec<u8>>,
28 pub timeout: Duration,
30}
31
32impl HttpRequest {
33 pub fn header(&self, name: &str) -> Option<&str> {
35 find(&self.headers, name)
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Default)]
41pub struct HttpResponse {
42 pub status: u16,
44 pub headers: Vec<(String, String)>,
46 pub body: Vec<u8>,
48}
49
50impl HttpResponse {
51 pub fn new(status: u16, body: impl Into<Vec<u8>>) -> Self {
53 Self {
54 status,
55 headers: Vec::new(),
56 body: body.into(),
57 }
58 }
59
60 pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
62 self.headers.push((name.into(), value.into()));
63 self
64 }
65
66 pub fn header(&self, name: &str) -> Option<&str> {
68 find(&self.headers, name)
69 }
70}
71
72fn find<'a>(headers: &'a [(String, String)], name: &str) -> Option<&'a str> {
73 headers
74 .iter()
75 .find(|(key, _)| key.eq_ignore_ascii_case(name))
76 .map(|(_, value)| value.as_str())
77}
78
79pub trait Transport: Send + Sync {
84 fn send(&self, request: HttpRequest) -> Result<HttpResponse, BoxError>;
86}
87
88impl<F> Transport for F
89where
90 F: Fn(HttpRequest) -> Result<HttpResponse, BoxError> + Send + Sync,
91{
92 fn send(&self, request: HttpRequest) -> Result<HttpResponse, BoxError> {
93 self(request)
94 }
95}
96
97#[derive(Debug, Clone)]
100pub struct UreqTransport {
101 agent: ureq::Agent,
102}
103
104impl UreqTransport {
105 pub fn new() -> Self {
107 let agent = ureq::Agent::config_builder()
108 .http_status_as_error(false)
109 .max_redirects(0)
111 .max_redirects_will_error(false)
112 .build()
113 .new_agent();
114 Self { agent }
115 }
116}
117
118impl Default for UreqTransport {
119 fn default() -> Self {
120 Self::new()
121 }
122}
123
124impl Transport for UreqTransport {
125 fn send(&self, request: HttpRequest) -> Result<HttpResponse, BoxError> {
126 let mut builder = ureq::http::Request::builder()
127 .method(request.method.as_str())
128 .uri(request.url.as_str());
129 for (name, value) in &request.headers {
130 builder = builder.header(name.as_str(), value.as_str());
131 }
132 let timeout = Some(request.timeout);
133 let mut response = match request.body {
134 Some(body) => builder
135 .body(body)?
136 .with_agent(&self.agent)
137 .configure()
138 .timeout_global(timeout)
139 .run()?,
140 None => builder
141 .body(())?
142 .with_agent(&self.agent)
143 .configure()
144 .timeout_global(timeout)
145 .run()?,
146 };
147 let headers = response
148 .headers()
149 .iter()
150 .filter_map(|(name, value)| {
151 Some((name.as_str().to_owned(), value.to_str().ok()?.to_owned()))
152 })
153 .collect();
154 let status = response.status().as_u16();
155 let body = response.body_mut().read_to_vec()?;
156 Ok(HttpResponse {
157 status,
158 headers,
159 body,
160 })
161 }
162}
163
164#[derive(Debug, Clone, Default, PartialEq, Eq)]
166pub struct RequestOptions {
167 pub api_key: Option<String>,
169}
170
171#[derive(Clone)]
176pub struct Client {
177 api_key: String,
178 base_url: String,
179 timeout: Duration,
180 transport: Arc<dyn Transport>,
181}
182
183impl fmt::Debug for Client {
184 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185 f.debug_struct("Client")
186 .field("base_url", &self.base_url)
187 .field("timeout", &self.timeout)
188 .finish_non_exhaustive()
189 }
190}
191
192pub struct ClientBuilder {
194 api_key: String,
195 base_url: String,
196 timeout: Duration,
197 transport: Option<Arc<dyn Transport>>,
198}
199
200impl fmt::Debug for ClientBuilder {
201 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202 f.debug_struct("ClientBuilder")
203 .field("base_url", &self.base_url)
204 .field("timeout", &self.timeout)
205 .finish_non_exhaustive()
206 }
207}
208
209impl ClientBuilder {
210 pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
215 self.base_url = base_url.into().trim_end_matches('/').to_owned();
216 self
217 }
218
219 pub fn timeout(mut self, timeout: Duration) -> Self {
221 self.timeout = timeout;
222 self
223 }
224
225 pub fn transport(mut self, transport: impl Transport + 'static) -> Self {
227 self.transport = Some(Arc::new(transport));
228 self
229 }
230
231 pub fn build(self) -> Client {
233 Client {
234 api_key: self.api_key,
235 base_url: self.base_url,
236 timeout: self.timeout,
237 transport: self
238 .transport
239 .unwrap_or_else(|| Arc::new(UreqTransport::new())),
240 }
241 }
242}
243
244#[derive(Deserialize)]
246pub(crate) struct Data<T> {
247 pub(crate) data: T,
248}
249
250impl Client {
251 pub fn new(api_key: impl Into<String>) -> Self {
253 Self::builder(api_key).build()
254 }
255
256 pub fn builder(api_key: impl Into<String>) -> ClientBuilder {
258 ClientBuilder {
259 api_key: api_key.into(),
260 base_url: DEFAULT_BASE_URL.to_owned(),
261 timeout: Duration::from_secs(30),
262 transport: None,
263 }
264 }
265
266 pub(crate) fn request<T: DeserializeOwned>(
269 &self,
270 method: &str,
271 path: &str,
272 query: &[(&str, String)],
273 form: Option<Form<'_>>,
274 options: &RequestOptions,
275 ) -> Result<T, Error> {
276 if !secure_base_url(&self.base_url) {
277 return Err(Error::InsecureBaseUrl);
278 }
279 let mut url = format!("{}{}", self.base_url, path);
280 let pairs: Vec<String> = query
281 .iter()
282 .filter(|(_, value)| !value.is_empty())
283 .map(|(name, value)| format!("{}={}", escape(name), escape(value)))
284 .collect();
285 if !pairs.is_empty() {
286 url.push('?');
287 url.push_str(&pairs.join("&"));
288 }
289 let api_key = options.api_key.as_deref().unwrap_or(&self.api_key);
290 let mut headers = vec![
291 ("Authorization".to_owned(), format!("Bearer {api_key}")),
292 ("Accept".to_owned(), "application/json".to_owned()),
293 ("User-Agent".to_owned(), format!("alpr-rust/{VERSION}")),
294 ];
295 let body = form.map(|form| {
296 let (body, content_type) = form.encode();
297 headers.push(("Content-Type".to_owned(), content_type));
298 body
299 });
300 let request = HttpRequest {
301 method: method.to_owned(),
302 url,
303 headers,
304 body,
305 timeout: self.timeout,
306 };
307 let response = self.transport.send(request).map_err(Error::Transport)?;
308 if response.status >= 300 {
309 return Err(Error::Api(ApiError::from_response(&response)));
310 }
311 if response.status == 204 {
312 return serde_json::from_slice(b"null").map_err(Error::Json);
314 }
315 serde_json::from_slice(&response.body).map_err(Error::Json)
316 }
317}
318
319fn secure_base_url(base_url: &str) -> bool {
321 let Ok(uri) = base_url.parse::<ureq::http::Uri>() else {
322 return false;
323 };
324 let host = uri
325 .host()
326 .unwrap_or("")
327 .trim_matches(['[', ']'])
328 .to_ascii_lowercase();
329 let loopback = host == "localhost"
330 || host
331 .parse::<std::net::IpAddr>()
332 .is_ok_and(|address| address.is_loopback());
333 match uri.scheme_str().map(str::to_ascii_lowercase).as_deref() {
334 Some("https") => true,
335 Some("http") => loopback,
336 _ => false,
337 }
338}
339
340pub(crate) fn escape(value: &str) -> String {
343 const HEX: &[u8; 16] = b"0123456789ABCDEF";
344 let mut out = String::with_capacity(value.len());
345 for byte in value.bytes() {
346 if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
347 out.push(byte as char);
348 } else {
349 out.push('%');
350 out.push(HEX[usize::from(byte >> 4)] as char);
351 out.push(HEX[usize::from(byte & 15)] as char);
352 }
353 }
354 out
355}