Compare commits

...

4 commits

Author SHA1 Message Date
Mingwei Samuel
30401e6004 Release 2.18.0 2023-05-10 13:44:04 -07:00
Mingwei Samuel
ded6a5af07 cargo clippy fixes, enable lints in CI 2023-05-10 13:04:25 -07:00
Mingwei Samuel
83a4b456d0 cargo fmt 2023-05-10 13:04:25 -07:00
Mingwei Samuel
31c2794863 rustfmt ignore templated generated files 2023-05-10 13:04:14 -07:00
31 changed files with 571 additions and 355 deletions

View file

@ -90,31 +90,31 @@ jobs:
RUSTLOG: riven=trace RUSTLOG: riven=trace
RGAPI_KEY: ${{ secrets.RGAPI_KEY }} RGAPI_KEY: ${{ secrets.RGAPI_KEY }}
# lints: lints:
# name: Lints name: Lints
# needs: pre_job needs: pre_job
# if: ${{ needs.pre_job.outputs.should_skip != 'true' }} if: ${{ needs.pre_job.outputs.should_skip != 'true' }}
# runs-on: ubuntu-latest runs-on: ubuntu-latest
# steps: steps:
# - name: Checkout sources - name: Checkout sources
# uses: actions/checkout@v2 uses: actions/checkout@v2
# - name: Install nightly toolchain - name: Install nightly toolchain
# uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
# with: with:
# profile: minimal profile: minimal
# toolchain: nightly toolchain: nightly
# override: true override: true
# components: rustfmt, clippy components: rustfmt, clippy
# - name: Run cargo fmt - name: Run cargo fmt
# uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
# with: with:
# command: fmt command: fmt
# args: --all -- --check args: --all -- --check
# - name: Run cargo clippy - name: Run cargo clippy
# uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
# with: with:
# command: clippy command: clippy
# args: -- -D warnings args: -- -D warnings

View file

@ -2,16 +2,16 @@
use std::convert::Infallible; use std::convert::Infallible;
use hyper::http; use http::{Method, Request, Response, StatusCode};
use http::{ Method, Request, Response, StatusCode };
use hyper::{ Body, Server };
use hyper::service::{ make_service_fn, service_fn };
use hyper::header::HeaderValue; use hyper::header::HeaderValue;
use hyper::http;
use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Server};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use tracing as log; use tracing as log;
use riven::{ RiotApi, RiotApiConfig };
use riven::consts::Route; use riven::consts::Route;
use riven::{RiotApi, RiotApiConfig};
lazy_static! { lazy_static! {
/// Create lazy static RiotApi instance. /// Create lazy static RiotApi instance.
@ -31,47 +31,64 @@ lazy_static! {
fn create_json_response(body: &'static str, status: StatusCode) -> Response<Body> { fn create_json_response(body: &'static str, status: StatusCode) -> Response<Body> {
let mut resp = Response::new(Body::from(body)); let mut resp = Response::new(Body::from(body));
*resp.status_mut() = status; *resp.status_mut() = status;
resp.headers_mut().insert(hyper::header::CONTENT_TYPE, HeaderValue::from_static("application/json")); resp.headers_mut().insert(
hyper::header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
resp resp
} }
/// Main request handler service. /// Main request handler service.
async fn handle_request(req: Request<Body>) -> Result<Response<Body>, Infallible> { async fn handle_request(req: Request<Body>) -> Result<Response<Body>, Infallible> {
let (parts, body) = req.into_parts(); let (parts, body) = req.into_parts();
let http::request::Parts { method, uri, .. } = parts; let http::request::Parts { method, uri, .. } = parts;
// Handle path. // Handle path.
let path_data_opt = parse_path(&method, uri.path()); let path_data_opt = parse_path(&method, uri.path());
let ( route, method_id, req_path ) = match path_data_opt { let (route, method_id, req_path) = match path_data_opt {
None => return Ok(create_json_response( None => {
r#"{"error":"Riot API endpoint method not found."}"#, StatusCode::NOT_FOUND)), return Ok(create_json_response(
r#"{"error":"Riot API endpoint method not found."}"#,
StatusCode::NOT_FOUND,
))
}
Some(path_data) => path_data, Some(path_data) => path_data,
}; };
log::debug!("Request to route {:?}, method ID {:?}: {} {:?}.", route, method_id, method, req_path); log::debug!(
"Request to route {:?}, method ID {:?}: {} {:?}.",
route,
method_id,
method,
req_path
);
// Convert http:request::Parts from hyper to reqwest's RequestBuilder. // Convert http:request::Parts from hyper to reqwest's RequestBuilder.
let body = match hyper::body::to_bytes(body).await { let body = match hyper::body::to_bytes(body).await {
Err(err) => { Err(err) => {
log::info!("Error handling request body: {:#?}", err); log::info!("Error handling request body: {:#?}", err);
return Ok(create_json_response( return Ok(create_json_response(
r#"{"error":"Failed to handle request body."}"#, StatusCode::BAD_REQUEST)); r#"{"error":"Failed to handle request body."}"#,
}, StatusCode::BAD_REQUEST,
));
}
Ok(bytes) => bytes, Ok(bytes) => bytes,
}; };
let req_builder = RIOT_API.request(method, route.into(), req_path) let req_builder = RIOT_API.request(method, route.into(), req_path).body(body);
.body(body);
// Send request to Riot API. // Send request to Riot API.
let resp_result = RIOT_API.execute_raw(method_id, route.into(), req_builder).await; let resp_result = RIOT_API
.execute_raw(method_id, route.into(), req_builder)
.await;
let resp_info = match resp_result { let resp_info = match resp_result {
Err(err) => { Err(err) => {
log::info!("Riot API error: {:#?}", err.source_reqwest_error()); log::info!("Riot API error: {:#?}", err.source_reqwest_error());
return Ok(create_json_response( return Ok(create_json_response(
r#"{"error":"Riot API request failed."}"#, StatusCode::INTERNAL_SERVER_ERROR)); r#"{"error":"Riot API request failed."}"#,
}, StatusCode::INTERNAL_SERVER_ERROR,
));
}
Ok(resp_info) => resp_info, Ok(resp_info) => resp_info,
}; };
@ -86,37 +103,43 @@ async fn handle_request(req: Request<Body>) -> Result<Response<Body>, Infallible
} }
// Otherwise copy body. // Otherwise copy body.
else { else {
*out_response.status_mut() = api_response.status(); *out_response.status_mut() = api_response.status();
// Using streams would be faster. // Using streams would be faster.
let bytes_result = api_response.bytes().await; let bytes_result = api_response.bytes().await;
let bytes = match bytes_result { let bytes = match bytes_result {
Err(_err) => return Ok(create_json_response( Err(_err) => {
r#"{"error":"Failed to get body from Riot API response."}"#, StatusCode::INTERNAL_SERVER_ERROR)), return Ok(create_json_response(
r#"{"error":"Failed to get body from Riot API response."}"#,
StatusCode::INTERNAL_SERVER_ERROR,
))
}
Ok(bytes) => bytes, Ok(bytes) => bytes,
}; };
*out_response.body_mut() = Body::from((&bytes[..]).to_vec()); *out_response.body_mut() = Body::from(bytes);
} }
Ok(out_response) Ok(out_response)
} }
/// Gets the region, method_id, and Riot API path based on the given http method and path. /// Gets the region, method_id, and Riot API path based on the given http method and path.
fn parse_path<'a>(http_method: &Method, req_path: &'a str) -> Option<( Route, &'static str, &'a str )> { fn parse_path<'a>(
http_method: &Method,
req_path: &'a str,
) -> Option<(Route, &'static str, &'a str)> {
// Split URI into region and rest of path. // Split URI into region and rest of path.
let req_path = req_path.trim_start_matches('/'); let req_path = req_path.trim_start_matches('/');
let ( route, req_path ) = req_path.split_at(req_path.find('/')?); let (route, req_path) = req_path.split_at(req_path.find('/')?);
let route: Route = route.to_uppercase().parse().ok()?; let route: Route = route.to_uppercase().parse().ok()?;
// Find method_id for given path. // Find method_id for given path.
let method_id = find_matching_method_id(http_method, req_path)?; let method_id = find_matching_method_id(http_method, req_path)?;
Some(( route, method_id, req_path )) Some((route, method_id, req_path))
} }
/// Finds the method_id given the request path. /// Finds the method_id given the request path.
fn find_matching_method_id(http_method: &Method, req_path: &str) -> Option<&'static str> { fn find_matching_method_id(http_method: &Method, req_path: &str) -> Option<&'static str> {
for ( endpoint_http_method, ref_path, method_id ) in &riven::meta::ALL_ENDPOINTS { for (endpoint_http_method, ref_path, method_id) in &riven::meta::ALL_ENDPOINTS {
if http_method == endpoint_http_method && paths_match(ref_path, req_path) { if http_method == endpoint_http_method && paths_match(ref_path, req_path) {
return Some(method_id); return Some(method_id);
} }
@ -160,7 +183,7 @@ pub async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
async { Ok::<_, Infallible>(service_fn(handle_request)) } async { Ok::<_, Infallible>(service_fn(handle_request)) }
}); });
let addr = ([ 127, 0, 0, 1 ], 3000).into(); let addr = ([127, 0, 0, 1], 3000).into();
let server = Server::bind(&addr).serve(make_svc); let server = Server::bind(&addr).serve(make_svc);

View file

@ -1,6 +1,6 @@
[package] [package]
name = "riven" name = "riven"
version = "2.17.0" version = "2.18.0"
authors = ["Mingwei Samuel <mingwei.samuel@gmail.com>"] authors = ["Mingwei Samuel <mingwei.samuel@gmail.com>"]
repository = "https://github.com/MingweiSamuel/Riven" repository = "https://github.com/MingweiSamuel/Riven"
description = "Riot Games API Library" description = "Riot Games API Library"

View file

@ -1,8 +1,8 @@
//! Configuration of RiotApi. //! Configuration of RiotApi.
use std::time::Duration; use std::time::Duration;
use reqwest::header::{HeaderMap, HeaderValue};
use reqwest::ClientBuilder; use reqwest::ClientBuilder;
use reqwest::header::{ HeaderMap, HeaderValue };
/// Configuration for instantiating RiotApi. /// Configuration for instantiating RiotApi.
/// ///
@ -72,7 +72,7 @@ impl RiotApiConfig {
let mut default_headers = HeaderMap::new(); let mut default_headers = HeaderMap::new();
default_headers.insert( default_headers.insert(
Self::RIOT_KEY_HEADER, Self::RIOT_KEY_HEADER,
HeaderValue::from_bytes(api_key.as_ref()).unwrap() HeaderValue::from_bytes(api_key.as_ref()).unwrap(),
); );
Self { Self {
@ -82,10 +82,7 @@ impl RiotApiConfig {
method_rate_usage_factor: Self::DEFAULT_RATE_USAGE_FACTOR, method_rate_usage_factor: Self::DEFAULT_RATE_USAGE_FACTOR,
burst_factor: Self::PRECONFIG_BURST_BURST_FACTOR, burst_factor: Self::PRECONFIG_BURST_BURST_FACTOR,
duration_overhead: Self::PRECONFIG_BURST_DURATION_OVERHEAD, duration_overhead: Self::PRECONFIG_BURST_DURATION_OVERHEAD,
client_builder: Some( client_builder: Some(ClientBuilder::new().default_headers(default_headers)),
ClientBuilder::new()
.default_headers(default_headers)
),
} }
} }
@ -191,7 +188,10 @@ impl RiotApiConfig {
self.method_rate_usage_factor = rate_usage_factor; self.method_rate_usage_factor = rate_usage_factor;
return self; return self;
} }
panic!("rate_usage_factor \"{}\" not in range (0, 1].", rate_usage_factor); panic!(
"rate_usage_factor \"{}\" not in range (0, 1].",
rate_usage_factor
);
} }
/// See [Self::set_rate_usage_factor]. Setting this is useful if you have multiple /// See [Self::set_rate_usage_factor]. Setting this is useful if you have multiple
@ -209,7 +209,10 @@ impl RiotApiConfig {
self.app_rate_usage_factor = app_rate_usage_factor; self.app_rate_usage_factor = app_rate_usage_factor;
return self; return self;
} }
panic!("app_rate_usage_factor \"{}\" not in range (0, 1].", app_rate_usage_factor); panic!(
"app_rate_usage_factor \"{}\" not in range (0, 1].",
app_rate_usage_factor
);
} }
/// See [Self::set_rate_usage_factor] and [Self::set_app_rate_usage_factor]. /// See [Self::set_rate_usage_factor] and [Self::set_app_rate_usage_factor].
@ -227,7 +230,10 @@ impl RiotApiConfig {
self.method_rate_usage_factor = method_rate_usage_factor; self.method_rate_usage_factor = method_rate_usage_factor;
return self; return self;
} }
panic!("method_rate_usage_factor \"{}\" not in range (0, 1].", method_rate_usage_factor); panic!(
"method_rate_usage_factor \"{}\" not in range (0, 1].",
method_rate_usage_factor
);
} }
/// Burst percentage controls how many burst requests are allowed and /// Burst percentage controls how many burst requests are allowed and

