torn-api 5.1.0

Auto-generated bindings for the v2 torn api
Documentation
use std::future::Future;

use futures::{Stream, StreamExt};
#[cfg(feature = "reqwest")]
use http::{header::AUTHORIZATION, HeaderMap, HeaderValue};
use serde::Deserialize;

#[cfg(feature = "reqwest")]
use crate::request::ApiRequest;
use crate::request::{ApiResponse, IntoRequest};
#[cfg(feature = "scopes")]
use crate::scopes::{
    BulkFactionScope, BulkForumScope, BulkKeyScope, BulkMarketScope, BulkRacingScope,
    BulkTornScope, BulkUserScope, FactionScope, ForumScope, KeyScope, MarketScope, RacingScope,
    TornScope, UserScope,
};

/// Central trait of the crate that is used to execute api requests.
pub trait Executor: Sized {
    type Error: From<serde_json::Error> + From<crate::ApiError> + Send;

    /// Execute an api request.
    fn execute<R>(
        self,
        request: R,
    ) -> impl Future<Output = (R::Discriminant, Result<ApiResponse, Self::Error>)> + Send
    where
        R: IntoRequest;

    /// Execute a request and deserialise the associated response type.
    fn fetch<R>(self, request: R) -> impl Future<Output = Result<R::Response, Self::Error>> + Send
    where
        R: IntoRequest,
    {
        // HACK: workaround for not using `async` in trait declaration.
        // The future is `Send` but `self` might not be.
        let fut = self.execute(request);
        async {
            let resp = fut.await.1?;

            let bytes = resp.body.unwrap();

            if bytes.starts_with(br#"{"error":{"#) {
                #[derive(Deserialize)]
                struct ErrorBody<'a> {
                    code: u16,
                    error: &'a str,
                }
                #[derive(Deserialize)]
                struct ErrorContainer<'a> {
                    #[serde(borrow)]
                    error: ErrorBody<'a>,
                }

                let error: ErrorContainer = serde_json::from_slice(&bytes)?;
                return Err(crate::ApiError::new(error.error.code, error.error.error).into());
            }

            let resp = serde_json::from_slice(&bytes)?;

            Ok(resp)
        }
    }
}

/// Trait that is used for the execution of bulk requests.
pub trait BulkExecutor: Sized {
    type Error: From<serde_json::Error> + From<crate::ApiError> + Send;

    /// Generate response stream from a set of api requests.
    fn execute<R>(
        self,
        requests: impl IntoIterator<Item = R>,
    ) -> impl Stream<Item = (R::Discriminant, Result<ApiResponse, Self::Error>)> + Unpin
    where
        R: IntoRequest;

    /// Generate a stream of deserialised responsed based on a set of api requests.
    fn fetch_many<R>(
        self,
        requests: impl IntoIterator<Item = R>,
    ) -> impl Stream<Item = (R::Discriminant, Result<R::Response, Self::Error>)> + Unpin
    where
        R: IntoRequest,
    {
        self.execute(requests).map(|(d, r)| {
            let r = match r {
                Ok(r) => r,
                Err(why) => return (d, Err(why)),
            };
            let bytes = r.body.unwrap();

            if bytes.starts_with(br#"{"error":{"#) {
                #[derive(Deserialize)]
                struct ErrorBody<'a> {
                    code: u16,
                    error: &'a str,
                }
                #[derive(Deserialize)]
                struct ErrorContainer<'a> {
                    #[serde(borrow)]
                    error: ErrorBody<'a>,
                }

                let error: ErrorContainer = match serde_json::from_slice(&bytes) {
                    Ok(error) => error,
                    Err(why) => return (d, Err(why.into())),
                };
                return (
                    d,
                    Err(crate::ApiError::new(error.error.code, error.error.error).into()),
                );
            }

            let resp = match serde_json::from_slice(&bytes) {
                Ok(resp) => resp,
                Err(why) => return (d, Err(why.into())),
            };

            (d, Ok(resp))
        })
    }
}

/// Convenience trait extension to provide easy access to all scopes
#[cfg(feature = "scopes")]
pub trait ExecutorExt: Executor + Sized {
    fn user(self) -> UserScope<Self>;

    fn faction(self) -> FactionScope<Self>;

    fn torn(self) -> TornScope<Self>;

    fn market(self) -> MarketScope<Self>;

    fn racing(self) -> RacingScope<Self>;

    fn forum(self) -> ForumScope<Self>;

    fn key(self) -> KeyScope<Self>;
}

#[cfg(feature = "scopes")]
impl<T> ExecutorExt for T
where
    T: Executor + Sized,
{
    fn user(self) -> UserScope<Self> {
        UserScope::new(self)
    }

    fn faction(self) -> FactionScope<Self> {
        FactionScope::new(self)
    }

    fn torn(self) -> TornScope<Self> {
        TornScope::new(self)
    }

    fn market(self) -> MarketScope<Self> {
        MarketScope::new(self)
    }

    fn racing(self) -> RacingScope<Self> {
        RacingScope::new(self)
    }

    fn forum(self) -> ForumScope<Self> {
        ForumScope::new(self)
    }

    fn key(self) -> KeyScope<Self> {
        KeyScope::new(self)
    }
}

