torn_api/
executor.rs

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