View file

@ -1,9 +1,9 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use num_enum::{ IntoPrimitive, TryFromPrimitive }; use num_enum::{IntoPrimitive, TryFromPrimitive};
use serde::{ Serialize, Deserialize }; use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use strum_macros::{ EnumString, Display, AsRefStr, IntoStaticStr }; use strum_macros::{AsRefStr, Display, EnumString, IntoStaticStr};
/// LoL and TFT rank divisions, I, II, III, IV, and (deprecated) V. /// LoL and TFT rank divisions, I, II, III, IV, and (deprecated) V.
/// ///
@ -12,25 +12,36 @@ use strum_macros::{ EnumString, Display, AsRefStr, IntoStaticStr };
/// Repr'd as equivalent numeric values, (1, 2, 3, 4, 5). /// Repr'd as equivalent numeric values, (1, 2, 3, 4, 5).
/// ///
/// Implements [IntoEnumIterator](super::IntoEnumIterator). Iterator excludes deprecated `Division::V`. /// Implements [IntoEnumIterator](super::IntoEnumIterator). Iterator excludes deprecated `Division::V`.
#[derive(Debug, Copy, Clone)] #[derive(
#[derive(Eq, PartialEq, Hash)] Debug,
#[derive(EnumString, Display, AsRefStr, IntoStaticStr)] Copy,
#[derive(IntoPrimitive, TryFromPrimitive)] Clone,
#[derive(Serialize, Deserialize)] Eq,
PartialEq,
Hash,
EnumString,
Display,
AsRefStr,
IntoStaticStr,
IntoPrimitive,
TryFromPrimitive,
Serialize,
Deserialize,
)]
#[repr(u8)] #[repr(u8)]
pub enum Division { pub enum Division {
/// Division 1, the best/highest division in a [`Tier`](crate::consts::Tier), or the only division in /// Division 1, the best/highest division in a [`Tier`](crate::consts::Tier), or the only division in
/// [apex tiers](crate::consts::Tier::is_apex). /// [apex tiers](crate::consts::Tier::is_apex).
I = 1, I = 1,
/// Division 2, the second highest division. /// Division 2, the second highest division.
II = 2, II = 2,
/// Division 3, the third highest division. /// Division 3, the third highest division.
III = 3, III = 3,
/// Division 4, the fourth and lowest division since 2019. /// Division 4, the fourth and lowest division since 2019.
IV = 4, IV = 4,
/// Division 5, the lowest division, only used before 2019. /// Division 5, the lowest division, only used before 2019.
#[deprecated(note="Removed for 2019.")] #[deprecated(note = "Removed for 2019.")]
V = 5, V = 5,
} }
/// Returns a DoubleEndedIterator of I, II, III, IV. /// Returns a DoubleEndedIterator of I, II, III, IV.
@ -39,7 +50,7 @@ pub enum Division {
impl IntoEnumIterator for Division { impl IntoEnumIterator for Division {
type Iterator = std::iter::Copied<std::slice::Iter<'static, Self>>; type Iterator = std::iter::Copied<std::slice::Iter<'static, Self>>;
fn iter() -> Self::Iterator { fn iter() -> Self::Iterator {
[ Self::I, Self::II, Self::III, Self::IV ].iter().copied() [Self::I, Self::II, Self::III, Self::IV].iter().copied()
} }
} }
@ -63,4 +74,4 @@ mod tests {
fn sort() { fn sort() {
assert!(Division::IV < Division::I); assert!(Division::IV < Division::I);
} }
} }

View file

