1
0
Fork 1
mirror of https://github.com/MingweiSamuel/Riven.git synced 2025-04-11 08:23:16 -07:00

feat: eserde feature for better maintenance ()

Using [`eserde`](https://github.com/mainmatter/eserde) at least for
testing makes it much, much easier to find all the schema changes Riot
makes every other Wednesday, and therefore makes this and
https://github.com/MingweiSamuel/riotapi-schema easier to maintain.

Example of error output:
```rust
Failed to get match JP1_498473890: Riot API request failed after 0 retries with status code Some(200).
Deserialization error: Something went wrong during deserialization:
- info.participants[0].PlayerScore2: invalid type: floating point `732.5479125976562`, expected i32 at line 1 column 1795
- info.participants[0].PlayerScore3: invalid type: floating point `0.3532763719558716`, expected i32 at line 1 column 1829
- info.participants[0].PlayerScore4: invalid type: floating point `-0.8999999761581421`, expected i32 at line 1 column 1864
- info.participants[0].PlayerScore5: invalid type: floating point `1428.712158203125`, expected i32 at line 1 column 1897
- info.participants[0].PlayerScore6: invalid type: floating point `3546.9150390625`, expected i32 at line 1 column 1928
- info.participants[0].PlayerScore7: invalid type: floating point `848.3316650390625`, expected i32 at line 1 column 1961
- info.participants[0].PlayerScore8: invalid type: floating point `23.0013484954834`, expected i32 at line 1 column 1993
- info.participants[0].missions.playerScore2: invalid type: floating point `732.5479125976562`, expected i32 at line 1 column 6427
- info.participants[0].missions.playerScore3: invalid type: floating point `0.3532763719558716`, expected i32 at line 1 column 6461
- info.participants[0].missions.playerScore4: invalid type: floating point `-0.8999999761581421`, expected i32 at line 1 column 6496
- info.participants[0].missions.playerScore5: invalid type: floating point `1428.712158203125`, expected i32 at line 1 column 6529
- info.participants[0].missions.playerScore6: invalid type: floating point `3546.9150390625`, expected i32 at line 1 column 6560
- info.participants[0].missions.playerScore7: invalid type: floating point `848.3316650390625`, expected i32 at line 1 column 6593
- info.participants[0].missions.playerScore8: invalid type: floating point `23.0013484954834`, expected i32 at line 1 column 6625
```
This commit is contained in:
Mingwei Samuel 2025-03-12 09:46:19 -07:00 committed by GitHub
parent a8c457fad7
commit ab80137e6f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 1150 additions and 955 deletions

View file

@ -74,12 +74,12 @@ jobs:
- uses: actions-rs/cargo@v1
with:
command: build
args: --all-targets --features nightly,deny-unknown,__proxy
args: --all-targets --features nightly,deny-unknown,eserde,__proxy
- uses: actions-rs/cargo@v1
with:
command: test
args: --no-fail-fast --features nightly,deny-unknown,__proxy
args: --no-fail-fast --features nightly,deny-unknown,eserde,__proxy
env:
RUST_BACKTRACE: 1
RUST_LOG: riven=debug
@ -89,7 +89,7 @@ jobs:
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- working-directory: riven
run: wasm-pack test --node -- --features nightly,deny-unknown
run: wasm-pack test --node -- --features nightly,deny-unknown,eserde
env:
RGAPI_KEY: ${{ secrets.RGAPI_KEY }}

View file

@ -11,6 +11,9 @@
"rust-analyzer.cargo.features": [
"nightly",
"tracing",
"deny-unknown"
]
}
"deny-unknown",
"eserde",
],
"evenBetterToml.formatter.compactArrays": false,
"evenBetterToml.formatter.columnWidth": 100
}

25
Cargo.lock generated
View file