/// Convenience trait extension to provide easy access to all bulk scopes
#[cfg(feature = "scopes")]
pub trait BulkExecutorExt: BulkExecutor + Sized {
    fn user_bulk(self) -> BulkUserScope<Self>;

    fn faction_bulk(self) -> BulkFactionScope<Self>;

    fn torn_bulk(self) -> BulkTornScope<Self>;

    fn market_bulk(self) -> BulkMarketScope<Self>;

    fn racing_bulk(self) -> BulkRacingScope<Self>;

    fn forum_bulk(self) -> BulkForumScope<Self>;

    fn key_bulk(self) -> BulkKeyScope<Self>;
}

#[cfg(feature = "scopes")]
impl<T> BulkExecutorExt for T
where
    T: BulkExecutor + Sized,
{
    fn user_bulk(self) -> BulkUserScope<Self> {
        BulkUserScope::new(self)
    }

    fn faction_bulk(self) -> BulkFactionScope<Self> {
        BulkFactionScope::new(self)
    }

    fn torn_bulk(self) -> BulkTornScope<Self> {
        BulkTornScope::new(self)
    }

    fn market_bulk(self) -> BulkMarketScope<Self> {
        BulkMarketScope::new(self)
    }

    fn racing_bulk(self) -> BulkRacingScope<Self> {
        BulkRacingScope::new(self)
    }

    fn forum_bulk(self) -> BulkForumScope<Self> {
        BulkForumScope::new(self)
    }

    fn key_bulk(self) -> BulkKeyScope<Self> {
        BulkKeyScope::new(self)
    }
}

#[cfg(feature = "reqwest")]
/// Default executor based on the reqwest HTTP client.
pub struct ReqwestClient(reqwest::Client);

#[cfg(feature = "reqwest")]
impl ReqwestClient {
    /// Instantiate a new client which will use the provided API key.
    pub fn new(api_key: &str) -> Self {
        let mut headers = HeaderMap::with_capacity(1);
        headers.insert(
            AUTHORIZATION,
            HeaderValue::from_str(&format!("ApiKey {api_key}")).unwrap(),
        );

        let client = reqwest::Client::builder()
            .default_headers(headers)
            .brotli(true)
            .build()
            .unwrap();

        Self(client)
    }
}

#[cfg(feature = "reqwest")]
impl ReqwestClient {
    async fn execute_api_request(&self, request: ApiRequest) -> Result<ApiResponse, crate::Error> {
        let url = request.url();

        let response = self.0.get(url).send().await?;
        let status = response.status();
        let body = response.bytes().await.ok();

        Ok(ApiResponse { status, body })
    }
}

#[cfg(feature = "reqwest")]
impl Executor for &ReqwestClient {
    type Error = crate::Error;

    async fn execute<R>(self, request: R) -> (R::Discriminant, Result<ApiResponse, Self::Error>)
    where
        R: IntoRequest,
    {
        let (d, request) = request.into_request();
        (d, self.execute_api_request(request).await)
    }
}

#[cfg(feature = "reqwest")]
impl BulkExecutor for &ReqwestClient {
    type Error = crate::Error;

    fn execute<R>(
        self,
        requests: impl IntoIterator<Item = R>,
    ) -> impl Stream<Item = (R::Discriminant, Result<ApiResponse, Self::Error>)>
    where
        R: IntoRequest,
    {
        futures::stream::iter(requests)
            .map(move |r| <Self as Executor>::execute(self, r))
            .buffer_unordered(25)
    }
}

#[cfg(all(test, feature = "reqwest"))]
mod test {
    use crate::{scopes::test::test_client, ApiError, Error};

    use super::*;

    #[cfg(feature = "scopes")]
    #[tokio::test]
    async fn api_error() {
        let client = test_client().await;

        let resp = client.faction().basic_for_id((-1).into(), |b| b).await;

        match resp {
            Err(Error::Api(ApiError::IncorrectIdEntityRelation)) => (),
            other => panic!("Expected incorrect id entity relation error, got {other:?}"),
        }
    }

    #[cfg(feature = "scopes")]
    #[tokio::test]
    async fn bulk_request() {
        let client = test_client().await;

        let stream = client
            .faction_bulk()
            .basic_for_id(vec![19.into(), 89.into()], |b| b);

        let mut responses: Vec<_> = stream.collect().await;

        let (_id1, basic1) = responses.pop().unwrap();
        basic1.unwrap();

        let (_id2, basic2) = responses.pop().unwrap();
        basic2.unwrap();
    }
}