@ -38,7 +38,7 @@ macro_rules! serde_strum_unknown {
impl<'de> serde::de::Deserialize<'de> for $name { impl<'de> serde::de::Deserialize<'de> for $name {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where where
D: serde::de::Deserializer<'de> D: serde::de::Deserializer<'de>,
{ {
#[cfg(not(feature = "deny-unknown-enum-variants-strings"))] #[cfg(not(feature = "deny-unknown-enum-variants-strings"))]
{ {
@ -46,20 +46,19 @@ macro_rules! serde_strum_unknown {
} }
#[cfg(feature = "deny-unknown-enum-variants-strings")] #[cfg(feature = "deny-unknown-enum-variants-strings")]
{ {
<&str>::deserialize(deserializer).map(Into::into) <&str>::deserialize(deserializer)
.and_then(|item| { .map(Into::into)
match item { .and_then(|item| match item {
Self::UNKNOWN(unknown) => Err(serde::de::Error::unknown_variant( Self::UNKNOWN(unknown) => Err(serde::de::Error::unknown_variant(
&*unknown, &*unknown,
<Self as strum::VariantNames>::VARIANTS, <Self as strum::VariantNames>::VARIANTS,
)), )),
other => Ok(other), other => Ok(other),
}
}) })
} }
} }
} }
} };
} }
macro_rules! arr { macro_rules! arr {

View file

@ -8,35 +8,43 @@
mod macros; mod macros;
#[rustfmt::skip]
mod champion; mod champion;
pub use champion::*; pub use champion::*;
mod division; mod division;
pub use division::*; pub use division::*;
#[rustfmt::skip]
mod game_mode; mod game_mode;
pub use game_mode::*; pub use game_mode::*;
#[rustfmt::skip]
mod game_type; mod game_type;
pub use game_type::*; pub use game_type::*;
#[rustfmt::skip]
mod map; mod map;
pub use map::*; pub use map::*;
#[rustfmt::skip]
mod queue_type; mod queue_type;
pub use queue_type::*; pub use queue_type::*;
#[rustfmt::skip]
mod queue; mod queue;
pub use queue::*; pub use queue::*;
pub mod ranks; pub mod ranks;
#[rustfmt::skip]
mod route; mod route;
pub use route::*; pub use route::*;
mod route_ext; mod route_ext;
pub use route_ext::*; pub use route_ext::*;
#[rustfmt::skip]
mod season; mod season;
pub use season::*; pub use season::*;

View file

@ -4,10 +4,10 @@ use std::iter::Peekable;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use super::{ Tier, Division }; use super::{Division, Tier};
/// (Tier, Division) tuple representing a rank. /// (Tier, Division) tuple representing a rank.
pub type Rank = ( Tier, Division ); pub type Rank = (Tier, Division);
/// Iterator for iterating `(Tier, Division)` rank tuples. /// Iterator for iterating `(Tier, Division)` rank tuples.
pub struct Iter { pub struct Iter {
@ -20,13 +20,12 @@ impl Iterator for Iter {
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
// First find the tier (innermost loop). // First find the tier (innermost loop).
// If none found, we go to next tier (in unwrap_or_else case). // If none found, we go to next tier (in unwrap_or_else case).
let div = self.div_iter.next() let div = self.div_iter.next().unwrap_or_else(|| {
.unwrap_or_else(|| { // If no divisions available, go to next tier, reset the divisions, and return I.
// If no divisions available, go to next tier, reset the divisions, and return I. self.tier_iter.next();
self.tier_iter.next(); self.div_iter = Division::iter();
self.div_iter = Division::iter(); self.div_iter.next().unwrap()
self.div_iter.next().unwrap() });
});
// Then find the tier. // Then find the tier.
let tier = *self.tier_iter.peek()?; let tier = *self.tier_iter.peek()?;
@ -36,7 +35,7 @@ impl Iterator for Iter {
self.div_iter = Division::iter(); self.div_iter = Division::iter();
} }
Some(( tier, div )) Some((tier, div))
} }
} }
@ -66,33 +65,16 @@ pub fn non_apex_iter() -> Iter {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ Tier, Division }; use super::{Division, Tier};
#[test] #[test]
fn iter() { fn iter() {
let mut it = super::iter(); let mut it = super::iter();
assert_eq!(Some(( Tier::CHALLENGER, Division::I )), it.next()); assert_eq!(Some((Tier::CHALLENGER, Division::I)), it.next());
assert_eq!(Some(( Tier::GRANDMASTER, Division::I )), it.next()); assert_eq!(Some((Tier::GRANDMASTER, Division::I)), it.next());
assert_eq!(Some(( Tier::MASTER, Division::I )), it.next()); assert_eq!(Some((Tier::MASTER, Division::I)), it.next());
assert_eq!(Some(( Tier::DIAMOND, Division::I )), it.next()); assert_eq!(Some((Tier::DIAMOND, Division::I)), it.next());
assert_eq!(Some(( Tier::DIAMOND, Division::II )), it.next()); assert_eq!(Some((Tier::DIAMOND, Division::II)), it.next());
let mut last = None;
for next in &mut it {
last = Some(next);
}
assert_eq!(Some(( Tier::IRON, Division::IV )), last);
assert_eq!(None, it.next());
}
#[test]
fn non_apex_iter() {
let mut it = super::non_apex_iter();
assert_eq!(Some((Tier::DIAMOND, Division::I)), it.next());
assert_eq!(Some((Tier::DIAMOND, Division::II)), it.next());
assert_eq!(Some((Tier::DIAMOND, Division::III)), it.next());
assert_eq!(Some((Tier::DIAMOND, Division::IV)), it.next());
assert_eq!(Some((Tier::PLATINUM, Division::I)), it.next());
let mut last = None; let mut last = None;
for next in &mut it { for next in &mut it {
last = Some(next); last = Some(next);
@ -100,4 +82,20 @@ mod tests {
assert_eq!(Some((Tier::IRON, Division::IV)), last); assert_eq!(Some((Tier::IRON, Division::IV)), last);
assert_eq!(None, it.next()); assert_eq!(None, it.next());
} }
}
#[test]
fn non_apex_iter() {
let mut it = super::non_apex_iter();
assert_eq!(Some((Tier::DIAMOND, Division::I)), it.next());
assert_eq!(Some((Tier::DIAMOND, Division::II)), it.next());
assert_eq!(Some((Tier::DIAMOND, Division::III)), it.next());
assert_eq!(Some((Tier::DIAMOND, Division::IV)), it.next());
assert_eq!(Some((Tier::PLATINUM, Division::I)), it.next());
let mut last = None;
for next in &mut it {
last = Some(next);
}
assert_eq!(Some((Tier::IRON, Division::IV)), last);
assert_eq!(None, it.next());
}
}

View file

@ -1,9 +1,7 @@
use super::{ RegionalRoute, PlatformRoute, ValPlatformRoute }; use super::{PlatformRoute, RegionalRoute, ValPlatformRoute};
/// Utility enum containing all routing variants. /// Utility enum containing all routing variants.
#[derive(Debug)] #[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Clone, Copy)]
#[derive(PartialEq, Eq, Hash, PartialOrd, Ord)]
#[derive(Clone, Copy)]
#[repr(u8)] #[repr(u8)]
#[non_exhaustive] #[non_exhaustive]
pub enum Route { pub enum Route {
@ -40,13 +38,13 @@ impl num_enum::TryFromPrimitive for Route {
const NAME: &'static str = stringify!(Route); const NAME: &'static str = stringify!(Route);
fn try_from_primitive(number: Self::Primitive) -> Result<Self, num_enum::TryFromPrimitiveError<Self>> { fn try_from_primitive(
number: Self::Primitive,
) -> Result<Self, num_enum::TryFromPrimitiveError<Self>> {
RegionalRoute::try_from_primitive(number) RegionalRoute::try_from_primitive(number)
.map(Route::Regional) .map(Route::Regional)
.or_else(|_| PlatformRoute::try_from_primitive(number) .or_else(|_| PlatformRoute::try_from_primitive(number).map(Route::Platform))
.map(Route::Platform)) .or_else(|_| ValPlatformRoute::try_from_primitive(number).map(Route::ValPlatform))
.or_else(|_| ValPlatformRoute::try_from_primitive(number)
.map(Route::ValPlatform))
.map_err(|_| num_enum::TryFromPrimitiveError { number }) .map_err(|_| num_enum::TryFromPrimitiveError { number })
} }
} }
@ -73,10 +71,8 @@ impl std::str::FromStr for Route {
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
RegionalRoute::from_str(s) RegionalRoute::from_str(s)
.map(Self::Regional) .map(Self::Regional)
.or_else(|_| PlatformRoute::from_str(s) .or_else(|_| PlatformRoute::from_str(s).map(Self::Platform))
.map(Self::Platform)) .or_else(|_| ValPlatformRoute::from_str(s).map(Self::ValPlatform))
.or_else(|_| ValPlatformRoute::from_str(s)
.map(Self::ValPlatform))
.map_err(|_| strum::ParseError::VariantNotFound) .map_err(|_| strum::ParseError::VariantNotFound)
} }
} }
@ -87,16 +83,11 @@ impl Route {
pub fn iter() -> impl Iterator<Item = Self> { pub fn iter() -> impl Iterator<Item = Self> {
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
let regional = RegionalRoute::iter() let regional = RegionalRoute::iter().map(Self::Regional);
.map(Self::Regional); let platform = PlatformRoute::iter().map(Self::Platform);
let platform = PlatformRoute::iter() let val_platform = ValPlatformRoute::iter().map(Self::ValPlatform);
.map(Self::Platform);
let val_platform = ValPlatformRoute::iter()
.map(Self::ValPlatform);
regional regional.chain(platform).chain(val_platform)
.chain(platform)
.chain(val_platform)
} }
} }
@ -106,9 +97,18 @@ mod tests {
#[test] #[test]
fn test_route_tostring() { fn test_route_tostring() {
assert_eq!("AMERICAS", Into::<&'static str>::into(Route::Regional(RegionalRoute::AMERICAS))); assert_eq!(
assert_eq!("KR", Into::<&'static str>::into(Route::Platform(PlatformRoute::KR))); "AMERICAS",
assert_eq!("KR", Into::<&'static str>::into(Route::ValPlatform(ValPlatformRoute::KR))); Into::<&'static str>::into(Route::Regional(RegionalRoute::AMERICAS))
);
assert_eq!(
"KR",
Into::<&'static str>::into(Route::Platform(PlatformRoute::KR))
);
assert_eq!(
"KR",
Into::<&'static str>::into(Route::ValPlatform(ValPlatformRoute::KR))
);
} }
#[test] #[test]
@ -132,7 +132,10 @@ mod tests {
assert_eq!("AMERICAS", RegionalRoute::AMERICAS.to_string()); assert_eq!("AMERICAS", RegionalRoute::AMERICAS.to_string());
assert_eq!("SEA", RegionalRoute::SEA.to_string()); assert_eq!("SEA", RegionalRoute::SEA.to_string());
assert_eq!("AMERICAS", Into::<&'static str>::into(RegionalRoute::AMERICAS)); assert_eq!(
"AMERICAS",
Into::<&'static str>::into(RegionalRoute::AMERICAS)
);
assert_eq!("SEA", Into::<&'static str>::into(RegionalRoute::SEA)); assert_eq!("SEA", Into::<&'static str>::into(RegionalRoute::SEA));
} }
@ -171,7 +174,10 @@ mod tests {
assert_eq!("AP", Into::<&'static str>::into(ValPlatformRoute::AP)); assert_eq!("AP", Into::<&'static str>::into(ValPlatformRoute::AP));
assert_eq!("KR", Into::<&'static str>::into(ValPlatformRoute::KR)); assert_eq!("KR", Into::<&'static str>::into(ValPlatformRoute::KR));
assert_eq!("ESPORTS", Into::<&'static str>::into(ValPlatformRoute::ESPORTS)); assert_eq!(
"ESPORTS",
Into::<&'static str>::into(ValPlatformRoute::ESPORTS)
);
} }
#[test] #[test]

View file

@ -1,11 +1,21 @@
use serde_repr::{ Serialize_repr, Deserialize_repr }; use num_enum::{IntoPrimitive, TryFromPrimitive};
use num_enum::{ IntoPrimitive, TryFromPrimitive }; use serde_repr::{Deserialize_repr, Serialize_repr};
/// League of Legends team. /// League of Legends team.
#[derive(Debug, Copy, Clone)] #[derive(
#[derive(Eq, PartialEq, Hash, Ord, PartialOrd)] Debug,
#[derive(Serialize_repr, Deserialize_repr)] Copy,
#[derive(IntoPrimitive, TryFromPrimitive)] Clone,
Eq,
PartialEq,
Hash,
Ord,
PartialOrd,
Serialize_repr,
Deserialize_repr,
IntoPrimitive,
TryFromPrimitive,
)]
#[repr(u16)] #[repr(u16)]
pub enum Team { pub enum Team {
/// Blue team (bottom left on Summoner's Rift). /// Blue team (bottom left on Summoner's Rift).

View file

@ -1,7 +1,7 @@
use num_enum::{ IntoPrimitive, TryFromPrimitive }; use num_enum::{IntoPrimitive, TryFromPrimitive};
use serde::{ Serialize, Deserialize }; use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use strum_macros::{ EnumString, Display, AsRefStr, IntoStaticStr }; use strum_macros::{AsRefStr, Display, EnumString, IntoStaticStr};
/// LoL and TFT ranked tiers, such as gold, diamond, challenger, etc. /// LoL and TFT ranked tiers, such as gold, diamond, challenger, etc.
/// ///
@ -10,36 +10,49 @@ use strum_macros::{ EnumString, Display, AsRefStr, IntoStaticStr };
/// Repr'd as arbitrary `u8` values. /// Repr'd as arbitrary `u8` values.
/// ///
/// Implements [IntoEnumIterator](super::IntoEnumIterator). /// Implements [IntoEnumIterator](super::IntoEnumIterator).
#[derive(Debug, Copy, Clone)] #[derive(
#[derive(Eq, PartialEq, Hash, PartialOrd, Ord)] Debug,
#[derive(IntoPrimitive, TryFromPrimitive)] Copy,
#[derive(EnumString, Display, AsRefStr, IntoStaticStr)] Clone,
#[derive(Serialize, Deserialize)] Eq,
PartialEq,
Hash,
PartialOrd,
Ord,
IntoPrimitive,
TryFromPrimitive,
EnumString,
Display,
AsRefStr,
IntoStaticStr,
Serialize,
Deserialize,
)]
#[repr(u8)] #[repr(u8)]
pub enum Tier { pub enum Tier {
/// Challenger, the highest tier, an apex tier. Repr: `220_u8`. /// Challenger, the highest tier, an apex tier. Repr: `220_u8`.
CHALLENGER = 220, CHALLENGER = 220,
/// Grand Master, an apex tier. Repr: `200_u8`. /// Grand Master, an apex tier. Repr: `200_u8`.
GRANDMASTER = 200, GRANDMASTER = 200,
/// Master, an apex tier. Repr: `180_u8`. /// Master, an apex tier. Repr: `180_u8`.
MASTER = 180, MASTER = 180,
/// Diamond, the higest non-apex tier. Repr: `140_u8`. /// Diamond, the higest non-apex tier. Repr: `140_u8`.
DIAMOND = 140, DIAMOND = 140,
/// Platinum. Repr: `120_u8`. /// Platinum. Repr: `120_u8`.
PLATINUM = 120, PLATINUM = 120,
/// Gold. Repr: `100_u8`. /// Gold. Repr: `100_u8`.
GOLD = 100, GOLD = 100,
/// Silver. Repr: `80_u8`. /// Silver. Repr: `80_u8`.
SILVER = 80, SILVER = 80,
/// Bronze. Repr: `60_u8`. /// Bronze. Repr: `60_u8`.
BRONZE = 60, BRONZE = 60,
/// Iron, the lowest tier. Repr: `40_u8`. /// Iron, the lowest tier. Repr: `40_u8`.
IRON = 40, IRON = 40,
/// Unranked, no tier. Repr: `0_u8`. /// Unranked, no tier. Repr: `0_u8`.
/// Also deserializes from "NONE" returned by `lol-challenges-v1.getChallengePercentiles`. /// Also deserializes from "NONE" returned by `lol-challenges-v1.getChallengePercentiles`.
#[serde(alias = "NONE")] #[serde(alias = "NONE")]
UNRANKED = 0, UNRANKED = 0,
} }
impl Tier { impl Tier {
@ -63,7 +76,7 @@ impl Tier {
/// If this tier is ranked. Returns true for iron through challenger, false for unranked. /// If this tier is ranked. Returns true for iron through challenger, false for unranked.
pub const fn is_ranked(self) -> bool { pub const fn is_ranked(self) -> bool {
// Casts needed for const. // Casts needed for const.
(Self::UNRANKED as u8) < (self as u8) (Self::UNRANKED as u8) < (self as u8)
} }
@ -77,7 +90,11 @@ impl Tier {
/// Converts UNRANKED to None and all ranked tiers to Some(...). /// Converts UNRANKED to None and all ranked tiers to Some(...).
pub fn to_ranked(self) -> Option<Self> { pub fn to_ranked(self) -> Option<Self> {
if self.is_unranked() { None } else { Some(self) } if self.is_unranked() {
None
} else {
Some(self)
}
} }
} }
@ -88,10 +105,18 @@ impl IntoEnumIterator for Tier {
type Iterator = std::iter::Copied<std::slice::Iter<'static, Self>>; type Iterator = std::iter::Copied<std::slice::Iter<'static, Self>>;
fn iter() -> Self::Iterator { fn iter() -> Self::Iterator {
[ [
Self::CHALLENGER, Self::GRANDMASTER, Self::MASTER, Self::CHALLENGER,
Self::DIAMOND, Self::PLATINUM, Self::GOLD, Self::GRANDMASTER,
Self::SILVER, Self::BRONZE, Self::IRON Self::MASTER,
].iter().copied() Self::DIAMOND,
Self::PLATINUM,
Self::GOLD,
Self::SILVER,
Self::BRONZE,
Self::IRON,
]
.iter()
.copied()
} }
} }

View file

@ -1,6 +1,6 @@
use std::fmt; use std::fmt;
use reqwest::{ Error, Response, StatusCode }; use reqwest::{Error, Response, StatusCode};
/// Result containing RiotApiError on failure. /// Result containing RiotApiError on failure.
pub type Result<T> = std::result::Result<T, RiotApiError>; pub type Result<T> = std::result::Result<T, RiotApiError>;
@ -17,7 +17,12 @@ pub struct RiotApiError {
status_code: Option<StatusCode>, status_code: Option<StatusCode>,
} }
impl RiotApiError { impl RiotApiError {
pub(crate) fn new(reqwest_error: Error, retries: u8, response: Option<Response>, status_code: Option<StatusCode>) -> Self { pub(crate) fn new(
reqwest_error: Error,
retries: u8,
response: Option<Response>,
status_code: Option<StatusCode>,
) -> Self {
Self { Self {
reqwest_error, reqwest_error,
retries, retries,

View file

@ -19,46 +19,46 @@
//! <!--<a href="https://travis-ci.com/MingweiSamuel/Riven"><img src="https://img.shields.io/travis/com/mingweisamuel/riven?style=flat-square" alt="Travis CI"></a>--> //! <!--<a href="https://travis-ci.com/MingweiSamuel/Riven"><img src="https://img.shields.io/travis/com/mingweisamuel/riven?style=flat-square" alt="Travis CI"></a>-->
//! <a href="https://github.com/rust-secure-code/safety-dance/"><img src="https://img.shields.io/badge/unsafe-forbidden-green.svg?style=flat-square" alt="unsafe forbidden"></a> //! <a href="https://github.com/rust-secure-code/safety-dance/"><img src="https://img.shields.io/badge/unsafe-forbidden-green.svg?style=flat-square" alt="unsafe forbidden"></a>
//! </p> //! </p>
//! //!
//! Rust Library for the [Riot Games API](https://developer.riotgames.com/). //! Rust Library for the [Riot Games API](https://developer.riotgames.com/).
//! //!
//! Riven's goals are _speed_, _reliability_, and _maintainability_. Riven handles rate limits and large requests with ease. //! Riven's goals are _speed_, _reliability_, and _maintainability_. Riven handles rate limits and large requests with ease.
//! Data structs and endpoints are automatically generated from the //! Data structs and endpoints are automatically generated from the
//! [Riot API Reference](https://developer.riotgames.com/api-methods/) ([Swagger](http://www.mingweisamuel.com/riotapi-schema/tool/)). //! [Riot API Reference](https://developer.riotgames.com/api-methods/) ([Swagger](http://www.mingweisamuel.com/riotapi-schema/tool/)).
//! //!
//! # Design //! # Design
//! //!
//! * Fast, asynchronous, thread-safe. //! * Fast, asynchronous, thread-safe.
//! * Automatically retries failed requests. //! * Automatically retries failed requests.
//! * Supports all endpoints, kept up-to-date using [riotapi-schema](https://github.com/MingweiSamuel/riotapi-schema). //! * Supports all endpoints, kept up-to-date using [riotapi-schema](https://github.com/MingweiSamuel/riotapi-schema).
//! //!
//! # Usage //! # Usage
//! //!
//! ```rust //! ```rust
//! use riven::RiotApi; //! use riven::RiotApi;
//! use riven::consts::PlatformRoute; //! use riven::consts::PlatformRoute;
//! //!
//! // Enter tokio async runtime. //! // Enter tokio async runtime.
//! let rt = tokio::runtime::Runtime::new().unwrap(); //! let rt = tokio::runtime::Runtime::new().unwrap();
//! rt.block_on(async { //! rt.block_on(async {
//! // Create RiotApi instance from key string. //! // Create RiotApi instance from key string.
//! let api_key = std::env!("RGAPI_KEY"); // "RGAPI-01234567-89ab-cdef-0123-456789abcdef"; //! let api_key = std::env!("RGAPI_KEY"); // "RGAPI-01234567-89ab-cdef-0123-456789abcdef";
//! let riot_api = RiotApi::new(api_key); //! let riot_api = RiotApi::new(api_key);
//! //!
//! // Get summoner data. //! // Get summoner data.
//! let summoner = riot_api.summoner_v4() //! let summoner = riot_api.summoner_v4()
//! .get_by_summoner_name(PlatformRoute::NA1, "잘 못").await //! .get_by_summoner_name(PlatformRoute::NA1, "잘 못").await
//! .expect("Get summoner failed.") //! .expect("Get summoner failed.")
//! .expect("There is no summoner with that name."); //! .expect("There is no summoner with that name.");
//! //!
//! // Print summoner name. //! // Print summoner name.
//! println!("{} Champion Masteries:", summoner.name); //! println!("{} Champion Masteries:", summoner.name);
//! //!
//! // Get champion mastery data. //! // Get champion mastery data.
//! let masteries = riot_api.champion_mastery_v4() //! let masteries = riot_api.champion_mastery_v4()
//! .get_all_champion_masteries(PlatformRoute::NA1, &summoner.id).await //! .get_all_champion_masteries(PlatformRoute::NA1, &summoner.id).await
//! .expect("Get champion masteries failed."); //! .expect("Get champion masteries failed.");
//! //!
//! // Print champion masteries. //! // Print champion masteries.
//! for (i, mastery) in masteries.iter().take(10).enumerate() { //! for (i, mastery) in masteries.iter().take(10).enumerate() {
//! println!("{: >2}) {: <9} {: >7} ({})", i + 1, //! println!("{: >2}) {: <9} {: >7} ({})", i + 1,
@ -85,104 +85,104 @@
//! contains additional usage information. The [tests](https://github.com/MingweiSamuel/Riven/tree/v/2.x.x/riven/tests) //! contains additional usage information. The [tests](https://github.com/MingweiSamuel/Riven/tree/v/2.x.x/riven/tests)
//! and [example proxy](https://github.com/MingweiSamuel/Riven/tree/v/2.x.x/example/proxy) //! and [example proxy](https://github.com/MingweiSamuel/Riven/tree/v/2.x.x/example/proxy)
//! provide more example usage. //! provide more example usage.
//! //!
//! ## Feature Flags //! ## Feature Flags
//! //!
//! ### Nightly vs Stable //! ### Nightly vs Stable
//! //!
//! Enable the `nightly` feature to use nightly-only functionality. This enables //! Enable the `nightly` feature to use nightly-only functionality. This enables
//! [nightly optimizations in the `parking_lot` crate](https://github.com/Amanieu/parking_lot#nightly-vs-stable). //! [nightly optimizations in the `parking_lot` crate](https://github.com/Amanieu/parking_lot#nightly-vs-stable).
//! //!
//! ```toml //! ```toml
//! riven = { version = "...", features = [ "nightly" ] } //! riven = { version = "...", features = [ "nightly" ] }
//! ``` //! ```
//! //!
//! ### rustls //! ### rustls
//! //!
//! Riven uses [reqwest](https://github.com/seanmonstar/reqwest) for making requests. By default, reqwest uses the native TLS library. //! Riven uses [reqwest](https://github.com/seanmonstar/reqwest) for making requests. By default, reqwest uses the native TLS library.
//! If you prefer using [rustls](https://github.com/ctz/rustls) you can do so by turning off the Riven default features //! If you prefer using [rustls](https://github.com/ctz/rustls) you can do so by turning off the Riven default features
//! and specifying the `rustls-tls` feature: //! and specifying the `rustls-tls` feature:
//! //!
//! ```toml //! ```toml
//! riven = { version = "...", default-features = false, features = [ "rustls-tls" ] } //! riven = { version = "...", default-features = false, features = [ "rustls-tls" ] }
//! ``` //! ```
//! //!
//! Riven is additionally able to produce [tracing](https://docs.rs/tracing) spans for requests if the `tracing` feature is enabled. This feature is disabled by default. //! Riven is additionally able to produce [tracing](https://docs.rs/tracing) spans for requests if the `tracing` feature is enabled. This feature is disabled by default.
//! //!
//! ## Docs //! ## Docs
//! //!
//! [On docs.rs](https://docs.rs/riven/). //! [On docs.rs](https://docs.rs/riven/).
//! //!
//! ## Error Handling //! ## Error Handling
//! //!
//! Riven returns either `Result<T>` or `Result<Option<T>>` within futures. //! Riven returns either `Result<T>` or `Result<Option<T>>` within futures.
//! //!
//! If the `Result` is errored, this indicates that the API request failed to //! If the `Result` is errored, this indicates that the API request failed to
//! complete successfully, which may be due to bad user input, Riot server errors, //! complete successfully, which may be due to bad user input, Riot server errors,
//! incorrect API key, etc. //! incorrect API key, etc.
//! //!
//! If the `Option` is `None`, this indicates that the request completed //! If the `Option` is `None`, this indicates that the request completed
//! successfully but no data was returned. This happens in several situations, such //! successfully but no data was returned. This happens in several situations, such
//! as getting a summoner (by name) or match (by id) that doesn't exist, or getting //! as getting a summoner (by name) or match (by id) that doesn't exist, or getting
//! spectator data for a summoner who is not in-game. //! spectator data for a summoner who is not in-game.
//! Specifically, the API returned a 404 HTTP status code in this situation. //! Specifically, the API returned a 404 HTTP status code in this situation.
//! //!
//! The error type used by Riven is `riven::RiotApiError`. It provides some basic //! The error type used by Riven is `riven::RiotApiError`. It provides some basic
//! diagnostic information, such as the source Reqwest error, the number of retries //! diagnostic information, such as the source Reqwest error, the number of retries
//! attempted, and the Reqwest `Response` object. //! attempted, and the Reqwest `Response` object.
//! //!
//! You can configure the number of time Riven retries using //! You can configure the number of time Riven retries using
//! `RiotApiConfig::set_retries(...)` and the `RiotApi::from_config(config)` //! `RiotApiConfig::set_retries(...)` and the `RiotApi::from_config(config)`
//! constructor. By default, Riven retries up to 3 times (4 requests total). //! constructor. By default, Riven retries up to 3 times (4 requests total).
//! Some errors, such as 400 client errors, are not retried as they would //! Some errors, such as 400 client errors, are not retried as they would
//! inevitably fail again. //! inevitably fail again.
//! //!
//! ## Semantic Versioning //! ## Semantic Versioning
//! //!
//! This package follows semantic versioning to an extent. However, the Riot API //! This package follows semantic versioning to an extent. However, the Riot API
//! itself changes often and does not follow semantic versioning, which makes //! itself changes often and does not follow semantic versioning, which makes
//! things difficult. Out-of-date versions will slowly partially cease to work due //! things difficult. Out-of-date versions will slowly partially cease to work due
//! to this. //! to this.
//! //!
//! When the API changes, this may result in breaking changes in the `models` //! When the API changes, this may result in breaking changes in the `models`
//! module, `endpoints` module, and some of the `consts` module. "Handle accessor" //! module, `endpoints` module, and some of the `consts` module. "Handle accessor"
//! methods may be removed from `RiotApi` if the corresponding endpoint is removed //! methods may be removed from `RiotApi` if the corresponding endpoint is removed
//! from the Riot API. These breaking changes will increment the **MINOR** version, //! from the Riot API. These breaking changes will increment the **MINOR** version,
//! not the major version. //! not the major version.
//! //!
//! Parts of Riven that do not depend on Riot API changes do follow semantic //! Parts of Riven that do not depend on Riot API changes do follow semantic
//! versioning. //! versioning.
//! //!
//! ## Additional Help //! ## Additional Help
//! //!
//! Feel free to [make an issue](https://github.com/MingweiSamuel/Riven/issues/new) //! Feel free to [make an issue](https://github.com/MingweiSamuel/Riven/issues/new)
//! if you are have any questions or trouble with Riven. //! if you are have any questions or trouble with Riven.
//! //!
//! # Development //! # Development
//! //!
//! NodeJS is used to generate code for Riven. The //! NodeJS is used to generate code for Riven. The
//! [`riven/srcgen`](https://github.com/MingweiSamuel/Riven/tree/v/2.x.x/riven/srcgen) //! [`riven/srcgen`](https://github.com/MingweiSamuel/Riven/tree/v/2.x.x/riven/srcgen)
//! folder contains the code and [doT.js](https://olado.github.io/doT/index.html) //! folder contains the code and [doT.js](https://olado.github.io/doT/index.html)
//! templates. `index.js` lists the JSON files downloaded and used to generate the //! templates. `index.js` lists the JSON files downloaded and used to generate the
//! code. //! code.
//! //!
//! To set up the srcgen, you will first need to install NodeJS. Then enter the //! To set up the srcgen, you will first need to install NodeJS. Then enter the
//! `riven/srcgen` folder and run `npm ci` (or `npm install`) to install //! `riven/srcgen` folder and run `npm ci` (or `npm install`) to install
//! dependencies. //! dependencies.
//! //!
//! To run the srcgen use `node riven/srcgen` from the repository root. //! To run the srcgen use `node riven/srcgen` from the repository root.
//! //!
//! //!
// Re-exported reqwest types. // Re-exported reqwest types.
pub use reqwest; pub use reqwest;
mod config; mod config;
pub use config::RiotApiConfig; pub use config::RiotApiConfig;
pub mod consts; pub mod consts;
#[rustfmt::skip]
pub mod endpoints; pub mod endpoints;
mod error; mod error;
@ -190,6 +190,7 @@ pub use error::*;
pub mod meta; pub mod meta;
#[rustfmt::skip]
pub mod models; pub mod models;
mod models_impls; mod models_impls;

View file

@ -1,5 +1,5 @@
use crate::models::match_v5::Participant;
use crate::consts::Champion; use crate::consts::Champion;
use crate::models::match_v5::Participant;
impl Participant { impl Participant {
/// This method takes the [`Self::champion_id`] field if it is valid /// This method takes the [`Self::champion_id`] field if it is valid

View file

@ -1,19 +1,17 @@
use std::cmp; use std::cmp;
use std::time::{ Duration, Instant }; use std::time::{Duration, Instant};
#[cfg(not(feature="tracing"))] #[cfg(feature = "tracing")]
use log as log;
#[cfg(feature="tracing")]
use tracing as log; use tracing as log;
use parking_lot::{ RwLock, RwLockUpgradableReadGuard }; use parking_lot::{RwLock, RwLockUpgradableReadGuard};
use reqwest::{ StatusCode, Response }; use reqwest::{Response, StatusCode};
use scan_fmt::scan_fmt; use scan_fmt::scan_fmt;
use tokio::sync::Notify; use tokio::sync::Notify;
use crate::RiotApiConfig;
use super::{ TokenBucket, VectorTokenBucket };
use super::RateLimitType; use super::RateLimitType;
use super::{TokenBucket, VectorTokenBucket};
use crate::RiotApiConfig;
pub struct RateLimit { pub struct RateLimit {
rate_limit_type: RateLimitType, rate_limit_type: RateLimitType,
@ -42,8 +40,8 @@ impl RateLimit {
const HEADER_XRATELIMITTYPE_SERVICE: &'static str = "service"; const HEADER_XRATELIMITTYPE_SERVICE: &'static str = "service";
pub fn new(rate_limit_type: RateLimitType) -> Self { pub fn new(rate_limit_type: RateLimitType) -> Self {
let initial_bucket = VectorTokenBucket::new( let initial_bucket =
Duration::from_secs(1), 1, Duration::new(0, 0), 1.0, 1.0); VectorTokenBucket::new(Duration::from_secs(1), 1, Duration::new(0, 0), 1.0, 1.0);
RateLimit { RateLimit {
rate_limit_type, rate_limit_type,
// Rate limit before getting from response: 1/s. // Rate limit before getting from response: 1/s.
@ -65,13 +63,19 @@ impl RateLimit {
} }
} }
fn acquire_both_or_duration(app_rate_limit: &Self, method_rate_limit: &Self) -> Option<Duration> { fn acquire_both_or_duration(
app_rate_limit: &Self,
method_rate_limit: &Self,
) -> Option<Duration> {
// Check retry after. // Check retry after.
{ {
let retry_after_delay = app_rate_limit.get_retry_after_delay() let retry_after_delay = app_rate_limit.get_retry_after_delay().and_then(|a| {
.and_then(|a| method_rate_limit.get_retry_after_delay().map(|m| cmp::max(a, m))); method_rate_limit
.get_retry_after_delay()
.map(|m| cmp::max(a, m))
});
if retry_after_delay.is_some() { if retry_after_delay.is_some() {
return retry_after_delay return retry_after_delay;
} }
} }
// Check buckets. // Check buckets.
@ -88,12 +92,18 @@ impl RateLimit {
bucket.get_tokens(1); bucket.get_tokens(1);
} }
log::trace!("Tokens obtained, buckets: APP {:?} METHOD {:?}", app_buckets, method_buckets); log::trace!(
"Tokens obtained, buckets: APP {:?} METHOD {:?}",
app_buckets,
method_buckets
);
None None
} }
pub fn get_retry_after_delay(&self) -> Option<Duration> { pub fn get_retry_after_delay(&self) -> Option<Duration> {
self.retry_after.read().and_then(|i| Instant::now().checked_duration_since(i)) self.retry_after
.read()
.and_then(|i| Instant::now().checked_duration_since(i))
} }
/// Update retry-after and rate limits based on an API response. /// Update retry-after and rate limits based on an API response.
@ -153,13 +163,25 @@ impl RateLimit {
} }
// Get retry after header. Only care if it exists. // Get retry after header. Only care if it exists.
let retry_after_header = response.headers() let retry_after_header = response
.headers()
.get(reqwest::header::RETRY_AFTER) .get(reqwest::header::RETRY_AFTER)
.and_then(|h| h .and_then(|h| {
.to_str() h.to_str()
.map_err(|e| log::error!("Failed to read retry-after header as visible ASCII string: {:?}.", e)).ok())?; .map_err(|e| {
log::error!(
"Failed to read retry-after header as visible ASCII string: {:?}.",
e
)
})
.ok()
})?;
log::info!("429 response, rate limit {:?}, retry-after {} secs.", self.rate_limit_type, retry_after_header); log::info!(
"429 response, rate limit {:?}, retry-after {} secs.",
self.rate_limit_type,
retry_after_header
);
// Header currently only returns ints, but float is more general. Can be zero. // Header currently only returns ints, but float is more general. Can be zero.
let retry_after_secs = retry_after_header let retry_after_secs = retry_after_header
@ -179,21 +201,42 @@ impl RateLimit {
fn on_response_rate_limits(&self, config: &RiotApiConfig, response: &Response) { fn on_response_rate_limits(&self, config: &RiotApiConfig, response: &Response) {
// Check if rate limits changed. // Check if rate limits changed.
let headers = response.headers(); let headers = response.headers();
let limit_header_opt = headers.get(self.rate_limit_type.limit_header()) let limit_header_opt = headers
.and_then(|h| h.to_str().map_err(|e| log::error!("Failed to read limit header as visible ASCII string: {:?}.", e)).ok()); .get(self.rate_limit_type.limit_header())
let count_header_opt = headers.get(self.rate_limit_type.count_header()) .and_then(|h| {
.and_then(|h| h.to_str().map_err(|e| log::error!("Failed to read count header as visible ASCII string: {:?}.", e)).ok()); h.to_str()
.map_err(|e| {
log::error!(
"Failed to read limit header as visible ASCII string: {:?}.",
e
)
})
.ok()
});
let count_header_opt = headers
.get(self.rate_limit_type.count_header())
.and_then(|h| {
h.to_str()
.map_err(|e| {
log::error!(
"Failed to read count header as visible ASCII string: {:?}.",
e
)
})
.ok()
});
if let (Some(limit_header), Some(count_header)) = (limit_header_opt, count_header_opt) { if let (Some(limit_header), Some(count_header)) = (limit_header_opt, count_header_opt) {
{ {
let buckets = self.buckets.upgradable_read(); let buckets = self.buckets.upgradable_read();
if !buckets_require_updating(limit_header, &*buckets) { if !buckets_require_updating(limit_header, &buckets) {
return; return;
} }
// Buckets require updating. Upgrade to write lock. // Buckets require updating. Upgrade to write lock.
let mut buckets = RwLockUpgradableReadGuard::upgrade(buckets); let mut buckets = RwLockUpgradableReadGuard::upgrade(buckets);
*buckets = buckets_from_header(config, limit_header, count_header, self.rate_limit_type); *buckets =
buckets_from_header(config, limit_header, count_header, self.rate_limit_type);
} }
// Notify waiters that buckets have updated (after unlocking). // Notify waiters that buckets have updated (after unlocking).
self.update_notify.notify_waiters(); self.update_notify.notify_waiters();
@ -207,7 +250,11 @@ fn buckets_require_updating(limit_header: &str, buckets: &[VectorTokenBucket]) -
} }
for (limit_header_entry, bucket) in limit_header.split(',').zip(buckets) { for (limit_header_entry, bucket) in limit_header.split(',').zip(buckets) {
// limit_header_entry "100:60" means 100 req per 60 sec. // limit_header_entry "100:60" means 100 req per 60 sec.
let bucket_entry = format!("{}:{}", bucket.get_total_limit(), bucket.get_bucket_duration().as_secs()); let bucket_entry = format!(
"{}:{}",
bucket.get_total_limit(),
bucket.get_bucket_duration().as_secs()
);
if limit_header_entry != bucket_entry { if limit_header_entry != bucket_entry {
return true; return true;
} }
@ -215,7 +262,12 @@ fn buckets_require_updating(limit_header: &str, buckets: &[VectorTokenBucket]) -
false false
} }
fn buckets_from_header(config: &RiotApiConfig, limit_header: &str, count_header: &str, rate_limit_type: RateLimitType) -> Vec<VectorTokenBucket> { fn buckets_from_header(
config: &RiotApiConfig,
limit_header: &str,
count_header: &str,
rate_limit_type: RateLimitType,
) -> Vec<VectorTokenBucket> {
// Limits: "20000:10,1200000:600" // Limits: "20000:10,1200000:600"
// Counts: "7:10,58:600" // Counts: "7:10,58:600"
let size = limit_header.split(',').count(); let size = limit_header.split(',').count();
@ -238,11 +290,20 @@ fn buckets_from_header(config: &RiotApiConfig, limit_header: &str, count_header:
let limit_f32 = limit as f32; let limit_f32 = limit as f32;
let scaled_burst_factor = config.burst_factor * limit_f32 / (limit_f32 + 1.0); let scaled_burst_factor = config.burst_factor * limit_f32 / (limit_f32 + 1.0);
let bucket = VectorTokenBucket::new(Duration::from_secs(limit_secs), limit, let bucket = VectorTokenBucket::new(
config.duration_overhead, scaled_burst_factor, rate_usage_factor); Duration::from_secs(limit_secs),
limit,
config.duration_overhead,
scaled_burst_factor,
rate_usage_factor,
);
bucket.get_tokens(count); bucket.get_tokens(count);
out.push(bucket); out.push(bucket);
} }
log::debug!("Set buckets to {} limit, {} count.", limit_header, count_header); log::debug!(
"Set buckets to {} limit, {} count.",
limit_header,
count_header
);
out out
} }

View file

@ -1,14 +1,12 @@
use std::future::Future; use std::future::Future;
use std::sync::Arc; use std::sync::Arc;
#[cfg(not(feature="tracing"))] #[cfg(feature = "tracing")]
use log as log;
#[cfg(feature="tracing")]
use tracing as log; use tracing as log;
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
use tracing::Instrument; use tracing::Instrument;
use reqwest::{ StatusCode, RequestBuilder }; use reqwest::{RequestBuilder, StatusCode};
use crate::util::InsertOnlyCHashMap; use crate::util::InsertOnlyCHashMap;
use crate::ResponseInfo; use crate::ResponseInfo;
@ -40,19 +38,21 @@ impl RegionalRequester {
} }
} }
pub fn execute<'a>(self: Arc<Self>, pub fn execute<'a>(
self: Arc<Self>,
config: &'a RiotApiConfig, config: &'a RiotApiConfig,
method_id: &'static str, request: RequestBuilder) method_id: &'static str,
-> impl Future<Output = Result<ResponseInfo>> + 'a request: RequestBuilder,
{ ) -> impl Future<Output = Result<ResponseInfo>> + 'a {
async move { async move {
let mut retries: u8 = 0; let mut retries: u8 = 0;
loop { loop {
let method_rate_limit: Arc<RateLimit> = self.method_rate_limits let method_rate_limit: Arc<RateLimit> = self
.method_rate_limits
.get_or_insert_with(method_id, || RateLimit::new(RateLimitType::Method)); .get_or_insert_with(method_id, || RateLimit::new(RateLimitType::Method));
// Rate limit. // Rate limit.
let rate_limit = RateLimit::acquire_both(&self.app_rate_limit, &*method_rate_limit); let rate_limit = RateLimit::acquire_both(&self.app_rate_limit, &method_rate_limit);
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
let rate_limit = rate_limit.instrument(tracing::info_span!("rate_limit")); let rate_limit = rate_limit.instrument(tracing::info_span!("rate_limit"));
rate_limit.await; rate_limit.await;
@ -79,24 +79,40 @@ impl RegionalRequester {
let status_none = Self::NONE_STATUS_CODES.contains(&status); let status_none = Self::NONE_STATUS_CODES.contains(&status);
// Success case. // Success case.
if status.is_success() || status_none { if status.is_success() || status_none {
log::trace!("Response {} (retried {} times), success, returning result.", status, retries); log::trace!(
"Response {} (retried {} times), success, returning result.",
status,
retries
);
break Ok(ResponseInfo { break Ok(ResponseInfo {
response, response,
retries, retries,
status_none, status_none,
}); });
} }
let err = response.error_for_status_ref().err().unwrap_or_else( let err = response.error_for_status_ref().err().unwrap_or_else(|| {
|| panic!("Unhandlable response status code, neither success nor failure: {}.", status)); panic!(
"Unhandlable response status code, neither success nor failure: {}.",
status
)
});
// Failure, may or may not be retryable. // Failure, may or may not be retryable.
// Not-retryable: no more retries or 4xx or ? (3xx, redirects exceeded). // Not-retryable: no more retries or 4xx or ? (3xx, redirects exceeded).
// Retryable: retries remaining, and 429 or 5xx. // Retryable: retries remaining, and 429 or 5xx.
if retries >= config.retries || if retries >= config.retries
(StatusCode::TOO_MANY_REQUESTS != status || (StatusCode::TOO_MANY_REQUESTS != status && !status.is_server_error())
&& !status.is_server_error())
{ {
log::debug!("Response {} (retried {} times), failure, returning error.", status, retries); log::debug!(
break Err(RiotApiError::new(err, retries, Some(response), Some(status))); "Response {} (retried {} times), failure, returning error.",
status,
retries
);
break Err(RiotApiError::new(
err,
retries,
Some(response),
Some(status),
));
} }
// Is retryable, do exponential backoff if retry-after wasn't specified. // Is retryable, do exponential backoff if retry-after wasn't specified.

View file

@ -1,5 +1,5 @@
use std::fmt;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::fmt;
use std::time::Duration; use std::time::Duration;
use parking_lot::{Mutex, MutexGuard}; use parking_lot::{Mutex, MutexGuard};
@ -58,32 +58,41 @@ pub struct VectorTokenBucket {
/// Limit allowed per burst_duration, for burst factor. /// Limit allowed per burst_duration, for burst factor.
burst_limit: usize, burst_limit: usize,
/// Record of timestamps (synchronized). /// Record of timestamps (synchronized).
timestamps: Mutex<VecDeque<Instant>>, timestamps: Mutex<VecDeque<Instant>>,
} }
impl VectorTokenBucket { impl VectorTokenBucket {
pub fn new(duration: Duration, given_total_limit: usize, pub fn new(
duration_overhead: Duration, burst_factor: f32, duration: Duration,
rate_usage_factor: f32) -> Self given_total_limit: usize,
{ duration_overhead: Duration,
debug_assert!(0.0 < rate_usage_factor && rate_usage_factor <= 1.0, burst_factor: f32,
"BAD rate_usage_factor {}.", rate_usage_factor); rate_usage_factor: f32,
debug_assert!(0.0 < burst_factor && burst_factor <= 1.0, ) -> Self {
"BAD burst_factor {}.", burst_factor); debug_assert!(
0.0 < rate_usage_factor && rate_usage_factor <= 1.0,
"BAD rate_usage_factor {}.",
rate_usage_factor
);
debug_assert!(
0.0 < burst_factor && burst_factor <= 1.0,
"BAD burst_factor {}.",
burst_factor
);
// Float ops may lose precision, but nothing should be that precise. // Float ops may lose precision, but nothing should be that precise.
// API always uses round numbers, burst_factor is frac of 256. // API always uses round numbers, burst_factor is frac of 256.
// Adjust everything by rate_usage_factor. // Adjust everything by rate_usage_factor.
let total_limit = std::cmp::max(1, let total_limit = std::cmp::max(
(given_total_limit as f32 * rate_usage_factor).floor() as usize); 1,
(given_total_limit as f32 * rate_usage_factor).floor() as usize,
);
// Effective duration. // Effective duration.
let d_eff = duration + duration_overhead; let d_eff = duration + duration_overhead;
let burst_duration = d_eff.mul_f32(burst_factor); let burst_duration = d_eff.mul_f32(burst_factor);
let burst_limit = std::cmp::max(1, let burst_limit = std::cmp::max(1, (total_limit as f32 * burst_factor).floor() as usize);
(total_limit as f32 * burst_factor).floor() as usize);
debug_assert!(burst_limit <= total_limit); debug_assert!(burst_limit <= total_limit);
VectorTokenBucket { VectorTokenBucket {
@ -113,21 +122,23 @@ impl VectorTokenBucket {
} }
impl TokenBucket for VectorTokenBucket { impl TokenBucket for VectorTokenBucket {
fn get_delay(&self) -> Option<Duration> { fn get_delay(&self) -> Option<Duration> {
let timestamps = self.update_get_timestamps(); let timestamps = self.update_get_timestamps();
// Full rate limit. // Full rate limit.
if let Some(ts) = timestamps.get(self.total_limit - 1) { if let Some(ts) = timestamps.get(self.total_limit - 1) {
// Return amount of time needed for timestamp `ts` to go away. // Return amount of time needed for timestamp `ts` to go away.
Instant::now().checked_duration_since(*ts) Instant::now()
.and_then(|passed_dur| (self.duration + self.duration_overhead) .checked_duration_since(*ts)
.checked_sub(passed_dur)) .and_then(|passed_dur| {
(self.duration + self.duration_overhead).checked_sub(passed_dur)
})
} }
// Otherwise burst rate limit. // Otherwise burst rate limit.
else if let Some(ts) = timestamps.get(self.burst_limit - 1) { else if let Some(ts) = timestamps.get(self.burst_limit - 1) {
// Return amount of time needed for timestamp `ts` to go away. // Return amount of time needed for timestamp `ts` to go away.
Instant::now().checked_duration_since(*ts) Instant::now()
.checked_duration_since(*ts)
.and_then(|passed_dur| self.burst_duration.checked_sub(passed_dur)) .and_then(|passed_dur| self.burst_duration.checked_sub(passed_dur))
} }
// No delay needed. // No delay needed.
@ -173,6 +184,12 @@ impl TokenBucket for VectorTokenBucket {
impl fmt::Debug for VectorTokenBucket { impl fmt::Debug for VectorTokenBucket {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}/{}:{})", self.timestamps.lock().len(), self.total_limit, self.duration.as_secs()) write!(
f,
"({}/{}:{})",
self.timestamps.lock().len(),
self.total_limit,
self.duration.as_secs()
)
} }
} }

View file

@ -1,19 +1,17 @@
use std::future::Future; use std::future::Future;
use std::sync::Arc; use std::sync::Arc;
#[cfg(not(feature="tracing"))] #[cfg(feature = "tracing")]
use log as log;
#[cfg(feature="tracing")]
use tracing as log; use tracing as log;
use reqwest::{ Client, RequestBuilder, Method }; use reqwest::{Client, Method, RequestBuilder};
use crate::Result;
use crate::ResponseInfo;
use crate::RiotApiConfig;
use crate::RiotApiError;
use crate::req::RegionalRequester; use crate::req::RegionalRequester;
use crate::util::InsertOnlyCHashMap; use crate::util::InsertOnlyCHashMap;
use crate::ResponseInfo;
use crate::Result;
use crate::RiotApiConfig;
use crate::RiotApiError;
/// For retrieving data from the Riot Games API. /// For retrieving data from the Riot Games API.
/// ///
@ -59,11 +57,15 @@ impl RiotApi {
/// Constructs a new instance from an API key (e.g. `"RGAPI-01234567-89ab-cdef-0123-456789abcdef"`) or a [RiotApiConfig]. /// Constructs a new instance from an API key (e.g. `"RGAPI-01234567-89ab-cdef-0123-456789abcdef"`) or a [RiotApiConfig].
pub fn new(config: impl Into<RiotApiConfig>) -> Self { pub fn new(config: impl Into<RiotApiConfig>) -> Self {
let mut config = config.into(); let mut config = config.into();
let client_builder = config.client_builder.take() let client_builder = config
.client_builder
.take()
.expect("CLIENT_BUILDER IN CONFIG SHOULD NOT BE NONE."); .expect("CLIENT_BUILDER IN CONFIG SHOULD NOT BE NONE.");
Self { Self {
config, config,
client: client_builder.build().expect("Failed to create client from builder."), client: client_builder
.build()
.expect("Failed to create client from builder."),
regional_requesters: InsertOnlyCHashMap::new(), regional_requesters: InsertOnlyCHashMap::new(),
} }
} }
@ -78,7 +80,8 @@ impl RiotApi {
/// * `path` - The URL path, appended to the base URL. /// * `path` - The URL path, appended to the base URL.
pub fn request(&self, method: Method, region_platform: &str, path: &str) -> RequestBuilder { pub fn request(&self, method: Method, region_platform: &str, path: &str) -> RequestBuilder {
let base_url_platform = self.config.base_url.replace("{}", region_platform); let base_url_platform = self.config.base_url.replace("{}", region_platform);
self.client.request(method, format!("{}{}", base_url_platform, path)) self.client
.request(method, format!("{}{}", base_url_platform, path))
} }
/// This method should generally not be used directly. Consider using endpoint wrappers instead. /// This method should generally not be used directly. Consider using endpoint wrappers instead.
@ -92,11 +95,15 @@ impl RiotApi {
/// ///
/// # Returns /// # Returns
/// A future resolving to a `Result` containg either a `T` (success) or a `RiotApiError` (failure). /// 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>(&'a self, pub async fn execute_val<'a, T: serde::de::DeserializeOwned + 'a>(
method_id: &'static str, region_platform: &'static str, request: RequestBuilder) &'a self,
-> Result<T> method_id: &'static str,
{ region_platform: &'static str,
let rinfo = self.execute_raw(method_id, region_platform, request).await?; request: RequestBuilder,
) -> Result<T> {
let rinfo = self
.execute_raw(method_id, region_platform, request)
.await?;
let retries = rinfo.retries; let retries = rinfo.retries;
let status = rinfo.response.status(); let status = rinfo.response.status();
let value = rinfo.response.json::<T>().await; let value = rinfo.response.json::<T>().await;
@ -114,11 +121,15 @@ impl RiotApi {
/// ///
/// # Returns /// # Returns
/// A future resolving to a `Result` containg either an `Option<T>` (success) or a `RiotApiError` (failure). /// 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>(&'a self, pub async fn execute_opt<'a, T: serde::de::DeserializeOwned + 'a>(
method_id: &'static str, region_platform: &'static str, request: RequestBuilder) &'a self,
-> Result<Option<T>> method_id: &'static str,
{ region_platform: &'static str,
let rinfo = self.execute_raw(method_id, region_platform, request).await?; request: RequestBuilder,
) -> Result<Option<T>> {
let rinfo = self
.execute_raw(method_id, region_platform, request)
.await?;
if rinfo.status_none { if rinfo.status_none {
return Ok(None); return Ok(None);
} }
@ -139,14 +150,20 @@ impl RiotApi {
/// ///
/// # Returns /// # Returns
/// A future resolving to a `Result` containg either `()` (success) or a `RiotApiError` (failure). /// A future resolving to a `Result` containg either `()` (success) or a `RiotApiError` (failure).
pub async fn execute(&self, pub async fn execute(
method_id: &'static str, region_platform: &'static str, request: RequestBuilder) &self,
-> Result<()> method_id: &'static str,
{ region_platform: &'static str,
let rinfo = self.execute_raw(method_id, region_platform, request).await?; request: RequestBuilder,
) -> Result<()> {
let rinfo = self
.execute_raw(method_id, region_platform, request)
.await?;
let retries = rinfo.retries; let retries = rinfo.retries;
let status = rinfo.response.status(); let status = rinfo.response.status();
rinfo.response.error_for_status() rinfo
.response
.error_for_status()
.map(|_| ()) .map(|_| ())
.map_err(|e| RiotApiError::new(e, retries, None, Some(status))) .map_err(|e| RiotApiError::new(e, retries, None, Some(status)))
} }
@ -164,18 +181,25 @@ impl RiotApi {
/// ///
/// # Returns /// # Returns
/// A future resolving to a `Result` containg either a `ResponseInfo` (success) or a `RiotApiError` (failure). /// A future resolving to a `Result` containg either a `ResponseInfo` (success) or a `RiotApiError` (failure).
pub fn execute_raw(&self, method_id: &'static str, region_platform: &'static str, request: RequestBuilder) pub fn execute_raw(
-> impl Future<Output = Result<ResponseInfo>> + '_ &self,
{ method_id: &'static str,
region_platform: &'static str,
request: RequestBuilder,
) -> impl Future<Output = Result<ResponseInfo>> + '_ {
self.regional_requester(region_platform) self.regional_requester(region_platform)
.execute(&self.config, method_id, request) .execute(&self.config, method_id, request)
} }
/// Get or create the RegionalRequester for the given region. /// Get or create the RegionalRequester for the given region.
fn regional_requester(&self, region_platform: &'static str) -> Arc<RegionalRequester> { fn regional_requester(&self, region_platform: &'static str) -> Arc<RegionalRequester> {
self.regional_requesters.get_or_insert_with(region_platform, || { self.regional_requesters
log::debug!("Creating requester for region platform {}.", region_platform); .get_or_insert_with(region_platform, || {
RegionalRequester::new() log::debug!(
}) "Creating requester for region platform {}.",
region_platform
);
RegionalRequester::new()
})
} }
} }

View file

@ -13,7 +13,7 @@ impl<K: Hash + Eq, V> InsertOnlyCHashMap<K, V> {
#[inline] #[inline]
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
base: Mutex::new(HashMap::new()) base: Mutex::new(HashMap::new()),
} }
} }
@ -27,10 +27,12 @@ impl<K: Hash + Eq, V> InsertOnlyCHashMap<K, V> {
// } // }
#[inline] #[inline]
pub fn get_or_insert_with<F: FnOnce() -> V>(&self, key: K, default: F) -> Arc<V> pub fn get_or_insert_with<F: FnOnce() -> V>(&self, key: K, default: F) -> Arc<V> {
{ Arc::clone(
Arc::clone(self.base.lock() self.base
.entry(key) .lock()
.or_insert_with(|| Arc::new(default()))) .entry(key)
.or_insert_with(|| Arc::new(default())),
)
} }
} }

View file

@ -7,18 +7,18 @@
#![deny(missing_docs)] #![deny(missing_docs)]
{{~ readme :line }} {{~ readme :line }}
//! {{= line }} //!{{= line ? (' ' + line) : '' }}
{{~}} {{~}}
// Re-exported reqwest types. // Re-exported reqwest types.
pub use reqwest; pub use reqwest;
mod config; mod config;
pub use config::RiotApiConfig; pub use config::RiotApiConfig;
pub mod consts; pub mod consts;
#[rustfmt::skip]
pub mod endpoints; pub mod endpoints;
mod error; mod error;
@ -26,6 +26,7 @@ pub use error::*;
pub mod meta; pub mod meta;
#[rustfmt::skip]
pub mod models; pub mod models;
mod models_impls; mod models_impls;

View file

@ -18,14 +18,13 @@ static MATCHES: &[&str] = &[
"NA1_4052515784", "NA1_4052515784",
"NA1_4062578191", "NA1_4062578191",
"NA1_4097036960", "NA1_4097036960",
// New games with `match-v5.ParticipantDto.challenges` field. // New games with `match-v5.ParticipantDto.challenges` field.
"NA1_4209556127", "NA1_4209556127",
"NA1_4212715433", "NA1_4212715433",
"NA1_4265913704", // `match-v5.ParticipantDto.challenges.mejaisFullStackInTime` "NA1_4265913704", // `match-v5.ParticipantDto.challenges.mejaisFullStackInTime`
]; ];
async_tests!{ async_tests! {
my_runner { my_runner {
// TODO FAILING since 2022/11/28 https://github.com/MingweiSamuel/Riven/actions/runs/3571320200/jobs/6003088646 // TODO FAILING since 2022/11/28 https://github.com/MingweiSamuel/Riven/actions/runs/3571320200/jobs/6003088646
// // Champion Mastery tests. // // Champion Mastery tests.

View file

@ -11,14 +11,14 @@ use riven::consts::*;
const ROUTE: PlatformRoute = PlatformRoute::EUW1; const ROUTE: PlatformRoute = PlatformRoute::EUW1;
async_tests!{ async_tests! {
my_runner { my_runner {
// Champion Mastery tests. // Champion Mastery tests.
championmastery_getscore_ma5tery: async { championmastery_getscore_ma5tery: async {
let sum = RIOT_API.summoner_v4().get_by_summoner_name(ROUTE, "ma5tery"); let sum = RIOT_API.summoner_v4().get_by_summoner_name(ROUTE, "ma5tery");
let sum = sum.await.map_err(|e| e.to_string())?.ok_or_else(|| "Failed to get summoner".to_owned())?; let sum = sum.await.map_err(|e| e.to_string())?.ok_or_else(|| "Failed to get summoner".to_owned())?;
let p = RIOT_API.champion_mastery_v4().get_champion_mastery_score(ROUTE, &*sum.id); let p = RIOT_API.champion_mastery_v4().get_champion_mastery_score(ROUTE, &sum.id);
let s = p.await.map_err(|e| e.to_string())?; let s = p.await.map_err(|e| e.to_string())?;
rassert!((969..=1000).contains(&s), "Unexpected ma5tery score: {}.", s); rassert!((969..=1000).contains(&s), "Unexpected ma5tery score: {}.", s);
Ok(()) Ok(())
@ -27,7 +27,7 @@ async_tests!{
let sum = RIOT_API.summoner_v4().get_by_summoner_name(ROUTE, "ma5tery"); let sum = RIOT_API.summoner_v4().get_by_summoner_name(ROUTE, "ma5tery");
let sum = sum.await.map_err(|e| e.to_string())?.ok_or_else(|| "Failed to get summoner".to_owned())?; let sum = sum.await.map_err(|e| e.to_string())?.ok_or_else(|| "Failed to get summoner".to_owned())?;
let p = RIOT_API.champion_mastery_v4().get_all_champion_masteries(ROUTE, &*sum.id); let p = RIOT_API.champion_mastery_v4().get_all_champion_masteries(ROUTE, &sum.id);
let s = p.await.map_err(|e| e.to_string())?; let s = p.await.map_err(|e| e.to_string())?;
rassert!(s.len() >= 142, "Expected masteries: {}.", s.len()); rassert!(s.len() >= 142, "Expected masteries: {}.", s.len());
Ok(()) Ok(())

View file

@ -53,13 +53,13 @@ async_tests! {
tft_combo: async { tft_combo: async {
let top_players = RIOT_API.tft_league_v1().get_top_rated_ladder(ROUTE, QueueType::RANKED_TFT_TURBO); let top_players = RIOT_API.tft_league_v1().get_top_rated_ladder(ROUTE, QueueType::RANKED_TFT_TURBO);
let top_players = top_players.await.map_err(|e| e.to_string())?; let top_players = top_players.await.map_err(|e| e.to_string())?;
rassert!(0 < top_players.len()); rassert!(!top_players.is_empty());
let top_player_entry = &top_players[0]; let top_player_entry = &top_players[0];
let top_player = RIOT_API.tft_summoner_v1().get_by_summoner_id(ROUTE, &*top_player_entry.summoner_id); let top_player = RIOT_API.tft_summoner_v1().get_by_summoner_id(ROUTE, &top_player_entry.summoner_id);
let top_player = top_player.await.map_err(|e| e.to_string())?; let top_player = top_player.await.map_err(|e| e.to_string())?;
println!("Top player is {} with `puuid` {}.", top_player.name, top_player.puuid); println!("Top player is {} with `puuid` {}.", top_player.name, top_player.puuid);
let match_ids = RIOT_API.tft_match_v1().get_match_ids_by_puuid( let match_ids = RIOT_API.tft_match_v1().get_match_ids_by_puuid(
ROUTE.to_regional(), &*top_player.puuid, Some(10), None, None, None); ROUTE.to_regional(), &top_player.puuid, Some(10), None, None, None);
let match_ids = match_ids.await.map_err(|e| e.to_string())?; let match_ids = match_ids.await.map_err(|e| e.to_string())?;
tft_match_v1_get(ROUTE.to_regional(), &*match_ids).await?; tft_match_v1_get(ROUTE.to_regional(), &*match_ids).await?;
Ok(()) Ok(())

View file

@ -40,7 +40,7 @@ async_tests! {
// Spot check 10% for `player-data`. // Spot check 10% for `player-data`.
for entry in leaderboard.iter().step_by(10) for entry in leaderboard.iter().step_by(10)
{ {
let _player_data = RIOT_API.lol_challenges_v1().get_player_data(ROUTE, &*entry.puuid) let _player_data = RIOT_API.lol_challenges_v1().get_player_data(ROUTE, &entry.puuid)
.await.map_err(|e| format!("Failed to get player data PUUID {}: {}", entry.puuid, e))?; .await.map_err(|e| format!("Failed to get player data PUUID {}: {}", entry.puuid, e))?;
} }

View file

@ -13,13 +13,18 @@ use riven::models::summoner_v4::*;
fn validate_summoners(s1: Summoner, s2: Summoner) -> Result<(), String> { fn validate_summoners(s1: Summoner, s2: Summoner) -> Result<(), String> {
rassert_eq!(s1.name, s2.name, "Names didn't match {}.", ""); rassert_eq!(s1.name, s2.name, "Names didn't match {}.", "");
rassert_eq!(s1.id, s2.id, "SummonerId didn't match {}.", ""); rassert_eq!(s1.id, s2.id, "SummonerId didn't match {}.", "");
rassert_eq!(s1.account_id, s2.account_id, "AccountId didn't match {}.", ""); rassert_eq!(
s1.account_id,
s2.account_id,
"AccountId didn't match {}.",
""
);
Ok(()) Ok(())
} }
const ROUTE: PlatformRoute = PlatformRoute::NA1; const ROUTE: PlatformRoute = PlatformRoute::NA1;
async_tests!{ async_tests! {
my_runner { my_runner {
// Summoner tests. // Summoner tests.
summoner_double: async { summoner_double: async {
@ -53,10 +58,10 @@ async_tests!{
// summoner must have double-up rank. // summoner must have double-up rank.
league_getforsummoner_tftbug: async { league_getforsummoner_tftbug: async {
// TODO(mingwei): get summoner from leaderboard to avoid updating this all the time. // TODO(mingwei): get summoner from leaderboard to avoid updating this all the time.
const SUMMONER_NAME: &'static str = "Vincentscc"; const SUMMONER_NAME: &str = "Vincentscc";
let summoner_fut = RIOT_API.summoner_v4().get_by_summoner_name(ROUTE, SUMMONER_NAME); let summoner_fut = RIOT_API.summoner_v4().get_by_summoner_name(ROUTE, SUMMONER_NAME);
let summoner = summoner_fut.await.map_err(|e| e.to_string())?.ok_or_else(|| format!("Failed to get \"{}\"", SUMMONER_NAME))?; let summoner = summoner_fut.await.map_err(|e| e.to_string())?.ok_or_else(|| format!("Failed to get \"{}\"", SUMMONER_NAME))?;
let league_fut = RIOT_API.league_v4().get_league_entries_for_summoner(ROUTE, &*summoner.id); let league_fut = RIOT_API.league_v4().get_league_entries_for_summoner(ROUTE, &summoner.id);
let leagues = league_fut.await.map_err(|e| e.to_string())?; let leagues = league_fut.await.map_err(|e| e.to_string())?;
let tft_league = leagues.iter().find(|league| QueueType::RANKED_TFT_DOUBLE_UP == league.queue_type); let tft_league = leagues.iter().find(|league| QueueType::RANKED_TFT_DOUBLE_UP == league.queue_type);
rassert!(tft_league.is_some()); rassert!(tft_league.is_some());

View file

@ -11,7 +11,7 @@ use riven::consts::*;
const ROUTE: PlatformRoute = PlatformRoute::PH2; const ROUTE: PlatformRoute = PlatformRoute::PH2;
async_tests!{ async_tests! {
my_runner { my_runner {
status: async { status: async {
let p = RIOT_API.lol_status_v4().get_platform_data(ROUTE); let p = RIOT_API.lol_status_v4().get_platform_data(ROUTE);

View file

@ -11,7 +11,7 @@ use riven::consts::*;
const ROUTE: PlatformRoute = PlatformRoute::SG2; const ROUTE: PlatformRoute = PlatformRoute::SG2;
async_tests!{ async_tests! {
my_runner { my_runner {
status: async { status: async {
let p = RIOT_API.lol_status_v4().get_platform_data(ROUTE); let p = RIOT_API.lol_status_v4().get_platform_data(ROUTE);

View file

@ -11,7 +11,7 @@ use riven::consts::*;
const ROUTE: PlatformRoute = PlatformRoute::TH2; const ROUTE: PlatformRoute = PlatformRoute::TH2;
async_tests!{ async_tests! {
my_runner { my_runner {
status: async { status: async {
let p = RIOT_API.lol_status_v4().get_platform_data(ROUTE); let p = RIOT_API.lol_status_v4().get_platform_data(ROUTE);

View file

@ -12,8 +12,7 @@ use riven::models::summoner_v4::Summoner;
const ROUTE: PlatformRoute = PlatformRoute::TR1; const ROUTE: PlatformRoute = PlatformRoute::TR1;
async_tests! {
async_tests!{
my_runner { my_runner {
league_summoner_bulk_test: async { league_summoner_bulk_test: async {
let p = RIOT_API.league_v4().get_challenger_league(ROUTE, QueueType::RANKED_SOLO_5x5); let p = RIOT_API.league_v4().get_challenger_league(ROUTE, QueueType::RANKED_SOLO_5x5);

View file

@ -11,7 +11,7 @@ use riven::consts::*;
const ROUTE: PlatformRoute = PlatformRoute::VN2; const ROUTE: PlatformRoute = PlatformRoute::VN2;
async_tests!{ async_tests! {
my_runner { my_runner {
status: async { status: async {
let p = RIOT_API.lol_status_v4().get_platform_data(ROUTE); let p = RIOT_API.lol_status_v4().get_platform_data(ROUTE);

View file

@ -23,7 +23,7 @@ pub async fn league_v4_match_v5_latest_combo(route: PlatformRoute) -> Result<(),
.get_challenger_league(route, QueueType::RANKED_SOLO_5x5); .get_challenger_league(route, QueueType::RANKED_SOLO_5x5);
let challenger_league = challenger_future.await.map_err(|e| e.to_string())?; let challenger_league = challenger_future.await.map_err(|e| e.to_string())?;
if &QueueType::RANKED_SOLO_5x5 != &challenger_league.queue { if QueueType::RANKED_SOLO_5x5 != challenger_league.queue {
return Err(format!("Unexpected `queue`: {}", challenger_league.queue)); return Err(format!("Unexpected `queue`: {}", challenger_league.queue));
} }
if challenger_league.entries.is_empty() { if challenger_league.entries.is_empty() {
@ -42,7 +42,7 @@ pub async fn league_v4_match_v5_latest_combo(route: PlatformRoute) -> Result<(),
let match_ids_future = RIOT_API.match_v5().get_match_ids_by_puuid( let match_ids_future = RIOT_API.match_v5().get_match_ids_by_puuid(
route.to_regional(), route.to_regional(),
&*summoner_info.puuid, &summoner_info.puuid,
Some(5), Some(5),
None, None,
None, None,