@ -277,6 +277,30 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "eserde"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b00bbe58b41137cb1f1421385eae8ae360ce02722ab24dcc91a45be30daeeea"
dependencies = [
"eserde_derive",
"itoa",
"serde",
"serde_json",
]
[[package]]
name = "eserde_derive"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92f146f29032c31be76a0b0a0275a5d11b5835711db47689fe4edd3017993615"
dependencies = [
"indexmap",
"proc-macro2",
"quote",
"syn 2.0.96",
]
[[package]]
name = "fake_instant"
version = "0.5.0"
@ -1146,6 +1170,7 @@ dependencies = [
"console_error_panic_hook",
"console_log",
"env_logger",
"eserde",
"fake_instant",
"futures",
"gloo-timers",

View file

@ -1,78 +1,77 @@
[package]
name = "riven"
version = "2.64.0"
authors = ["Mingwei Samuel <mingwei.samuel@gmail.com>"]
authors = [ "Mingwei Samuel <mingwei.samuel@gmail.com>" ]
repository = "https://github.com/MingweiSamuel/Riven"
description = "Riot Games API Library"
readme = "../README.md"
license = "MIT"
edition = "2018"
rust-version = "1.71.1"
include = ["src/**", "../README.md", "/examples"]
keywords = ["riot-games", "riot", "league", "league-of-legends"]
categories = ["api-bindings", "web-programming::http-client", "wasm"]
include = [ "src/**", "../README.md", "/examples" ]
keywords = [ "riot-games", "riot", "league", "league-of-legends" ]
categories = [ "api-bindings", "web-programming::http-client", "wasm" ]
[lib]
crate-type = ["cdylib", "rlib"]
crate-type = [ "cdylib", "rlib" ]
[package.metadata.docs.rs]
features = ["nightly"]
features = [ "nightly" ]
[features]
default = ["default-tls"]
default = [ "default-tls" ]
nightly = ["parking_lot/nightly"]
nightly = [ "parking_lot/nightly" ]
default-tls = ["reqwest/default-tls"]
native-tls = ["reqwest/native-tls"]
rustls-tls = ["reqwest/rustls-tls"]
default-tls = [ "reqwest/default-tls" ]
native-tls = [ "reqwest/native-tls" ]
rustls-tls = [ "reqwest/rustls-tls" ]
metrics = ["dep:metrics"]
metrics = [ "dep:metrics" ]
eserde = [ "dep:eserde" ]
deny-unknown = ["deny-unknown-fields", "deny-unknown-enum-variants"]
deny-unknown = [ "deny-unknown-fields", "deny-unknown-enum-variants" ]
# If enabled, extra unknown fields encountered during deserialization will
# cause an error instead of being ignored.
deny-unknown-fields = []
deny-unknown-fields = [ ]
# If enabled, deserialization of unknown enum variants will cause an error
# instead of being deserialized to `UNKNOWN` or other integer variants.
deny-unknown-enum-variants = [
"deny-unknown-enum-variants-strings",
"deny-unknown-enum-variants-integers",
]
deny-unknown-enum-variants-strings = []
deny-unknown-enum-variants-integers = []
deny-unknown-enum-variants-strings = [ ]
deny-unknown-enum-variants-integers = [ ]
__proxy = []
__proxy = [ ]
[[example]]
name = "proxy"
required-features = ["__proxy"]
required-features = [ "__proxy" ]
[dependencies]
eserde = { optional = true, version = "0.1.6", features = [ "json" ] }
futures = "0.3.0"
log = "0.4.8"
memo-map = "0.3.0"
metrics = { version = "0.24.0", optional = true }
metrics = { optional = true, version = "0.24.0" }
num_enum = "0.5.0"
parking_lot = "0.12.0"
reqwest = { version = "0.11.2", default-features = false, features = [
"gzip",
"json",
] }
serde = { version = "1.0.85", features = ["derive"] }
reqwest = { version = "0.11.2", default-features = false, features = [ "gzip" ] }
serde = { version = "1.0.85", features = [ "derive" ] }
serde_derive = "1.0.85"
serde_json = "1.0.1"
serde_repr = "0.1.0"
slab = "0.4.4"
strum = "0.20.0"
strum_macros = "0.20.0"
tracing = { version = "0.1.22", optional = true }
tracing = { optional = true, version = "0.1.22" }
[target.'cfg(not(target_family = "wasm"))'.dependencies]
tokio = { version = "1.20.0", default-features = false, features = ["time"] }
tokio = { version = "1.20.0", default-features = false, features = [ "time" ] }
[target.'cfg(target_family = "wasm")'.dependencies]
gloo-timers = { version = "0.3", features = ["futures"] }
gloo-timers = { version = "0.3", features = [ "futures" ] }
web-time = "1.0.0"
[dev-dependencies]
@ -82,8 +81,8 @@ tracing = "0.1.22"
tracing-subscriber = "0.3.17"
[target.'cfg(not(target_family = "wasm"))'.dev-dependencies]
hyper = { version = "0.14.5", features = ["server"] }
tokio = { version = "1.20.0", features = ["macros", "rt-multi-thread"] }
hyper = { version = "0.14.5", features = [ "server" ] }
tokio = { version = "1.20.0", features = [ "macros", "rt-multi-thread" ] }
tokio-shared-rt = "0.1.0"
[target.'cfg(target_family = "wasm")'.dev-dependencies]

View file

@ -84,7 +84,7 @@ async fn handle_request(req: Request<Body>) -> Result<Response<Body>, Infallible
.await;
let resp_info = match resp_result {
Err(err) => {
log::info!("Riot API error: {:#?}", err.source_reqwest_error());
log::info!("Riot API error: {}", err);
return Ok(create_json_response(
r#"{"error":"Riot API request failed."}"#,
StatusCode::INTERNAL_SERVER_ERROR,

View file

@ -1,7 +1,8 @@
#![allow(clippy::upper_case_acronyms)]
use std::cmp::Ordering;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator;
use strum_macros::{AsRefStr, Display, EnumString, IntoStaticStr};
@ -25,8 +26,8 @@ use strum_macros::{AsRefStr, Display, EnumString, IntoStaticStr};
IntoStaticStr,
IntoPrimitive,
TryFromPrimitive,
Serialize,
Deserialize,
serde::Serialize,
crate::de::Deserialize,
)]
#[repr(u8)]
pub enum Division {

View file

@ -7,14 +7,13 @@
// //
///////////////////////////////////////////////
use serde::{ Serialize, Deserialize };
use strum_macros::{ EnumString, Display, AsRefStr, IntoStaticStr };
/// League of Legends game type: matched game, custom game, or tutorial game.
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq, Hash)]
#[derive(EnumString, Display, AsRefStr, IntoStaticStr)]
#[derive(Serialize, Deserialize)]
#[derive(serde::Serialize, crate::de::Deserialize)]
#[repr(u8)]
pub enum GameType {
/// Custom games

View file

@ -65,6 +65,7 @@ macro_rules! serde_strum_unknown {
}
}
}
impl_edeserialize!($name);
};
}
@ -172,6 +173,7 @@ macro_rules! newtype_enum {
}
}
}
impl_edeserialize!($name);
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@ -185,3 +187,21 @@ macro_rules! newtype_enum {
}
}
}
macro_rules! impl_edeserialize {
($($t:ty),* $(,)?) => {
$(
#[cfg(feature = "eserde")]
impl<'de> eserde::EDeserialize<'de> for $t {
fn deserialize_for_errors<D>(deserializer: D) -> Result<(), ()>
where
D: serde::Deserializer<'de>
{
<Self as serde::de::Deserialize<'de>>::deserialize(deserializer).map(|_| ()).map_err(|e| {
eserde::reporter::ErrorReporter::report(e);
})
}
}
)*
};
}

