bulk updates
This commit is contained in:
parent
0115b6e615
commit
a7c640511c
16 changed files with 1091 additions and 443 deletions
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "torn-api"
|
||||
version = "0.4.2"
|
||||
version = "0.5.0"
|
||||
edition = "2021"
|
||||
authors = ["Pyrit [2111649]"]
|
||||
license = "MIT"
|
||||
|
|
@ -24,11 +24,12 @@ chrono = { version = "0.4", features = [ "serde" ], default-features = false }
|
|||
async-trait = "0.1"
|
||||
thiserror = "1"
|
||||
num-traits = "0.2"
|
||||
futures = "0.3"
|
||||
|
||||
reqwest = { version = "0.11", default-features = false, features = [ "json" ], optional = true }
|
||||
awc = { version = "3", default-features = false, optional = true }
|
||||
|
||||
torn-api-macros = { path = "../torn-api-macros", version = "0.1" }
|
||||
torn-api-macros = { path = "../torn-api-macros", version = "0.1.1" }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = { version = "2.7.0" }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use async_trait::async_trait;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::ApiClient;
|
||||
use crate::local::ApiClient;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AwcApiClientError {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ mod tests {
|
|||
|
||||
let response = Client::default()
|
||||
.torn_api(key)
|
||||
.faction(|b| b.selections(&[Selection::Basic]))
|
||||
.faction(None, |b| b.selections(&[Selection::Basic]))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,22 @@
|
|||
#![warn(clippy::all, clippy::perf, clippy::style, clippy::suspicious)]
|
||||
|
||||
pub mod faction;
|
||||
pub mod local;
|
||||
pub mod send;
|
||||
pub mod torn;
|
||||
pub mod user;
|
||||
|
||||
#[cfg(feature = "awc")]
|
||||
pub mod awc;
|
||||
mod awc;
|
||||
|
||||
#[cfg(feature = "reqwest")]
|
||||
pub mod reqwest;
|
||||
mod reqwest;
|
||||
|
||||
mod de_util;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use std::fmt::Write;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use num_traits::{AsPrimitive, PrimInt};
|
||||
use serde::de::{DeserializeOwned, Error as DeError};
|
||||
use thiserror::Error;
|
||||
|
||||
|
|
@ -54,20 +57,27 @@ impl ApiResponse {
|
|||
where
|
||||
D: DeserializeOwned,
|
||||
{
|
||||
serde_json::from_value(self.value.clone())
|
||||
D::deserialize(&self.value)
|
||||
}
|
||||
|
||||
fn decode_field<D>(&self, field: &'static str) -> serde_json::Result<D>
|
||||
where
|
||||
D: DeserializeOwned,
|
||||
{
|
||||
let value = self
|
||||
.value
|
||||
self.value
|
||||
.get(field)
|
||||
.ok_or_else(|| serde_json::Error::missing_field(field))?
|
||||
.clone();
|
||||
.ok_or_else(|| serde_json::Error::missing_field(field))
|
||||
.and_then(D::deserialize)
|
||||
}
|
||||
|
||||
serde_json::from_value(value)
|
||||
fn decode_field_with<'de, V, F>(&'de self, field: &'static str, fun: F) -> serde_json::Result<V>
|
||||
where
|
||||
F: FnOnce(&'de serde_json::Value) -> serde_json::Result<V>,
|
||||
{
|
||||
self.value
|
||||
.get(field)
|
||||
.ok_or_else(|| serde_json::Error::missing_field(field))
|
||||
.and_then(fun)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -83,138 +93,6 @@ pub trait ApiCategoryResponse: Send + Sync {
|
|||
fn from_response(response: ApiResponse) -> Self;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait ThreadSafeApiClient: Send + Sync {
|
||||
type Error: std::error::Error + Sync + Send;
|
||||
|
||||
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,
|
||||
{
|
||||
ThreadSafeApiProvider::new(self, DirectExecutor::new(key.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
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()))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub trait RequestExecutor<C>
|
||||
where
|
||||
C: ApiClient,
|
||||
{
|
||||
type Error: std::error::Error;
|
||||
|
||||
async fn execute<A>(&self, client: &C, request: ApiRequest<A>) -> Result<A, Self::Error>
|
||||
where
|
||||
A: ApiCategoryResponse;
|
||||
}
|
||||
|
||||
#[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 }
|
||||
}
|
||||
|
||||
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 ThreadSafeApiProvider<'a, C, E>
|
||||
where
|
||||
C: ThreadSafeApiClient,
|
||||
E: ThreadSafeRequestExecutor<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>,
|
||||
|
|
@ -224,7 +102,7 @@ impl<C> DirectExecutor<C> {
|
|||
fn new(key: String) -> Self {
|
||||
Self {
|
||||
key,
|
||||
_marker: std::marker::PhantomData,
|
||||
_marker: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -241,51 +119,12 @@ where
|
|||
Response(#[from] ResponseError),
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl<C> RequestExecutor<C> for DirectExecutor<C>
|
||||
where
|
||||
C: ApiClient,
|
||||
{
|
||||
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)?))
|
||||
}
|
||||
}
|
||||
|
||||
#[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)?))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ApiRequest<A>
|
||||
where
|
||||
A: ApiCategoryResponse,
|
||||
{
|
||||
selections: Vec<&'static str>,
|
||||
id: Option<u64>,
|
||||
from: Option<DateTime<Utc>>,
|
||||
to: Option<DateTime<Utc>>,
|
||||
comment: Option<String>,
|
||||
|
|
@ -299,7 +138,6 @@ where
|
|||
fn default() -> Self {
|
||||
Self {
|
||||
selections: Vec::default(),
|
||||
id: None,
|
||||
from: None,
|
||||
to: None,
|
||||
comment: None,
|
||||
|
|
@ -312,37 +150,28 @@ impl<A> ApiRequest<A>
|
|||
where
|
||||
A: ApiCategoryResponse,
|
||||
{
|
||||
pub fn url(&self, key: &str) -> String {
|
||||
let mut query_fragments = vec![
|
||||
format!("selections={}", self.selections.join(",")),
|
||||
format!("key={}", key),
|
||||
];
|
||||
pub fn url(&self, key: &str, id: Option<i64>) -> String {
|
||||
let mut url = format!("https://api.torn.com/{}/", A::Selection::category());
|
||||
|
||||
if let Some(id) = id {
|
||||
write!(url, "{}", id).unwrap();
|
||||
}
|
||||
|
||||
write!(url, "?selections={}&key={}", self.selections.join(","), key).unwrap();
|
||||
|
||||
if let Some(from) = self.from {
|
||||
query_fragments.push(format!("from={}", from.timestamp()));
|
||||
write!(url, "&from={}", from.timestamp()).unwrap();
|
||||
}
|
||||
|
||||
if let Some(to) = self.to {
|
||||
query_fragments.push(format!("to={}", to.timestamp()));
|
||||
write!(url, "&to={}", to.timestamp()).unwrap();
|
||||
}
|
||||
|
||||
if let Some(comment) = &self.comment {
|
||||
query_fragments.push(format!("comment={}", comment));
|
||||
write!(url, "&comment={}", comment).unwrap();
|
||||
}
|
||||
|
||||
let query = query_fragments.join("&");
|
||||
|
||||
let id_fragment = match self.id {
|
||||
Some(id) => id.to_string(),
|
||||
None => "".to_owned(),
|
||||
};
|
||||
|
||||
format!(
|
||||
"https://api.torn.com/{}/{}?{}",
|
||||
A::Selection::category(),
|
||||
id_fragment,
|
||||
query
|
||||
)
|
||||
url
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -363,15 +192,6 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn id<I>(mut self, id: I) -> Self
|
||||
where
|
||||
I: PrimInt + AsPrimitive<u64>,
|
||||
{
|
||||
self.request.id = Some(id.as_());
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn selections(mut self, selections: &[A::Selection]) -> Self {
|
||||
self.request
|
||||
|
|
@ -399,8 +219,6 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub mod prelude {}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::sync::Once;
|
||||
|
|
@ -411,9 +229,9 @@ pub(crate) mod tests {
|
|||
pub use ::reqwest::Client;
|
||||
|
||||
#[cfg(all(not(feature = "reqwest"), feature = "awc"))]
|
||||
pub use crate::ApiClient as ClientTrait;
|
||||
pub use crate::local::ApiClient as ClientTrait;
|
||||
#[cfg(feature = "reqwest")]
|
||||
pub use crate::ThreadSafeApiClient as ClientTrait;
|
||||
pub use crate::send::ApiClient as ClientTrait;
|
||||
|
||||
#[cfg(all(not(feature = "reqwest"), feature = "awc"))]
|
||||
pub use actix_rt::test as async_test;
|
||||
|
|
@ -441,7 +259,11 @@ pub(crate) mod tests {
|
|||
async fn reqwest() {
|
||||
let key = setup();
|
||||
|
||||
Client::default().torn_api(key).user(|b| b).await.unwrap();
|
||||
Client::default()
|
||||
.torn_api(key)
|
||||
.user(None, |b| b)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(feature = "awc")]
|
||||
|
|
@ -449,6 +271,10 @@ pub(crate) mod tests {
|
|||
async fn awc() {
|
||||
let key = setup();
|
||||
|
||||
Client::default().torn_api(key).user(|b| b).await.unwrap();
|
||||
Client::default()
|
||||
.torn_api(key)
|
||||
.user(None, |b| b)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
235
torn-api/src/local.rs
Normal file
235
torn-api/src/local.rs
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::{
|
||||
faction, torn, user, ApiCategoryResponse, ApiClientError, ApiRequest, ApiRequestBuilder,
|
||||
ApiResponse, DirectExecutor,
|
||||
};
|
||||
|
||||
pub struct ApiProvider<'a, C, E, I = i32>
|
||||
where
|
||||
C: ApiClient,
|
||||
E: RequestExecutor<C>,
|
||||
I: num_traits::AsPrimitive<i64>,
|
||||
{
|
||||
client: &'a C,
|
||||
executor: E,
|
||||
_marker: std::marker::PhantomData<I>,
|
||||
}
|
||||
|
||||
impl<'a, C, E, I> ApiProvider<'a, C, E, I>
|
||||
where
|
||||
C: ApiClient,
|
||||
E: RequestExecutor<C>,
|
||||
I: num_traits::AsPrimitive<i64> + std::hash::Hash + std::cmp::Eq,
|
||||
i64: num_traits::AsPrimitive<I>,
|
||||
{
|
||||
pub fn new(client: &'a C, executor: E) -> ApiProvider<'a, C, E, I> {
|
||||
Self {
|
||||
client,
|
||||
executor,
|
||||
_marker: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn user<F>(&self, id: Option<I>, build: F) -> Result<user::Response, E::Error>
|
||||
where
|
||||
F: FnOnce(ApiRequestBuilder<user::Response>) -> ApiRequestBuilder<user::Response>,
|
||||
{
|
||||
let mut builder = ApiRequestBuilder::new();
|
||||
builder = build(builder);
|
||||
|
||||
self.executor
|
||||
.execute(self.client, builder.request, id.map(|i| i.as_()))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn users<F, L>(
|
||||
&self,
|
||||
ids: L,
|
||||
build: F,
|
||||
) -> HashMap<I, Result<user::Response, E::Error>>
|
||||
where
|
||||
F: FnOnce(ApiRequestBuilder<user::Response>) -> ApiRequestBuilder<user::Response>,
|
||||
L: IntoIterator<Item = I>,
|
||||
{
|
||||
let mut builder = ApiRequestBuilder::new();
|
||||
builder = build(builder);
|
||||
|
||||
self.executor
|
||||
.execute_many(
|
||||
self.client,
|
||||
builder.request,
|
||||
ids.into_iter().map(|i| i.as_()).collect(),
|
||||
)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|(i, r)| (num_traits::AsPrimitive::as_(i), r))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn faction<F>(&self, id: Option<I>, build: F) -> Result<faction::Response, E::Error>
|
||||
where
|
||||
F: FnOnce(ApiRequestBuilder<faction::Response>) -> ApiRequestBuilder<faction::Response>,
|
||||
{
|
||||
let mut builder = ApiRequestBuilder::new();
|
||||
builder = build(builder);
|
||||
|
||||
self.executor
|
||||
.execute(self.client, builder.request, id.map(|i| i.as_()))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn factions<F, L>(
|
||||
&self,
|
||||
ids: L,
|
||||
build: F,
|
||||
) -> HashMap<I, Result<faction::Response, E::Error>>
|
||||
where
|
||||
F: FnOnce(ApiRequestBuilder<faction::Response>) -> ApiRequestBuilder<faction::Response>,
|
||||
L: IntoIterator<Item = I>,
|
||||
{
|
||||
let mut builder = ApiRequestBuilder::new();
|
||||
builder = build(builder);
|
||||
|
||||
self.executor
|
||||
.execute_many(
|
||||
self.client,
|
||||
builder.request,
|
||||
ids.into_iter().map(|i| i.as_()).collect(),
|
||||
)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|(i, r)| (num_traits::AsPrimitive::as_(i), r))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn torn<F>(&self, id: Option<I>, build: F) -> Result<torn::Response, E::Error>
|
||||
where
|
||||
F: FnOnce(ApiRequestBuilder<torn::Response>) -> ApiRequestBuilder<torn::Response>,
|
||||
{
|
||||
let mut builder = ApiRequestBuilder::new();
|
||||
builder = build(builder);
|
||||
|
||||
self.executor
|
||||
.execute(self.client, builder.request, id.map(|i| i.as_()))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn torns<F, L>(
|
||||
&self,
|
||||
ids: L,
|
||||
build: F,
|
||||
) -> HashMap<I, Result<torn::Response, E::Error>>
|
||||
where
|
||||
F: FnOnce(ApiRequestBuilder<torn::Response>) -> ApiRequestBuilder<torn::Response>,
|
||||
L: IntoIterator<Item = I>,
|
||||
{
|
||||
let mut builder = ApiRequestBuilder::new();
|
||||
builder = build(builder);
|
||||
|
||||
self.executor
|
||||
.execute_many(
|
||||
self.client,
|
||||
builder.request,
|
||||
ids.into_iter().map(|i| i.as_()).collect(),
|
||||
)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|(i, r)| (num_traits::AsPrimitive::as_(i), r))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub trait RequestExecutor<C>
|
||||
where
|
||||
C: ApiClient,
|
||||
{
|
||||
type Error: std::error::Error;
|
||||
|
||||
async fn execute<A>(
|
||||
&self,
|
||||
client: &C,
|
||||
request: ApiRequest<A>,
|
||||
id: Option<i64>,
|
||||
) -> Result<A, Self::Error>
|
||||
where
|
||||
A: ApiCategoryResponse;
|
||||
|
||||
async fn execute_many<A>(
|
||||
&self,
|
||||
client: &C,
|
||||
request: ApiRequest<A>,
|
||||
ids: Vec<i64>,
|
||||
) -> HashMap<i64, Result<A, Self::Error>>
|
||||
where
|
||||
A: ApiCategoryResponse;
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl<C> RequestExecutor<C> for DirectExecutor<C>
|
||||
where
|
||||
C: ApiClient,
|
||||
{
|
||||
type Error = ApiClientError<C::Error>;
|
||||
|
||||
async fn execute<A>(
|
||||
&self,
|
||||
client: &C,
|
||||
request: ApiRequest<A>,
|
||||
id: Option<i64>,
|
||||
) -> Result<A, Self::Error>
|
||||
where
|
||||
A: ApiCategoryResponse,
|
||||
{
|
||||
let url = request.url(&self.key, id);
|
||||
|
||||
let value = client.request(url).await.map_err(ApiClientError::Client)?;
|
||||
|
||||
Ok(A::from_response(ApiResponse::from_value(value)?))
|
||||
}
|
||||
|
||||
async fn execute_many<A>(
|
||||
&self,
|
||||
client: &C,
|
||||
request: ApiRequest<A>,
|
||||
ids: Vec<i64>,
|
||||
) -> HashMap<i64, Result<A, Self::Error>>
|
||||
where
|
||||
A: ApiCategoryResponse,
|
||||
{
|
||||
let request_ref = &request;
|
||||
futures::future::join_all(ids.into_iter().map(|i| async move {
|
||||
let url = request_ref.url(&self.key, Some(i));
|
||||
|
||||
let value = client.request(url).await.map_err(ApiClientError::Client);
|
||||
|
||||
(
|
||||
i,
|
||||
value
|
||||
.and_then(|v| ApiResponse::from_value(v).map_err(Into::into))
|
||||
.map(A::from_response),
|
||||
)
|
||||
}))
|
||||
.await
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
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()))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
use async_trait::async_trait;
|
||||
|
||||
use crate::ThreadSafeApiClient;
|
||||
use crate::send::ApiClient;
|
||||
|
||||
#[async_trait]
|
||||
impl ThreadSafeApiClient for reqwest::Client {
|
||||
impl ApiClient for reqwest::Client {
|
||||
type Error = reqwest::Error;
|
||||
|
||||
async fn request(&self, url: String) -> Result<serde_json::Value, Self::Error> {
|
||||
|
|
|
|||
235
torn-api/src/send.rs
Normal file
235
torn-api/src/send.rs
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::{
|
||||
faction, torn, user, ApiCategoryResponse, ApiClientError, ApiRequest, ApiRequestBuilder,
|
||||
ApiResponse, DirectExecutor,
|
||||
};
|
||||
|
||||
pub struct ApiProvider<'a, C, E, I = i32>
|
||||
where
|
||||
C: ApiClient,
|
||||
E: RequestExecutor<C>,
|
||||
I: num_traits::AsPrimitive<i64>,
|
||||
{
|
||||
client: &'a C,
|
||||
executor: E,
|
||||
_marker: std::marker::PhantomData<I>,
|
||||
}
|
||||
|
||||
impl<'a, C, E, I> ApiProvider<'a, C, E, I>
|
||||
where
|
||||
C: ApiClient,
|
||||
E: RequestExecutor<C>,
|
||||
I: num_traits::AsPrimitive<i64> + std::hash::Hash + std::cmp::Eq,
|
||||
i64: num_traits::AsPrimitive<I>,
|
||||
{
|
||||
pub fn new(client: &'a C, executor: E) -> ApiProvider<'a, C, E, I> {
|
||||
Self {
|
||||
client,
|
||||
executor,
|
||||
_marker: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn user<F>(&self, id: Option<I>, build: F) -> Result<user::Response, E::Error>
|
||||
where
|
||||
F: FnOnce(ApiRequestBuilder<user::Response>) -> ApiRequestBuilder<user::Response>,
|
||||
{
|
||||
let mut builder = ApiRequestBuilder::new();
|
||||
builder = build(builder);
|
||||
|
||||
self.executor
|
||||
.execute(self.client, builder.request, id.map(|i| i.as_()))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn users<F, L>(
|
||||
&self,
|
||||
ids: L,
|
||||
build: F,
|
||||
) -> HashMap<I, Result<user::Response, E::Error>>
|
||||
where
|
||||
F: FnOnce(ApiRequestBuilder<user::Response>) -> ApiRequestBuilder<user::Response>,
|
||||
L: IntoIterator<Item = I>,
|
||||
{
|
||||
let mut builder = ApiRequestBuilder::new();
|
||||
builder = build(builder);
|
||||
|
||||
self.executor
|
||||
.execute_many(
|
||||
self.client,
|
||||
builder.request,
|
||||
ids.into_iter().map(|i| i.as_()).collect(),
|
||||
)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|(i, r)| (num_traits::AsPrimitive::as_(i), r))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn faction<F>(&self, id: Option<I>, build: F) -> Result<faction::Response, E::Error>
|
||||
where
|
||||
F: FnOnce(ApiRequestBuilder<faction::Response>) -> ApiRequestBuilder<faction::Response>,
|
||||
{
|
||||
let mut builder = ApiRequestBuilder::new();
|
||||
builder = build(builder);
|
||||
|
||||
self.executor
|
||||
.execute(self.client, builder.request, id.map(|i| i.as_()))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn factions<F, L>(
|
||||
&self,
|
||||
ids: L,
|
||||
build: F,
|
||||
) -> HashMap<I, Result<faction::Response, E::Error>>
|
||||
where
|
||||
F: FnOnce(ApiRequestBuilder<faction::Response>) -> ApiRequestBuilder<faction::Response>,
|
||||
L: IntoIterator<Item = I>,
|
||||
{
|
||||
let mut builder = ApiRequestBuilder::new();
|
||||
builder = build(builder);
|
||||
|
||||
self.executor
|
||||
.execute_many(
|
||||
self.client,
|
||||
builder.request,
|
||||
ids.into_iter().map(|i| i.as_()).collect(),
|
||||
)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|(i, r)| (num_traits::AsPrimitive::as_(i), r))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn torn<F>(&self, id: Option<I>, build: F) -> Result<torn::Response, E::Error>
|
||||
where
|
||||
F: FnOnce(ApiRequestBuilder<torn::Response>) -> ApiRequestBuilder<torn::Response>,
|
||||
{
|
||||
let mut builder = ApiRequestBuilder::new();
|
||||
builder = build(builder);
|
||||
|
||||
self.executor
|
||||
.execute(self.client, builder.request, id.map(|i| i.as_()))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn torns<F, L>(
|
||||
&self,
|
||||
ids: L,
|
||||
build: F,
|
||||
) -> HashMap<I, Result<torn::Response, E::Error>>
|
||||
where
|
||||
F: FnOnce(ApiRequestBuilder<torn::Response>) -> ApiRequestBuilder<torn::Response>,
|
||||
L: IntoIterator<Item = I>,
|
||||
{
|
||||
let mut builder = ApiRequestBuilder::new();
|
||||
builder = build(builder);
|
||||
|
||||
self.executor
|
||||
.execute_many(
|
||||
self.client,
|
||||
builder.request,
|
||||
ids.into_iter().map(|i| i.as_()).collect(),
|
||||
)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|(i, r)| (num_traits::AsPrimitive::as_(i), r))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait RequestExecutor<C>
|
||||
where
|
||||
C: ApiClient,
|
||||
{
|
||||
type Error: std::error::Error + Send + Sync;
|
||||
|
||||
async fn execute<A>(
|
||||
&self,
|
||||
client: &C,
|
||||
request: ApiRequest<A>,
|
||||
id: Option<i64>,
|
||||
) -> Result<A, Self::Error>
|
||||
where
|
||||
A: ApiCategoryResponse;
|
||||
|
||||
async fn execute_many<A>(
|
||||
&self,
|
||||
client: &C,
|
||||
request: ApiRequest<A>,
|
||||
ids: Vec<i64>,
|
||||
) -> HashMap<i64, Result<A, Self::Error>>
|
||||
where
|
||||
A: ApiCategoryResponse;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<C> RequestExecutor<C> for DirectExecutor<C>
|
||||
where
|
||||
C: ApiClient,
|
||||
{
|
||||
type Error = ApiClientError<C::Error>;
|
||||
|
||||
async fn execute<A>(
|
||||
&self,
|
||||
client: &C,
|
||||
request: ApiRequest<A>,
|
||||
id: Option<i64>,
|
||||
) -> Result<A, Self::Error>
|
||||
where
|
||||
A: ApiCategoryResponse,
|
||||
{
|
||||
let url = request.url(&self.key, id);
|
||||
|
||||
let value = client.request(url).await.map_err(ApiClientError::Client)?;
|
||||
|
||||
Ok(A::from_response(ApiResponse::from_value(value)?))
|
||||
}
|
||||
|
||||
async fn execute_many<A>(
|
||||
&self,
|
||||
client: &C,
|
||||
request: ApiRequest<A>,
|
||||
ids: Vec<i64>,
|
||||
) -> HashMap<i64, Result<A, Self::Error>>
|
||||
where
|
||||
A: ApiCategoryResponse,
|
||||
{
|
||||
let request_ref = &request;
|
||||
futures::future::join_all(ids.into_iter().map(|i| async move {
|
||||
let url = request_ref.url(&self.key, Some(i));
|
||||
|
||||
let value = client.request(url).await.map_err(ApiClientError::Client);
|
||||
|
||||
(
|
||||
i,
|
||||
value
|
||||
.and_then(|v| ApiResponse::from_value(v).map_err(Into::into))
|
||||
.map(A::from_response),
|
||||
)
|
||||
}))
|
||||
.await
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait ApiClient: Send + Sync {
|
||||
type Error: std::error::Error + Sync + Send;
|
||||
|
||||
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()))
|
||||
}
|
||||
}
|
||||
100
torn-api/src/torn.rs
Normal file
100
torn-api/src/torn.rs
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
use serde::{
|
||||
de::{self, MapAccess, Visitor},
|
||||
Deserialize,
|
||||
};
|
||||
|
||||
use torn_api_macros::ApiCategory;
|
||||
|
||||
use crate::user;
|
||||
|
||||
#[derive(Debug, Clone, Copy, ApiCategory)]
|
||||
#[api(category = "torn")]
|
||||
pub enum Selection {
|
||||
#[api(
|
||||
field = "competition",
|
||||
with = "decode_competition",
|
||||
type = "Option<Competition>"
|
||||
)]
|
||||
Competition,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct EliminationLeaderboard {
|
||||
pub position: i16,
|
||||
pub team: user::EliminationTeam,
|
||||
pub score: i16,
|
||||
pub lives: i16,
|
||||
pub participants: i16,
|
||||
pub wins: i32,
|
||||
pub losses: i32,
|
||||
}
|
||||
|
||||
pub enum Competition {
|
||||
Elimination { teams: Vec<EliminationLeaderboard> },
|
||||
}
|
||||
|
||||
fn decode_competition<'de, D>(deserializer: D) -> Result<Option<Competition>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct CompetitionVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for CompetitionVisitor {
|
||||
type Value = Option<Competition>;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("struct Competition")
|
||||
}
|
||||
|
||||
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
|
||||
where
|
||||
V: MapAccess<'de>,
|
||||
{
|
||||
let mut name = None;
|
||||
let mut teams = None;
|
||||
|
||||
while let Some(key) = map.next_key()? {
|
||||
match key {
|
||||
"name" => {
|
||||
name = Some(map.next_value()?);
|
||||
}
|
||||
"teams" => {
|
||||
teams = Some(map.next_value()?);
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
|
||||
let name = name.ok_or_else(|| de::Error::missing_field("name"))?;
|
||||
|
||||
match name {
|
||||
"Elimination" => Ok(Some(Competition::Elimination {
|
||||
teams: teams.ok_or_else(|| de::Error::missing_field("teams"))?,
|
||||
})),
|
||||
"" => Ok(None),
|
||||
v => Err(de::Error::unknown_variant(v, &["Elimination", ""])),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_map(CompetitionVisitor)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::tests::{async_test, setup, Client, ClientTrait};
|
||||
|
||||
#[async_test]
|
||||
async fn competition() {
|
||||
let key = setup();
|
||||
|
||||
let response = Client::default()
|
||||
.torn_api(key)
|
||||
.torn(None, |b| b.selections(&[Selection::Competition]))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
response.competition().unwrap();
|
||||
}
|
||||
}
|
||||
|
|
@ -195,7 +195,7 @@ pub enum EliminationTeam {
|
|||
DirtyCops,
|
||||
LaughingStock,
|
||||
JeanTherapy,
|
||||
#[serde(rename = "statants-soldiers")]
|
||||
#[serde(rename = "satants-soldiers")]
|
||||
SatansSoldiers,
|
||||
WolfPack,
|
||||
Sleepyheads,
|
||||
|
|
@ -399,7 +399,7 @@ mod tests {
|
|||
|
||||
let response = Client::default()
|
||||
.torn_api(key)
|
||||
.user(|b| {
|
||||
.user(None, |b| {
|
||||
b.selections(&[
|
||||
Selection::Basic,
|
||||
Selection::Discord,
|
||||
|
|
@ -424,7 +424,7 @@ mod tests {
|
|||
|
||||
let response = Client::default()
|
||||
.torn_api(key)
|
||||
.user(|b| b.id(28).selections(&[Selection::Profile]))
|
||||
.user(Some(28), |b| b.selections(&[Selection::Profile]))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -439,11 +439,24 @@ mod tests {
|
|||
|
||||
let response = Client::default()
|
||||
.torn_api(key)
|
||||
.user(|b| b.selections(&[Selection::Profile]))
|
||||
.user(None, |b| b.selections(&[Selection::Profile]))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let profile = response.profile().unwrap();
|
||||
assert!(profile.competition.is_some());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn bulk() {
|
||||
let key = setup();
|
||||
|
||||
let response = Client::default()
|
||||
.torn_api(key)
|
||||
.users([1, 2111649], |b| b.selections(&[Selection::Basic]))
|
||||
.await;
|
||||
|
||||
response.get(&1).as_ref().unwrap().as_ref().unwrap();
|
||||
response.get(&2111649).as_ref().unwrap().as_ref().unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue