2019-10-14 06:38:22 +00:00
|
|
|
use std::collections::HashMap;
|
|
|
|
use std::future::Future;
|
2019-10-14 08:00:20 +00:00
|
|
|
use parking_lot::{
|
|
|
|
RwLock,
|
|
|
|
RwLockReadGuard,
|
|
|
|
RwLockWriteGuard,
|
|
|
|
MappedRwLockReadGuard,
|
|
|
|
MappedRwLockWriteGuard,
|
|
|
|
};
|
2019-10-14 06:38:22 +00:00
|
|
|
|
|
|
|
use async_std::task;
|
|
|
|
use reqwest::{
|
|
|
|
Client,
|
|
|
|
StatusCode,
|
|
|
|
};
|
|
|
|
use serde::de::DeserializeOwned;
|
|
|
|
|
|
|
|
use super::rate_limit::RateLimit;
|
|
|
|
use super::rate_limit_type::RateLimitType;
|
|
|
|
use crate::riot_api_config::RiotApiConfig;
|
|
|
|
use crate::consts::region::Region;
|
|
|
|
|
|
|
|
pub struct RegionalRequester<'a> {
|
|
|
|
/// Configuration settings.
|
|
|
|
riot_api_config: &'a RiotApiConfig<'a>,
|
|
|
|
/// Client for making requests.
|
|
|
|
client: &'a Client,
|
|
|
|
|
|
|
|
/// Represents the app rate limit.
|
|
|
|
app_rate_limit: RateLimit,
|
|
|
|
/// Represents method rate limits.
|
2019-10-14 08:00:20 +00:00
|
|
|
method_rate_limits: RwLock<HashMap<&'a str, RateLimit>>,
|
2019-10-14 06:38:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl <'a> RegionalRequester<'a> {
|
|
|
|
/// Request header name for the Riot API key.
|
|
|
|
const RIOT_KEY_HEADER: &'static str = "X-Riot-Token";
|
|
|
|
|
|
|
|
/// HttpStatus codes that are considered a success, but will return None.
|
|
|
|
const NONE_STATUS_CODES: [u16; 3] = [ 204, 404, 422 ];
|
|
|
|
|
|
|
|
|
|
|
|
pub fn new(riot_api_config: &'a RiotApiConfig<'a>, client: &'a Client) -> RegionalRequester<'a> {
|
|
|
|
RegionalRequester {
|
|
|
|
riot_api_config: riot_api_config,
|
|
|
|
client: client,
|
|
|
|
app_rate_limit: RateLimit::new(RateLimitType::Application),
|
2019-10-14 08:00:20 +00:00
|
|
|
method_rate_limits: RwLock::new(HashMap::new()),
|
2019-10-14 06:38:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn get<T: DeserializeOwned>(
|
|
|
|
&mut self, method_id: &'a str, relative_url: &'_ str,
|
|
|
|
region: &'_ Region<'_>, query: &[(&'_ str, &'_ str)]) -> Result<Option<T>, reqwest::Error> {
|
|
|
|
|
|
|
|
let mut attempts: u8 = 0;
|
|
|
|
for _ in 0..=self.riot_api_config.retries {
|
|
|
|
attempts += 1;
|
|
|
|
|
|
|
|
// Rate limiting.
|
2019-10-14 08:00:20 +00:00
|
|
|
while let Some(delay) = {
|
|
|
|
let method_rate_limit = &self.get_insert_rate_limit(method_id);
|
|
|
|
RateLimit::get_both_or_delay(&self.app_rate_limit, method_rate_limit)
|
|
|
|
} {
|
2019-10-14 06:38:22 +00:00
|
|
|
task::sleep(delay).await;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Send request.
|
|
|
|
let url = &*format!("https://{}.api.riotgames.com{}", region.platform, relative_url);
|
|
|
|
let result = self.client.get(url)
|
|
|
|
.header(Self::RIOT_KEY_HEADER, self.riot_api_config.api_key)
|
|
|
|
.query(query)
|
|
|
|
.send()
|
|
|
|
.await;
|
|
|
|
|
|
|
|
let response = match result {
|
|
|
|
Err(e) => return Err(e),
|
|
|
|
Ok(r) => r,
|
|
|
|
};
|
|
|
|
|
|
|
|
// Update rate limits (if needed).
|
2019-10-14 08:00:20 +00:00
|
|
|
{
|
|
|
|
self.app_rate_limit.on_response(&response);
|
|
|
|
self.method_rate_limits.read().get(method_id).unwrap().on_response(&response);
|
|
|
|
}
|
2019-10-14 06:38:22 +00:00
|
|
|
|
|
|
|
// Handle response.
|
|
|
|
let status = response.status();
|
|
|
|
// Success, return None.
|
|
|
|
if Self::is_none_status_code(&status) {
|
|
|
|
return Ok(None);
|
|
|
|
}
|
|
|
|
// Success, return a value.
|
|
|
|
if status.is_success() {
|
|
|
|
let value = response.json::<T>().await;
|
|
|
|
return match value {
|
|
|
|
Err(e) => Err(e),
|
|
|
|
Ok(v) => Ok(Some(v)),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Retryable.
|
|
|
|
if StatusCode::TOO_MANY_REQUESTS == status || status.is_server_error() {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
// Failure (non-retryable).
|
|
|
|
if status.is_client_error() {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
panic!("NOT HANDLED: {}!", status);
|
|
|
|
}
|
|
|
|
// TODO: return error.
|
|
|
|
panic!("FAILED AFTER {} ATTEMPTS!", attempts);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get2<T: 'a + DeserializeOwned>(&'a mut self, method_id: &'a str, relative_url: &'a str,
|
|
|
|
region: &'a Region<'_>, query: &'a [(&'a str, &'a str)]) -> impl Future<Output = Result<Option<T>, reqwest::Error>> + 'a {
|
|
|
|
|
|
|
|
self.get(method_id, relative_url, region, query)
|
|
|
|
}
|
|
|
|
|
2019-10-14 08:00:20 +00:00
|
|
|
fn get_insert_rate_limit(&self, method_id: &'a str) -> MappedRwLockReadGuard<RateLimit> {
|
|
|
|
// This is really stupid?
|
|
|
|
{
|
|
|
|
let map_guard = self.method_rate_limits.read();
|
|
|
|
if map_guard.contains_key(method_id) {
|
|
|
|
return RwLockReadGuard::map(map_guard, |mrl| mrl.get(method_id).unwrap());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let map_guard = self.method_rate_limits.write();
|
|
|
|
let val_write = RwLockWriteGuard::map(
|
|
|
|
map_guard, |mrl| mrl.entry(method_id)
|
|
|
|
.or_insert(RateLimit::new(RateLimitType::Method))
|
|
|
|
);
|
|
|
|
MappedRwLockWriteGuard::downgrade(val_write)
|
|
|
|
}
|
|
|
|
|
2019-10-14 06:38:22 +00:00
|
|
|
fn is_none_status_code(status: &StatusCode) -> bool {
|
|
|
|
Self::NONE_STATUS_CODES.contains(&status.as_u16())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
fn send_sync() {
|
|
|
|
fn is_send_sync<T: Send + Sync>() {}
|
|
|
|
is_send_sync::<RegionalRequester>();
|
|
|
|
}
|
|
|
|
}
|