torn_api/
executor.rs

1use std::future::Future;
2
3use futures::{Stream, StreamExt};
4#[cfg(feature = "reqwest")]
5use http::{header::AUTHORIZATION, HeaderMap, HeaderValue};
6use serde::Deserialize;
7
8#[cfg(feature = "reqwest")]
9use crate::request::ApiRequest;
10use crate::request::{ApiResponse, IntoRequest};
11#[cfg(feature = "scopes")]
12use crate::scopes::{
13    BulkFactionScope, BulkForumScope, BulkKeyScope, BulkMarketScope, BulkRacingScope,
14    BulkTornScope, BulkUserScope, FactionScope, ForumScope, KeyScope, MarketScope, RacingScope,
15    TornScope, UserScope,
16};
17
18/// Central trait of the crate that is used to execute api requests.
19pub trait Executor: Sized {
20    type Error: From<serde_json::Error> + From<crate::ApiError> + Send;
21
22    /// Execute an api request.
23    fn execute<R>(
24        self,
25        request: R,
26    ) -> impl Future<Output = (R::Discriminant, Result<ApiResponse, Self::Error>)> + Send
27    where
28        R: IntoRequest;
29
30    /// Execute a request and deserialise the associated response type.
31    fn fetch<R>(self, request: R) -> impl Future<Output = Result<R::Response, Self::Error>> + Send
32    where
33        R: IntoRequest,
34    {
35        // HACK: workaround for not using `async` in trait declaration.
36        // The future is `Send` but `self` might not be.
37        let fut = self.execute(request);
38        async {
39            let resp = fut.await.1?;
40
41            let bytes = resp.body.unwrap();
42
43            if bytes.starts_with(br#"{"error":{"#) {
44                #[derive(Deserialize)]
45                struct ErrorBody<'a> {
46                    code: u16,
47                    error: &'a str,
48                }
49                #[derive(Deserialize)]
50                struct ErrorContainer<'a> {
51                    #[serde(borrow)]
52                    error: ErrorBody<'a>,
53                }
54
55                let error: ErrorContainer = serde_json::from_slice(&bytes)?;
56                return Err(crate::ApiError::new(error.error.code, error.error.error).into());
57            }
58
59            let resp = serde_json::from_slice(&bytes)?;
60
61            Ok(resp)
62        }
63    }
64}
65
66/// Trait that is used for the execution of bulk requests.
67pub trait BulkExecutor: Sized {
68    type Error: From<serde_json::Error> + From<crate::ApiError> + Send;
69
70    /// Generate response stream from a set of api requests.
71    fn execute<R>(
72        self,
73        requests: impl IntoIterator<Item = R>,
74    ) -> impl Stream<Item = (R::Discriminant, Result<ApiResponse, Self::Error>)> + Unpin
75    where
76        R: IntoRequest;
77
78    /// Generate a stream of deserialised responsed based on a set of api requests.
79    fn fetch_many<R>(
80        self,
81        requests: impl IntoIterator<Item = R>,
82    ) -> impl Stream<Item = (R::Discriminant, Result<R::Response, Self::Error>)> + Unpin
83    where
84        R: IntoRequest,
85    {
86        self.execute(requests).map(|(d, r)| {
87            let r = match r {
88                Ok(r) => r,
89                Err(why) => return (d, Err(why)),
90            };
91            let bytes = r.body.unwrap();
92
93            if bytes.starts_with(br#"{"error":{"#) {
94                #[derive(Deserialize)]
95                struct ErrorBody<'a> {
96                    code: u16,
97                    error: &'a str,
98                }
99                #[derive(Deserialize)]
100                struct ErrorContainer<'a> {
101                    #[serde(borrow)]
102                    error: ErrorBody<'a>,
103                }
104
105                let error: ErrorContainer = match serde_json::from_slice(&bytes) {
106                    Ok(error) => error,
107                    Err(why) => return (d, Err(why.into())),
108                };
109                return (
110                    d,
111                    Err(crate::ApiError::new(error.error.code, error.error.error).into()),
112                );
113            }
114
115            let resp = match serde_json::from_slice(&bytes) {
116                Ok(resp) => resp,
117                Err(why) => return (d, Err(why.into())),
118            };
119
120            (d, Ok(resp))
121        })
122    }
123}
124
125/// Convenience trait extension to provide easy access to all scopes
126#[cfg(feature = "scopes")]
127pub trait ExecutorExt: Executor + Sized {
128    fn user(self) -> UserScope<Self>;
129
130    fn faction(self) -> FactionScope<Self>;
131
132    fn torn(self) -> TornScope<Self>;
133
134    fn market(self) -> MarketScope<Self>;
135
136    fn racing(self) -> RacingScope<Self>;
137
138    fn forum(self) -> ForumScope<Self>;
139
140    fn key(self) -> KeyScope<Self>;
141}
142
143#[cfg(feature = "scopes")]
144impl<T> ExecutorExt for T
145where
146    T: Executor + Sized,
147{
148    fn user(self) -> UserScope<Self> {
149        UserScope::new(self)
150    }
151
152    fn faction(self) -> FactionScope<Self> {
153        FactionScope::new(self)
154    }
155
156    fn torn(self) -> TornScope<Self> {
157        TornScope::new(self)
158    }
159
160    fn market(self) -> MarketScope<Self> {
161        MarketScope::new(self)
162    }
163
164    fn racing(self) -> RacingScope<Self> {
165        RacingScope::new(self)
166    }
167
168    fn forum(self) -> ForumScope<Self> {
169        ForumScope::new(self)
170    }
171
172    fn key(self) -> KeyScope<Self> {
173        KeyScope::new(self)
174    }
175}
176
177/// Convenience trait extension to provide easy access to all bulk scopes
178#[cfg(feature = "scopes")]
179pub trait BulkExecutorExt: BulkExecutor + Sized {
180    fn user_bulk(self) -> BulkUserScope<Self>;
181
182    fn faction_bulk(self) -> BulkFactionScope<Self>;
183
184    fn torn_bulk(self) -> BulkTornScope<Self>;
185
186    fn market_bulk(self) -> BulkMarketScope<Self>;
187
188    fn racing_bulk(self) -> BulkRacingScope<Self>;
189
190    fn forum_bulk(self) -> BulkForumScope<Self>;
191
192    fn key_bulk(self) -> BulkKeyScope<Self>;
193}
194
195#[cfg(feature = "scopes")]
196impl<T> BulkExecutorExt for T
197where
198    T: BulkExecutor + Sized,
199{
200    fn user_bulk(self) -> BulkUserScope<Self> {
201        BulkUserScope::new(self)
202    }
203
204    fn faction_bulk(self) -> BulkFactionScope<Self> {
205        BulkFactionScope::new(self)
206    }
207
208    fn torn_bulk(self) -> BulkTornScope<Self> {
209        BulkTornScope::new(self)
210    }
211
212    fn market_bulk(self) -> BulkMarketScope<Self> {
213        BulkMarketScope::new(self)
214    }
215
216    fn racing_bulk(self) -> BulkRacingScope<Self> {
217        BulkRacingScope::new(self)
218    }
219
220    fn forum_bulk(self) -> BulkForumScope<Self> {
221        BulkForumScope::new(self)
222    }
223
224    fn key_bulk(self) -> BulkKeyScope<Self> {
225        BulkKeyScope::new(self)
226    }
227}
228
229#[cfg(feature = "reqwest")]
230/// Default executor based on the reqwest HTTP client.
231pub struct ReqwestClient(reqwest::Client);
232
233#[cfg(feature = "reqwest")]
234impl ReqwestClient {
235    /// Instantiate a new client which will use the provided API key.
236    pub fn new(api_key: &str) -> Self {
237        let mut headers = HeaderMap::with_capacity(1);
238        headers.insert(
239            AUTHORIZATION,
240            HeaderValue::from_str(&format!("ApiKey {api_key}")).unwrap(),
241        );
242
243        let client = reqwest::Client::builder()
244            .default_headers(headers)
245            .brotli(true)
246            .build()
247            .unwrap();
248
249        Self(client)
250    }
251}
252
253#[cfg(feature = "reqwest")]
254impl ReqwestClient {
255    async fn execute_api_request(&self, request: ApiRequest) -> Result<ApiResponse, crate::Error> {
256        let url = request.url();
257
258        let response = self.0.get(url).send().await?;
259        let status = response.status();
260        let body = response.bytes().await.ok();
261
262        Ok(ApiResponse { status, body })
263    }
264}
265
266#[cfg(feature = "reqwest")]
267impl Executor for &ReqwestClient {
268    type Error = crate::Error;
269
270    async fn execute<R>(self, request: R) -> (R::Discriminant, Result<ApiResponse, Self::Error>)
271    where
272        R: IntoRequest,
273    {
274        let (d, request) = request.into_request();
275        (d, self.execute_api_request(request).await)
276    }
277}
278
279#[cfg(feature = "reqwest")]
280impl BulkExecutor for &ReqwestClient {
281    type Error = crate::Error;
282
283    fn execute<R>(
284        self,
285        requests: impl IntoIterator<Item = R>,
286    ) -> impl Stream<Item = (R::Discriminant, Result<ApiResponse, Self::Error>)>
287    where
288        R: IntoRequest,
289    {
290        futures::stream::iter(requests)
291            .map(move |r| <Self as Executor>::execute(self, r))
292            .buffer_unordered(25)
293    }
294}
295
296#[cfg(all(test, feature = "reqwest"))]
297mod test {
298    use crate::{scopes::test::test_client, ApiError, Error};
299
300    use super::*;
301
302    #[cfg(feature = "scopes")]
303    #[tokio::test]
304    async fn api_error() {
305        let client = test_client().await;
306
307        let resp = client.faction().basic_for_id((-1).into(), |b| b).await;
308
309        match resp {
310            Err(Error::Api(ApiError::IncorrectIdEntityRelation)) => (),
311            other => panic!("Expected incorrect id entity relation error, got {other:?}"),
312        }
313    }
314
315    #[cfg(feature = "scopes")]
316    #[tokio::test]
317    async fn bulk_request() {
318        let client = test_client().await;
319
320        let stream = client
321            .faction_bulk()
322            .basic_for_id(vec![19.into(), 89.into()], |b| b);
323
324        let mut responses: Vec<_> = stream.collect().await;
325
326        let (_id1, basic1) = responses.pop().unwrap();
327        basic1.unwrap();
328
329        let (_id2, basic2) = responses.pop().unwrap();
330        basic2.unwrap();
331    }
332}