View file

@ -6,6 +6,7 @@
// Do not directly edit! //
// //
///////////////////////////////////////////////
#![allow(clippy::upper_case_acronyms)]
use num_enum::{ IntoPrimitive, TryFromPrimitive };
use strum_macros::{ EnumString, EnumIter, Display, IntoStaticStr };
@ -332,7 +333,7 @@ pub enum ValPlatformRoute {
#[derive(PartialEq, Eq, Hash, PartialOrd, Ord)]
#[derive(IntoPrimitive, TryFromPrimitive)]
#[derive(EnumString, EnumIter, Display, IntoStaticStr)]
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, crate::de::Deserialize)]
#[derive(Clone, Copy)]
#[repr(u8)]
#[non_exhaustive]

View file

@ -29,3 +29,5 @@ pub enum Team {
/// "killerTeamId" when Baron Nashor spawns and kills Rift Herald.
OTHER = 300,
}
impl_edeserialize!(Team);

View file

@ -1,5 +1,6 @@
#![allow(clippy::upper_case_acronyms)]
use num_enum::{IntoPrimitive, TryFromPrimitive};
use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator;
use strum_macros::{AsRefStr, Display, EnumString, IntoStaticStr};
@ -25,8 +26,8 @@ use strum_macros::{AsRefStr, Display, EnumString, IntoStaticStr};
Display,
AsRefStr,
IntoStaticStr,
Serialize,
Deserialize,
serde::Serialize,
crate::de::Deserialize,
)]
#[repr(u8)]
pub enum Tier {

View file

@ -8,7 +8,7 @@
///////////////////////////////////////////////
// http://www.mingweisamuel.com/riotapi-schema/tool/
// Version 1b37b11075bdad584e6a46a1bdeffa7ad35c4303
// Version 0ec1ee73a0d4f3138f9cbbe51b2787d41c3d8892
//! Automatically generated endpoint handles.
#![allow(clippy::let_and_return, clippy::too_many_arguments)]
@ -1183,7 +1183,9 @@ impl<'a> LorDeckV1<'a> {
let request = self.base.request(Method::POST, route_str, "/lor/deck/v1/decks/me");
let mut request = request.bearer_auth(access_token);
if let Some(clear) = self.base.get_rso_clear_header() { request = request.header(clear, "") }
let request = request.body(serde_json::ser::to_vec(body).unwrap());
let request = request
.body(serde_json::ser::to_vec(body).unwrap())
.header(reqwest::header::CONTENT_TYPE, "application/json");
let future = self.base.execute_val::<String>("lor-deck-v1.createDeck", route_str, request);
#[cfg(feature = "tracing")]
let future = future.instrument(tracing::info_span!("lor-deck-v1.createDeck", route = route_str));
@ -2053,7 +2055,9 @@ impl<'a> TournamentStubV5<'a> {
let request = self.base.request(Method::POST, route_str, "/lol/tournament-stub/v5/codes");
let request = request.query(&[ ("tournamentId", tournament_id) ]);
let request = if let Some(count) = count { request.query(&[ ("count", count) ]) } else { request };
let request = request.body(serde_json::ser::to_vec(body).unwrap());
let request = request
.body(serde_json::ser::to_vec(body).unwrap())
.header(reqwest::header::CONTENT_TYPE, "application/json");
let future = self.base.execute_val::<Vec<String>>("tournament-stub-v5.createTournamentCode", route_str, request);
#[cfg(feature = "tracing")]
let future = future.instrument(tracing::info_span!("tournament-stub-v5.createTournamentCode", route = route_str));
@ -2118,7 +2122,9 @@ impl<'a> TournamentStubV5<'a> {
{
let route_str = route.into();
let request = self.base.request(Method::POST, route_str, "/lol/tournament-stub/v5/providers");
let request = request.body(serde_json::ser::to_vec(body).unwrap());
let request = request
.body(serde_json::ser::to_vec(body).unwrap())
.header(reqwest::header::CONTENT_TYPE, "application/json");
let future = self.base.execute_val::<i32>("tournament-stub-v5.registerProviderData", route_str, request);
#[cfg(feature = "tracing")]
let future = future.instrument(tracing::info_span!("tournament-stub-v5.registerProviderData", route = route_str));
@ -2139,7 +2145,9 @@ impl<'a> TournamentStubV5<'a> {
{
let route_str = route.into();
let request = self.base.request(Method::POST, route_str, "/lol/tournament-stub/v5/tournaments");
let request = request.body(serde_json::ser::to_vec(body).unwrap());
let request = request
.body(serde_json::ser::to_vec(body).unwrap())
.header(reqwest::header::CONTENT_TYPE, "application/json");
let future = self.base.execute_val::<i32>("tournament-stub-v5.registerTournament", route_str, request);
#[cfg(feature = "tracing")]
let future = future.instrument(tracing::info_span!("tournament-stub-v5.registerTournament", route = route_str));
@ -2176,7 +2184,9 @@ impl<'a> TournamentV5<'a> {
let request = self.base.request(Method::POST, route_str, "/lol/tournament/v5/codes");
let request = request.query(&[ ("tournamentId", tournament_id) ]);
let request = if let Some(count) = count { request.query(&[ ("count", count) ]) } else { request };
let request = request.body(serde_json::ser::to_vec(body).unwrap());
let request = request
.body(serde_json::ser::to_vec(body).unwrap())
.header(reqwest::header::CONTENT_TYPE, "application/json");
let future = self.base.execute_val::<Vec<String>>("tournament-v5.createTournamentCode", route_str, request);
#[cfg(feature = "tracing")]
let future = future.instrument(tracing::info_span!("tournament-v5.createTournamentCode", route = route_str));
@ -2219,7 +2229,9 @@ impl<'a> TournamentV5<'a> {
{
let route_str = route.into();
let request = self.base.request(Method::PUT, route_str, &format!("/lol/tournament/v5/codes/{}", tournament_code));
let request = request.body(serde_json::ser::to_vec(body).unwrap());
let request = request
.body(serde_json::ser::to_vec(body).unwrap())
.header(reqwest::header::CONTENT_TYPE, "application/json");
let future = self.base.execute("tournament-v5.updateCode", route_str, request);
#[cfg(feature = "tracing")]
let future = future.instrument(tracing::info_span!("tournament-v5.updateCode", route = route_str));
@ -2290,7 +2302,9 @@ impl<'a> TournamentV5<'a> {
{
let route_str = route.into();
let request = self.base.request(Method::POST, route_str, "/lol/tournament/v5/providers");
let request = request.body(serde_json::ser::to_vec(body).unwrap());
let request = request
.body(serde_json::ser::to_vec(body).unwrap())
.header(reqwest::header::CONTENT_TYPE, "application/json");
let future = self.base.execute_val::<i32>("tournament-v5.registerProviderData", route_str, request);
#[cfg(feature = "tracing")]
let future = future.instrument(tracing::info_span!("tournament-v5.registerProviderData", route = route_str));
@ -2311,7 +2325,9 @@ impl<'a> TournamentV5<'a> {
{
let route_str = route.into();
let request = self.base.request(Method::POST, route_str, "/lol/tournament/v5/tournaments");
let request = request.body(serde_json::ser::to_vec(body).unwrap());
let request = request
.body(serde_json::ser::to_vec(body).unwrap())
.header(reqwest::header::CONTENT_TYPE, "application/json");
let future = self.base.execute_val::<i32>("tournament-v5.registerTournament", route_str, request);
#[cfg(feature = "tracing")]
let future = future.instrument(tracing::info_span!("tournament-v5.registerTournament", route = route_str));

View file

@ -1,55 +1,73 @@
use std::fmt;
use reqwest::{Error, Response, StatusCode};
use reqwest::{Response, StatusCode};
/// Result containing RiotApiError on failure.
pub type Result<T> = std::result::Result<T, RiotApiError>;
/// An error that occurred while processing a Riot API request.
///
/// Although Riven may make multiple requests due to retries, this will always
/// contain exactly one reqwest::Error for the final request which failed.
#[derive(Debug)]
pub struct RiotApiError {
reqwest_error: Error,
reqwest_errors: Vec<reqwest::Error>,
de_error: Option<crate::de::Error>,
retries: u8,
response: Option<Response>,
status_code: Option<StatusCode>,
}
impl RiotApiError {
pub(crate) fn new(
reqwest_error: Error,
reqwest_errors: Vec<reqwest::Error>,
serde_error: Option<crate::de::Error>,
retries: u8,
response: Option<Response>,
status_code: Option<StatusCode>,
) -> Self {
Self {
reqwest_error,
reqwest_errors,
de_error: serde_error,
retries,
response,
status_code,
}
}
/// The reqwest::Error for the final failed request.
pub fn source_reqwest_error(&self) -> &Error {
&self.reqwest_error
/// Returns the final `reqwest::Error`, for the final failed request, or panics if this was a deserialization error.
#[deprecated = "Use `reqwest_errors()` or `de_error()` instead."]
pub fn source_reqwest_error(&self) -> &reqwest::Error {
self.reqwest_errors.last().unwrap()
}
/// Returns all `reqwest::Error`s across all retries, in the chronological order they occurred.
///
/// May be empty if there was a deserialization error.
pub fn reqwest_errors(&self) -> &[reqwest::Error] {
&self.reqwest_errors
}
/// Returns the final deserialization error if any occured.
pub fn de_error(&self) -> Option<&crate::de::Error> {
self.de_error.as_ref()
}
/// The number of retires attempted. Zero means exactly one request, zero retries.
pub fn retries(&self) -> u8 {
self.retries
}
/// The failed response.
/// The failed, unparsed response.
/// `Some(&reqwest::Response)` if the request was sent and failed.
/// `None` if the request was not sent, OR if parsing the response JSON failed.
pub fn response(&self) -> Option<&Response> {
self.response.as_ref()
}
/// The failed response.
/// `Some(reqwest::Response)` if the request was sent and failed.
/// `None` if the request was not sent, OR if parsing the response JSON failed.
pub fn take_response(&mut self) -> Option<Response> {
self.response.take()
}
/// The failed response's HTTP status code.
/// `Some(reqwest::StatusCode)` if the request was sent and failed, OR if parsing the response JSON failed.
/// `None` if the request was not sent.
@ -59,11 +77,31 @@ impl RiotApiError {
}
impl fmt::Display for RiotApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:#?}", self)
writeln!(
f,
"Riot API request failed after {} retries with status code {:?}{}:",
self.retries(),
self.status_code(),
if self.response().is_some() {
" (response not parsed)"
} else {
""
},
)?;
for (i, reqwest_error) in self.reqwest_errors().iter().enumerate() {
writeln!(f, "- Reqwest error {}: {}", i + 1, reqwest_error)?;
}
if let Some(serde_error) = self.de_error() {
writeln!(f, "- Deserialization error: {}", serde_error)?;
}
Ok(())
}
}
impl std::error::Error for RiotApiError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.reqwest_error)
self.reqwest_errors()
.last()
.map(|e| e as _)
.or_else(|| self.de_error().map(|e| e as _))
}
}

View file

@ -190,8 +190,12 @@
//! To run the srcgen use `node riven/srcgen` from the repository root.
//!
// Re-exported reqwest types.
// Re-exported crates.
pub use reqwest;
pub use serde;
#[cfg(feature = "eserde")]
pub use eserde;
mod config;
pub use config::RiotApiConfig;
@ -236,3 +240,17 @@ pub mod time {
#[cfg(target_family = "wasm")]
pub use gloo_timers::future::sleep;
}
/// Deserialization utilities from either `serde_json` or `eserde`.
#[rustfmt::skip]
pub mod de {
#[cfg(not(feature = "eserde"))]
pub use serde::Deserialize;
#[cfg(not(feature = "eserde"))]
pub use serde_json::{Error, from_str, from_slice};
#[cfg(feature = "eserde")]
pub use eserde::{Deserialize, EDeserialize as Deserialize, DeserializationErrors as Error};
#[cfg(feature = "eserde")]
pub use eserde::json::{from_str, from_slice};
}

View file

@ -8,7 +8,7 @@
///////////////////////////////////////////////
// http://www.mingweisamuel.com/riotapi-schema/tool/
// Version 1b37b11075bdad584e6a46a1bdeffa7ad35c4303
// Version 0ec1ee73a0d4f3138f9cbbe51b2787d41c3d8892
//! Metadata about the Riot API and Riven.
//!

File diff suppressed because it is too large Load diff

View file

@ -35,6 +35,7 @@ impl RegionalRequester {
request: RequestBuilder,
) -> Result<ResponseInfo> {
let mut retries: u8 = 0;
let mut reqwest_errors = Vec::new();
loop {
let method_rate_limit = self
.method_rate_limits
@ -58,15 +59,19 @@ impl RegionalRequester {
Ok(response) => response,
// Check for lower level errors, like connection errors.
Err(e) => {
reqwest_errors.push(e);
if retries >= config.retries {
log::debug!(
"Request failed (retried {} times), failure, returning error.",
retries
);
break Err(RiotApiError::new(e, retries, None, None));
break Err(RiotApiError::new(reqwest_errors, None, retries, None, None));
}
let delay = Duration::from_secs(2_u64.pow(retries as u32));
log::debug!("Request failed with cause \"{}\", (retried {} times), using exponential backoff, retrying after {:?}.", e.to_string(), retries, delay);
log::debug!(
"Request failed with cause \"{}\", (retried {} times), using exponential backoff, retrying after {:?}.",
reqwest_errors.last().unwrap().to_string(), retries, delay,
);
let backoff = sleep(delay);
#[cfg(feature = "tracing")]
let backoff = backoff.instrument(tracing::info_span!("backoff"));
@ -95,14 +100,16 @@ impl RegionalRequester {
response,
retries,
status_none,
reqwest_errors,
});
}
let err = response.error_for_status_ref().err().unwrap_or_else(|| {
reqwest_errors.push(response.error_for_status_ref().err().unwrap_or_else(|| {
panic!(
"Unhandlable response status code, neither success nor failure: {}.",
status
)
});
}));
// Failure, may or may not be retryable.
// Not-retryable: no more retries or 4xx or ? (3xx, redirects exceeded).
// Retryable: retries remaining, and 429 or 5xx.
@ -115,7 +122,8 @@ impl RegionalRequester {
retries
);
break Err(RiotApiError::new(
err,
reqwest_errors,
None,
retries,
Some(response),
Some(status),

View file

@ -1,6 +1,9 @@
use reqwest::Response;
use crate::{Result, RiotApiError};
/// A "raw" unparsed successful response from the Riot API, for internal or advanced use cases.
#[non_exhaustive]
pub struct ResponseInfo {
/// The reqwest response.
pub response: Response,
@ -8,4 +11,45 @@ pub struct ResponseInfo {
pub retries: u8,
/// If the response has an HTTP status code indicating a `None` response (i.e. 204, 404).
pub status_none: bool,
/// Any reqwest errors that occurred. A response may still be successful even if this is non-empty due to retrying.
pub reqwest_errors: Vec<reqwest::Error>,
}
impl ResponseInfo {
/// Helper to deserialize the JSON-encoded value from a `ResponseInfo`, properly handling errors.
pub(crate) async fn json<T: for<'de> crate::de::Deserialize<'de>>(self) -> Result<T> {
let Self {
response,
retries,
status_none: _,
mut reqwest_errors,
} = self;
let status = response.status();
let bytes = match response.bytes().await {
Ok(bytes) => bytes,
Err(err) => {
reqwest_errors.push(err);
return Err(RiotApiError::new(
reqwest_errors,
None,
retries,
None, // `.bytes()` consumes the response.
Some(status),
));
}
};
let value = match crate::de::from_slice::<T>(&bytes) {
Ok(value) => value,
Err(serde_err) => {
return Err(RiotApiError::new(
reqwest_errors,
Some(serde_err),
retries,
None, // `.bytes()` consumes the response.
Some(status),
));
}
};
Ok(value)
}
}

View file

@ -90,7 +90,7 @@ impl RiotApi {
///
/// # Returns
/// A future resolving to a `Result` containg either a `T` (success) or a `RiotApiError` (failure).
pub async fn execute_val<'a, T: serde::de::DeserializeOwned + 'a>(
pub async fn execute_val<'a, T: for<'de> crate::de::Deserialize<'de> + 'a>(
&'a self,
method_id: &'static str,
region_platform: &'static str,
@ -99,10 +99,7 @@ impl RiotApi {
let rinfo = self
.execute_raw(method_id, region_platform, request)
.await?;
let retries = rinfo.retries;
let status = rinfo.response.status();
let value = rinfo.response.json::<T>().await;
value.map_err(|e| RiotApiError::new(e, retries, None, Some(status)))
rinfo.json::<T>().await
}
/// This method should generally not be used directly. Consider using endpoint wrappers instead.
@ -116,7 +113,7 @@ impl RiotApi {
///
/// # Returns
/// A future resolving to a `Result` containg either an `Option<T>` (success) or a `RiotApiError` (failure).
pub async fn execute_opt<'a, T: serde::de::DeserializeOwned + 'a>(
pub async fn execute_opt<'a, T: for<'de> crate::de::Deserialize<'de> + 'a>(
&'a self,
method_id: &'static str,
region_platform: &'static str,
@ -128,10 +125,7 @@ impl RiotApi {
if rinfo.status_none {
return Ok(None);
}
let retries = rinfo.retries;
let status = rinfo.response.status();
let value = rinfo.response.json::<Option<T>>().await;
value.map_err(|e| RiotApiError::new(e, retries, None, Some(status)))
rinfo.json::<Option<T>>().await
}
/// This method should generally not be used directly. Consider using endpoint wrappers instead.
@ -156,11 +150,17 @@ impl RiotApi {
.await?;
let retries = rinfo.retries;
let status = rinfo.response.status();
rinfo
.response
.error_for_status()
.map(|_| ())
.map_err(|e| RiotApiError::new(e, retries, None, Some(status)))
if status.is_client_error() || status.is_server_error() {
Err(RiotApiError::new(
rinfo.reqwest_errors,
None,
retries,
Some(rinfo.response),
Some(status),
))
} else {
Ok(())
}
}
/// This method should generally not be used directly. Consider using endpoint wrappers instead.

View file

@ -3,14 +3,13 @@
const gameTypes = require('./.gameTypes.json');
}}{{= dotUtils.preamble() }}
use serde::{ Serialize, Deserialize };
use strum_macros::{ EnumString, Display, AsRefStr, IntoStaticStr };
/// League of Legends game type: matched game, custom game, or tutorial game.
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq, Hash)]
#[derive(EnumString, Display, AsRefStr, IntoStaticStr)]
#[derive(Serialize, Deserialize)]
#[derive(serde::Serialize, crate::de::Deserialize)]
#[repr(u8)]
pub enum GameType {
{{

View file

@ -2,6 +2,7 @@
const dotUtils = require('./dotUtils.js');
const routesTable = require('./.routesTable.json');
}}{{= dotUtils.preamble() }}
#![allow(clippy::upper_case_acronyms)]
use num_enum::{ IntoPrimitive, TryFromPrimitive };
use strum_macros::{ EnumString, EnumIter, Display, IntoStaticStr };
@ -171,7 +172,7 @@ pub enum ValPlatformRoute {
#[derive(PartialEq, Eq, Hash, PartialOrd, Ord)]
#[derive(IntoPrimitive, TryFromPrimitive)]
#[derive(EnumString, EnumIter, Display, IntoStaticStr)]
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, crate::de::Deserialize)]
#[derive(Clone, Copy)]
#[repr(u8)]
#[non_exhaustive]

View file

@ -199,7 +199,9 @@ impl<'a> {{= endpoint }}<'a> {
{{= dotUtils.formatAddHeaderParam(headerParam) }}
{{~}}
{{? bodyType }}
let request = request.body(serde_json::ser::to_vec(body).unwrap());
let request = request
.body(serde_json::ser::to_vec(body).unwrap())
.header(reqwest::header::CONTENT_TYPE, "application/json");
{{?}}
let future = self.base.execute{{= hasReturn ? (returnOptional ? '_opt' : '_val') : '' }}{{= returnTypeTurbofish }}("{{= operationId }}", route_str, request);
#[cfg(feature = "tracing")]

View file

@ -10,8 +10,12 @@
//!{{= line ? (' ' + line) : '' }}
{{~}}
// Re-exported reqwest types.
// Re-exported crates.
pub use reqwest;
pub use serde;
#[cfg(feature = "eserde")]
pub use eserde;
mod config;
pub use config::RiotApiConfig;
@ -56,3 +60,17 @@ pub mod time {
#[cfg(target_family = "wasm")]
pub use gloo_timers::future::sleep;
}
/// Deserialization utilities from either `serde_json` or `eserde`.
#[rustfmt::skip]
pub mod de {
#[cfg(not(feature = "eserde"))]
pub use serde::Deserialize;
#[cfg(not(feature = "eserde"))]
pub use serde_json::{Error, from_str, from_slice};
#[cfg(feature = "eserde")]
pub use eserde::{Deserialize, EDeserialize as Deserialize, DeserializationErrors as Error};
#[cfg(feature = "eserde")]
pub use eserde::json::{from_str, from_slice};
}

View file

@ -25,7 +25,7 @@
const endpoint_pascal_case = dotUtils.changeCase.pascalCase(endpoint);
}}
/// Data structs used by [`{{= endpoint_pascal_case }}`](crate::endpoints::{{= endpoint_pascal_case }}).
///
///
/// Note: this module is automatically generated.
#[allow(dead_code)]
pub mod {{= dotUtils.changeCase.snakeCase(endpoint) }} {
@ -37,7 +37,7 @@ pub mod {{= dotUtils.changeCase.snakeCase(endpoint) }} {
const props = schema.properties;
const requiredSet = new Set(schema.required);
}}
/// {{= schemaName }} data object.
/// `{{= endpoint }}.{{= rawSchemaName }}` data object.
{{? schema.description }}
/// # Description
/// {{= schema.description.split('\n').map(x => x.trim()).join('<br>\r\n /// ') }}
@ -45,7 +45,7 @@ pub mod {{= dotUtils.changeCase.snakeCase(endpoint) }} {
/// Note: This struct is automatically generated
{{?}}
#[derive(Clone, Debug)]
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, crate::de::Deserialize)]
#[cfg_attr(feature = "deny-unknown-fields", serde(deny_unknown_fields))]
pub struct {{= schemaName }} {
{{
@ -65,7 +65,7 @@ pub mod {{= dotUtils.changeCase.snakeCase(endpoint) }} {
{{?}}
{{= dotUtils.formatJsonProperty(propKey, prop) }}
{{? optional }}
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default, skip_serializing_if = "Option::is_none")]
{{?}}
{{? 'championId' === propKey && (prop.description || '').includes('this field returned invalid championIds') }}
///

View file

@ -178,7 +178,7 @@ pub async fn tft_match_v1_get(
let p = riot_api().tft_match_v1().get_match(route, matche);
let m = p
.await
.map_err(|e| format!("Failed to get tft match {}: {:?}", matche, e))?
.map_err(|e| format!("Failed to get tft match {}: {}", matche, e))?
.ok_or(format!("Match {} not found.", matche))?;
if matche != &*m.metadata.match_id {
@ -219,7 +219,7 @@ pub async fn match_v5_get(
let p = riot_api().match_v5().get_match(route, matche);
let m = p
.await
.map_err(|e| format!("Failed to get match {}: {:?}", matche, e))?
.map_err(|e| format!("Failed to get match {}: {}", matche, e))?
.ok_or(format!("Match {} not found.", matche))?;
if matche != &*m.metadata.match_id {
@ -279,7 +279,7 @@ pub async fn match_v5_get_timeline(
let p = riot_api().match_v5().get_timeline(route, matche);
let m = p
.await
.map_err(|e| format!("Failed to get match timeline {}: {:?}", matche, e))?
.map_err(|e| format!("Failed to get match timeline {}: {}", matche, e))?
.ok_or(format!("Match {} not found.", matche))?;
if matche != &*m.metadata.match_id {
return Err(format!(
@ -474,7 +474,7 @@ pub async fn val_match_v1_get(
let p = riot_api().val_match_v1().get_match(route, matche);
let m = p
.await
.map_err(|e| format!("Failed to get val match {}: {:?}", matche, e))?
.map_err(|e| format!("Failed to get val match {}: {}", matche, e))?
.ok_or(format!("Match {} not found.", matche))?;
// TODO(mingwei): Check the match a bit.

View file

@ -7,7 +7,7 @@ cargo +stable check --all-targets --features metrics,tracing,__proxy
# Ensure nightly builds.
cargo check --all-targets --features nightly,metrics,tracing,__proxy
cargo build --all-targets --features nightly,deny-unknown,__proxy
cargo build --all-targets --features nightly,deny-unknown,eserde,__proxy
# Run nightly tests.
bash test.bash

View file

@ -9,4 +9,4 @@ cd riven
wasm-pack build -- --features nightly,tracing,metrics
# Run tests.
wasm-pack test --node -- --features nightly,deny-unknown
wasm-pack test --node -- --features nightly,deny-unknown,eserde

View file

@ -1,4 +1,4 @@
#!/bin/bash
set -euxo pipefail
RGAPI_KEY="$(cat apikey.txt)" RUST_BACKTRACE=full RUST_LOG=riven=debug cargo test --no-fail-fast --features nightly,tracing,deny-unknown -- --nocapture
RGAPI_KEY="$(cat apikey.txt)" RUST_BACKTRACE=full RUST_LOG=riven=debug cargo test --no-fail-fast --features nightly,tracing,eserde,deny-unknown,eserde -- --nocapture