torn_api/
executor.rs

1use http::{HeaderMap, HeaderValue, header::AUTHORIZATION};
2use serde::Deserialize;
3
4use crate::{
5    request::{ApiResponse, IntoRequest},
6    scopes::{FactionScope, ForumScope, MarketScope, RacingScope, TornScope, UserScope},
7};
8
9pub trait Executor {
10    type Error: From<serde_json::Error> + From<crate::ApiError> + Send;
11
12    fn execute<R>(
13        &self,
14        request: R,
15    ) -> impl Future<Output = Result<ApiResponse<R::Discriminant>, Self::Error>> + Send
16    where
17        R: IntoRequest;
18
19    fn fetch<R>(&self, request: R) -> impl Future<Output = Result<R::Response, Self::Error>> + Send
20    where
21        R: IntoRequest,
22    {
23        // HACK: workaround for not using `async` in trait declaration.
24        // The future is `Send` but `&self` might not be.
25        let fut = self.execute(request);
26        async {
27            let resp = fut.await?;
28
29            let bytes = resp.body.unwrap();
30
31            if bytes.starts_with(br#"{"error":{"#) {
32                #[derive(Deserialize)]
33                struct ErrorBody<'a> {
34                    code: u16,
35                    error: &'a str,
36                }
37                #[derive(Deserialize)]
38                struct ErrorContainer<'a> {
39                    #[serde(borrow)]
40                    error: ErrorBody<'a>,
41                }
42
43                let error: ErrorContainer = serde_json::from_slice(&bytes)?;
44                return Err(crate::ApiError::new(error.error.code, error.error.error).into());
45            }
46
47            let resp = serde_json::from_slice(&bytes)?;
48
49            Ok(resp)
50        }
51    }
52}
53
54pub struct ReqwestClient(reqwest::Client);
55
56impl ReqwestClient {
57    pub fn new(api_key: &str) -> Self {
58        let mut headers = HeaderMap::with_capacity(1);
59        headers.insert(
60            AUTHORIZATION,
61            HeaderValue::from_str(&format!("ApiKey {api_key}")).unwrap(),
62        );
63
64        let client = reqwest::Client::builder()
65            .default_headers(headers)
66            .brotli(true)
67            .build()
68            .unwrap();
69
70        Self(client)
71    }
72}
73
74pub trait ExecutorExt: Executor + Sized {
75    fn user(&self) -> UserScope<'_, Self>;
76
77    fn faction(&self) -> FactionScope<'_, Self>;
78
79    fn torn(&self) -> TornScope<'_, Self>;
80
81    fn market(&self) -> MarketScope<'_, Self>;
82
83    fn racing(&self) -> RacingScope<'_, Self>;
84
85    fn forum(&self) -> ForumScope<'_, Self>;
86}
87
88impl<T> ExecutorExt for T
89where
90    T: Executor + Sized,
91{
92    fn user(&self) -> UserScope<'_, Self> {
93        UserScope::new(self)
94    }
95
96    fn faction(&self) -> FactionScope<'_, Self> {
97        FactionScope::new(self)
98    }
99
100    fn torn(&self) -> TornScope<'_, Self> {
101        TornScope::new(self)
102    }
103
104    fn market(&self) -> MarketScope<'_, Self> {
105        MarketScope::new(self)
106    }
107
108    fn racing(&self) -> RacingScope<'_, Self> {
109        RacingScope::new(self)
110    }
111
112    fn forum(&self) -> ForumScope<'_, Self> {
113        ForumScope::new(self)
114    }
115}
116
117impl Executor for ReqwestClient {
118    type Error = crate::Error;
119
120    async fn execute<R>(&self, request: R) -> Result<ApiResponse<R::Discriminant>, Self::Error>
121    where
122        R: IntoRequest,
123    {
124        let request = request.into_request();
125        let url = request.url();
126
127        let response = self.0.get(url).send().await?;
128        let status = response.status();
129        let body = response.bytes().await.ok();
130
131        Ok(ApiResponse {
132            discriminant: request.disriminant,
133            status,
134            body,
135        })
136    }
137}
138
139#[cfg(test)]
140mod test {
141    use crate::{ApiError, Error, scopes::test::test_client};
142
143    use super::*;
144
145    #[tokio::test]
146    async fn api_error() {
147        let client = test_client().await;
148
149        let resp = client.faction().basic_for_id((-1).into(), |b| b).await;
150
151        match resp {
152            Err(Error::Api(ApiError::IncorrectIdEntityRelation)) => (),
153            other => panic!("Expected incorrect id entity relation error, got {other:?}"),
154        }
155    }
156}