1use std::fmt;
4use std::time::Duration;
5
6use crate::api::{ApiError, Request, BASE_URL};
7
8const TIMEOUT: Duration = Duration::from_secs(10);
9
10const MAX_BODY_BYTES: u64 = 1_000_000;
12
13pub struct Client {
14 agent: ureq::Agent,
15 base_url: String,
16 api_key: String,
17}
18
19#[derive(Debug)]
20#[non_exhaustive]
21pub enum Error {
22 Transport(TransportError),
23 Api(ApiError),
24}
25
26#[derive(Debug)]
30pub struct TransportError(Cause);
31
32#[derive(Debug)]
33enum Cause {
34 Http(ureq::Error),
35 UnusableKey,
36}
37
38impl Client {
39 pub fn new(api_key: impl Into<String>) -> Self {
42 Self {
43 agent: agent(TIMEOUT),
44 base_url: BASE_URL.to_owned(),
45 api_key: api_key.into().trim().to_owned(),
46 }
47 }
48
49 pub fn with_timeout(self, timeout: Duration) -> Self {
52 Self {
53 agent: agent(timeout.min(Duration::from_secs(86_400))),
55 ..self
56 }
57 }
58
59 pub fn with_base_url(self, base_url: impl Into<String>) -> Self {
61 Self {
62 base_url: base_url.into().trim_end_matches('/').to_owned(),
63 ..self
64 }
65 }
66
67 pub fn send<T>(&self, request: &Request<T>) -> Result<T, Error> {
68 if !self.api_key.bytes().all(|byte| byte.is_ascii_graphic()) || self.api_key.is_empty() {
70 return Err(Error::Transport(TransportError(Cause::UnusableKey)));
71 }
72 let call = self
73 .agent
74 .get(format!("{}{}", self.base_url, request.path))
75 .header("X-API-Key", &self.api_key);
76 let mut response = request
77 .query
78 .iter()
79 .fold(call, |call, (key, value)| call.query(key, value))
80 .call()
81 .map_err(failed)?;
82 let body = response
83 .body_mut()
84 .with_config()
85 .limit(MAX_BODY_BYTES)
86 .read_to_vec()
87 .map_err(failed)?;
88 let body = String::from_utf8_lossy(&body);
90 Ok(request.decode(response.status().as_u16(), &body)?)
91 }
92}
93
94fn agent(timeout: Duration) -> ureq::Agent {
95 ureq::Agent::config_builder()
96 .http_status_as_error(false)
98 .max_redirects(0)
100 .timeout_global(Some(timeout))
101 .build()
102 .into()
103}
104
105fn failed(error: ureq::Error) -> Error {
106 Error::Transport(TransportError(Cause::Http(error)))
107}
108
109impl fmt::Display for TransportError {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 match &self.0 {
112 Cause::Http(error) => error.fmt(f),
113 Cause::UnusableKey => f.write_str("unusable API key"),
114 }
115 }
116}
117
118impl std::error::Error for TransportError {
121 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
122 match &self.0 {
123 Cause::Http(error) => error.source(),
124 Cause::UnusableKey => None,
125 }
126 }
127}
128
129impl From<ApiError> for Error {
130 fn from(error: ApiError) -> Self {
131 Self::Api(error)
132 }
133}
134
135impl fmt::Display for Error {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 match self {
138 Self::Transport(error) => error.fmt(f),
139 Self::Api(error) => error.fmt(f),
140 }
141 }
142}
143
144impl std::error::Error for Error {
145 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
146 match self {
147 Self::Transport(error) => error.source(),
148 Self::Api(error) => error.source(),
149 }
150 }
151}