mirror of https://github.com/MingweiSamuel/Riven
Restructure, add RiotApi.get_raw_response(...) method useful for proxies.
New RiotApi.get_raw_response(...) method similar to existing RiotApi.get(...) and RiotApi.get_optional(...), but does not parse the result. Useful for proxies which just want to forward the content body. Also adds tests to JP of failure cases.pull/16/head
parent
ea1af34854
commit
14e6faa24e
|
@ -24,6 +24,9 @@ pub mod models;
|
||||||
|
|
||||||
mod req;
|
mod req;
|
||||||
|
|
||||||
|
mod response_info;
|
||||||
|
pub use response_info::*;
|
||||||
|
|
||||||
mod riot_api;
|
mod riot_api;
|
||||||
pub use riot_api::*;
|
pub use riot_api::*;
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ use reqwest::{ Client, StatusCode, Url };
|
||||||
use tokio::time::delay_for;
|
use tokio::time::delay_for;
|
||||||
|
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
|
use crate::ResponseInfo;
|
||||||
use crate::RiotApiError;
|
use crate::RiotApiError;
|
||||||
use crate::RiotApiConfig;
|
use crate::RiotApiConfig;
|
||||||
use crate::util::InsertOnlyCHashMap;
|
use crate::util::InsertOnlyCHashMap;
|
||||||
|
@ -14,9 +15,9 @@ use super::RateLimit;
|
||||||
use super::RateLimitType;
|
use super::RateLimitType;
|
||||||
|
|
||||||
pub struct RegionalRequester {
|
pub struct RegionalRequester {
|
||||||
/// Represents the app rate limit.
|
/// The app rate limit.
|
||||||
app_rate_limit: RateLimit,
|
app_rate_limit: RateLimit,
|
||||||
/// Represents method rate limits.
|
/// Method rate limits.
|
||||||
method_rate_limits: InsertOnlyCHashMap<&'static str, RateLimit>,
|
method_rate_limits: InsertOnlyCHashMap<&'static str, RateLimit>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,29 +38,10 @@ impl RegionalRequester {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_optional<'a, T: serde::de::DeserializeOwned>(self: Arc<Self>,
|
pub fn get<'a>(self: Arc<Self>,
|
||||||
config: &'a RiotApiConfig, client: &'a Client,
|
config: &'a RiotApiConfig, client: &'a Client,
|
||||||
method_id: &'static str, region_platform: &'a str, path: String, query: Option<String>)
|
method_id: &'static str, region_platform: &'a str, path: String, query: Option<String>)
|
||||||
-> impl Future<Output = Result<Option<T>>> + 'a
|
-> impl Future<Output = Result<ResponseInfo>> + 'a
|
||||||
{
|
|
||||||
async move {
|
|
||||||
let response_result = self.get(config, client,
|
|
||||||
method_id, region_platform, path, query).await;
|
|
||||||
response_result.map(|value| Some(value))
|
|
||||||
.or_else(|e| {
|
|
||||||
if let Some(status) = e.status_code() {
|
|
||||||
if Self::NONE_STATUS_CODES.contains(&status) {
|
|
||||||
return Ok(None);
|
|
||||||
}}
|
|
||||||
Err(e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get<'a, T: serde::de::DeserializeOwned>(self: Arc<Self>,
|
|
||||||
config: &'a RiotApiConfig, client: &'a Client,
|
|
||||||
method_id: &'static str, region_platform: &'a str, path: String, query: Option<String>)
|
|
||||||
-> impl Future<Output = Result<T>> + 'a
|
|
||||||
{
|
{
|
||||||
async move {
|
async move {
|
||||||
#[cfg(feature = "nightly")] let query = query.as_deref();
|
#[cfg(feature = "nightly")] let query = query.as_deref();
|
||||||
|
@ -94,27 +76,29 @@ impl RegionalRequester {
|
||||||
|
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
// Handle normal success / failure cases.
|
// Handle normal success / failure cases.
|
||||||
match response.error_for_status_ref() {
|
let status_none = Self::NONE_STATUS_CODES.contains(&status);
|
||||||
// Success.
|
// Success case.
|
||||||
Ok(_response) => {
|
if status.is_success() || status_none {
|
||||||
log::trace!("Response {} (retried {} times), parsed result.", status, retries);
|
log::trace!("Response {} (retried {} times), success, returning result.", status, retries);
|
||||||
let value = response.json::<T>().await;
|
break Ok(ResponseInfo {
|
||||||
break value.map_err(|e| RiotApiError::new(e, retries, None, Some(status)));
|
response: response,
|
||||||
},
|
retries: retries,
|
||||||
|
status_none: status_none,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let err = 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.
|
// Failure, may or may not be retryable.
|
||||||
Err(err) => {
|
|
||||||
// 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), returning.", status, retries);
|
log::debug!("Response {} (retried {} times), failure, returning error.", status, retries);
|
||||||
break Err(RiotApiError::new(err, retries, Some(response), Some(status)));
|
break Err(RiotApiError::new(err, retries, Some(response), Some(status)));
|
||||||
}
|
}
|
||||||
log::debug!("Response {} (retried {} times), retrying.", status, retries);
|
log::debug!("Response {} (retried {} times), retrying.", status, retries);
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
retries += 1;
|
retries += 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
use reqwest::Response;
|
||||||
|
|
||||||
|
/// A "raw" unparsed successful response from the Riot API, for internal or advanced use cases.
|
||||||
|
pub struct ResponseInfo {
|
||||||
|
/// The reqwest response.
|
||||||
|
pub response: Response,
|
||||||
|
/// The number of retries used, zero for first-try success.
|
||||||
|
pub retries: u8,
|
||||||
|
/// If the response has an HTTP status code indicating a `None` response (i.e. 204, 404).
|
||||||
|
pub status_none: bool,
|
||||||
|
}
|
|
@ -5,7 +5,9 @@ use log;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
|
use crate::ResponseInfo;
|
||||||
use crate::RiotApiConfig;
|
use crate::RiotApiConfig;
|
||||||
|
use crate::RiotApiError;
|
||||||
use crate::req::RegionalRequester;
|
use crate::req::RegionalRequester;
|
||||||
use crate::util::InsertOnlyCHashMap;
|
use crate::util::InsertOnlyCHashMap;
|
||||||
|
|
||||||
|
@ -74,12 +76,22 @@ impl RiotApi {
|
||||||
/// * `region_platform` - The stringified platform, prepended to `.api.riotgames.com` to create the hostname.
|
/// * `region_platform` - The stringified platform, prepended to `.api.riotgames.com` to create the hostname.
|
||||||
/// * `path` - The path relative to the hostname.
|
/// * `path` - The path relative to the hostname.
|
||||||
/// * `query` - An optional query string.
|
/// * `query` - An optional query string.
|
||||||
pub fn get_optional<'a, T: serde::de::DeserializeOwned + 'a>(&'a self,
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A future resolving to a `Result` containg either a `Option<T>` (success) or a `RiotApiError` (failure).
|
||||||
|
pub async fn get_optional<'a, T: serde::de::DeserializeOwned + 'a>(&'a self,
|
||||||
method_id: &'static str, region_platform: &'static str, path: String, query: Option<String>)
|
method_id: &'static str, region_platform: &'static str, path: String, query: Option<String>)
|
||||||
-> impl Future<Output = Result<Option<T>>> + 'a
|
-> Result<Option<T>>
|
||||||
{
|
{
|
||||||
self.regional_requester(region_platform)
|
let rinfo = self.get_raw_response(method_id, region_platform, path, query).await?;
|
||||||
.get_optional(&self.config, &self.client, method_id, region_platform, path, query)
|
if rinfo.status_none {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let retries = rinfo.retries;
|
||||||
|
let status = rinfo.response.status();
|
||||||
|
let value = rinfo.response.json::<T>().await;
|
||||||
|
value.map(|v| Some(v))
|
||||||
|
.map_err(|e| RiotApiError::new(e, retries, None, Some(status)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This method is not meant to be used directly.
|
/// This method is not meant to be used directly.
|
||||||
|
@ -91,9 +103,37 @@ impl RiotApi {
|
||||||
/// * `region_platform` - The stringified platform, prepended to `.api.riotgames.com` to create the hostname.
|
/// * `region_platform` - The stringified platform, prepended to `.api.riotgames.com` to create the hostname.
|
||||||
/// * `path` - The path relative to the hostname.
|
/// * `path` - The path relative to the hostname.
|
||||||
/// * `query` - An optional query string.
|
/// * `query` - An optional query string.
|
||||||
pub fn get<'a, T: serde::de::DeserializeOwned + 'a>(&'a self,
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A future resolving to a `Result` containg either a `T` (success) or a `RiotApiError` (failure).
|
||||||
|
pub async fn get<'a, T: serde::de::DeserializeOwned + 'a>(&'a self,
|
||||||
method_id: &'static str, region_platform: &'static str, path: String, query: Option<String>)
|
method_id: &'static str, region_platform: &'static str, path: String, query: Option<String>)
|
||||||
-> impl Future<Output = Result<T>> + 'a
|
-> Result<T>
|
||||||
|
{
|
||||||
|
let rinfo = self.get_raw_response(method_id, region_platform, path, query).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)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This method is not meant to be used directly.
|
||||||
|
///
|
||||||
|
/// This sends a GET request based on the given parameters and returns a raw `ResponseInfo`.
|
||||||
|
///
|
||||||
|
/// This can be used to implement a Riot API proxy without needing to deserialize and reserialize JSON responses.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// * `method_id` - A unique string id representing the endpoint method for per-method rate limiting.
|
||||||
|
/// * `region_platform` - The stringified platform, prepended to `.api.riotgames.com` to create the hostname.
|
||||||
|
/// * `path` - The path relative to the hostname.
|
||||||
|
/// * `query` - An optional query string.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A future resolving to a `Result` containg either a `ResponseInfo` (success) or a `RiotApiError` (failure).
|
||||||
|
pub fn get_raw_response<'a>(&'a self,
|
||||||
|
method_id: &'static str, region_platform: &'static str, path: String, query: Option<String>)
|
||||||
|
-> impl Future<Output = Result<ResponseInfo>> + 'a
|
||||||
{
|
{
|
||||||
self.regional_requester(region_platform)
|
self.regional_requester(region_platform)
|
||||||
.get(&self.config, &self.client, method_id, region_platform, path, query)
|
.get(&self.config, &self.client, method_id, region_platform, path, query)
|
||||||
|
|
|
@ -9,6 +9,7 @@ use colored::*;
|
||||||
|
|
||||||
use riven::consts::*;
|
use riven::consts::*;
|
||||||
|
|
||||||
|
|
||||||
async_tests!{
|
async_tests!{
|
||||||
my_runner {
|
my_runner {
|
||||||
// Summoner tests.
|
// Summoner tests.
|
||||||
|
@ -18,5 +19,32 @@ async_tests!{
|
||||||
rassert_eq!("私の頭がかたい", s.name);
|
rassert_eq!("私の頭がかたい", s.name);
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Failure cases.
|
||||||
|
// Make sure get_raw_response(...) with invalid path fails as expected.
|
||||||
|
raw_response_invalid: async {
|
||||||
|
let p = RIOT_API.get_raw_response("summoner-v4.getBySummonerName", Region::JP.into(), "INVALID/PATH".to_owned(), None);
|
||||||
|
let r = p.await;
|
||||||
|
rassert!(r.is_err());
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
// summoner_v4().get_by_summoner_name(...) normally returns an option.
|
||||||
|
// If we use `get` (instead of `get_optional`) make sure it errors.
|
||||||
|
get_nonoptional_invalid: async {
|
||||||
|
let path_string = format!("/lol/summoner/v4/summoners/by-name/{}", "SUMMONER THAT DOES NOT EXIST");
|
||||||
|
let p = RIOT_API.get::<riven::models::summoner_v4::Summoner>(
|
||||||
|
"summoner-v4.getBySummonerName", Region::JP.into(), path_string, None);
|
||||||
|
let r = p.await;
|
||||||
|
rassert!(r.is_err());
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
// Make sure 403 is handled as expected.
|
||||||
|
tournament_forbidden: async {
|
||||||
|
let p = RIOT_API.tournament_v4().get_tournament_code(Region::JP, "INVALID_CODE");
|
||||||
|
let r = p.await;
|
||||||
|
rassert!(r.is_err());
|
||||||
|
rassert_eq!(Some(reqwest::StatusCode::FORBIDDEN), r.unwrap_err().status_code());
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue