feat: implemented bulk requests

This commit is contained in:
TotallyNot 2025-04-29 18:26:00 +02:00
parent 4dd4fd37d4
commit c17f93f600
Signed by: pyrite
GPG key ID: 7F1BA9170CD35D15
10 changed files with 767 additions and 176 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "torn-api"
version = "1.0.3"
version = "1.1.0"
edition = "2021"
description = "Auto-generated bindings for the v2 torn api"
license-file = { workspace = true }
@ -27,12 +27,16 @@ reqwest = { version = "0.12", default-features = false, features = [
"brotli",
] }
thiserror = "2"
futures = { version = "0.3", default-features = false, features = [
"std",
"async-await",
] }
[dev-dependencies]
tokio = { version = "1", features = ["full"] }
[build-dependencies]
torn-api-codegen = { path = "../torn-api-codegen", version = "0.1.5" }
torn-api-codegen = { path = "../torn-api-codegen", version = "0.2" }
syn = { workspace = true, features = ["parsing"] }
proc-macro2 = { workspace = true }
prettyplease = "0.2"

View file

@ -1,23 +1,27 @@
use std::future::Future;
use futures::{Stream, StreamExt};
use http::{header::AUTHORIZATION, HeaderMap, HeaderValue};
use serde::Deserialize;
use crate::request::{ApiResponse, IntoRequest};
use crate::request::{ApiRequest, ApiResponse, IntoRequest};
#[cfg(feature = "scopes")]
use crate::scopes::{FactionScope, ForumScope, MarketScope, RacingScope, TornScope, UserScope};
use crate::scopes::{
BulkFactionScope, BulkForumScope, BulkMarketScope, BulkRacingScope, BulkTornScope,
BulkUserScope, FactionScope, ForumScope, MarketScope, RacingScope, TornScope, UserScope,
};
pub trait Executor {
pub trait Executor: Sized {
type Error: From<serde_json::Error> + From<crate::ApiError> + Send;
fn execute<R>(
&self,
self,
request: R,
) -> impl Future<Output = Result<ApiResponse<R::Discriminant>, Self::Error>> + Send
) -> impl Future<Output = (R::Discriminant, Result<ApiResponse, Self::Error>)> + Send
where
R: IntoRequest;
fn fetch<R>(&self, request: R) -> impl Future<Output = Result<R::Response, Self::Error>> + Send
fn fetch<R>(self, request: R) -> impl Future<Output = Result<R::Response, Self::Error>> + Send
where
R: IntoRequest,
{
@ -25,7 +29,7 @@ pub trait Executor {
// The future is `Send` but `&self` might not be.
let fut = self.execute(request);
async {
let resp = fut.await?;
let resp = fut.await.1?;
let bytes = resp.body.unwrap();
@ -52,6 +56,152 @@ pub trait Executor {
}
}
pub trait BulkExecutor<'e>: 'e + Sized {
type Error: From<serde_json::Error> + From<crate::ApiError> + Send;
fn execute<R>(
self,
requests: impl IntoIterator<Item = R>,
) -> impl Stream<Item = (R::Discriminant, Result<ApiResponse, Self::Error>)>
where
R: IntoRequest;
fn fetch_many<R>(
self,
requests: impl IntoIterator<Item = R>,
) -> impl Stream<Item = (R::Discriminant, Result<R::Response, Self::Error>)>
where
R: IntoRequest,
{
self.execute(requests).map(|(d, r)| {
let r = match r {
Ok(r) => r,
Err(why) => return (d, Err(why)),
};
let bytes = r.body.unwrap();
if bytes.starts_with(br#"{"error":{"#) {
#[derive(Deserialize)]
struct ErrorBody<'a> {
code: u16,
error: &'a str,
}
#[derive(Deserialize)]
struct ErrorContainer<'a> {
#[serde(borrow)]
error: ErrorBody<'a>,
}
let error: ErrorContainer = match serde_json::from_slice(&bytes) {
Ok(error) => error,
Err(why) => return (d, Err(why.into())),
};
return (
d,
Err(crate::ApiError::new(error.error.code, error.error.error).into()),
);
}
let resp = match serde_json::from_slice(&bytes) {
Ok(resp) => resp,
Err(why) => return (d, Err(why.into())),
};
(d, Ok(resp))
})
}
}
#[cfg(feature = "scopes")]
pub trait ExecutorExt: Executor + Sized {
fn user(self) -> UserScope<Self>;
fn faction(self) -> FactionScope<Self>;
fn torn(self) -> TornScope<Self>;
fn market(self) -> MarketScope<Self>;
fn racing(self) -> RacingScope<Self>;
fn forum(self) -> ForumScope<Self>;
}
#[cfg(feature = "scopes")]
impl<T> ExecutorExt for T
where
T: Executor + Sized,
{
fn user(self) -> UserScope<Self> {
UserScope::new(self)
}
fn faction(self) -> FactionScope<Self> {
FactionScope::new(self)
}
fn torn(self) -> TornScope<Self> {
TornScope::new(self)
}
fn market(self) -> MarketScope<Self> {
MarketScope::new(self)
}
fn racing(self) -> RacingScope<Self> {
RacingScope::new(self)
}
fn forum(self) -> ForumScope<Self> {
ForumScope::new(self)
}
}
#[cfg(feature = "scopes")]
pub trait BulkExecutorExt<'e>: BulkExecutor<'e> + Sized {
fn user_bulk(self) -> BulkUserScope<'e, Self>;
fn faction_bulk(self) -> BulkFactionScope<'e, Self>;
fn torn_bulk(self) -> BulkTornScope<'e, Self>;
fn market_bulk(self) -> BulkMarketScope<'e, Self>;
fn racing_bulk(self) -> BulkRacingScope<'e, Self>;
fn forum_bulk(self) -> BulkForumScope<'e, Self>;
}
#[cfg(feature = "scopes")]
impl<'e, T> BulkExecutorExt<'e> for T
where
T: BulkExecutor<'e> + Sized,
{
fn user_bulk(self) -> BulkUserScope<'e, Self> {
BulkUserScope::new(self)
}
fn faction_bulk(self) -> BulkFactionScope<'e, Self> {
BulkFactionScope::new(self)
}
fn torn_bulk(self) -> BulkTornScope<'e, Self> {
BulkTornScope::new(self)
}
fn market_bulk(self) -> BulkMarketScope<'e, Self> {
BulkMarketScope::new(self)
}
fn racing_bulk(self) -> BulkRacingScope<'e, Self> {
BulkRacingScope::new(self)
}
fn forum_bulk(self) -> BulkForumScope<'e, Self> {
BulkForumScope::new(self)
}
}
pub struct ReqwestClient(reqwest::Client);
impl ReqwestClient {
@ -72,70 +222,43 @@ impl ReqwestClient {
}
}
#[cfg(feature = "scopes")]
pub trait ExecutorExt: Executor + Sized {
fn user(&self) -> UserScope<'_, Self>;
fn faction(&self) -> FactionScope<'_, Self>;
fn torn(&self) -> TornScope<'_, Self>;
fn market(&self) -> MarketScope<'_, Self>;
fn racing(&self) -> RacingScope<'_, Self>;
fn forum(&self) -> ForumScope<'_, Self>;
}
#[cfg(feature = "scopes")]
impl<T> ExecutorExt for T
where
T: Executor + Sized,
{
fn user(&self) -> UserScope<'_, Self> {
UserScope::new(self)
}
fn faction(&self) -> FactionScope<'_, Self> {
FactionScope::new(self)
}
fn torn(&self) -> TornScope<'_, Self> {
TornScope::new(self)
}
fn market(&self) -> MarketScope<'_, Self> {
MarketScope::new(self)
}
fn racing(&self) -> RacingScope<'_, Self> {
RacingScope::new(self)
}
fn forum(&self) -> ForumScope<'_, Self> {
ForumScope::new(self)
}
}
impl Executor for ReqwestClient {
type Error = crate::Error;
async fn execute<R>(&self, request: R) -> Result<ApiResponse<R::Discriminant>, Self::Error>
where
R: IntoRequest,
{
let request = request.into_request();
impl ReqwestClient {
async fn execute_api_request(&self, request: ApiRequest) -> Result<ApiResponse, crate::Error> {
let url = request.url();
let response = self.0.get(url).send().await?;
let status = response.status();
let body = response.bytes().await.ok();
Ok(ApiResponse {
discriminant: request.disriminant,
status,
body,
})
Ok(ApiResponse { status, body })
}
}
impl Executor for &ReqwestClient {
type Error = crate::Error;
async fn execute<R>(self, request: R) -> (R::Discriminant, Result<ApiResponse, Self::Error>)
where
R: IntoRequest,
{
let (d, request) = request.into_request();
(d, self.execute_api_request(request).await)
}
}
impl<'e> BulkExecutor<'e> for &'e ReqwestClient {
type Error = crate::Error;
fn execute<R>(
self,
requests: impl IntoIterator<Item = R>,
) -> impl Stream<Item = (R::Discriminant, Result<ApiResponse, Self::Error>)>
where
R: IntoRequest,
{
futures::stream::iter(requests)
.map(move |r| <Self as Executor>::execute(self, r))
.buffer_unordered(25)
}
}
@ -157,4 +280,22 @@ mod test {
other => panic!("Expected incorrect id entity relation error, got {other:?}"),
}
}
#[cfg(feature = "scopes")]
#[tokio::test]
async fn bulk_request() {
let client = test_client().await;
let stream = client
.faction_bulk()
.basic_for_id(vec![19.into(), 89.into()], |b| b);
let mut responses: Vec<_> = stream.collect().await;
let (_id1, basic1) = responses.pop().unwrap();
basic1.unwrap();
let (_id2, basic2) = responses.pop().unwrap();
basic2.unwrap();
}
}

