bulk updates
This commit is contained in:
parent
f452c44311
commit
5630e51adb
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "torn-api-macros"
|
name = "torn-api-macros"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Pyrit [2111649]"]
|
authors = ["Pyrit [2111649]"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
@ -12,6 +12,7 @@ description = "Macros implementation of #[derive(ApiCategory)]"
|
||||||
proc-macro = true
|
proc-macro = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
syn = { version = "1.0", features = [ "extra-traits" ] }
|
syn = { version = "1", features = [ "extra-traits" ] }
|
||||||
quote = "1.0"
|
proc-macro2 = "1"
|
||||||
|
quote = "1"
|
||||||
convert_case = "0.5"
|
convert_case = "0.5"
|
||||||
|
|
|
@ -17,11 +17,12 @@ enum ApiField {
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct ApiAttribute {
|
struct ApiAttribute {
|
||||||
type_: syn::Ident,
|
|
||||||
field: ApiField,
|
field: ApiField,
|
||||||
name: syn::Ident,
|
name: syn::Ident,
|
||||||
raw_value: String,
|
raw_value: String,
|
||||||
variant: syn::Ident,
|
variant: syn::Ident,
|
||||||
|
type_name: proc_macro2::TokenStream,
|
||||||
|
with: Option<syn::Ident>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_lit_string(lit: syn::Lit) -> String {
|
fn get_lit_string(lit: syn::Lit) -> String {
|
||||||
|
@ -71,6 +72,7 @@ fn impl_api_category(ast: &syn::DeriveInput) -> TokenStream {
|
||||||
Ok(syn::Meta::List(l)) => {
|
Ok(syn::Meta::List(l)) => {
|
||||||
let mut type_: Option<String> = None;
|
let mut type_: Option<String> = None;
|
||||||
let mut field: Option<ApiField> = None;
|
let mut field: Option<ApiField> = None;
|
||||||
|
let mut with: Option<String> = None;
|
||||||
for nested in l.nested.into_iter() {
|
for nested in l.nested.into_iter() {
|
||||||
match nested {
|
match nested {
|
||||||
syn::NestedMeta::Meta(syn::Meta::NameValue(m))
|
syn::NestedMeta::Meta(syn::Meta::NameValue(m))
|
||||||
|
@ -82,6 +84,15 @@ fn impl_api_category(ast: &syn::DeriveInput) -> TokenStream {
|
||||||
panic!("type can only be specified once");
|
panic!("type can only be specified once");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
syn::NestedMeta::Meta(syn::Meta::NameValue(m))
|
||||||
|
if m.path.is_ident("with") =>
|
||||||
|
{
|
||||||
|
if with.is_none() {
|
||||||
|
with = Some(get_lit_string(m.lit));
|
||||||
|
} else {
|
||||||
|
panic!("with can only be specified once");
|
||||||
|
}
|
||||||
|
}
|
||||||
syn::NestedMeta::Meta(syn::Meta::NameValue(m))
|
syn::NestedMeta::Meta(syn::Meta::NameValue(m))
|
||||||
if m.path.is_ident("field") =>
|
if m.path.is_ident("field") =>
|
||||||
{
|
{
|
||||||
|
@ -109,12 +120,17 @@ fn impl_api_category(ast: &syn::DeriveInput) -> TokenStream {
|
||||||
let name =
|
let name =
|
||||||
format_ident!("{}", variant.ident.to_string().to_case(Case::Snake));
|
format_ident!("{}", variant.ident.to_string().to_case(Case::Snake));
|
||||||
let raw_value = variant.ident.to_string().to_lowercase();
|
let raw_value = variant.ident.to_string().to_lowercase();
|
||||||
|
|
||||||
return Some(ApiAttribute {
|
return Some(ApiAttribute {
|
||||||
type_: quote::format_ident!("{}", type_.expect("type")),
|
|
||||||
field: field.expect("one of field/flatten"),
|
field: field.expect("one of field/flatten"),
|
||||||
name,
|
name,
|
||||||
raw_value,
|
raw_value,
|
||||||
variant: variant.ident.clone(),
|
variant: variant.ident.clone(),
|
||||||
|
type_name: type_
|
||||||
|
.expect("Need to specify type name")
|
||||||
|
.parse()
|
||||||
|
.expect("failed to parse type name"),
|
||||||
|
with: with.map(|w| format_ident!("{}", w)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
_ => panic!("Couldn't parse api attribute"),
|
_ => panic!("Couldn't parse api attribute"),
|
||||||
|
@ -127,21 +143,34 @@ fn impl_api_category(ast: &syn::DeriveInput) -> TokenStream {
|
||||||
|
|
||||||
let accessors = fields.iter().map(
|
let accessors = fields.iter().map(
|
||||||
|ApiAttribute {
|
|ApiAttribute {
|
||||||
type_, field, name, ..
|
field,
|
||||||
}| match field {
|
name,
|
||||||
ApiField::Property(prop) => {
|
type_name,
|
||||||
|
with,
|
||||||
|
..
|
||||||
|
}| match (field, with) {
|
||||||
|
(ApiField::Property(prop), None) => {
|
||||||
let prop_str = prop.to_string();
|
let prop_str = prop.to_string();
|
||||||
quote! {
|
quote! {
|
||||||
pub fn #name(&self) -> serde_json::Result<#type_> {
|
pub fn #name(&self) -> serde_json::Result<#type_name> {
|
||||||
self.0.decode_field(#prop_str)
|
self.0.decode_field(#prop_str)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ApiField::Flattened => quote! {
|
(ApiField::Property(prop), Some(f)) => {
|
||||||
pub fn #name(&self) -> serde_json::Result<#type_> {
|
let prop_str = prop.to_string();
|
||||||
|
quote! {
|
||||||
|
pub fn #name(&self) -> serde_json::Result<#type_name> {
|
||||||
|
self.0.decode_field_with(#prop_str, #f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(ApiField::Flattened, None) => quote! {
|
||||||
|
pub fn #name(&self) -> serde_json::Result<#type_name> {
|
||||||
self.0.decode()
|
self.0.decode()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
(ApiField::Flattened, Some(_)) => todo!(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "torn-api"
|
name = "torn-api"
|
||||||
version = "0.4.2"
|
version = "0.5.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Pyrit [2111649]"]
|
authors = ["Pyrit [2111649]"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
@ -24,11 +24,12 @@ chrono = { version = "0.4", features = [ "serde" ], default-features = false }
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
|
futures = "0.3"
|
||||||
|
|
||||||
reqwest = { version = "0.11", default-features = false, features = [ "json" ], optional = true }
|
reqwest = { version = "0.11", default-features = false, features = [ "json" ], optional = true }
|
||||||
awc = { version = "3", default-features = false, 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]
|
[dev-dependencies]
|
||||||
actix-rt = { version = "2.7.0" }
|
actix-rt = { version = "2.7.0" }
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::ApiClient;
|
use crate::local::ApiClient;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum AwcApiClientError {
|
pub enum AwcApiClientError {
|
||||||
|
|
|
@ -47,7 +47,7 @@ mod tests {
|
||||||
|
|
||||||
let response = Client::default()
|
let response = Client::default()
|
||||||
.torn_api(key)
|
.torn_api(key)
|
||||||
.faction(|b| b.selections(&[Selection::Basic]))
|
.faction(None, |b| b.selections(&[Selection::Basic]))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,22 @@
|
||||||
#![warn(clippy::all, clippy::perf, clippy::style, clippy::suspicious)]
|
#![warn(clippy::all, clippy::perf, clippy::style, clippy::suspicious)]
|
||||||
|
|
||||||
pub mod faction;
|
pub mod faction;
|
||||||
|
pub mod local;
|
||||||
|
pub mod send;
|
||||||
|
pub mod torn;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|
||||||
#[cfg(feature = "awc")]
|
#[cfg(feature = "awc")]
|
||||||
pub mod awc;
|
mod awc;
|
||||||
|
|
||||||
#[cfg(feature = "reqwest")]
|
#[cfg(feature = "reqwest")]
|
||||||
pub mod reqwest;
|
mod reqwest;
|
||||||
|
|
||||||
mod de_util;
|
mod de_util;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use std::fmt::Write;
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use num_traits::{AsPrimitive, PrimInt};
|
|
||||||
use serde::de::{DeserializeOwned, Error as DeError};
|
use serde::de::{DeserializeOwned, Error as DeError};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
@ -54,20 +57,27 @@ impl ApiResponse {
|
||||||
where
|
where
|
||||||
D: DeserializeOwned,
|
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>
|
fn decode_field<D>(&self, field: &'static str) -> serde_json::Result<D>
|
||||||
where
|
where
|
||||||
D: DeserializeOwned,
|
D: DeserializeOwned,
|
||||||
{
|
{
|
||||||
let value = self
|
self.value
|
||||||
.value
|
|
||||||
.get(field)
|
.get(field)
|
||||||
.ok_or_else(|| serde_json::Error::missing_field(field))?
|
.ok_or_else(|| serde_json::Error::missing_field(field))
|
||||||
.clone();
|
.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;
|
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> {
|
pub struct DirectExecutor<C> {
|
||||||
key: String,
|
key: String,
|
||||||
_marker: std::marker::PhantomData<C>,
|
_marker: std::marker::PhantomData<C>,
|
||||||
|
@ -224,7 +102,7 @@ impl<C> DirectExecutor<C> {
|
||||||
fn new(key: String) -> Self {
|
fn new(key: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
key,
|
key,
|
||||||
_marker: std::marker::PhantomData,
|
_marker: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -241,51 +119,12 @@ where
|
||||||
Response(#[from] ResponseError),
|
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)]
|
#[derive(Debug)]
|
||||||
pub struct ApiRequest<A>
|
pub struct ApiRequest<A>
|
||||||
where
|
where
|
||||||
A: ApiCategoryResponse,
|
A: ApiCategoryResponse,
|
||||||
{
|
{
|
||||||
selections: Vec<&'static str>,
|
selections: Vec<&'static str>,
|
||||||
id: Option<u64>,
|
|
||||||
from: Option<DateTime<Utc>>,
|
from: Option<DateTime<Utc>>,
|
||||||
to: Option<DateTime<Utc>>,
|
to: Option<DateTime<Utc>>,
|
||||||
comment: Option<String>,
|
comment: Option<String>,
|
||||||
|
@ -299,7 +138,6 @@ where
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
selections: Vec::default(),
|
selections: Vec::default(),
|
||||||
id: None,
|
|
||||||
from: None,
|
from: None,
|
||||||
to: None,
|
to: None,
|
||||||
comment: None,
|
comment: None,
|
||||||
|
@ -312,37 +150,28 @@ impl<A> ApiRequest<A>
|
||||||
where
|
where
|
||||||
A: ApiCategoryResponse,
|
A: ApiCategoryResponse,
|
||||||
{
|
{
|
||||||
pub fn url(&self, key: &str) -> String {
|
pub fn url(&self, key: &str, id: Option<i64>) -> String {
|
||||||
let mut query_fragments = vec![
|
let mut url = format!("https://api.torn.com/{}/", A::Selection::category());
|
||||||
format!("selections={}", self.selections.join(",")),
|
|
||||||
format!("key={}", key),
|
if let Some(id) = id {
|
||||||
];
|
write!(url, "{}", id).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(url, "?selections={}&key={}", self.selections.join(","), key).unwrap();
|
||||||
|
|
||||||
if let Some(from) = self.from {
|
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 {
|
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 {
|
if let Some(comment) = &self.comment {
|
||||||
query_fragments.push(format!("comment={}", comment));
|
write!(url, "&comment={}", comment).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let query = query_fragments.join("&");
|
url
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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]
|
#[must_use]
|
||||||
pub fn selections(mut self, selections: &[A::Selection]) -> Self {
|
pub fn selections(mut self, selections: &[A::Selection]) -> Self {
|
||||||
self.request
|
self.request
|
||||||
|
@ -399,8 +219,6 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod prelude {}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) mod tests {
|
pub(crate) mod tests {
|
||||||
use std::sync::Once;
|
use std::sync::Once;
|
||||||
|
@ -411,9 +229,9 @@ pub(crate) mod tests {
|
||||||
pub use ::reqwest::Client;
|
pub use ::reqwest::Client;
|
||||||
|
|
||||||
#[cfg(all(not(feature = "reqwest"), feature = "awc"))]
|
#[cfg(all(not(feature = "reqwest"), feature = "awc"))]
|
||||||
pub use crate::ApiClient as ClientTrait;
|
pub use crate::local::ApiClient as ClientTrait;
|
||||||
#[cfg(feature = "reqwest")]
|
#[cfg(feature = "reqwest")]
|
||||||
pub use crate::ThreadSafeApiClient as ClientTrait;
|
pub use crate::send::ApiClient as ClientTrait;
|
||||||
|
|
||||||
#[cfg(all(not(feature = "reqwest"), feature = "awc"))]
|
#[cfg(all(not(feature = "reqwest"), feature = "awc"))]
|
||||||
pub use actix_rt::test as async_test;
|
pub use actix_rt::test as async_test;
|
||||||
|
@ -441,7 +259,11 @@ pub(crate) mod tests {
|
||||||
async fn reqwest() {
|
async fn reqwest() {
|
||||||
let key = setup();
|
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")]
|
#[cfg(feature = "awc")]
|
||||||
|
@ -449,6 +271,10 @@ pub(crate) mod tests {
|
||||||
async fn awc() {
|
async fn awc() {
|
||||||
let key = setup();
|
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 async_trait::async_trait;
|
||||||
|
|
||||||
use crate::ThreadSafeApiClient;
|
use crate::send::ApiClient;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl ThreadSafeApiClient for reqwest::Client {
|
impl ApiClient for reqwest::Client {
|
||||||
type Error = reqwest::Error;
|
type Error = reqwest::Error;
|
||||||
|
|
||||||
async fn request(&self, url: String) -> Result<serde_json::Value, Self::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,
|
DirtyCops,
|
||||||
LaughingStock,
|
LaughingStock,
|
||||||
JeanTherapy,
|
JeanTherapy,
|
||||||
#[serde(rename = "statants-soldiers")]
|
#[serde(rename = "satants-soldiers")]
|
||||||
SatansSoldiers,
|
SatansSoldiers,
|
||||||
WolfPack,
|
WolfPack,
|
||||||
Sleepyheads,
|
Sleepyheads,
|
||||||
|
@ -399,7 +399,7 @@ mod tests {
|
||||||
|
|
||||||
let response = Client::default()
|
let response = Client::default()
|
||||||
.torn_api(key)
|
.torn_api(key)
|
||||||
.user(|b| {
|
.user(None, |b| {
|
||||||
b.selections(&[
|
b.selections(&[
|
||||||
Selection::Basic,
|
Selection::Basic,
|
||||||
Selection::Discord,
|
Selection::Discord,
|
||||||
|
@ -424,7 +424,7 @@ mod tests {
|
||||||
|
|
||||||
let response = Client::default()
|
let response = Client::default()
|
||||||
.torn_api(key)
|
.torn_api(key)
|
||||||
.user(|b| b.id(28).selections(&[Selection::Profile]))
|
.user(Some(28), |b| b.selections(&[Selection::Profile]))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -439,11 +439,24 @@ mod tests {
|
||||||
|
|
||||||
let response = Client::default()
|
let response = Client::default()
|
||||||
.torn_api(key)
|
.torn_api(key)
|
||||||
.user(|b| b.selections(&[Selection::Profile]))
|
.user(None, |b| b.selections(&[Selection::Profile]))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let profile = response.profile().unwrap();
|
let profile = response.profile().unwrap();
|
||||||
assert!(profile.competition.is_some());
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "torn-key-pool"
|
name = "torn-key-pool"
|
||||||
version = "0.3.1"
|
version = "0.4.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/TotallyNot/torn-api.rs.git"
|
repository = "https://github.com/TotallyNot/torn-api.rs.git"
|
||||||
|
@ -18,7 +18,7 @@ tokio-runtime = [ "dep:tokio", "dep:rand" ]
|
||||||
actix-runtime = [ "dep:actix-rt", "dep:rand" ]
|
actix-runtime = [ "dep:actix-rt", "dep:rand" ]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
torn-api = { path = "../torn-api", default-features = false, version = "0.4" }
|
torn-api = { path = "../torn-api", default-features = false, version = "0.5" }
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ indoc = { version = "1", optional = true }
|
||||||
tokio = { version = "1", optional = true, default-features = false, features = ["time"] }
|
tokio = { version = "1", optional = true, default-features = false, features = ["time"] }
|
||||||
actix-rt = { version = "2", optional = true, default-features = false }
|
actix-rt = { version = "2", optional = true, default-features = false }
|
||||||
rand = { version = "0.8", optional = true }
|
rand = { version = "0.8", optional = true }
|
||||||
|
futures = "0.3"
|
||||||
|
|
||||||
reqwest = { version = "0.11", default-features = false, features = [ "json" ], optional = true }
|
reqwest = { version = "0.11", default-features = false, features = [ "json" ], optional = true }
|
||||||
awc = { version = "3", default-features = false, optional = true }
|
awc = { version = "3", default-features = false, optional = true }
|
||||||
|
@ -40,4 +41,3 @@ tokio = { version = "1.20.1", features = ["test-util", "rt", "macros"] }
|
||||||
tokio-test = "0.4.2"
|
tokio-test = "0.4.2"
|
||||||
reqwest = { version = "0.11", default-features = true }
|
reqwest = { version = "0.11", default-features = true }
|
||||||
awc = { version = "3", features = [ "rustls" ] }
|
awc = { version = "3", features = [ "rustls" ] }
|
||||||
futures = "0.3.24"
|
|
||||||
|
|
|
@ -3,13 +3,15 @@
|
||||||
#[cfg(feature = "postgres")]
|
#[cfg(feature = "postgres")]
|
||||||
pub mod postgres;
|
pub mod postgres;
|
||||||
|
|
||||||
|
pub mod local;
|
||||||
|
pub mod send;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use torn_api::{
|
use torn_api::ResponseError;
|
||||||
ApiCategoryResponse, ApiClient, ApiProvider, ApiRequest, ApiResponse, RequestExecutor,
|
|
||||||
ResponseError, ThreadSafeApiClient, ThreadSafeApiProvider, ThreadSafeRequestExecutor,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum KeyPoolError<S, C>
|
pub enum KeyPoolError<S, C>
|
||||||
|
@ -18,7 +20,7 @@ where
|
||||||
C: std::error::Error,
|
C: std::error::Error,
|
||||||
{
|
{
|
||||||
#[error("Key pool storage driver error: {0:?}")]
|
#[error("Key pool storage driver error: {0:?}")]
|
||||||
Storage(#[source] S),
|
Storage(#[source] Arc<S>),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Client(#[from] C),
|
Client(#[from] C),
|
||||||
|
@ -45,6 +47,12 @@ pub trait KeyPoolStorage {
|
||||||
|
|
||||||
async fn acquire_key(&self, domain: KeyDomain) -> Result<Self::Key, Self::Error>;
|
async fn acquire_key(&self, domain: KeyDomain) -> Result<Self::Key, Self::Error>;
|
||||||
|
|
||||||
|
async fn acquire_many_keys(
|
||||||
|
&self,
|
||||||
|
domain: KeyDomain,
|
||||||
|
number: i64,
|
||||||
|
) -> Result<Vec<Self::Key>, Self::Error>;
|
||||||
|
|
||||||
async fn flag_key(&self, key: Self::Key, code: u8) -> Result<bool, Self::Error>;
|
async fn flag_key(&self, key: Self::Key, code: u8) -> Result<bool, Self::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,161 +78,3 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait(?Send)]
|
|
||||||
impl<'client, C, S> RequestExecutor<C> for KeyPoolExecutor<'client, C, S>
|
|
||||||
where
|
|
||||||
C: ApiClient,
|
|
||||||
S: KeyPoolStorage + 'static,
|
|
||||||
{
|
|
||||||
type Error = KeyPoolError<S::Error, C::Error>;
|
|
||||||
|
|
||||||
async fn execute<A>(&self, client: &C, request: ApiRequest<A>) -> Result<A, Self::Error>
|
|
||||||
where
|
|
||||||
A: ApiCategoryResponse,
|
|
||||||
{
|
|
||||||
loop {
|
|
||||||
let key = self
|
|
||||||
.storage
|
|
||||||
.acquire_key(self.domain)
|
|
||||||
.await
|
|
||||||
.map_err(KeyPoolError::Storage)?;
|
|
||||||
let url = request.url(key.value());
|
|
||||||
let value = client.request(url).await?;
|
|
||||||
|
|
||||||
match ApiResponse::from_value(value) {
|
|
||||||
Err(ResponseError::Api { code, reason }) => {
|
|
||||||
if !self
|
|
||||||
.storage
|
|
||||||
.flag_key(key, code)
|
|
||||||
.await
|
|
||||||
.map_err(KeyPoolError::Storage)?
|
|
||||||
{
|
|
||||||
return Err(KeyPoolError::Response(ResponseError::Api { code, reason }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(parsing_error) => return Err(KeyPoolError::Response(parsing_error)),
|
|
||||||
Ok(res) => return Ok(A::from_response(res)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<'client, C, S> ThreadSafeRequestExecutor<C> for KeyPoolExecutor<'client, C, S>
|
|
||||||
where
|
|
||||||
C: ThreadSafeApiClient,
|
|
||||||
S: KeyPoolStorage + Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
type Error = KeyPoolError<S::Error, C::Error>;
|
|
||||||
|
|
||||||
async fn execute<A>(&self, client: &C, request: ApiRequest<A>) -> Result<A, Self::Error>
|
|
||||||
where
|
|
||||||
A: ApiCategoryResponse,
|
|
||||||
{
|
|
||||||
loop {
|
|
||||||
let key = self
|
|
||||||
.storage
|
|
||||||
.acquire_key(self.domain)
|
|
||||||
.await
|
|
||||||
.map_err(KeyPoolError::Storage)?;
|
|
||||||
let url = request.url(key.value());
|
|
||||||
let value = client.request(url).await?;
|
|
||||||
|
|
||||||
match ApiResponse::from_value(value) {
|
|
||||||
Err(ResponseError::Api { code, reason }) => {
|
|
||||||
if !self
|
|
||||||
.storage
|
|
||||||
.flag_key(key, code)
|
|
||||||
.await
|
|
||||||
.map_err(KeyPoolError::Storage)?
|
|
||||||
{
|
|
||||||
return Err(KeyPoolError::Response(ResponseError::Api { code, reason }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(parsing_error) => return Err(KeyPoolError::Response(parsing_error)),
|
|
||||||
Ok(res) => return Ok(A::from_response(res)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct KeyPool<C, S>
|
|
||||||
where
|
|
||||||
C: ApiClient,
|
|
||||||
S: KeyPoolStorage,
|
|
||||||
{
|
|
||||||
client: C,
|
|
||||||
storage: S,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C, S> KeyPool<C, S>
|
|
||||||
where
|
|
||||||
C: ApiClient,
|
|
||||||
S: KeyPoolStorage + 'static,
|
|
||||||
{
|
|
||||||
pub fn new(client: C, storage: S) -> Self {
|
|
||||||
Self { client, storage }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn torn_api(&self, domain: KeyDomain) -> ApiProvider<C, KeyPoolExecutor<C, S>> {
|
|
||||||
ApiProvider::new(&self.client, KeyPoolExecutor::new(&self.storage, domain))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct ThreadSafeKeyPool<C, S>
|
|
||||||
where
|
|
||||||
C: ThreadSafeApiClient,
|
|
||||||
S: KeyPoolStorage + Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
client: C,
|
|
||||||
storage: S,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C, S> ThreadSafeKeyPool<C, S>
|
|
||||||
where
|
|
||||||
C: ThreadSafeApiClient,
|
|
||||||
S: KeyPoolStorage + Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
pub fn new(client: C, storage: S) -> Self {
|
|
||||||
Self { client, storage }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn torn_api(&self, domain: KeyDomain) -> ThreadSafeApiProvider<C, KeyPoolExecutor<C, S>> {
|
|
||||||
ThreadSafeApiProvider::new(&self.client, KeyPoolExecutor::new(&self.storage, domain))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait WithStorage {
|
|
||||||
fn with_storage<'a, S>(
|
|
||||||
&'a self,
|
|
||||||
storage: &'a S,
|
|
||||||
domain: KeyDomain,
|
|
||||||
) -> ApiProvider<Self, KeyPoolExecutor<Self, S>>
|
|
||||||
where
|
|
||||||
Self: ApiClient + Sized,
|
|
||||||
S: KeyPoolStorage + 'static,
|
|
||||||
{
|
|
||||||
ApiProvider::new(self, KeyPoolExecutor::new(storage, domain))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_storage_sync<'a, S>(
|
|
||||||
&'a self,
|
|
||||||
storage: &'a S,
|
|
||||||
domain: KeyDomain,
|
|
||||||
) -> ThreadSafeApiProvider<Self, KeyPoolExecutor<Self, S>>
|
|
||||||
where
|
|
||||||
Self: ThreadSafeApiClient + Sized,
|
|
||||||
S: KeyPoolStorage + Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
ThreadSafeApiProvider::new(self, KeyPoolExecutor::new(storage, domain))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "reqwest")]
|
|
||||||
impl WithStorage for reqwest::Client {}
|
|
||||||
|
|
||||||
#[cfg(feature = "awc")]
|
|
||||||
impl WithStorage for awc::Client {}
|
|
||||||
|
|
161
torn-key-pool/src/local.rs
Normal file
161
torn-key-pool/src/local.rs
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use torn_api::{
|
||||||
|
local::{ApiClient, ApiProvider, RequestExecutor},
|
||||||
|
ApiCategoryResponse, ApiRequest, ApiResponse, ResponseError,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{ApiKey, KeyDomain, KeyPoolError, KeyPoolExecutor, KeyPoolStorage};
|
||||||
|
|
||||||
|
#[async_trait(?Send)]
|
||||||
|
impl<'client, C, S> RequestExecutor<C> for KeyPoolExecutor<'client, C, S>
|
||||||
|
where
|
||||||
|
C: ApiClient,
|
||||||
|
S: KeyPoolStorage + 'static,
|
||||||
|
{
|
||||||
|
type Error = KeyPoolError<S::Error, C::Error>;
|
||||||
|
|
||||||
|
async fn execute<A>(
|
||||||
|
&self,
|
||||||
|
client: &C,
|
||||||
|
request: ApiRequest<A>,
|
||||||
|
id: Option<i64>,
|
||||||
|
) -> Result<A, Self::Error>
|
||||||
|
where
|
||||||
|
A: ApiCategoryResponse,
|
||||||
|
{
|
||||||
|
loop {
|
||||||
|
let key = self
|
||||||
|
.storage
|
||||||
|
.acquire_key(self.domain)
|
||||||
|
.await
|
||||||
|
.map_err(|e| KeyPoolError::Storage(Arc::new(e)))?;
|
||||||
|
let url = request.url(key.value(), id);
|
||||||
|
let value = client.request(url).await?;
|
||||||
|
|
||||||
|
match ApiResponse::from_value(value) {
|
||||||
|
Err(ResponseError::Api { code, reason }) => {
|
||||||
|
if !self
|
||||||
|
.storage
|
||||||
|
.flag_key(key, code)
|
||||||
|
.await
|
||||||
|
.map_err(Arc::new)
|
||||||
|
.map_err(KeyPoolError::Storage)?
|
||||||
|
{
|
||||||
|
return Err(KeyPoolError::Response(ResponseError::Api { code, reason }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(parsing_error) => return Err(KeyPoolError::Response(parsing_error)),
|
||||||
|
Ok(res) => return Ok(A::from_response(res)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_many<A>(
|
||||||
|
&self,
|
||||||
|
client: &C,
|
||||||
|
request: ApiRequest<A>,
|
||||||
|
ids: Vec<i64>,
|
||||||
|
) -> HashMap<i64, Result<A, Self::Error>>
|
||||||
|
where
|
||||||
|
A: ApiCategoryResponse,
|
||||||
|
{
|
||||||
|
let keys = match self
|
||||||
|
.storage
|
||||||
|
.acquire_many_keys(self.domain, ids.len() as i64)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(keys) => keys,
|
||||||
|
Err(why) => {
|
||||||
|
let shared = Arc::new(why);
|
||||||
|
return ids
|
||||||
|
.into_iter()
|
||||||
|
.map(|i| (i, Err(Self::Error::Storage(shared.clone()))))
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let request_ref = &request;
|
||||||
|
|
||||||
|
futures::future::join_all(std::iter::zip(ids, keys).map(|(id, mut key)| async move {
|
||||||
|
loop {
|
||||||
|
let url = request_ref.url(key.value(), Some(id));
|
||||||
|
let value = match client.request(url).await {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(why) => return (id, Err(Self::Error::Client(why))),
|
||||||
|
};
|
||||||
|
|
||||||
|
match ApiResponse::from_value(value) {
|
||||||
|
Err(ResponseError::Api { code, reason }) => {
|
||||||
|
match self.storage.flag_key(key, code).await {
|
||||||
|
Ok(false) => {
|
||||||
|
return (
|
||||||
|
id,
|
||||||
|
Err(KeyPoolError::Response(ResponseError::Api {
|
||||||
|
code,
|
||||||
|
reason,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Ok(true) => (),
|
||||||
|
Err(why) => return (id, Err(KeyPoolError::Storage(Arc::new(why)))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(parsing_error) => return (id, Err(KeyPoolError::Response(parsing_error))),
|
||||||
|
Ok(res) => return (id, Ok(A::from_response(res))),
|
||||||
|
};
|
||||||
|
|
||||||
|
key = match self.storage.acquire_key(self.domain).await {
|
||||||
|
Ok(k) => k,
|
||||||
|
Err(why) => return (id, Err(Self::Error::Storage(Arc::new(why)))),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct KeyPool<C, S>
|
||||||
|
where
|
||||||
|
C: ApiClient,
|
||||||
|
S: KeyPoolStorage,
|
||||||
|
{
|
||||||
|
client: C,
|
||||||
|
storage: S,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C, S> KeyPool<C, S>
|
||||||
|
where
|
||||||
|
C: ApiClient,
|
||||||
|
S: KeyPoolStorage + 'static,
|
||||||
|
{
|
||||||
|
pub fn new(client: C, storage: S) -> Self {
|
||||||
|
Self { client, storage }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn torn_api(&self, domain: KeyDomain) -> ApiProvider<C, KeyPoolExecutor<C, S>> {
|
||||||
|
ApiProvider::new(&self.client, KeyPoolExecutor::new(&self.storage, domain))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait WithStorage {
|
||||||
|
fn with_storage<'a, S>(
|
||||||
|
&'a self,
|
||||||
|
storage: &'a S,
|
||||||
|
domain: KeyDomain,
|
||||||
|
) -> ApiProvider<Self, KeyPoolExecutor<Self, S>>
|
||||||
|
where
|
||||||
|
Self: ApiClient + Sized,
|
||||||
|
S: KeyPoolStorage + 'static,
|
||||||
|
{
|
||||||
|
ApiProvider::new(self, KeyPoolExecutor::new(storage, domain))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "awc")]
|
||||||
|
impl WithStorage for awc::Client {}
|
|
@ -4,7 +4,7 @@ use indoc::indoc;
|
||||||
use sqlx::{FromRow, PgPool};
|
use sqlx::{FromRow, PgPool};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::{ApiKey, KeyDomain, KeyPool, KeyPoolStorage};
|
use crate::{ApiKey, KeyDomain, KeyPoolStorage};
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum PgStorageError {
|
pub enum PgStorageError {
|
||||||
|
@ -102,18 +102,12 @@ impl KeyPoolStorage for PgKeyPoolStorage {
|
||||||
with key as (
|
with key as (
|
||||||
select
|
select
|
||||||
id,
|
id,
|
||||||
user_id,
|
|
||||||
faction_id,
|
|
||||||
key,
|
|
||||||
case
|
case
|
||||||
when extract(minute from last_used)=extract(minute from now()) then uses
|
when extract(minute from last_used)=extract(minute from now()) then uses
|
||||||
else 0::smallint
|
else 0::smallint
|
||||||
end as uses,
|
end as uses
|
||||||
user,
|
|
||||||
faction,
|
|
||||||
last_used
|
|
||||||
from api_keys {}
|
from api_keys {}
|
||||||
order by last_used asc limit 1 FOR UPDATE
|
order by last_used asc limit 1
|
||||||
)
|
)
|
||||||
update api_keys set
|
update api_keys set
|
||||||
uses = key.uses + 1,
|
uses = key.uses + 1,
|
||||||
|
@ -162,6 +156,70 @@ impl KeyPoolStorage for PgKeyPoolStorage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn acquire_many_keys(
|
||||||
|
&self,
|
||||||
|
domain: KeyDomain,
|
||||||
|
number: i64,
|
||||||
|
) -> Result<Vec<Self::Key>, Self::Error> {
|
||||||
|
let predicate = match domain {
|
||||||
|
KeyDomain::Public => "".to_owned(),
|
||||||
|
KeyDomain::User(id) => format!("where and user_id={} and user", id),
|
||||||
|
KeyDomain::Faction(id) => format!("where and faction_id={} and faction", id),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut tx = self.pool.begin().await?;
|
||||||
|
|
||||||
|
let mut keys: Vec<PgKey> = sqlx::query_as(&indoc::formatdoc!(
|
||||||
|
r#"
|
||||||
|
select
|
||||||
|
id,
|
||||||
|
user_id,
|
||||||
|
faction_id,
|
||||||
|
key,
|
||||||
|
case
|
||||||
|
when extract(minute from last_used)=extract(minute from now()) then uses
|
||||||
|
else 0::smallint
|
||||||
|
end as uses,
|
||||||
|
"user",
|
||||||
|
faction,
|
||||||
|
last_used
|
||||||
|
from api_keys {} order by last_used limit $1 for update
|
||||||
|
"#,
|
||||||
|
predicate
|
||||||
|
))
|
||||||
|
.bind(number)
|
||||||
|
.fetch_all(&mut tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut result = Vec::with_capacity(number as usize);
|
||||||
|
'outer: for _ in 0..(((number as usize) / keys.len()) + 1) {
|
||||||
|
for key in &mut keys {
|
||||||
|
if key.uses == self.limit || result.len() == (number as usize) {
|
||||||
|
break 'outer;
|
||||||
|
} else {
|
||||||
|
key.uses += 1;
|
||||||
|
result.push(key.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlx::query(indoc! {r#"
|
||||||
|
update api_keys set
|
||||||
|
uses = tmp.uses,
|
||||||
|
last_used = now()
|
||||||
|
from (select unnest($1::int4[]) as id, unnest($2::int2[]) as uses) as tmp
|
||||||
|
where api_keys.id = tmp.id
|
||||||
|
"#})
|
||||||
|
.bind(keys.iter().map(|k| k.id).collect::<Vec<_>>())
|
||||||
|
.bind(keys.iter().map(|k| k.uses).collect::<Vec<_>>())
|
||||||
|
.execute(&mut tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
async fn flag_key(&self, key: Self::Key, code: u8) -> Result<bool, Self::Error> {
|
async fn flag_key(&self, key: Self::Key, code: u8) -> Result<bool, Self::Error> {
|
||||||
// TODO: put keys in cooldown when appropriate
|
// TODO: put keys in cooldown when appropriate
|
||||||
match code {
|
match code {
|
||||||
|
@ -177,27 +235,6 @@ impl KeyPoolStorage for PgKeyPoolStorage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type PgKeyPool<A> = KeyPool<A, PgKeyPoolStorage>;
|
|
||||||
|
|
||||||
impl<A> PgKeyPool<A>
|
|
||||||
where
|
|
||||||
A: torn_api::ApiClient,
|
|
||||||
{
|
|
||||||
pub async fn connect(
|
|
||||||
client: A,
|
|
||||||
database_url: &str,
|
|
||||||
limit: i16,
|
|
||||||
) -> Result<Self, PgStorageError> {
|
|
||||||
let db_pool = PgPool::connect(database_url).await?;
|
|
||||||
let storage = PgKeyPoolStorage::new(db_pool, limit);
|
|
||||||
storage.initialise().await?;
|
|
||||||
|
|
||||||
let key_pool = Self::new(client, storage);
|
|
||||||
|
|
||||||
Ok(key_pool)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use std::sync::{Arc, Once};
|
use std::sync::{Arc, Once};
|
||||||
|
@ -253,13 +290,12 @@ mod test {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.get("uses");
|
.get("uses");
|
||||||
|
|
||||||
let futures = (0..30).into_iter().map(|_| {
|
let keys = storage
|
||||||
let storage = storage.clone();
|
.acquire_many_keys(KeyDomain::Public, 30)
|
||||||
async move {
|
.await
|
||||||
storage.acquire_key(KeyDomain::Public).await.unwrap();
|
.unwrap();
|
||||||
}
|
|
||||||
});
|
assert_eq!(keys.len(), 30);
|
||||||
futures::future::join_all(futures).await;
|
|
||||||
|
|
||||||
let after: i16 = sqlx::query("select uses from api_keys")
|
let after: i16 = sqlx::query("select uses from api_keys")
|
||||||
.fetch_one(&storage.pool)
|
.fetch_one(&storage.pool)
|
||||||
|
|
161
torn-key-pool/src/send.rs
Normal file
161
torn-key-pool/src/send.rs
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use torn_api::{
|
||||||
|
send::{ApiClient, ApiProvider, RequestExecutor},
|
||||||
|
ApiCategoryResponse, ApiRequest, ApiResponse, ResponseError,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{ApiKey, KeyDomain, KeyPoolError, KeyPoolExecutor, KeyPoolStorage};
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<'client, C, S> RequestExecutor<C> for KeyPoolExecutor<'client, C, S>
|
||||||
|
where
|
||||||
|
C: ApiClient,
|
||||||
|
S: KeyPoolStorage + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
type Error = KeyPoolError<S::Error, C::Error>;
|
||||||
|
|
||||||
|
async fn execute<A>(
|
||||||
|
&self,
|
||||||
|
client: &C,
|
||||||
|
request: ApiRequest<A>,
|
||||||
|
id: Option<i64>,
|
||||||
|
) -> Result<A, Self::Error>
|
||||||
|
where
|
||||||
|
A: ApiCategoryResponse,
|
||||||
|
{
|
||||||
|
loop {
|
||||||
|
let key = self
|
||||||
|
.storage
|
||||||
|
.acquire_key(self.domain)
|
||||||
|
.await
|
||||||
|
.map_err(|e| KeyPoolError::Storage(Arc::new(e)))?;
|
||||||
|
let url = request.url(key.value(), id);
|
||||||
|
let value = client.request(url).await?;
|
||||||
|
|
||||||
|
match ApiResponse::from_value(value) {
|
||||||
|
Err(ResponseError::Api { code, reason }) => {
|
||||||
|
if !self
|
||||||
|
.storage
|
||||||
|
.flag_key(key, code)
|
||||||
|
.await
|
||||||
|
.map_err(Arc::new)
|
||||||
|
.map_err(KeyPoolError::Storage)?
|
||||||
|
{
|
||||||
|
return Err(KeyPoolError::Response(ResponseError::Api { code, reason }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(parsing_error) => return Err(KeyPoolError::Response(parsing_error)),
|
||||||
|
Ok(res) => return Ok(A::from_response(res)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_many<A>(
|
||||||
|
&self,
|
||||||
|
client: &C,
|
||||||
|
request: ApiRequest<A>,
|
||||||
|
ids: Vec<i64>,
|
||||||
|
) -> HashMap<i64, Result<A, Self::Error>>
|
||||||
|
where
|
||||||
|
A: ApiCategoryResponse,
|
||||||
|
{
|
||||||
|
let keys = match self
|
||||||
|
.storage
|
||||||
|
.acquire_many_keys(self.domain, ids.len() as i64)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(keys) => keys,
|
||||||
|
Err(why) => {
|
||||||
|
let shared = Arc::new(why);
|
||||||
|
return ids
|
||||||
|
.into_iter()
|
||||||
|
.map(|i| (i, Err(Self::Error::Storage(shared.clone()))))
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let request_ref = &request;
|
||||||
|
|
||||||
|
futures::future::join_all(std::iter::zip(ids, keys).map(|(id, mut key)| async move {
|
||||||
|
loop {
|
||||||
|
let url = request_ref.url(key.value(), Some(id));
|
||||||
|
let value = match client.request(url).await {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(why) => return (id, Err(Self::Error::Client(why))),
|
||||||
|
};
|
||||||
|
|
||||||
|
match ApiResponse::from_value(value) {
|
||||||
|
Err(ResponseError::Api { code, reason }) => {
|
||||||
|
match self.storage.flag_key(key, code).await {
|
||||||
|
Ok(false) => {
|
||||||
|
return (
|
||||||
|
id,
|
||||||
|
Err(KeyPoolError::Response(ResponseError::Api {
|
||||||
|
code,
|
||||||
|
reason,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Ok(true) => (),
|
||||||
|
Err(why) => return (id, Err(KeyPoolError::Storage(Arc::new(why)))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(parsing_error) => return (id, Err(KeyPoolError::Response(parsing_error))),
|
||||||
|
Ok(res) => return (id, Ok(A::from_response(res))),
|
||||||
|
};
|
||||||
|
|
||||||
|
key = match self.storage.acquire_key(self.domain).await {
|
||||||
|
Ok(k) => k,
|
||||||
|
Err(why) => return (id, Err(Self::Error::Storage(Arc::new(why)))),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct KeyPool<C, S>
|
||||||
|
where
|
||||||
|
C: ApiClient,
|
||||||
|
S: KeyPoolStorage,
|
||||||
|
{
|
||||||
|
client: C,
|
||||||
|
storage: S,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C, S> KeyPool<C, S>
|
||||||
|
where
|
||||||
|
C: ApiClient,
|
||||||
|
S: KeyPoolStorage + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
pub fn new(client: C, storage: S) -> Self {
|
||||||
|
Self { client, storage }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn torn_api(&self, domain: KeyDomain) -> ApiProvider<C, KeyPoolExecutor<C, S>> {
|
||||||
|
ApiProvider::new(&self.client, KeyPoolExecutor::new(&self.storage, domain))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait WithStorage {
|
||||||
|
fn with_storage<'a, S>(
|
||||||
|
&'a self,
|
||||||
|
storage: &'a S,
|
||||||
|
domain: KeyDomain,
|
||||||
|
) -> ApiProvider<Self, KeyPoolExecutor<Self, S>>
|
||||||
|
where
|
||||||
|
Self: ApiClient + Sized,
|
||||||
|
S: KeyPoolStorage + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
ApiProvider::new(self, KeyPoolExecutor::new(storage, domain))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "reqwest")]
|
||||||
|
impl WithStorage for reqwest::Client {}
|
Loading…
Reference in a new issue