1use std::fmt;
4use std::time::Duration;
5
6use crate::api::{ApiError, Request, BASE_URL};
7
8const TIMEOUT: Duration = Duration::from_secs(10);
9
10pub struct Client {
11 agent: ureq::Agent,
12 base_url: String,
13 api_key: String,
14}
15
16#[derive(Debug)]
17#[non_exhaustive]
18pub enum Error {
19 Transport(TransportError),
20 Api(ApiError),
21}
22
23#[derive(Debug)]
27pub struct TransportError(ureq::Error);
28
29impl Client {
30 pub fn new(api_key: impl Into<String>) -> Self {
31 Self {
32 agent: agent(TIMEOUT),
33 base_url: BASE_URL.to_owned(),
34 api_key: api_key.into(),
35 }
36 }
37
38 pub fn with_timeout(self, timeout: Duration) -> Self {
40 Self {
41 agent: agent(timeout),
42 ..self
43 }
44 }
45
46 pub fn with_base_url(self, base_url: impl Into<String>) -> Self {
48 Self {
49 base_url: base_url.into(),
50 ..self
51 }
52 }
53
54 pub fn send<T>(&self, request: &Request<T>) -> Result<T, Error> {
55 let call = self
56 .agent
57 .get(format!("{}{}", self.base_url, request.path))
58 .header("X-API-Key", &self.api_key);
59 let mut response = request
60 .query
61 .iter()
62 .fold(call, |call, (key, value)| call.query(key, value))
63 .call()
64 .map_err(failed)?;
65 let body = response.body_mut().read_to_string().map_err(failed)?;
66 Ok(request.decode(response.status().as_u16(), &body)?)
67 }
68}
69
70fn agent(timeout: Duration) -> ureq::Agent {
71 ureq::Agent::config_builder()
72 .http_status_as_error(false)
74 .max_redirects(0)
76 .timeout_global(Some(timeout))
77 .build()
78 .into()
79}
80
81fn failed(error: ureq::Error) -> Error {
82 Error::Transport(TransportError(error))
83}
84
85impl fmt::Display for TransportError {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 self.0.fmt(f)
88 }
89}
90
91impl std::error::Error for TransportError {
92 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
93 Some(&self.0)
94 }
95}
96
97impl From<ApiError> for Error {
98 fn from(error: ApiError) -> Self {
99 Self::Api(error)
100 }
101}
102
103impl fmt::Display for Error {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 match self {
106 Self::Transport(error) => error.fmt(f),
107 Self::Api(error) => error.fmt(f),
108 }
109 }
110}
111
112impl std::error::Error for Error {
113 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
114 match self {
115 Self::Transport(error) => Some(error),
116 Self::Api(error) => Some(error),
117 }
118 }
119}