View file

@ -5,13 +5,12 @@ use http::StatusCode;
pub mod models;
#[derive(Default)]
pub struct ApiRequest<D = ()> {
pub disriminant: D,
pub struct ApiRequest {
pub path: String,
pub parameters: Vec<(&'static str, String)>,
}
impl<D> ApiRequest<D> {
impl ApiRequest {
pub fn url(&self) -> String {
let mut url = format!("https://api.torn.com/v2{}?", self.path);
@ -23,8 +22,7 @@ impl<D> ApiRequest<D> {
}
}
pub struct ApiResponse<D = ()> {
pub discriminant: D,
pub struct ApiResponse {
pub body: Option<Bytes>,
pub status: StatusCode,
}
@ -32,7 +30,26 @@ pub struct ApiResponse<D = ()> {
pub trait IntoRequest: Send {
type Discriminant: Send;
type Response: for<'de> serde::Deserialize<'de> + Send;
fn into_request(self) -> ApiRequest<Self::Discriminant>;
fn into_request(self) -> (Self::Discriminant, ApiRequest);
}
pub(crate) struct WrappedApiRequest<R>
where
R: IntoRequest,
{
discriminant: R::Discriminant,
request: ApiRequest,
}
impl<R> IntoRequest for WrappedApiRequest<R>
where
R: IntoRequest,
{
type Discriminant = R::Discriminant;
type Response = R::Response;
fn into_request(self) -> (Self::Discriminant, ApiRequest) {
(self.discriminant, self.request)
}
}
#[cfg(test)]