simplified traits
This commit is contained in:
parent
df40047ec3
commit
758ab39a1d
9 changed files with 357 additions and 210 deletions
|
|
@ -3,6 +3,12 @@
|
|||
pub mod faction;
|
||||
pub mod user;
|
||||
|
||||
#[cfg(feature = "awc")]
|
||||
pub mod awc;
|
||||
|
||||
#[cfg(feature = "reqwest")]
|
||||
pub mod reqwest;
|
||||
|
||||
mod de_util;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
|
@ -10,33 +16,21 @@ use chrono::{DateTime, Utc};
|
|||
use serde::de::{DeserializeOwned, Error as DeError};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ClientError {
|
||||
#[error("api returned error '{reason}', code = '{code}'")]
|
||||
Api { code: u8, reason: String },
|
||||
|
||||
#[cfg(feature = "reqwest")]
|
||||
#[error("api request failed with network error")]
|
||||
Reqwest(#[from] reqwest::Error),
|
||||
|
||||
#[cfg(feature = "awc")]
|
||||
#[error("api request failed with network error")]
|
||||
AwcSend(#[from] awc::error::SendRequestError),
|
||||
|
||||
#[cfg(feature = "awc")]
|
||||
#[error("api request failed to read payload")]
|
||||
AwcPayload(#[from] awc::error::JsonPayloadError),
|
||||
|
||||
#[error("api response couldn't be deserialized")]
|
||||
Deserialize(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
pub struct ApiResponse {
|
||||
value: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ResponseError {
|
||||
#[error("API: {reason}")]
|
||||
Api { code: u8, reason: String },
|
||||
|
||||
#[error(transparent)]
|
||||
Parsing(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
impl ApiResponse {
|
||||
fn from_value(mut value: serde_json::Value) -> Result<Self, ClientError> {
|
||||
pub fn from_value(mut value: serde_json::Value) -> Result<Self, ResponseError> {
|
||||
#[derive(serde::Deserialize)]
|
||||
struct ApiErrorDto {
|
||||
code: u8,
|
||||
|
|
@ -46,7 +40,7 @@ impl ApiResponse {
|
|||
match value.get_mut("error") {
|
||||
Some(error) => {
|
||||
let dto: ApiErrorDto = serde_json::from_value(error.take())?;
|
||||
Err(ClientError::Api {
|
||||
Err(ResponseError::Api {
|
||||
code: dto.code,
|
||||
reason: dto.reason,
|
||||
})
|
||||
|
|
@ -88,111 +82,199 @@ pub trait ApiCategoryResponse: Send + Sync {
|
|||
fn from_response(response: ApiResponse) -> Self;
|
||||
}
|
||||
|
||||
#[cfg(feature = "awc")]
|
||||
#[async_trait(?Send)]
|
||||
pub trait ApiClient {
|
||||
async fn request(&self, url: String) -> Result<ApiResponse, ClientError>;
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "awc"))]
|
||||
#[async_trait]
|
||||
pub trait ApiClient: Send + Sync {
|
||||
async fn request(&self, url: String) -> Result<ApiResponse, ClientError>;
|
||||
}
|
||||
pub trait ThreadSafeApiClient: Send + Sync {
|
||||
type Error: std::error::Error + Sync + Send;
|
||||
|
||||
pub trait DirectApiClient: ApiClient {
|
||||
fn torn_api(&self, key: String) -> DirectExecutor<Self>
|
||||
async fn request(&self, url: String) -> Result<serde_json::Value, Self::Error>;
|
||||
|
||||
fn torn_api<S>(&self, key: S) -> ThreadSafeApiProvider<Self, DirectExecutor<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
S: ToString,
|
||||
{
|
||||
DirectExecutor::from_client(self, key)
|
||||
ThreadSafeApiProvider::new(self, DirectExecutor::new(key.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait BackedApiClient: ApiClient {}
|
||||
|
||||
#[cfg(feature = "reqwest")]
|
||||
#[cfg_attr(feature = "awc", async_trait(?Send))]
|
||||
#[cfg_attr(not(feature = "awc"), async_trait)]
|
||||
impl crate::ApiClient for reqwest::Client {
|
||||
async fn request(&self, url: String) -> Result<ApiResponse, crate::ClientError> {
|
||||
let value: serde_json::Value = self.get(url).send().await?.json().await?;
|
||||
Ok(ApiResponse::from_value(value)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "reqwest")]
|
||||
impl crate::DirectApiClient for reqwest::Client {}
|
||||
|
||||
#[cfg(feature = "awc")]
|
||||
#[async_trait(?Send)]
|
||||
impl crate::ApiClient for awc::Client {
|
||||
async fn request(&self, url: String) -> Result<ApiResponse, crate::ClientError> {
|
||||
let value: serde_json::Value = self.get(url).send().await?.json().await?;
|
||||
Ok(ApiResponse::from_value(value)?)
|
||||
pub trait ApiClient {
|
||||
type Error: std::error::Error;
|
||||
|
||||
async fn request(&self, url: String) -> Result<serde_json::Value, Self::Error>;
|
||||
|
||||
fn torn_api<S>(&self, key: S) -> ApiProvider<Self, DirectExecutor<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
S: ToString,
|
||||
{
|
||||
ApiProvider::new(self, DirectExecutor::new(key.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "awc")]
|
||||
impl crate::DirectApiClient for awc::Client {}
|
||||
#[async_trait(?Send)]
|
||||
pub trait RequestExecutor<C>
|
||||
where
|
||||
C: ApiClient,
|
||||
{
|
||||
type Error: std::error::Error;
|
||||
|
||||
#[cfg_attr(feature = "awc", async_trait(?Send))]
|
||||
#[cfg_attr(not(feature = "awc"), async_trait)]
|
||||
pub trait ApiRequestExecutor<'client> {
|
||||
type Err: std::error::Error;
|
||||
|
||||
async fn excute<A>(&self, request: ApiRequest<A>) -> Result<A, Self::Err>
|
||||
async fn execute<A>(&self, client: &C, request: ApiRequest<A>) -> Result<A, Self::Error>
|
||||
where
|
||||
A: ApiCategoryResponse;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn user<'executor>(
|
||||
&'executor self,
|
||||
) -> ApiRequestBuilder<'client, 'executor, Self, user::Response> {
|
||||
ApiRequestBuilder::new(self)
|
||||
#[async_trait]
|
||||
pub trait ThreadSafeRequestExecutor<C>
|
||||
where
|
||||
C: ThreadSafeApiClient,
|
||||
{
|
||||
type Error: std::error::Error + Send + Sync;
|
||||
|
||||
async fn execute<A>(&self, client: &C, request: ApiRequest<A>) -> Result<A, Self::Error>
|
||||
where
|
||||
A: ApiCategoryResponse;
|
||||
}
|
||||
|
||||
pub struct ApiProvider<'a, C, E>
|
||||
where
|
||||
C: ApiClient,
|
||||
E: RequestExecutor<C>,
|
||||
{
|
||||
client: &'a C,
|
||||
executor: E,
|
||||
}
|
||||
|
||||
impl<'a, C, E> ApiProvider<'a, C, E>
|
||||
where
|
||||
C: ApiClient,
|
||||
E: RequestExecutor<C>,
|
||||
{
|
||||
pub fn new(client: &'a C, executor: E) -> ApiProvider<'a, C, E> {
|
||||
Self { client, executor }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn faction<'executor>(
|
||||
&'executor self,
|
||||
) -> ApiRequestBuilder<'client, 'executor, Self, faction::Response> {
|
||||
ApiRequestBuilder::new(self)
|
||||
pub async fn user<F>(&self, build: F) -> Result<user::Response, E::Error>
|
||||
where
|
||||
F: FnOnce(ApiRequestBuilder<user::Response>) -> ApiRequestBuilder<user::Response>,
|
||||
{
|
||||
let mut builder = ApiRequestBuilder::<user::Response>::new();
|
||||
builder = build(builder);
|
||||
|
||||
self.executor.execute(self.client, builder.request).await
|
||||
}
|
||||
|
||||
pub async fn faction<F>(&self, build: F) -> Result<faction::Response, E::Error>
|
||||
where
|
||||
F: FnOnce(ApiRequestBuilder<faction::Response>) -> ApiRequestBuilder<faction::Response>,
|
||||
{
|
||||
let mut builder = ApiRequestBuilder::<faction::Response>::new();
|
||||
builder = build(builder);
|
||||
|
||||
self.executor.execute(self.client, builder.request).await
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DirectExecutor<'client, C>
|
||||
pub struct ThreadSafeApiProvider<'a, C, E>
|
||||
where
|
||||
C: ApiClient,
|
||||
C: ThreadSafeApiClient,
|
||||
E: ThreadSafeRequestExecutor<C>,
|
||||
{
|
||||
client: &'client C,
|
||||
client: &'a C,
|
||||
executor: E,
|
||||
}
|
||||
|
||||
impl<'a, C, E> ThreadSafeApiProvider<'a, C, E>
|
||||
where
|
||||
C: ThreadSafeApiClient,
|
||||
E: ThreadSafeRequestExecutor<C>,
|
||||
{
|
||||
pub fn new(client: &'a C, executor: E) -> ThreadSafeApiProvider<'a, C, E> {
|
||||
Self { client, executor }
|
||||
}
|
||||
|
||||
pub async fn user<F>(&self, build: F) -> Result<user::Response, E::Error>
|
||||
where
|
||||
F: FnOnce(ApiRequestBuilder<user::Response>) -> ApiRequestBuilder<user::Response>,
|
||||
{
|
||||
let mut builder = ApiRequestBuilder::<user::Response>::new();
|
||||
builder = build(builder);
|
||||
|
||||
self.executor.execute(self.client, builder.request).await
|
||||
}
|
||||
|
||||
pub async fn faction<F>(&self, build: F) -> Result<faction::Response, E::Error>
|
||||
where
|
||||
F: FnOnce(ApiRequestBuilder<faction::Response>) -> ApiRequestBuilder<faction::Response>,
|
||||
{
|
||||
let mut builder = ApiRequestBuilder::<faction::Response>::new();
|
||||
builder = build(builder);
|
||||
|
||||
self.executor.execute(self.client, builder.request).await
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DirectExecutor<C> {
|
||||
key: String,
|
||||
_marker: std::marker::PhantomData<C>,
|
||||
}
|
||||
|
||||
impl<'client, C> DirectExecutor<'client, C>
|
||||
where
|
||||
C: ApiClient,
|
||||
{
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn from_client(client: &'client C, key: String) -> Self {
|
||||
Self { client, key }
|
||||
impl<C> DirectExecutor<C> {
|
||||
fn new(key: String) -> Self {
|
||||
Self {
|
||||
key,
|
||||
_marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "awc", async_trait(?Send))]
|
||||
#[cfg_attr(not(feature = "awc"), async_trait)]
|
||||
impl<'client, C> ApiRequestExecutor<'client> for DirectExecutor<'client, C>
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ApiClientError<C>
|
||||
where
|
||||
C: std::error::Error,
|
||||
{
|
||||
#[error(transparent)]
|
||||
Client(C),
|
||||
|
||||
#[error(transparent)]
|
||||
Response(#[from] ResponseError),
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl<C> RequestExecutor<C> for DirectExecutor<C>
|
||||
where
|
||||
C: ApiClient,
|
||||
{
|
||||
type Err = ClientError;
|
||||
type Error = ApiClientError<C::Error>;
|
||||
|
||||
async fn excute<A>(&self, request: ApiRequest<A>) -> Result<A, Self::Err>
|
||||
async fn execute<A>(&self, client: &C, request: ApiRequest<A>) -> Result<A, Self::Error>
|
||||
where
|
||||
A: ApiCategoryResponse,
|
||||
{
|
||||
let url = request.url(&self.key);
|
||||
|
||||
self.client.request(url).await.map(A::from_response)
|
||||
let value = client.request(url).await.map_err(ApiClientError::Client)?;
|
||||
|
||||
Ok(A::from_response(ApiResponse::from_value(value)?))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<C> ThreadSafeRequestExecutor<C> for DirectExecutor<C>
|
||||
where
|
||||
C: ThreadSafeApiClient,
|
||||
{
|
||||
type Error = ApiClientError<C::Error>;
|
||||
|
||||
async fn execute<A>(&self, client: &C, request: ApiRequest<A>) -> Result<A, Self::Error>
|
||||
where
|
||||
A: ApiCategoryResponse,
|
||||
{
|
||||
let url = request.url(&self.key);
|
||||
|
||||
let value = client.request(url).await.map_err(ApiClientError::Client)?;
|
||||
|
||||
Ok(A::from_response(ApiResponse::from_value(value)?))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -263,26 +345,20 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub struct ApiRequestBuilder<'client, 'executor, E, A>
|
||||
pub struct ApiRequestBuilder<A>
|
||||
where
|
||||
E: ApiRequestExecutor<'client> + ?Sized,
|
||||
A: ApiCategoryResponse,
|
||||
{
|
||||
executor: &'executor E,
|
||||
request: ApiRequest<A>,
|
||||
_phantom: std::marker::PhantomData<&'client E>,
|
||||
}
|
||||
|
||||
impl<'client, 'executor, E, A> ApiRequestBuilder<'client, 'executor, E, A>
|
||||
impl<A> ApiRequestBuilder<A>
|
||||
where
|
||||
E: ApiRequestExecutor<'client> + ?Sized,
|
||||
A: ApiCategoryResponse,
|
||||
{
|
||||
pub(crate) fn new(executor: &'executor E) -> Self {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
executor,
|
||||
request: ApiRequest::default(),
|
||||
_phantom: std::marker::PhantomData::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -317,49 +393,23 @@ where
|
|||
self.request.comment = Some(comment);
|
||||
self
|
||||
}
|
||||
|
||||
/// Executes the api request.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use torn_api::{prelude::*, ClientError};
|
||||
/// use reqwest::Client;
|
||||
/// # async {
|
||||
///
|
||||
/// let key = "XXXXXXXXX".to_owned();
|
||||
/// let response = Client::new()
|
||||
/// .torn_api(key)
|
||||
/// .user()
|
||||
/// .send()
|
||||
/// .await;
|
||||
///
|
||||
/// // invalid key
|
||||
/// assert!(matches!(response, Err(ClientError::Api { code: 2, .. })));
|
||||
/// # };
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Will return an `Err` if the API returns an API error, the request fails due to a network
|
||||
/// error, or if the response body doesn't contain valid json.
|
||||
pub async fn send(self) -> Result<A, <E as ApiRequestExecutor<'client>>::Err> {
|
||||
self.executor.excute(self.request).await
|
||||
}
|
||||
}
|
||||
|
||||
pub mod prelude {
|
||||
pub use super::{ApiClient, ApiRequestExecutor, DirectApiClient};
|
||||
}
|
||||
pub mod prelude {}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::sync::Once;
|
||||
|
||||
#[cfg(all(not(feature = "reqwest"), feature = "awc"))]
|
||||
pub use awc::Client;
|
||||
pub use ::awc::Client;
|
||||
#[cfg(feature = "reqwest")]
|
||||
pub use reqwest::Client;
|
||||
pub use ::reqwest::Client;
|
||||
|
||||
#[cfg(all(not(feature = "reqwest"), feature = "awc"))]
|
||||
pub use crate::ApiClient as ClientTrait;
|
||||
#[cfg(feature = "reqwest")]
|
||||
pub use crate::ThreadSafeApiClient as ClientTrait;
|
||||
|
||||
#[cfg(all(not(feature = "reqwest"), feature = "awc"))]
|
||||
pub use actix_rt::test as async_test;
|
||||
|
|
@ -387,12 +437,7 @@ pub(crate) mod tests {
|
|||
async fn reqwest() {
|
||||
let key = setup();
|
||||
|
||||
reqwest::Client::default()
|
||||
.torn_api(key)
|
||||
.user()
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
Client::default().torn_api(key).user(|b| b).await.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(feature = "awc")]
|
||||
|
|
@ -400,11 +445,6 @@ pub(crate) mod tests {
|
|||
async fn awc() {
|
||||
let key = setup();
|
||||
|
||||
awc::Client::default()
|
||||
.torn_api(key)
|
||||
.user()
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
Client::default().torn_api(key).user(|b| b).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue