marketstack/
marketstack.rs

1use std::convert::TryInto;
2use std::fmt::{self, Debug};
3
4use async_trait::async_trait;
5use bytes::Bytes;
6use http::{request, Response as HttpResponse};
7use log::{debug, error};
8use reqwest::blocking::Client;
9use reqwest::Client as AsyncClient;
10use thiserror::Error;
11use url::Url;
12
13use crate::api;
14use crate::auth::Auth;
15
16#[derive(Debug, Error)]
17#[non_exhaustive]
18pub enum RestError {
19    #[error("communication with marketstack: {}", source)]
20    Communication {
21        #[from]
22        source: reqwest::Error,
23    },
24    #[error("`http` error: {}", source)]
25    Http {
26        #[from]
27        source: http::Error,
28    },
29}
30
31#[derive(Debug, Error)]
32#[non_exhaustive]
33pub enum MarketstackError {
34    #[error("failed to parse url: {}", source)]
35    UrlParse {
36        #[from]
37        source: url::ParseError,
38    },
39    #[error("communication with marketstack: {}", source)]
40    Communication {
41        #[from]
42        source: reqwest::Error,
43    },
44    #[error("marketstack HTTP error: {}", status)]
45    Http { status: reqwest::StatusCode },
46    #[error("no response from marketstack")]
47    NoResponse {},
48    #[error("could not parse {} data from JSON: {}", typename, source)]
49    DataType {
50        #[source]
51        source: serde_json::Error,
52        typename: &'static str,
53    },
54    #[error("api error: {}", source)]
55    Api {
56        #[from]
57        source: api::ApiError<RestError>,
58    },
59}
60
61type MarketstackResult<T> = Result<T, MarketstackError>;
62
63/// A representation of the Marketstack API.
64#[derive(Clone)]
65pub struct Marketstack {
66    /// The client to use for API calls.
67    client: Client,
68    /// The base URL to use for API calls.
69    rest_url: Url,
70    /// The authentication information to use when communicating with Marketstack.
71    auth: Auth,
72}
73
74impl Debug for Marketstack {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        f.debug_struct("Marketstack")
77            .field("rest_url", &self.rest_url)
78            .finish()
79    }
80}
81
82impl Marketstack {
83    /// Create a new Marketstack API representation.
84    ///
85    /// The `token` should be a valid [personal access token](https://marketstack.com/documentation).
86    /// Errors out if `token` is invalid.
87    pub fn new<H, T>(host: H, token: T) -> MarketstackResult<Self>
88    where
89        H: AsRef<str>,
90        T: Into<String>,
91    {
92        Self::new_impl("https", host.as_ref(), Auth::Token(token.into()))
93    }
94
95    /// Create a new non-SSL Marketstack API representation.
96    ///
97    /// A `token` will still be required for insecure access.
98    pub fn new_insecure<H, T>(host: H, token: T) -> MarketstackResult<Self>
99    where
100        H: AsRef<str>,
101        T: Into<String>,
102    {
103        Self::new_impl("http", host.as_ref(), Auth::Token(token.into()))
104    }
105
106    /// Internal method to create a new Marketstack client.
107    fn new_impl(protocol: &str, host: &str, auth: Auth) -> MarketstackResult<Self> {
108        let rest_url = Url::parse(&format!("{}://{}/v1/", protocol, host))?;
109
110        // NOTE: If cert validation is implemented / required, then add it here as `ClientCert`
111        let client = Client::builder()
112            .danger_accept_invalid_certs(true)
113            .build()?;
114
115        let api = Marketstack {
116            client,
117            rest_url,
118            auth,
119        };
120
121        // Ensure the API is working.
122        api.auth.check_connection(&api)?;
123
124        Ok(api)
125    }
126
127    /// Create a new Marketstack API client builder.
128    pub fn builder<H, T>(host: H, token: T) -> MarketstackBuilder
129    where
130        H: Into<String>,
131        T: Into<String>,
132    {
133        MarketstackBuilder::new(host, token)
134    }
135
136    fn rest_simple(
137        &self,
138        request: http::request::Builder,
139        body: Vec<u8>,
140    ) -> Result<HttpResponse<Bytes>, api::ApiError<<Self as api::RestClient>::Error>> {
141        let call = || -> Result<_, RestError> {
142            let http_request = request.body(body)?;
143            let request = http_request.try_into()?;
144            let rsp = self.client.execute(request)?;
145
146            let mut http_rsp = HttpResponse::builder()
147                .status(rsp.status())
148                .version(rsp.version());
149            let headers = http_rsp.headers_mut().unwrap();
150            for (key, value) in rsp.headers() {
151                headers.insert(key, value.clone());
152            }
153            Ok(http_rsp.body(rsp.bytes()?)?)
154        };
155        call().map_err(api::ApiError::client)
156    }
157}
158
159/// Builder pattern implementation for Marketstack and AsyncMarketstack.
160pub struct MarketstackBuilder {
161    protocol: &'static str,
162    host: String,
163    token: Auth,
164}
165
166impl MarketstackBuilder {
167    /// Create a new Marketstack API client builder.
168    pub fn new<H, T>(host: H, token: T) -> Self
169    where
170        H: Into<String>,
171        T: Into<String>,
172    {
173        Self {
174            protocol: "https",
175            host: host.into(),
176            token: Auth::Token(token.into()),
177        }
178    }
179
180    /// Switch to an insecure protocol (http instead of https).
181    pub fn insecure(&mut self) -> &mut Self {
182        self.protocol = "http";
183        self
184    }
185
186    pub fn build(&self) -> MarketstackResult<Marketstack> {
187        Marketstack::new_impl(self.protocol, &self.host, self.token.clone())
188    }
189
190    pub async fn build_async(&self) -> MarketstackResult<AsyncMarketstack> {
191        AsyncMarketstack::new_impl(self.protocol, &self.host, self.token.clone()).await
192    }
193}
194
195impl api::RestClient for Marketstack {
196    type Error = RestError;
197
198    fn rest_endpoint(&self, endpoint: &str) -> Result<Url, api::ApiError<Self::Error>> {
199        Ok(self.rest_url.join(endpoint)?)
200    }
201
202    fn get_auth(&self) -> Option<Auth> {
203        Some(self.auth.clone())
204    }
205}
206
207impl api::Client for Marketstack {
208    fn rest(
209        &self,
210        request: request::Builder,
211        body: Vec<u8>,
212    ) -> Result<HttpResponse<Bytes>, api::ApiError<Self::Error>> {
213        self.rest_simple(request, body)
214    }
215}
216
217/// A represenation of the asynchronous Marketstack API.
218#[derive(Clone)]
219pub struct AsyncMarketstack {
220    /// The client to use for API calls.
221    client: reqwest::Client,
222    /// The base URL to use for API calls.
223    rest_url: Url,
224    /// The authentication information to use when communicating with Marketstack.
225    auth: Auth,
226}
227
228impl Debug for AsyncMarketstack {
229    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230        f.debug_struct("AsyncMarketstack")
231            .field("rest_url", &self.rest_url)
232            .finish()
233    }
234}
235
236#[async_trait]
237impl api::RestClient for AsyncMarketstack {
238    type Error = RestError;
239
240    fn rest_endpoint(&self, endpoint: &str) -> Result<Url, api::ApiError<Self::Error>> {
241        debug!(target: "marketstack", "REST api call {}", endpoint);
242        Ok(self.rest_url.join(endpoint)?)
243    }
244
245    fn get_auth(&self) -> Option<Auth> {
246        Some(self.auth.clone())
247    }
248}
249
250#[async_trait]
251impl api::AsyncClient for AsyncMarketstack {
252    async fn rest_async(
253        &self,
254        request: http::request::Builder,
255        body: Vec<u8>,
256    ) -> Result<HttpResponse<Bytes>, api::ApiError<<Self as api::RestClient>::Error>> {
257        self.rest_async_simple(request, body).await
258    }
259}
260
261impl AsyncMarketstack {
262    /// Internal method to create a new Marketstack client.
263    async fn new_impl(protocol: &str, host: &str, auth: Auth) -> MarketstackResult<Self> {
264        let rest_url = Url::parse(&format!("{}://{}/v1/", protocol, host))?;
265
266        let client = AsyncClient::builder()
267            .danger_accept_invalid_certs(true)
268            .build()?;
269
270        let api = AsyncMarketstack {
271            client,
272            rest_url,
273            auth,
274        };
275
276        // Ensure the API is working.
277        api.auth.check_connection_async(&api).await?;
278
279        Ok(api)
280    }
281
282    async fn rest_async_simple(
283        &self,
284        request: http::request::Builder,
285        body: Vec<u8>,
286    ) -> Result<HttpResponse<Bytes>, api::ApiError<<Self as api::RestClient>::Error>> {
287        use futures_util::TryFutureExt;
288        let call = || async {
289            let http_request = request.body(body)?;
290            let request = http_request.try_into()?;
291            let rsp = self.client.execute(request).await?;
292
293            let mut http_rsp = HttpResponse::builder()
294                .status(rsp.status())
295                .version(rsp.version());
296            let headers = http_rsp.headers_mut().unwrap();
297            for (key, value) in rsp.headers() {
298                headers.insert(key, value.clone());
299            }
300            Ok(http_rsp.body(rsp.bytes().await?)?)
301        };
302        call().map_err(api::ApiError::client).await
303    }
304
305    /// Create a new AyncMarketstack API representation.
306    ///
307    /// The `token` should be a valid [personal access token](https://marketstack.com/documentation).
308    /// Errors out if `token` is invalid.
309    pub async fn new<H, T>(host: H, token: T) -> MarketstackResult<Self>
310    where
311        H: AsRef<str>,
312        T: Into<String>,
313    {
314        Self::new_impl("https", host.as_ref(), Auth::Token(token.into())).await
315    }
316
317    /// Create a new non-SSL AsyncMarketstack API representation.
318    ///
319    /// A `token` will still be required for insecure access.
320    pub async fn new_insecure<H, T>(host: H, token: T) -> MarketstackResult<Self>
321    where
322        H: AsRef<str>,
323        T: Into<String>,
324    {
325        Self::new_impl("http", host.as_ref(), Auth::Token(token.into())).await
326    }
327}