2019-10-22 04:45:38 +00:00
|
|
|
use std::future::Future;
|
2019-10-15 04:29:32 +00:00
|
|
|
use std::sync::Arc;
|
2019-10-14 06:38:22 +00:00
|
|
|
|
2019-10-19 21:39:53 +00:00
|
|
|
use reqwest::{ Client, StatusCode, Url };
|
2019-10-20 07:54:01 +00:00
|
|
|
use tokio_timer::delay_for;
|
2019-10-14 06:38:22 +00:00
|
|
|
|
|
|
|
use crate::riot_api_config::RiotApiConfig;
|
2019-10-17 21:44:26 +00:00
|
|
|
use crate::consts::Region;
|
|
|
|
use crate::util::InsertOnlyCHashMap;
|
|
|
|
|
|
|
|
use super::RateLimit;
|
|
|
|
use super::RateLimitType;
|
2019-10-14 06:38:22 +00:00
|
|
|
|
2019-10-22 04:45:38 +00:00
|
|
|
pub struct RegionalRequester {
|
|
|
|
|
|
|
|
|
2019-10-14 06:38:22 +00:00
|
|
|
|
|
|
|
/// Represents the app rate limit.
|
|
|
|
app_rate_limit: RateLimit,
|
|
|
|
/// Represents method rate limits.
|
2019-10-22 04:45:38 +00:00
|
|
|
method_rate_limits: InsertOnlyCHashMap<&'static str, RateLimit>,
|
2019-10-14 06:38:22 +00:00
|
|
|
}
|
|
|
|
|
2019-10-22 04:45:38 +00:00
|
|
|
impl RegionalRequester {
|
2019-10-14 06:38:22 +00:00
|
|
|
/// 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 ];
|
|
|
|
|
|
|
|
|
2019-10-22 04:45:38 +00:00
|
|
|
pub fn new() -> Self {
|
2019-10-15 04:29:32 +00:00
|
|
|
Self {
|
2019-10-14 06:38:22 +00:00
|
|
|
app_rate_limit: RateLimit::new(RateLimitType::Application),
|
2019-10-17 21:44:26 +00:00
|
|
|
method_rate_limits: InsertOnlyCHashMap::new(),
|
2019-10-14 06:38:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-22 04:45:38 +00:00
|
|
|
pub fn get<'a, T: serde::de::DeserializeOwned>(self: Arc<Self>,
|
|
|
|
config: &'a RiotApiConfig, client: &'a Client,
|
2019-10-22 18:16:17 +00:00
|
|
|
method_id: &'static str, region: Region, path: String, query: Option<String>)
|
2019-10-22 04:45:38 +00:00
|
|
|
-> impl Future<Output = Result<Option<T>, reqwest::Error>> + 'a
|
2019-10-17 21:44:26 +00:00
|
|
|
{
|
2019-10-22 04:45:38 +00:00
|
|
|
async move {
|
2019-10-22 18:16:17 +00:00
|
|
|
let query = query.as_deref();
|
2019-10-14 06:38:22 +00:00
|
|
|
|
2019-10-22 04:45:38 +00:00
|
|
|
let mut attempts: u8 = 0;
|
|
|
|
for _ in 0..=config.retries {
|
|
|
|
attempts += 1;
|
2019-10-17 21:44:26 +00:00
|
|
|
|
2019-10-22 04:45:38 +00:00
|
|
|
let method_rate_limit: Arc<RateLimit> = self.method_rate_limits
|
|
|
|
.get_or_insert_with(method_id, || RateLimit::new(RateLimitType::Method));
|
2019-10-14 06:38:22 +00:00
|
|
|
|
2019-10-22 04:45:38 +00:00
|
|
|
// Rate limiting.
|
|
|
|
while let Some(delay) = RateLimit::get_both_or_delay(&self.app_rate_limit, &*method_rate_limit) {
|
|
|
|
delay_for(delay).await;
|
2019-10-14 06:38:22 +00:00
|
|
|
}
|
2019-10-22 04:45:38 +00:00
|
|
|
|
|
|
|
// Send request.
|
|
|
|
let url_base = format!("https://{}.api.riotgames.com", region.platform);
|
|
|
|
let mut url = Url::parse(&*url_base)
|
|
|
|
.unwrap_or_else(|_| panic!("Failed to parse url_base: \"{}\".", url_base));
|
|
|
|
url.set_path(&*path);
|
|
|
|
url.set_query(query);
|
|
|
|
|
|
|
|
let result = client.get(url)
|
|
|
|
.header(Self::RIOT_KEY_HEADER, &*config.api_key)
|
|
|
|
.send()
|
|
|
|
.await;
|
|
|
|
|
|
|
|
let response = match result {
|
|
|
|
Err(e) => return Err(e),
|
|
|
|
Ok(r) => r,
|
|
|
|
};
|
|
|
|
|
|
|
|
// Maybe update rate limits (based on response headers).
|
|
|
|
self.app_rate_limit.on_response(&response);
|
|
|
|
method_rate_limit.on_response(&response);
|
|
|
|
|
|
|
|
// 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);
|
2019-10-14 06:38:22 +00:00
|
|
|
}
|
2019-10-22 04:45:38 +00:00
|
|
|
// TODO: return error.
|
|
|
|
panic!("FAILED AFTER {} ATTEMPTS!", attempts);
|
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 {
|
2019-10-18 20:18:26 +00:00
|
|
|
// use super::*;
|
2019-10-14 06:38:22 +00:00
|
|
|
}
|