Convert CRLF to LF

pull/27/head
Mingwei Samuel 2021-06-30 16:34:34 -07:00
parent 2989c4483e
commit 6307a0aa13
42 changed files with 4178 additions and 4178 deletions

12
LICENSE
View File

@ -1,7 +1,7 @@
Copyright 2019 Mingwei Samuel
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
Copyright 2019 Mingwei Samuel
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

284
README.md
View File

@ -1,142 +1,142 @@
<h1 align="center">
Riven<br>
</h1>
<p align="center">
<a href="https://github.com/MingweiSamuel/Riven/"><img src="https://cdn.communitydragon.org/latest/champion/Riven/square" width="20" height="20" alt="Riven Github"></a>
<a href="https://crates.io/crates/riven"><img src="https://img.shields.io/crates/v/riven?style=flat-square&logo=rust" alt="Crates.io"></a>
<a href="https://docs.rs/riven/"><img src="https://img.shields.io/badge/docs.rs-Riven-blue?style=flat-square&logo=read-the-docs&logoColor=white" alt="Docs.rs"></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>
</p>
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.
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/)).
## Design
* Fast, asynchronous, thread-safe.
* Automatically retries failed requests.
* TFT API Support.
## Usage
```rust
use riven::RiotApi;
use riven::consts::PlatformRoute;
// Enter tokio async runtime.
let mut rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
// Create RiotApi instance from key string.
let api_key = "RGAPI-01234567-89ab-cdef-0123-456789abcdef";
# /* (doc testing) */ let api_key = std::env!("RGAPI_KEY");
let riot_api = RiotApi::with_key(api_key);
// Get summoner data.
let summoner = riot_api.summoner_v4()
.get_by_summoner_name(PlatformRoute::NA1, "잘 못").await
.expect("Get summoner failed.")
.expect("There is no summoner with that name.");
// Print summoner name.
println!("{} Champion Masteries:", summoner.name);
// Get champion mastery data.
let masteries = riot_api.champion_mastery_v4()
.get_all_champion_masteries(PlatformRoute::NA1, &summoner.id).await
.expect("Get champion masteries failed.");
// Print champioon masteries.
for (i, mastery) in masteries.iter().take(10).enumerate() {
println!("{: >2}) {: <9} {: >7} ({})", i + 1,
mastery.champion_id.to_string(),
mastery.champion_points, mastery.champion_level);
}
});
```
Output:
```text
잘 못 Champion Masteries:
1) Riven 1219895 (7)
2) Fiora 229714 (5)
3) Katarina 175985 (5)
4) Lee Sin 150546 (7)
5) Jax 100509 (5)
6) Gnar 76373 (6)
7) Kai'Sa 64271 (5)
8) Caitlyn 46479 (5)
9) Irelia 46465 (5)
10) Vladimir 37176 (5)
```
### Nightly vs Stable
Enable the `nightly` feature to use nightly-only functionality. Mainly enables
[nightly optimizations in the `parking_lot` crate](https://github.com/Amanieu/parking_lot#nightly-vs-stable).
Also required for running async integration tests.
### Docs
[On docs.rs](https://docs.rs/riven/).
### Error Handling
Riven returns either `Result<T>` or `Result<Option<T>>` within futures.
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,
incorrect API key, etc.
If the `Option` is `None`, this indicates that the request completed
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
spectator data for a summoner who is not in-game.
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
diagnostic information, such as the source Reqwest error, the number of retries
attempted, and the Reqwest `Response` object.
You can configure the number of time Riven retries using
`RiotApiConfig::set_retries(...)` and the `RiotApi::with_config(config)`
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
inevitably fail again.
### Semantic Versioning
This package follows semantic versioning to an extent. However, the Riot API
itself changes often and does not follow semantic versioning, which makes
things difficult. Out-of-date versions will slowly partially cease to work due
to this.
When the API changes, this may result in breaking changes in the `models`
module, `endpoints` module, and some of the `consts` module. "Handle accessor"
methods may be removed from `RiotApi` if the corresponding endpoint is removed
from the Riot API. These breaking changes will increment the **MINOR** version,
not the major version.
Parts of Riven that do not depend on Riot API changes do follow semantic
versioning.
### Additional Help
Feel free to [make an issue](https://github.com/MingweiSamuel/Riven/issues/new)
if you are have any questions or trouble with Riven.
## Development
NodeJS is used to generate code for Riven. The
[`srcgen/`](https://github.com/MingweiSamuel/Riven/tree/master/srcgen)
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
code.
To set up the srcgen, you will first need to install NodeJS. Then enter the
srcgen folder and run `npm ci` (or `npm install`) to install dependencies.
To run the srcgen use `node srcgen` from the main folder.
<h1 align="center">
Riven<br>
</h1>
<p align="center">
<a href="https://github.com/MingweiSamuel/Riven/"><img src="https://cdn.communitydragon.org/latest/champion/Riven/square" width="20" height="20" alt="Riven Github"></a>
<a href="https://crates.io/crates/riven"><img src="https://img.shields.io/crates/v/riven?style=flat-square&logo=rust" alt="Crates.io"></a>
<a href="https://docs.rs/riven/"><img src="https://img.shields.io/badge/docs.rs-Riven-blue?style=flat-square&logo=read-the-docs&logoColor=white" alt="Docs.rs"></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>
</p>
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.
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/)).
## Design
* Fast, asynchronous, thread-safe.
* Automatically retries failed requests.
* TFT API Support.
## Usage
```rust
use riven::RiotApi;
use riven::consts::PlatformRoute;
// Enter tokio async runtime.
let mut rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
// Create RiotApi instance from key string.
let api_key = "RGAPI-01234567-89ab-cdef-0123-456789abcdef";
# /* (doc testing) */ let api_key = std::env!("RGAPI_KEY");
let riot_api = RiotApi::with_key(api_key);
// Get summoner data.
let summoner = riot_api.summoner_v4()
.get_by_summoner_name(PlatformRoute::NA1, "잘 못").await
.expect("Get summoner failed.")
.expect("There is no summoner with that name.");
// Print summoner name.
println!("{} Champion Masteries:", summoner.name);
// Get champion mastery data.
let masteries = riot_api.champion_mastery_v4()
.get_all_champion_masteries(PlatformRoute::NA1, &summoner.id).await
.expect("Get champion masteries failed.");
// Print champioon masteries.
for (i, mastery) in masteries.iter().take(10).enumerate() {
println!("{: >2}) {: <9} {: >7} ({})", i + 1,
mastery.champion_id.to_string(),
mastery.champion_points, mastery.champion_level);
}
});
```
Output:
```text
잘 못 Champion Masteries:
1) Riven 1219895 (7)
2) Fiora 229714 (5)
3) Katarina 175985 (5)
4) Lee Sin 150546 (7)
5) Jax 100509 (5)
6) Gnar 76373 (6)
7) Kai'Sa 64271 (5)
8) Caitlyn 46479 (5)
9) Irelia 46465 (5)
10) Vladimir 37176 (5)
```
### Nightly vs Stable
Enable the `nightly` feature to use nightly-only functionality. Mainly enables
[nightly optimizations in the `parking_lot` crate](https://github.com/Amanieu/parking_lot#nightly-vs-stable).
Also required for running async integration tests.
### Docs
[On docs.rs](https://docs.rs/riven/).
### Error Handling
Riven returns either `Result<T>` or `Result<Option<T>>` within futures.
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,
incorrect API key, etc.
If the `Option` is `None`, this indicates that the request completed
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
spectator data for a summoner who is not in-game.
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
diagnostic information, such as the source Reqwest error, the number of retries
attempted, and the Reqwest `Response` object.
You can configure the number of time Riven retries using
`RiotApiConfig::set_retries(...)` and the `RiotApi::with_config(config)`
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
inevitably fail again.
### Semantic Versioning
This package follows semantic versioning to an extent. However, the Riot API
itself changes often and does not follow semantic versioning, which makes
things difficult. Out-of-date versions will slowly partially cease to work due
to this.
When the API changes, this may result in breaking changes in the `models`
module, `endpoints` module, and some of the `consts` module. "Handle accessor"
methods may be removed from `RiotApi` if the corresponding endpoint is removed
from the Riot API. These breaking changes will increment the **MINOR** version,
not the major version.
Parts of Riven that do not depend on Riot API changes do follow semantic
versioning.
### Additional Help
Feel free to [make an issue](https://github.com/MingweiSamuel/Riven/issues/new)
if you are have any questions or trouble with Riven.
## Development
NodeJS is used to generate code for Riven. The
[`srcgen/`](https://github.com/MingweiSamuel/Riven/tree/master/srcgen)
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
code.
To set up the srcgen, you will first need to install NodeJS. Then enter the
srcgen folder and run `npm ci` (or `npm install`) to install dependencies.
To run the srcgen use `node srcgen` from the main folder.

View File

@ -1,213 +1,213 @@
//! Configuration of RiotApi.
use std::time::Duration;
use reqwest::ClientBuilder;
use reqwest::header::{ HeaderMap, HeaderValue };
/// Configuration for instantiating RiotApi.
///
///
#[derive(Debug)]
pub struct RiotApiConfig {
pub(crate) base_url: String,
pub(crate) retries: u8,
pub(crate) burst_pct: f32,
pub(crate) duration_overhead: Duration,
pub(crate) client_builder: Option<ClientBuilder>,
}
impl RiotApiConfig {
/// Request header name for the Riot API key.
///
/// When using `set_client_builder`, the supplied builder should include
/// this default header with the Riot API key as the value.
const RIOT_KEY_HEADER: &'static str = "X-Riot-Token";
/// `"https://{}.api.riotgames.com"`
///
/// Default base URL, including `{}` placeholder for region platform.
pub const DEFAULT_BASE_URL: &'static str = "https://{}.api.riotgames.com";
/// `3`
///
/// Default number of retries.
pub const DEFAULT_RETRIES: u8 = 3;
/// `0.99`
///
/// Default `burst_pct`, also used by `preconfig_burst`.
pub const PRECONFIG_BURST_BURST_PCT: f32 = 0.99;
/// `989` ms
///
/// Default `duration_overhead`, also used by `preconfig_burst`.
pub const PRECONFIG_BURST_DURATION_OVERHEAD: Duration = Duration::from_millis(989);
/// `0.47`
///
/// `burst_pct` used by `preconfig_throughput`.
pub const PRECONFIG_THROUGHPUT_BURST_PCT: f32 = 0.47;
/// `10` ms.
///
/// `duration_overhead` used by `preconfig_throughput`.
pub const PRECONFIG_THROUGHPUT_DURATION_OVERHEAD: Duration = Duration::from_millis(10);
/// Creates a new `RiotApiConfig` with the given `api_key` with the following
/// configuration:
///
/// * `retries = 3` (`RiotApiConfig::DEFAULT_RETRIES`).
/// * `purst_pct = 0.99` (`preconfig_burst`).
/// * `duration_overhead = 989 ms` (`preconfig_burst`).
///
/// `api_key` should be a Riot Games API key from
/// [https://developer.riotgames.com/](https://developer.riotgames.com/),
/// and should look like `"RGAPI-01234567-89ab-cdef-0123-456789abcdef"`.
pub fn with_key(api_key: impl AsRef<[u8]>) -> Self {
let mut default_headers = HeaderMap::new();
default_headers.insert(
Self::RIOT_KEY_HEADER,
HeaderValue::from_bytes(api_key.as_ref()).unwrap()
);
Self {
base_url: Self::DEFAULT_BASE_URL.into(),
retries: Self::DEFAULT_RETRIES,
burst_pct: Self::PRECONFIG_BURST_BURST_PCT,
duration_overhead: Self::PRECONFIG_BURST_DURATION_OVERHEAD,
client_builder: Some(
ClientBuilder::new()
.default_headers(default_headers)
),
}
}
/// Creates a new `RiotApiConfig` with the given client builder.
///
/// The client builder default headers should include a value for
/// `RiotApiConfig::RIOT_KEY_HEADER`, otherwise authentication will fail.
///
/// * `retries = 3` (`RiotApiConfig::DEFAULT_RETRIES`).
/// * `purst_pct = 0.99` (`preconfig_burst`).
/// * `duration_overhead = 989 ms` (`preconfig_burst`).
pub fn with_client_builder(client_builder: ClientBuilder) -> Self {
Self {
base_url: Self::DEFAULT_BASE_URL.to_owned(),
retries: Self::DEFAULT_RETRIES,
burst_pct: Self::PRECONFIG_BURST_BURST_PCT,
duration_overhead: Self::PRECONFIG_BURST_DURATION_OVERHEAD,
client_builder: Some(client_builder),
}
}
/// Sets rate limiting settings to preconfigured values optimized for burst,
/// low latency:
///
/// * `burst_pct = 0.99` (`PRECONFIG_BURST_BURST_PCT`).
/// * `duration_overhead = 989 ms` (`PRECONFIG_BURST_DURATION_OVERHEAD_MILLIS`).
///
/// # Returns
/// `self`, for chaining.
pub fn preconfig_burst(mut self) -> Self {
self.burst_pct = Self::PRECONFIG_BURST_BURST_PCT;
self.duration_overhead = Self::PRECONFIG_BURST_DURATION_OVERHEAD;
self
}
/// Sets the rate limiting settings to preconfigured values optimized for
/// high throughput:
///
/// * `burst_pct = 0.47` (`PRECONFIG_THROUGHPUT_BURST_PCT`).
/// * `duration_overhead = 10 ms` (`PRECONFIG_THROUGHPUT_DURATION_OVERHEAD_MILLIS`).
///
/// # Returns
/// `self`, for chaining.
pub fn preconfig_throughput(mut self) -> Self {
self.burst_pct = Self::PRECONFIG_THROUGHPUT_BURST_PCT;
self.duration_overhead = Self::PRECONFIG_THROUGHPUT_DURATION_OVERHEAD;
self
}
/// Set the base url for requests. The string should contain a `"{}"`
/// literal which will be replaced with the region platform name. (However
/// multiple or zero `"{}"`s may be included if needed).
///
/// # Returns
/// `self`, for chaining.
pub fn set_base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = base_url.into();
self
}
/// Set number of times to retry requests. Naturally, only retryable requests
/// will be retried: responses with status codes 5xx or 429 (after waiting
/// for retry-after headers). A value of `0` means one request will be sent
/// and it will not be retried if it fails.
///
/// # Returns
/// `self`, for chaining.
pub fn set_retries(mut self, retries: u8) -> Self {
self.retries = retries;
self
}
/// Burst percentage controls how many burst requests are allowed and
/// therefore how requests are spread out. Higher equals more burst,
/// less spread. Lower equals less burst, more spread.
///
/// The value must be in the range (0, 1];
/// Between 0, exclusive, and 1, inclusive. However values should generally
/// be larger than 0.25.
///
/// Burst percentage behaves as follows:<br>
/// A burst percentage of x% means, for each token bucket, "x% of the
/// tokens can be used in x% of the bucket duration." So, for example, if x
/// is 90%, a bucket would allow 90% of the requests to be made without
/// any delay. Then, after waiting 90% of the bucket's duration, the
/// remaining 10% of requests could be made.
///
/// A burst percentage of 100% results in no request spreading, which would
/// allow for the largest bursts and lowest latency, but could result in
/// 429s as bucket boundaries occur.
///
/// A burst percentage of near 0% results in high spreading causing
/// temporally equidistant requests. This prevents 429s but has the highest
/// latency. Additionally, if the number of tokens is high, this may lower
/// the overall throughput due to the rate at which requests can be
/// scheduled.
///
/// Therefore, for interactive applications like summoner & match history
/// lookup, a higher percentage may be better. For data-collection apps
/// like champion winrate aggregation, a medium-low percentage may be
/// better.
///
/// # Panics
/// If `burst_pct` is not in range (0, 1].
///
/// # Returns
/// `self`, for chaining.
pub fn set_burst_pct(mut self, burst_pct: f32) -> Self {
// Use inverted check to handle NaN.
if 0.0 < burst_pct && burst_pct < 1.0 {
self.burst_pct = burst_pct;
return self;
}
panic!("burst_pct \"{}\" not in range (0, 1].", burst_pct);
}
/// Sets the additional bucket duration to consider when rate limiting.
/// Increasing this value will decrease the chances of 429s, but will lower
/// the overall throughput.
///
/// In a sense, the `duration_overhead` is how much to "widen" the temporal
/// width of buckets.
///
/// Given a particular Riot Game API rate limit bucket that allows N requests
/// per D duration, when counting requests this library will consider requests
/// sent in the past `D + duration_overhead` duration.
///
/// # Returns
/// `self`, for chaining.
pub fn set_duration_overhead(mut self, duration_overhead: Duration) -> Self {
self.duration_overhead = duration_overhead;
self
}
}
//! Configuration of RiotApi.
use std::time::Duration;
use reqwest::ClientBuilder;
use reqwest::header::{ HeaderMap, HeaderValue };
/// Configuration for instantiating RiotApi.
///
///
#[derive(Debug)]
pub struct RiotApiConfig {
pub(crate) base_url: String,
pub(crate) retries: u8,
pub(crate) burst_pct: f32,
pub(crate) duration_overhead: Duration,
pub(crate) client_builder: Option<ClientBuilder>,
}
impl RiotApiConfig {
/// Request header name for the Riot API key.
///
/// When using `set_client_builder`, the supplied builder should include
/// this default header with the Riot API key as the value.
const RIOT_KEY_HEADER: &'static str = "X-Riot-Token";
/// `"https://{}.api.riotgames.com"`
///
/// Default base URL, including `{}` placeholder for region platform.
pub const DEFAULT_BASE_URL: &'static str = "https://{}.api.riotgames.com";
/// `3`
///
/// Default number of retries.
pub const DEFAULT_RETRIES: u8 = 3;
/// `0.99`
///
/// Default `burst_pct`, also used by `preconfig_burst`.
pub const PRECONFIG_BURST_BURST_PCT: f32 = 0.99;
/// `989` ms
///
/// Default `duration_overhead`, also used by `preconfig_burst`.
pub const PRECONFIG_BURST_DURATION_OVERHEAD: Duration = Duration::from_millis(989);
/// `0.47`
///
/// `burst_pct` used by `preconfig_throughput`.
pub const PRECONFIG_THROUGHPUT_BURST_PCT: f32 = 0.47;
/// `10` ms.
///
/// `duration_overhead` used by `preconfig_throughput`.
pub const PRECONFIG_THROUGHPUT_DURATION_OVERHEAD: Duration = Duration::from_millis(10);
/// Creates a new `RiotApiConfig` with the given `api_key` with the following
/// configuration:
///
/// * `retries = 3` (`RiotApiConfig::DEFAULT_RETRIES`).
/// * `purst_pct = 0.99` (`preconfig_burst`).
/// * `duration_overhead = 989 ms` (`preconfig_burst`).
///
/// `api_key` should be a Riot Games API key from
/// [https://developer.riotgames.com/](https://developer.riotgames.com/),
/// and should look like `"RGAPI-01234567-89ab-cdef-0123-456789abcdef"`.
pub fn with_key(api_key: impl AsRef<[u8]>) -> Self {
let mut default_headers = HeaderMap::new();
default_headers.insert(
Self::RIOT_KEY_HEADER,
HeaderValue::from_bytes(api_key.as_ref()).unwrap()
);
Self {
base_url: Self::DEFAULT_BASE_URL.into(),
retries: Self::DEFAULT_RETRIES,
burst_pct: Self::PRECONFIG_BURST_BURST_PCT,
duration_overhead: Self::PRECONFIG_BURST_DURATION_OVERHEAD,
client_builder: Some(
ClientBuilder::new()
.default_headers(default_headers)
),
}
}
/// Creates a new `RiotApiConfig` with the given client builder.
///
/// The client builder default headers should include a value for
/// `RiotApiConfig::RIOT_KEY_HEADER`, otherwise authentication will fail.
///
/// * `retries = 3` (`RiotApiConfig::DEFAULT_RETRIES`).
/// * `purst_pct = 0.99` (`preconfig_burst`).
/// * `duration_overhead = 989 ms` (`preconfig_burst`).
pub fn with_client_builder(client_builder: ClientBuilder) -> Self {
Self {
base_url: Self::DEFAULT_BASE_URL.to_owned(),
retries: Self::DEFAULT_RETRIES,
burst_pct: Self::PRECONFIG_BURST_BURST_PCT,
duration_overhead: Self::PRECONFIG_BURST_DURATION_OVERHEAD,
client_builder: Some(client_builder),
}
}
/// Sets rate limiting settings to preconfigured values optimized for burst,
/// low latency:
///
/// * `burst_pct = 0.99` (`PRECONFIG_BURST_BURST_PCT`).
/// * `duration_overhead = 989 ms` (`PRECONFIG_BURST_DURATION_OVERHEAD_MILLIS`).
///
/// # Returns
/// `self`, for chaining.
pub fn preconfig_burst(mut self) -> Self {
self.burst_pct = Self::PRECONFIG_BURST_BURST_PCT;
self.duration_overhead = Self::PRECONFIG_BURST_DURATION_OVERHEAD;
self
}
/// Sets the rate limiting settings to preconfigured values optimized for
/// high throughput:
///
/// * `burst_pct = 0.47` (`PRECONFIG_THROUGHPUT_BURST_PCT`).
/// * `duration_overhead = 10 ms` (`PRECONFIG_THROUGHPUT_DURATION_OVERHEAD_MILLIS`).
///
/// # Returns
/// `self`, for chaining.
pub fn preconfig_throughput(mut self) -> Self {
self.burst_pct = Self::PRECONFIG_THROUGHPUT_BURST_PCT;
self.duration_overhead = Self::PRECONFIG_THROUGHPUT_DURATION_OVERHEAD;
self
}
/// Set the base url for requests. The string should contain a `"{}"`
/// literal which will be replaced with the region platform name. (However
/// multiple or zero `"{}"`s may be included if needed).
///
/// # Returns
/// `self`, for chaining.
pub fn set_base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = base_url.into();
self
}
/// Set number of times to retry requests. Naturally, only retryable requests
/// will be retried: responses with status codes 5xx or 429 (after waiting
/// for retry-after headers). A value of `0` means one request will be sent
/// and it will not be retried if it fails.
///
/// # Returns
/// `self`, for chaining.
pub fn set_retries(mut self, retries: u8) -> Self {
self.retries = retries;
self
}
/// Burst percentage controls how many burst requests are allowed and
/// therefore how requests are spread out. Higher equals more burst,
/// less spread. Lower equals less burst, more spread.
///
/// The value must be in the range (0, 1];
/// Between 0, exclusive, and 1, inclusive. However values should generally
/// be larger than 0.25.
///
/// Burst percentage behaves as follows:<br>
/// A burst percentage of x% means, for each token bucket, "x% of the
/// tokens can be used in x% of the bucket duration." So, for example, if x
/// is 90%, a bucket would allow 90% of the requests to be made without
/// any delay. Then, after waiting 90% of the bucket's duration, the
/// remaining 10% of requests could be made.
///
/// A burst percentage of 100% results in no request spreading, which would
/// allow for the largest bursts and lowest latency, but could result in
/// 429s as bucket boundaries occur.
///
/// A burst percentage of near 0% results in high spreading causing
/// temporally equidistant requests. This prevents 429s but has the highest
/// latency. Additionally, if the number of tokens is high, this may lower
/// the overall throughput due to the rate at which requests can be
/// scheduled.
///
/// Therefore, for interactive applications like summoner & match history
/// lookup, a higher percentage may be better. For data-collection apps
/// like champion winrate aggregation, a medium-low percentage may be
/// better.
///
/// # Panics
/// If `burst_pct` is not in range (0, 1].
///
/// # Returns
/// `self`, for chaining.
pub fn set_burst_pct(mut self, burst_pct: f32) -> Self {
// Use inverted check to handle NaN.
if 0.0 < burst_pct && burst_pct < 1.0 {
self.burst_pct = burst_pct;
return self;
}
panic!("burst_pct \"{}\" not in range (0, 1].", burst_pct);
}
/// Sets the additional bucket duration to consider when rate limiting.
/// Increasing this value will decrease the chances of 429s, but will lower
/// the overall throughput.
///
/// In a sense, the `duration_overhead` is how much to "widen" the temporal
/// width of buckets.
///
/// Given a particular Riot Game API rate limit bucket that allows N requests
/// per D duration, when counting requests this library will consider requests
/// sent in the past `D + duration_overhead` duration.
///
/// # Returns
/// `self`, for chaining.
pub fn set_duration_overhead(mut self, duration_overhead: Duration) -> Self {
self.duration_overhead = duration_overhead;
self
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,60 +1,60 @@
use std::cmp::Ordering;
use num_enum::{ IntoPrimitive, TryFromPrimitive };
use strum::IntoEnumIterator;
use strum_macros::{ EnumString, Display, AsRefStr, IntoStaticStr };
/// LoL and TFT rank divisions, I, II, III, IV, and (deprecated) V.
///
/// Ordered such that "higher" divisions are greater than "lower" ones: `Division::I > Division::IV`.
///
/// Repr'd as equivalent numeric values, (1, 2, 3, 4, 5).
///
/// Implements [IntoEnumIterator](super::IntoEnumIterator). Iterator excludes deprecated `Division::V`.
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq, Hash)]
#[derive(EnumString, Display, AsRefStr, IntoStaticStr)]
#[derive(IntoPrimitive, TryFromPrimitive)]
#[repr(u8)]
pub enum Division {
I = 1,
II = 2,
III = 3,
IV = 4,
#[deprecated(note="Removed for 2019.")]
V = 5,
}
serde_string!(Division);
/// Returns a DoubleEndedIterator of I, II, III, IV.
/// Ordered from high rank (I) to low (IV).
/// Excludes V, which is deprecated.
impl IntoEnumIterator for Division {
type Iterator = std::iter::Copied<std::slice::Iter<'static, Self>>;
fn iter() -> Self::Iterator {
[ Self::I, Self::II, Self::III, Self::IV ].iter().copied()
}
}
impl Ord for Division {
fn cmp(&self, other: &Self) -> Ordering {
u8::from(*self).cmp(&u8::from(*other)).reverse()
}
}
impl PartialOrd for Division {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sort() {
assert!(Division::IV < Division::I);
}
use std::cmp::Ordering;
use num_enum::{ IntoPrimitive, TryFromPrimitive };
use strum::IntoEnumIterator;
use strum_macros::{ EnumString, Display, AsRefStr, IntoStaticStr };
/// LoL and TFT rank divisions, I, II, III, IV, and (deprecated) V.
///
/// Ordered such that "higher" divisions are greater than "lower" ones: `Division::I > Division::IV`.
///
/// Repr'd as equivalent numeric values, (1, 2, 3, 4, 5).
///
/// Implements [IntoEnumIterator](super::IntoEnumIterator). Iterator excludes deprecated `Division::V`.
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq, Hash)]
#[derive(EnumString, Display, AsRefStr, IntoStaticStr)]
#[derive(IntoPrimitive, TryFromPrimitive)]
#[repr(u8)]
pub enum Division {
I = 1,
II = 2,
III = 3,
IV = 4,
#[deprecated(note="Removed for 2019.")]
V = 5,
}
serde_string!(Division);
/// Returns a DoubleEndedIterator of I, II, III, IV.
/// Ordered from high rank (I) to low (IV).
/// Excludes V, which is deprecated.
impl IntoEnumIterator for Division {
type Iterator = std::iter::Copied<std::slice::Iter<'static, Self>>;
fn iter() -> Self::Iterator {
[ Self::I, Self::II, Self::III, Self::IV ].iter().copied()
}
}
impl Ord for Division {
fn cmp(&self, other: &Self) -> Ordering {
u8::from(*self).cmp(&u8::from(*other)).reverse()
}
}
impl PartialOrd for Division {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sort() {
assert!(Division::IV < Division::I);
}
}

View File

@ -1,65 +1,65 @@
///////////////////////////////////////////////
///////////////////////////////////////////////
// //
// ! //
// This file is automatically generated! //
// Do not directly edit! //
// //
///////////////////////////////////////////////
use strum_macros::{ EnumString, Display, AsRefStr, IntoStaticStr };
/// League of Legends game mode, such as Classic,
/// ARAM, URF, One For All, Ascension, etc.
#[non_exhaustive]
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq, Hash)]
#[derive(EnumString, Display, AsRefStr, IntoStaticStr)]
#[repr(u8)]
pub enum GameMode {
/// ARAM games
ARAM,
/// All Random Summoner's Rift games
ARSR,
/// Ascension games
ASCENSION,
/// Blood Hunt Assassin games
ASSASSINATE,
/// Classic Summoner's Rift and Twisted Treeline games
CLASSIC,
/// Dark Star: Singularity games
DARKSTAR,
/// Doom Bot games
DOOMBOTSTEEMO,
/// Snowdown Showdown games
FIRSTBLOOD,
/// Nexus Blitz games, deprecated in patch 9.2 in favor of gameMode NEXUSBLITZ.
GAMEMODEX,
/// Legend of the Poro King games
KINGPORO,
/// Nexus Blitz games.
NEXUSBLITZ,
/// Dominion/Crystal Scar games
ODIN,
/// Odyssey: Extraction games
ODYSSEY,
/// One for All games
ONEFORALL,
/// PROJECT: Hunters games
PROJECT,
/// Nexus Siege games
SIEGE,
/// Star Guardian Invasion games
STARGUARDIAN,
/// Tutorial games
TUTORIAL,
/// Tutorial: Welcome to League.
TUTORIAL_MODULE_1,
/// Tutorial: Power Up.
TUTORIAL_MODULE_2,
/// Tutorial: Shop for Gear.
TUTORIAL_MODULE_3,
/// URF games
URF,
}
serde_string!(GameMode);
///////////////////////////////////////////////
use strum_macros::{ EnumString, Display, AsRefStr, IntoStaticStr };
/// League of Legends game mode, such as Classic,
/// ARAM, URF, One For All, Ascension, etc.
#[non_exhaustive]
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq, Hash)]
#[derive(EnumString, Display, AsRefStr, IntoStaticStr)]
#[repr(u8)]
pub enum GameMode {
/// ARAM games
ARAM,
/// All Random Summoner's Rift games
ARSR,
/// Ascension games
ASCENSION,
/// Blood Hunt Assassin games
ASSASSINATE,
/// Classic Summoner's Rift and Twisted Treeline games
CLASSIC,
/// Dark Star: Singularity games
DARKSTAR,
/// Doom Bot games
DOOMBOTSTEEMO,
/// Snowdown Showdown games
FIRSTBLOOD,
/// Nexus Blitz games, deprecated in patch 9.2 in favor of gameMode NEXUSBLITZ.
GAMEMODEX,
/// Legend of the Poro King games
KINGPORO,
/// Nexus Blitz games.
NEXUSBLITZ,
/// Dominion/Crystal Scar games
ODIN,
/// Odyssey: Extraction games
ODYSSEY,
/// One for All games
ONEFORALL,
/// PROJECT: Hunters games
PROJECT,
/// Nexus Siege games
SIEGE,
/// Star Guardian Invasion games
STARGUARDIAN,
/// Tutorial games
TUTORIAL,
/// Tutorial: Welcome to League.
TUTORIAL_MODULE_1,
/// Tutorial: Power Up.
TUTORIAL_MODULE_2,
/// Tutorial: Shop for Gear.
TUTORIAL_MODULE_3,
/// URF games
URF,
}
serde_string!(GameMode);

View File

@ -1,25 +1,25 @@
///////////////////////////////////////////////
///////////////////////////////////////////////
// //
// ! //
// This file is automatically generated! //
// Do not directly edit! //
// //
///////////////////////////////////////////////
use strum_macros::{ EnumString, Display, AsRefStr, IntoStaticStr };
/// League of Legends game type: matched game, custom game, or tutorial game.
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq, Hash)]
#[derive(EnumString, Display, AsRefStr, IntoStaticStr)]
#[repr(u8)]
pub enum GameType {
/// Custom games
CUSTOM_GAME,
/// all other games
MATCHED_GAME,
/// Tutorial games
TUTORIAL_GAME,
}
serde_string!(GameType);
///////////////////////////////////////////////
use strum_macros::{ EnumString, Display, AsRefStr, IntoStaticStr };
/// League of Legends game type: matched game, custom game, or tutorial game.
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq, Hash)]
#[derive(EnumString, Display, AsRefStr, IntoStaticStr)]
#[repr(u8)]
pub enum GameType {
/// Custom games
CUSTOM_GAME,
/// all other games
MATCHED_GAME,
/// Tutorial games
TUTORIAL_GAME,
}
serde_string!(GameType);

View File

@ -1,23 +1,23 @@
#![macro_use]
macro_rules! serde_string {
( $x:ty ) => {
impl<'de> serde::de::Deserialize<'de> for $x
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
}
impl serde::ser::Serialize for $x {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
serializer.serialize_str(self.as_ref())
}
}
};
}
#![macro_use]
macro_rules! serde_string {
( $x:ty ) => {
impl<'de> serde::de::Deserialize<'de> for $x
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
}
impl serde::ser::Serialize for $x {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
serializer.serialize_str(self.as_ref())
}
}
};
}

View File

@ -1,65 +1,65 @@
///////////////////////////////////////////////
///////////////////////////////////////////////
// //
// ! //
// This file is automatically generated! //
// Do not directly edit! //
// //
///////////////////////////////////////////////
use serde_repr::{ Serialize_repr, Deserialize_repr };
use num_enum::{ IntoPrimitive, TryFromPrimitive };
/// League of Legends maps.
#[non_exhaustive]
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq, Hash, PartialOrd, Ord)]
#[derive(Serialize_repr, Deserialize_repr)]
#[derive(IntoPrimitive, TryFromPrimitive)]
#[repr(u8)]
pub enum Map {
/// Summoner's Rift
/// Original Summer variant
SUMMONERS_RIFT_ORIGINAL_SUMMER_VARIANT = 1,
/// Summoner's Rift
/// Original Autumn variant
SUMMONERS_RIFT_ORIGINAL_AUTUMN_VARIANT = 2,
/// The Proving Grounds
/// Tutorial Map
THE_PROVING_GROUNDS = 3,
/// Twisted Treeline
/// Original Version
TWISTED_TREELINE_ORIGINAL_VERSION = 4,
/// The Crystal Scar
/// Dominion map
THE_CRYSTAL_SCAR = 8,
/// Twisted Treeline
/// Last TT map
TWISTED_TREELINE = 10,
/// Summoner's Rift
/// Current Version
SUMMONERS_RIFT = 11,
/// Howling Abyss
/// ARAM map
HOWLING_ABYSS = 12,
/// Butcher's Bridge
/// Alternate ARAM map
BUTCHERS_BRIDGE = 14,
/// Cosmic Ruins
/// Dark Star: Singularity map
COSMIC_RUINS = 16,
/// Valoran City Park
/// Star Guardian Invasion map
VALORAN_CITY_PARK = 18,
/// Substructure 43
/// PROJECT: Hunters map
SUBSTRUCTURE_43 = 19,
/// Crash Site
/// Odyssey: Extraction map
CRASH_SITE = 20,
/// Nexus Blitz
/// Nexus Blitz map
NEXUS_BLITZ = 21,
/// Convergence
/// Teamfight Tactics map
CONVERGENCE = 22,
}
///////////////////////////////////////////////
use serde_repr::{ Serialize_repr, Deserialize_repr };
use num_enum::{ IntoPrimitive, TryFromPrimitive };
/// League of Legends maps.
#[non_exhaustive]
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq, Hash, PartialOrd, Ord)]
#[derive(Serialize_repr, Deserialize_repr)]
#[derive(IntoPrimitive, TryFromPrimitive)]
#[repr(u8)]
pub enum Map {
/// Summoner's Rift
/// Original Summer variant
SUMMONERS_RIFT_ORIGINAL_SUMMER_VARIANT = 1,
/// Summoner's Rift
/// Original Autumn variant
SUMMONERS_RIFT_ORIGINAL_AUTUMN_VARIANT = 2,
/// The Proving Grounds
/// Tutorial Map
THE_PROVING_GROUNDS = 3,
/// Twisted Treeline
/// Original Version
TWISTED_TREELINE_ORIGINAL_VERSION = 4,
/// The Crystal Scar
/// Dominion map
THE_CRYSTAL_SCAR = 8,
/// Twisted Treeline
/// Last TT map
TWISTED_TREELINE = 10,
/// Summoner's Rift
/// Current Version
SUMMONERS_RIFT = 11,
/// Howling Abyss
/// ARAM map
HOWLING_ABYSS = 12,
/// Butcher's Bridge
/// Alternate ARAM map
BUTCHERS_BRIDGE = 14,
/// Cosmic Ruins
/// Dark Star: Singularity map
COSMIC_RUINS = 16,
/// Valoran City Park
/// Star Guardian Invasion map
VALORAN_CITY_PARK = 18,
/// Substructure 43
/// PROJECT: Hunters map
SUBSTRUCTURE_43 = 19,
/// Crash Site
/// Odyssey: Extraction map
CRASH_SITE = 20,
/// Nexus Blitz
/// Nexus Blitz map
NEXUS_BLITZ = 21,
/// Convergence
/// Teamfight Tactics map
CONVERGENCE = 22,
}

View File

@ -1,50 +1,50 @@
//! Constant data and Enums used with the Riot Games API.
//!
//! This module uses SCREAMING_SNAKE_CASE for enum variants, as enums in this
//! crate should be considered collections of constants.
#![allow(deprecated)]
#![allow(non_camel_case_types)]
mod macro_serde_string;
mod champion;
pub use champion::*;
mod division;
pub use division::*;
mod game_mode;
pub use game_mode::*;
mod game_type;
pub use game_type::*;
mod map;
pub use map::*;
mod queue_type;
pub use queue_type::*;
mod queue;
pub use queue::*;
pub mod ranks;
mod route;
pub use route::*;
mod season;
pub use season::*;
/// Trait allowing iteration of enum types, implemented by several enums in this module.
/// Re-exported from strum.
///
///
pub use strum::IntoEnumIterator;
mod team;
pub use team::*;
mod tier;
pub use tier::*;
//! Constant data and Enums used with the Riot Games API.
//!
//! This module uses SCREAMING_SNAKE_CASE for enum variants, as enums in this
//! crate should be considered collections of constants.
#![allow(deprecated)]
#![allow(non_camel_case_types)]
mod macro_serde_string;
mod champion;
pub use champion::*;
mod division;
pub use division::*;
mod game_mode;
pub use game_mode::*;
mod game_type;
pub use game_type::*;
mod map;
pub use map::*;
mod queue_type;
pub use queue_type::*;
mod queue;
pub use queue::*;
pub mod ranks;
mod route;
pub use route::*;
mod season;
pub use season::*;
/// Trait allowing iteration of enum types, implemented by several enums in this module.
/// Re-exported from strum.
///
///
pub use strum::IntoEnumIterator;
mod team;
pub use team::*;
mod tier;
pub use tier::*;

View File

@ -1,250 +1,250 @@
///////////////////////////////////////////////
///////////////////////////////////////////////
// //
// ! //
// This file is automatically generated! //
// Do not directly edit! //
// //
///////////////////////////////////////////////
use serde_repr::{ Serialize_repr, Deserialize_repr };
use num_enum::{ IntoPrimitive, TryFromPrimitive };
/// League of Legends matchmaking queue.
#[non_exhaustive]
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq)]
#[derive(Serialize_repr, Deserialize_repr)]
#[derive(IntoPrimitive, TryFromPrimitive)]
#[repr(u16)]
pub enum Queue {
/// Games on Custom games
CUSTOM = 0,
/// 5v5 Blind Pick games on Summoner's Rift
/// Deprecated in patch 7.19 in favor of queueId 430
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 430")]
SUMMONERS_RIFT_5V5_BLIND_PICK_DEPRECATED_2 = 2,
/// 5v5 Ranked Solo games on Summoner's Rift
/// Deprecated in favor of queueId 420
#[deprecated(note="Deprecated in favor of queueId 420")]
SUMMONERS_RIFT_5V5_RANKED_SOLO_DEPRECATED_4 = 4,
/// 5v5 Ranked Premade games on Summoner's Rift
/// Game mode deprecated
#[deprecated(note="Game mode deprecated")]
SUMMONERS_RIFT_5V5_RANKED_PREMADE = 6,
/// Co-op vs AI games on Summoner's Rift
/// Deprecated in favor of queueId 32 and 33
#[deprecated(note="Deprecated in favor of queueId 32 and 33")]
SUMMONERS_RIFT_CO_OP_VS_AI = 7,
/// 3v3 Normal games on Twisted Treeline
/// Deprecated in patch 7.19 in favor of queueId 460
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 460")]
TWISTED_TREELINE_3V3_NORMAL = 8,
/// 3v3 Ranked Flex games on Twisted Treeline
/// Deprecated in patch 7.19 in favor of queueId 470
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 470")]
TWISTED_TREELINE_3V3_RANKED_FLEX_DEPRECATED_9 = 9,
/// 5v5 Draft Pick games on Summoner's Rift
/// Deprecated in favor of queueId 400
#[deprecated(note="Deprecated in favor of queueId 400")]
SUMMONERS_RIFT_5V5_DRAFT_PICK_DEPRECATED_14 = 14,
/// 5v5 Dominion Blind Pick games on Crystal Scar
/// Game mode deprecated
#[deprecated(note="Game mode deprecated")]
CRYSTAL_SCAR_5V5_DOMINION_BLIND_PICK = 16,
/// 5v5 Dominion Draft Pick games on Crystal Scar
/// Game mode deprecated
#[deprecated(note="Game mode deprecated")]
CRYSTAL_SCAR_5V5_DOMINION_DRAFT_PICK = 17,
/// Dominion Co-op vs AI games on Crystal Scar
/// Game mode deprecated
#[deprecated(note="Game mode deprecated")]
CRYSTAL_SCAR_DOMINION_CO_OP_VS_AI = 25,
/// Co-op vs AI Intro Bot games on Summoner's Rift
/// Deprecated in patch 7.19 in favor of queueId 830
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 830")]
SUMMONERS_RIFT_CO_OP_VS_AI_INTRO_BOT_DEPRECATED_31 = 31,
/// Co-op vs AI Beginner Bot games on Summoner's Rift
/// Deprecated in patch 7.19 in favor of queueId 840
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 840")]
SUMMONERS_RIFT_CO_OP_VS_AI_BEGINNER_BOT_DEPRECATED_32 = 32,
/// Co-op vs AI Intermediate Bot games on Summoner's Rift
/// Deprecated in patch 7.19 in favor of queueId 850
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 850")]
SUMMONERS_RIFT_CO_OP_VS_AI_INTERMEDIATE_BOT_DEPRECATED_33 = 33,
/// 3v3 Ranked Team games on Twisted Treeline
/// Game mode deprecated
#[deprecated(note="Game mode deprecated")]
TWISTED_TREELINE_3V3_RANKED_TEAM = 41,
/// 5v5 Ranked Team games on Summoner's Rift
/// Game mode deprecated
#[deprecated(note="Game mode deprecated")]
SUMMONERS_RIFT_5V5_RANKED_TEAM = 42,
/// Co-op vs AI games on Twisted Treeline
/// Deprecated in patch 7.19 in favor of queueId 800
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 800")]
TWISTED_TREELINE_CO_OP_VS_AI = 52,
/// 5v5 Team Builder games on Summoner's Rift
/// Game mode deprecated
#[deprecated(note="Game mode deprecated")]
SUMMONERS_RIFT_5V5_TEAM_BUILDER = 61,
/// 5v5 ARAM games on Howling Abyss
/// Deprecated in patch 7.19 in favor of queueId 450
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 450")]
HOWLING_ABYSS_5V5_ARAM_DEPRECATED_65 = 65,
/// ARAM Co-op vs AI games on Howling Abyss
/// Game mode deprecated
#[deprecated(note="Game mode deprecated")]
HOWLING_ABYSS_ARAM_CO_OP_VS_AI = 67,
/// One for All games on Summoner's Rift
/// Deprecated in patch 8.6 in favor of queueId 1020
#[deprecated(note="Deprecated in patch 8.6 in favor of queueId 1020")]
SUMMONERS_RIFT_ONE_FOR_ALL_DEPRECATED_70 = 70,
/// 1v1 Snowdown Showdown games on Howling Abyss
HOWLING_ABYSS_1V1_SNOWDOWN_SHOWDOWN = 72,
/// 2v2 Snowdown Showdown games on Howling Abyss
HOWLING_ABYSS_2V2_SNOWDOWN_SHOWDOWN = 73,
/// 6v6 Hexakill games on Summoner's Rift
SUMMONERS_RIFT_6V6_HEXAKILL = 75,
/// Ultra Rapid Fire games on Summoner's Rift
SUMMONERS_RIFT_ULTRA_RAPID_FIRE = 76,
/// One For All: Mirror Mode games on Howling Abyss
HOWLING_ABYSS_ONE_FOR_ALL_MIRROR_MODE = 78,
/// Co-op vs AI Ultra Rapid Fire games on Summoner's Rift
SUMMONERS_RIFT_CO_OP_VS_AI_ULTRA_RAPID_FIRE = 83,
/// Doom Bots Rank 1 games on Summoner's Rift
/// Deprecated in patch 7.19 in favor of queueId 950
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 950")]
SUMMONERS_RIFT_DOOM_BOTS_RANK_1 = 91,
/// Doom Bots Rank 2 games on Summoner's Rift
/// Deprecated in patch 7.19 in favor of queueId 950
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 950")]
SUMMONERS_RIFT_DOOM_BOTS_RANK_2 = 92,
/// Doom Bots Rank 5 games on Summoner's Rift
/// Deprecated in patch 7.19 in favor of queueId 950
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 950")]
SUMMONERS_RIFT_DOOM_BOTS_RANK_5 = 93,
/// Ascension games on Crystal Scar
/// Deprecated in patch 7.19 in favor of queueId 910
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 910")]
CRYSTAL_SCAR_ASCENSION_DEPRECATED_96 = 96,
/// 6v6 Hexakill games on Twisted Treeline
TWISTED_TREELINE_6V6_HEXAKILL = 98,
/// 5v5 ARAM games on Butcher's Bridge
BUTCHERS_BRIDGE_5V5_ARAM = 100,
/// Legend of the Poro King games on Howling Abyss
/// Deprecated in patch 7.19 in favor of queueId 920
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 920")]
HOWLING_ABYSS_LEGEND_OF_THE_PORO_KING_DEPRECATED_300 = 300,
/// Nemesis games on Summoner's Rift
SUMMONERS_RIFT_NEMESIS = 310,
/// Black Market Brawlers games on Summoner's Rift
SUMMONERS_RIFT_BLACK_MARKET_BRAWLERS = 313,
/// Nexus Siege games on Summoner's Rift
/// Deprecated in patch 7.19 in favor of queueId 940
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 940")]
SUMMONERS_RIFT_NEXUS_SIEGE_DEPRECATED_315 = 315,
/// Definitely Not Dominion games on Crystal Scar
CRYSTAL_SCAR_DEFINITELY_NOT_DOMINION = 317,
/// ARURF games on Summoner's Rift
/// Deprecated in patch 7.19 in favor of queueId 900
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 900")]
SUMMONERS_RIFT_ARURF = 318,
/// All Random games on Summoner's Rift
SUMMONERS_RIFT_ALL_RANDOM = 325,
/// 5v5 Draft Pick games on Summoner's Rift
SUMMONERS_RIFT_5V5_DRAFT_PICK = 400,
/// 5v5 Ranked Dynamic games on Summoner's Rift
/// Game mode deprecated in patch 6.22
#[deprecated(note="Game mode deprecated in patch 6.22")]
SUMMONERS_RIFT_5V5_RANKED_DYNAMIC = 410,
/// 5v5 Ranked Solo games on Summoner's Rift
SUMMONERS_RIFT_5V5_RANKED_SOLO = 420,
/// 5v5 Blind Pick games on Summoner's Rift
SUMMONERS_RIFT_5V5_BLIND_PICK = 430,
/// 5v5 Ranked Flex games on Summoner's Rift
SUMMONERS_RIFT_5V5_RANKED_FLEX = 440,
/// 5v5 ARAM games on Howling Abyss
HOWLING_ABYSS_5V5_ARAM = 450,
/// 3v3 Blind Pick games on Twisted Treeline
/// Deprecated in patch 9.23
#[deprecated(note="Deprecated in patch 9.23")]
TWISTED_TREELINE_3V3_BLIND_PICK = 460,
/// 3v3 Ranked Flex games on Twisted Treeline
/// Deprecated in patch 9.23
#[deprecated(note="Deprecated in patch 9.23")]
TWISTED_TREELINE_3V3_RANKED_FLEX_DEPRECATED_470 = 470,
/// Blood Hunt Assassin games on Summoner's Rift
SUMMONERS_RIFT_BLOOD_HUNT_ASSASSIN = 600,
/// Dark Star: Singularity games on Cosmic Ruins
COSMIC_RUINS_DARK_STAR_SINGULARITY = 610,
/// Clash games on Summoner's Rift
SUMMONERS_RIFT_CLASH = 700,
/// Co-op vs. AI Intermediate Bot games on Twisted Treeline
/// Deprecated in patch 9.23
#[deprecated(note="Deprecated in patch 9.23")]
TWISTED_TREELINE_CO_OP_VS_AI_INTERMEDIATE_BOT = 800,
/// Co-op vs. AI Intro Bot games on Twisted Treeline
/// Deprecated in patch 9.23
#[deprecated(note="Deprecated in patch 9.23")]
TWISTED_TREELINE_CO_OP_VS_AI_INTRO_BOT = 810,
/// Co-op vs. AI Beginner Bot games on Twisted Treeline
TWISTED_TREELINE_CO_OP_VS_AI_BEGINNER_BOT = 820,
/// Co-op vs. AI Intro Bot games on Summoner's Rift
SUMMONERS_RIFT_CO_OP_VS_AI_INTRO_BOT = 830,
/// Co-op vs. AI Beginner Bot games on Summoner's Rift
SUMMONERS_RIFT_CO_OP_VS_AI_BEGINNER_BOT = 840,
/// Co-op vs. AI Intermediate Bot games on Summoner's Rift
SUMMONERS_RIFT_CO_OP_VS_AI_INTERMEDIATE_BOT = 850,
/// URF games on Summoner's Rift
SUMMONERS_RIFT_URF = 900,
/// Ascension games on Crystal Scar
CRYSTAL_SCAR_ASCENSION = 910,
/// Legend of the Poro King games on Howling Abyss
HOWLING_ABYSS_LEGEND_OF_THE_PORO_KING = 920,
/// Nexus Siege games on Summoner's Rift
SUMMONERS_RIFT_NEXUS_SIEGE = 940,
/// Doom Bots Voting games on Summoner's Rift
SUMMONERS_RIFT_DOOM_BOTS_VOTING = 950,
/// Doom Bots Standard games on Summoner's Rift
SUMMONERS_RIFT_DOOM_BOTS_STANDARD = 960,
/// Star Guardian Invasion: Normal games on Valoran City Park
VALORAN_CITY_PARK_STAR_GUARDIAN_INVASION_NORMAL = 980,
/// Star Guardian Invasion: Onslaught games on Valoran City Park
VALORAN_CITY_PARK_STAR_GUARDIAN_INVASION_ONSLAUGHT = 990,
/// PROJECT: Hunters games on Overcharge
OVERCHARGE_PROJECT_HUNTERS = 1000,
/// Snow ARURF games on Summoner's Rift
SUMMONERS_RIFT_SNOW_ARURF = 1010,
/// One for All games on Summoner's Rift
SUMMONERS_RIFT_ONE_FOR_ALL = 1020,
/// Odyssey Extraction: Intro games on Crash Site
CRASH_SITE_ODYSSEY_EXTRACTION_INTRO = 1030,
/// Odyssey Extraction: Cadet games on Crash Site
CRASH_SITE_ODYSSEY_EXTRACTION_CADET = 1040,
/// Odyssey Extraction: Crewmember games on Crash Site
CRASH_SITE_ODYSSEY_EXTRACTION_CREWMEMBER = 1050,
/// Odyssey Extraction: Captain games on Crash Site
CRASH_SITE_ODYSSEY_EXTRACTION_CAPTAIN = 1060,
/// Odyssey Extraction: Onslaught games on Crash Site
CRASH_SITE_ODYSSEY_EXTRACTION_ONSLAUGHT = 1070,
/// Teamfight Tactics games on Convergence
CONVERGENCE_TEAMFIGHT_TACTICS = 1090,
/// Ranked Teamfight Tactics games on Convergence
CONVERGENCE_RANKED_TEAMFIGHT_TACTICS = 1100,
/// Teamfight Tactics Tutorial games on Convergence
CONVERGENCE_TEAMFIGHT_TACTICS_TUTORIAL = 1110,
/// Teamfight Tactics 1v0 testing games on Convergence
CONVERGENCE_TEAMFIGHT_TACTICS_1V0_TESTING = 1111,
/// Nexus Blitz games on Nexus Blitz
/// Deprecated in patch 9.2 in favor of queueId 1300
#[deprecated(note="Deprecated in patch 9.2 in favor of queueId 1300")]
NEXUS_BLITZ_NEXUS_BLITZ_DEPRECATED_1200 = 1200,
/// Nexus Blitz games on Nexus Blitz
NEXUS_BLITZ_NEXUS_BLITZ = 1300,
/// Tutorial 1 games on Summoner's Rift
SUMMONERS_RIFT_TUTORIAL_1 = 2000,
/// Tutorial 2 games on Summoner's Rift
SUMMONERS_RIFT_TUTORIAL_2 = 2010,
/// Tutorial 3 games on Summoner's Rift
SUMMONERS_RIFT_TUTORIAL_3 = 2020,
///////////////////////////////////////////////
use serde_repr::{ Serialize_repr, Deserialize_repr };
use num_enum::{ IntoPrimitive, TryFromPrimitive };
/// League of Legends matchmaking queue.
#[non_exhaustive]
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq)]
#[derive(Serialize_repr, Deserialize_repr)]
#[derive(IntoPrimitive, TryFromPrimitive)]
#[repr(u16)]
pub enum Queue {
/// Games on Custom games
CUSTOM = 0,
/// 5v5 Blind Pick games on Summoner's Rift
/// Deprecated in patch 7.19 in favor of queueId 430
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 430")]
SUMMONERS_RIFT_5V5_BLIND_PICK_DEPRECATED_2 = 2,
/// 5v5 Ranked Solo games on Summoner's Rift
/// Deprecated in favor of queueId 420
#[deprecated(note="Deprecated in favor of queueId 420")]
SUMMONERS_RIFT_5V5_RANKED_SOLO_DEPRECATED_4 = 4,
/// 5v5 Ranked Premade games on Summoner's Rift
/// Game mode deprecated
#[deprecated(note="Game mode deprecated")]
SUMMONERS_RIFT_5V5_RANKED_PREMADE = 6,
/// Co-op vs AI games on Summoner's Rift
/// Deprecated in favor of queueId 32 and 33
#[deprecated(note="Deprecated in favor of queueId 32 and 33")]
SUMMONERS_RIFT_CO_OP_VS_AI = 7,
/// 3v3 Normal games on Twisted Treeline
/// Deprecated in patch 7.19 in favor of queueId 460
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 460")]
TWISTED_TREELINE_3V3_NORMAL = 8,
/// 3v3 Ranked Flex games on Twisted Treeline
/// Deprecated in patch 7.19 in favor of queueId 470
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 470")]
TWISTED_TREELINE_3V3_RANKED_FLEX_DEPRECATED_9 = 9,
/// 5v5 Draft Pick games on Summoner's Rift
/// Deprecated in favor of queueId 400
#[deprecated(note="Deprecated in favor of queueId 400")]
SUMMONERS_RIFT_5V5_DRAFT_PICK_DEPRECATED_14 = 14,
/// 5v5 Dominion Blind Pick games on Crystal Scar
/// Game mode deprecated
#[deprecated(note="Game mode deprecated")]
CRYSTAL_SCAR_5V5_DOMINION_BLIND_PICK = 16,
/// 5v5 Dominion Draft Pick games on Crystal Scar
/// Game mode deprecated
#[deprecated(note="Game mode deprecated")]
CRYSTAL_SCAR_5V5_DOMINION_DRAFT_PICK = 17,
/// Dominion Co-op vs AI games on Crystal Scar
/// Game mode deprecated
#[deprecated(note="Game mode deprecated")]
CRYSTAL_SCAR_DOMINION_CO_OP_VS_AI = 25,
/// Co-op vs AI Intro Bot games on Summoner's Rift
/// Deprecated in patch 7.19 in favor of queueId 830
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 830")]
SUMMONERS_RIFT_CO_OP_VS_AI_INTRO_BOT_DEPRECATED_31 = 31,
/// Co-op vs AI Beginner Bot games on Summoner's Rift
/// Deprecated in patch 7.19 in favor of queueId 840
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 840")]
SUMMONERS_RIFT_CO_OP_VS_AI_BEGINNER_BOT_DEPRECATED_32 = 32,
/// Co-op vs AI Intermediate Bot games on Summoner's Rift
/// Deprecated in patch 7.19 in favor of queueId 850
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 850")]
SUMMONERS_RIFT_CO_OP_VS_AI_INTERMEDIATE_BOT_DEPRECATED_33 = 33,
/// 3v3 Ranked Team games on Twisted Treeline
/// Game mode deprecated
#[deprecated(note="Game mode deprecated")]
TWISTED_TREELINE_3V3_RANKED_TEAM = 41,
/// 5v5 Ranked Team games on Summoner's Rift
/// Game mode deprecated
#[deprecated(note="Game mode deprecated")]
SUMMONERS_RIFT_5V5_RANKED_TEAM = 42,
/// Co-op vs AI games on Twisted Treeline
/// Deprecated in patch 7.19 in favor of queueId 800
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 800")]
TWISTED_TREELINE_CO_OP_VS_AI = 52,
/// 5v5 Team Builder games on Summoner's Rift
/// Game mode deprecated
#[deprecated(note="Game mode deprecated")]
SUMMONERS_RIFT_5V5_TEAM_BUILDER = 61,
/// 5v5 ARAM games on Howling Abyss
/// Deprecated in patch 7.19 in favor of queueId 450
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 450")]
HOWLING_ABYSS_5V5_ARAM_DEPRECATED_65 = 65,
/// ARAM Co-op vs AI games on Howling Abyss
/// Game mode deprecated
#[deprecated(note="Game mode deprecated")]
HOWLING_ABYSS_ARAM_CO_OP_VS_AI = 67,
/// One for All games on Summoner's Rift
/// Deprecated in patch 8.6 in favor of queueId 1020
#[deprecated(note="Deprecated in patch 8.6 in favor of queueId 1020")]
SUMMONERS_RIFT_ONE_FOR_ALL_DEPRECATED_70 = 70,
/// 1v1 Snowdown Showdown games on Howling Abyss
HOWLING_ABYSS_1V1_SNOWDOWN_SHOWDOWN = 72,
/// 2v2 Snowdown Showdown games on Howling Abyss
HOWLING_ABYSS_2V2_SNOWDOWN_SHOWDOWN = 73,
/// 6v6 Hexakill games on Summoner's Rift
SUMMONERS_RIFT_6V6_HEXAKILL = 75,
/// Ultra Rapid Fire games on Summoner's Rift
SUMMONERS_RIFT_ULTRA_RAPID_FIRE = 76,
/// One For All: Mirror Mode games on Howling Abyss
HOWLING_ABYSS_ONE_FOR_ALL_MIRROR_MODE = 78,
/// Co-op vs AI Ultra Rapid Fire games on Summoner's Rift
SUMMONERS_RIFT_CO_OP_VS_AI_ULTRA_RAPID_FIRE = 83,
/// Doom Bots Rank 1 games on Summoner's Rift
/// Deprecated in patch 7.19 in favor of queueId 950
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 950")]
SUMMONERS_RIFT_DOOM_BOTS_RANK_1 = 91,
/// Doom Bots Rank 2 games on Summoner's Rift
/// Deprecated in patch 7.19 in favor of queueId 950
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 950")]
SUMMONERS_RIFT_DOOM_BOTS_RANK_2 = 92,
/// Doom Bots Rank 5 games on Summoner's Rift
/// Deprecated in patch 7.19 in favor of queueId 950
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 950")]
SUMMONERS_RIFT_DOOM_BOTS_RANK_5 = 93,
/// Ascension games on Crystal Scar
/// Deprecated in patch 7.19 in favor of queueId 910
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 910")]
CRYSTAL_SCAR_ASCENSION_DEPRECATED_96 = 96,
/// 6v6 Hexakill games on Twisted Treeline
TWISTED_TREELINE_6V6_HEXAKILL = 98,
/// 5v5 ARAM games on Butcher's Bridge
BUTCHERS_BRIDGE_5V5_ARAM = 100,
/// Legend of the Poro King games on Howling Abyss
/// Deprecated in patch 7.19 in favor of queueId 920
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 920")]
HOWLING_ABYSS_LEGEND_OF_THE_PORO_KING_DEPRECATED_300 = 300,
/// Nemesis games on Summoner's Rift
SUMMONERS_RIFT_NEMESIS = 310,
/// Black Market Brawlers games on Summoner's Rift
SUMMONERS_RIFT_BLACK_MARKET_BRAWLERS = 313,
/// Nexus Siege games on Summoner's Rift
/// Deprecated in patch 7.19 in favor of queueId 940
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 940")]
SUMMONERS_RIFT_NEXUS_SIEGE_DEPRECATED_315 = 315,
/// Definitely Not Dominion games on Crystal Scar
CRYSTAL_SCAR_DEFINITELY_NOT_DOMINION = 317,
/// ARURF games on Summoner's Rift
/// Deprecated in patch 7.19 in favor of queueId 900
#[deprecated(note="Deprecated in patch 7.19 in favor of queueId 900")]
SUMMONERS_RIFT_ARURF = 318,
/// All Random games on Summoner's Rift
SUMMONERS_RIFT_ALL_RANDOM = 325,
/// 5v5 Draft Pick games on Summoner's Rift
SUMMONERS_RIFT_5V5_DRAFT_PICK = 400,
/// 5v5 Ranked Dynamic games on Summoner's Rift
/// Game mode deprecated in patch 6.22
#[deprecated(note="Game mode deprecated in patch 6.22")]
SUMMONERS_RIFT_5V5_RANKED_DYNAMIC = 410,
/// 5v5 Ranked Solo games on Summoner's Rift
SUMMONERS_RIFT_5V5_RANKED_SOLO = 420,
/// 5v5 Blind Pick games on Summoner's Rift
SUMMONERS_RIFT_5V5_BLIND_PICK = 430,
/// 5v5 Ranked Flex games on Summoner's Rift
SUMMONERS_RIFT_5V5_RANKED_FLEX = 440,
/// 5v5 ARAM games on Howling Abyss
HOWLING_ABYSS_5V5_ARAM = 450,
/// 3v3 Blind Pick games on Twisted Treeline
/// Deprecated in patch 9.23
#[deprecated(note="Deprecated in patch 9.23")]
TWISTED_TREELINE_3V3_BLIND_PICK = 460,
/// 3v3 Ranked Flex games on Twisted Treeline
/// Deprecated in patch 9.23
#[deprecated(note="Deprecated in patch 9.23")]
TWISTED_TREELINE_3V3_RANKED_FLEX_DEPRECATED_470 = 470,
/// Blood Hunt Assassin games on Summoner's Rift
SUMMONERS_RIFT_BLOOD_HUNT_ASSASSIN = 600,
/// Dark Star: Singularity games on Cosmic Ruins
COSMIC_RUINS_DARK_STAR_SINGULARITY = 610,
/// Clash games on Summoner's Rift
SUMMONERS_RIFT_CLASH = 700,
/// Co-op vs. AI Intermediate Bot games on Twisted Treeline
/// Deprecated in patch 9.23
#[deprecated(note="Deprecated in patch 9.23")]
TWISTED_TREELINE_CO_OP_VS_AI_INTERMEDIATE_BOT = 800,
/// Co-op vs. AI Intro Bot games on Twisted Treeline
/// Deprecated in patch 9.23
#[deprecated(note="Deprecated in patch 9.23")]
TWISTED_TREELINE_CO_OP_VS_AI_INTRO_BOT = 810,
/// Co-op vs. AI Beginner Bot games on Twisted Treeline
TWISTED_TREELINE_CO_OP_VS_AI_BEGINNER_BOT = 820,
/// Co-op vs. AI Intro Bot games on Summoner's Rift
SUMMONERS_RIFT_CO_OP_VS_AI_INTRO_BOT = 830,
/// Co-op vs. AI Beginner Bot games on Summoner's Rift
SUMMONERS_RIFT_CO_OP_VS_AI_BEGINNER_BOT = 840,
/// Co-op vs. AI Intermediate Bot games on Summoner's Rift
SUMMONERS_RIFT_CO_OP_VS_AI_INTERMEDIATE_BOT = 850,
/// URF games on Summoner's Rift
SUMMONERS_RIFT_URF = 900,
/// Ascension games on Crystal Scar
CRYSTAL_SCAR_ASCENSION = 910,
/// Legend of the Poro King games on Howling Abyss
HOWLING_ABYSS_LEGEND_OF_THE_PORO_KING = 920,
/// Nexus Siege games on Summoner's Rift
SUMMONERS_RIFT_NEXUS_SIEGE = 940,
/// Doom Bots Voting games on Summoner's Rift
SUMMONERS_RIFT_DOOM_BOTS_VOTING = 950,
/// Doom Bots Standard games on Summoner's Rift
SUMMONERS_RIFT_DOOM_BOTS_STANDARD = 960,
/// Star Guardian Invasion: Normal games on Valoran City Park
VALORAN_CITY_PARK_STAR_GUARDIAN_INVASION_NORMAL = 980,
/// Star Guardian Invasion: Onslaught games on Valoran City Park
VALORAN_CITY_PARK_STAR_GUARDIAN_INVASION_ONSLAUGHT = 990,
/// PROJECT: Hunters games on Overcharge
OVERCHARGE_PROJECT_HUNTERS = 1000,
/// Snow ARURF games on Summoner's Rift
SUMMONERS_RIFT_SNOW_ARURF = 1010,
/// One for All games on Summoner's Rift
SUMMONERS_RIFT_ONE_FOR_ALL = 1020,
/// Odyssey Extraction: Intro games on Crash Site
CRASH_SITE_ODYSSEY_EXTRACTION_INTRO = 1030,
/// Odyssey Extraction: Cadet games on Crash Site
CRASH_SITE_ODYSSEY_EXTRACTION_CADET = 1040,
/// Odyssey Extraction: Crewmember games on Crash Site
CRASH_SITE_ODYSSEY_EXTRACTION_CREWMEMBER = 1050,
/// Odyssey Extraction: Captain games on Crash Site
CRASH_SITE_ODYSSEY_EXTRACTION_CAPTAIN = 1060,
/// Odyssey Extraction: Onslaught games on Crash Site
CRASH_SITE_ODYSSEY_EXTRACTION_ONSLAUGHT = 1070,
/// Teamfight Tactics games on Convergence
CONVERGENCE_TEAMFIGHT_TACTICS = 1090,
/// Ranked Teamfight Tactics games on Convergence
CONVERGENCE_RANKED_TEAMFIGHT_TACTICS = 1100,
/// Teamfight Tactics Tutorial games on Convergence
CONVERGENCE_TEAMFIGHT_TACTICS_TUTORIAL = 1110,
/// Teamfight Tactics 1v0 testing games on Convergence
CONVERGENCE_TEAMFIGHT_TACTICS_1V0_TESTING = 1111,
/// Nexus Blitz games on Nexus Blitz
/// Deprecated in patch 9.2 in favor of queueId 1300
#[deprecated(note="Deprecated in patch 9.2 in favor of queueId 1300")]
NEXUS_BLITZ_NEXUS_BLITZ_DEPRECATED_1200 = 1200,
/// Nexus Blitz games on Nexus Blitz
NEXUS_BLITZ_NEXUS_BLITZ = 1300,
/// Tutorial 1 games on Summoner's Rift
SUMMONERS_RIFT_TUTORIAL_1 = 2000,
/// Tutorial 2 games on Summoner's Rift
SUMMONERS_RIFT_TUTORIAL_2 = 2010,
/// Tutorial 3 games on Summoner's Rift
SUMMONERS_RIFT_TUTORIAL_3 = 2020,
}

View File

@ -1,41 +1,41 @@
use strum_macros::{ EnumString, Display, AsRefStr, IntoStaticStr };
/// LoL or TFT ranked queue types.
#[non_exhaustive]
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq, Hash)]
#[derive(EnumString, Display, AsRefStr, IntoStaticStr)]
pub enum QueueType {
// League of Legends, Summoner's Rift (5v5), Ranked Solo Queue.
RANKED_SOLO_5x5,
// League of Legends, Summoner's Rift (5v5), Flex Queue.
RANKED_FLEX_SR,
// League of Legends, Twisted Treeline (3v3), Flex Queue.
RANKED_FLEX_TT,
// Ranked Teamfight Tactics.
RANKED_TFT,
// Ranked Teamfight Tactics, Hyper Roll gamemode.
RANKED_TFT_TURBO,
}
serde_string!(QueueType);
#[cfg(test)]
mod test {
use super::*;
#[test]
fn check_as_ref() {
assert_eq!("RANKED_SOLO_5x5", QueueType::RANKED_SOLO_5x5.as_ref());
}
#[test]
fn check_to_string() {
assert_eq!("RANKED_SOLO_5x5", QueueType::RANKED_SOLO_5x5.to_string());
}
#[test]
fn check_from_string() {
assert_eq!(Some(QueueType::RANKED_SOLO_5x5), "RANKED_SOLO_5x5".parse().ok());
}
}
use strum_macros::{ EnumString, Display, AsRefStr, IntoStaticStr };
/// LoL or TFT ranked queue types.
#[non_exhaustive]
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq, Hash)]
#[derive(EnumString, Display, AsRefStr, IntoStaticStr)]
pub enum QueueType {
// League of Legends, Summoner's Rift (5v5), Ranked Solo Queue.
RANKED_SOLO_5x5,
// League of Legends, Summoner's Rift (5v5), Flex Queue.
RANKED_FLEX_SR,
// League of Legends, Twisted Treeline (3v3), Flex Queue.
RANKED_FLEX_TT,
// Ranked Teamfight Tactics.
RANKED_TFT,
// Ranked Teamfight Tactics, Hyper Roll gamemode.
RANKED_TFT_TURBO,
}
serde_string!(QueueType);
#[cfg(test)]
mod test {
use super::*;
#[test]
fn check_as_ref() {
assert_eq!("RANKED_SOLO_5x5", QueueType::RANKED_SOLO_5x5.as_ref());
}
#[test]
fn check_to_string() {
assert_eq!("RANKED_SOLO_5x5", QueueType::RANKED_SOLO_5x5.to_string());
}
#[test]
fn check_from_string() {
assert_eq!(Some(QueueType::RANKED_SOLO_5x5), "RANKED_SOLO_5x5".parse().ok());
}
}

View File

@ -1,36 +1,36 @@
///////////////////////////////////////////////
///////////////////////////////////////////////
// //
// ! //
// This file is automatically generated! //
// Do not directly edit! //
// //
///////////////////////////////////////////////
use serde_repr::{ Serialize_repr, Deserialize_repr };
use num_enum::{ IntoPrimitive, TryFromPrimitive };
/// League of Legends matchmaking seasons.
#[non_exhaustive]
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq, Hash, PartialOrd, Ord)]
#[derive(Serialize_repr, Deserialize_repr)]
#[derive(IntoPrimitive, TryFromPrimitive)]
#[repr(u8)]
pub enum Season {
PRESEASON_3 = 0,
SEASON_3 = 1,
PRESEASON_2014 = 2,
SEASON_2014 = 3,
PRESEASON_2015 = 4,
SEASON_2015 = 5,
PRESEASON_2016 = 6,
SEASON_2016 = 7,
PRESEASON_2017 = 8,
SEASON_2017 = 9,
PRESEASON_2018 = 10,
SEASON_2018 = 11,
PRESEASON_2019 = 12,
SEASON_2019 = 13,
PRESEASON_2020 = 14,
SEASON_2020 = 15,
}
///////////////////////////////////////////////
use serde_repr::{ Serialize_repr, Deserialize_repr };
use num_enum::{ IntoPrimitive, TryFromPrimitive };
/// League of Legends matchmaking seasons.
#[non_exhaustive]
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq, Hash, PartialOrd, Ord)]
#[derive(Serialize_repr, Deserialize_repr)]
#[derive(IntoPrimitive, TryFromPrimitive)]
#[repr(u8)]
pub enum Season {
PRESEASON_3 = 0,
SEASON_3 = 1,
PRESEASON_2014 = 2,
SEASON_2014 = 3,
PRESEASON_2015 = 4,
SEASON_2015 = 5,
PRESEASON_2016 = 6,
SEASON_2016 = 7,
PRESEASON_2017 = 8,
SEASON_2017 = 9,
PRESEASON_2018 = 10,
SEASON_2018 = 11,
PRESEASON_2019 = 12,
SEASON_2019 = 13,
PRESEASON_2020 = 14,
SEASON_2020 = 15,
}

View File

@ -1,15 +1,15 @@
use serde_repr::{ Serialize_repr, Deserialize_repr };
use num_enum::{ IntoPrimitive, TryFromPrimitive };
/// League of Legends team.
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq, Hash, Ord, PartialOrd)]
#[derive(Serialize_repr, Deserialize_repr)]
#[derive(IntoPrimitive, TryFromPrimitive)]
#[repr(u8)]
pub enum Team {
/// Blue team (bottom left on Summoner's Rift).
BLUE = 100,
/// Red team (top right on Summoner's Rift).
RED = 200,
}
use serde_repr::{ Serialize_repr, Deserialize_repr };
use num_enum::{ IntoPrimitive, TryFromPrimitive };
/// League of Legends team.
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq, Hash, Ord, PartialOrd)]
#[derive(Serialize_repr, Deserialize_repr)]
#[derive(IntoPrimitive, TryFromPrimitive)]
#[repr(u8)]
pub enum Team {
/// Blue team (bottom left on Summoner's Rift).
BLUE = 100,
/// Red team (top right on Summoner's Rift).
RED = 200,
}

View File

@ -1,189 +1,189 @@
use strum::IntoEnumIterator;
use num_enum::{ IntoPrimitive, TryFromPrimitive };
use strum_macros::{ EnumString, Display, AsRefStr, IntoStaticStr };
/// LoL and TFT ranked tiers, such as gold, diamond, challenger, etc.
///
/// Sorts from lowest rank to highest rank.
///
/// Repr'd as arbitrary `u8` values.
///
/// Implements [IntoEnumIterator](super::IntoEnumIterator).
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq, Hash, PartialOrd, Ord)]
#[derive(IntoPrimitive, TryFromPrimitive)]
#[derive(EnumString, Display, AsRefStr, IntoStaticStr)]
#[repr(u8)]
pub enum Tier {
/// Challenger, the highest tier, an apex tier. Repr: `220_u8`.
CHALLENGER = 220,
/// Grand Master, an apex tier. Repr: `200_u8`.
GRANDMASTER = 200,
/// Master, an apex tier. Repr: `180_u8`.
MASTER = 180,
/// Diamond, the higest non-apex tier. Repr: `140_u8`.
DIAMOND = 140,
/// Platinum. Repr: `120_u8`.
PLATINUM = 120,
/// Gold. Repr: `100_u8`.
GOLD = 100,
/// Silver. Repr: `80_u8`.
SILVER = 80,
/// Bronze. Repr: `60_u8`.
BRONZE = 60,
/// Iron, the lowest tier. Repr: `40_u8`.
IRON = 40,
/// Unranked, no tier. Repr: `0_u8`.
UNRANKED = 0,
}
serde_string!(Tier);
impl Tier {
/// If this tier is an apex tier: master, grandmaster, or challenger.
/// Returns false for unranked.
///
/// These tiers are NOT queryable by LeagueV4Endpoints::get_league_entries(...).
pub const fn is_apex(self) -> bool {
// Casts needed for const.
(Self::MASTER as u8) <= (self as u8)
}
/// If this tier is a "standard" tier: iron through diamond.
/// Returns false for unranked.
///
/// ONLY these tiers are queryable by LeagueV4Endpoints::get_league_entries(...).
pub fn is_standard(self) -> bool {
// Casts needed for const.
((Self::UNRANKED as u8) < (self as u8)) && ((self as u8) < (Self::MASTER as u8))
}
/// If this tier is ranked. Returns true for iron through challenger, false for unranked.
pub const fn is_ranked(self) -> bool {
// Casts needed for const.
(Self::UNRANKED as u8) < (self as u8)
}
/// If this tier is unranked (`Tier::UNRANKED`).
///
/// UNRANKED is returned by `Participant.highest_achieved_season_tier`.
pub const fn is_unranked(self) -> bool {
// Casts needed for const.
(self as u8) <= (Self::UNRANKED as u8)
}
/// Converts UNRANKED to None and all ranked tiers to Some(...).
pub fn to_ranked(self) -> Option<Self> {
if self.is_unranked() { None } else { Some(self) }
}
}
/// Returns a DoubleEndedIterator of I, II, III, IV.
/// Ordered from high rank (I) to low (IV).
/// Excludes V, which is deprecated.
impl IntoEnumIterator for Tier {
type Iterator = std::iter::Copied<std::slice::Iter<'static, Self>>;
fn iter() -> Self::Iterator {
[
Self::CHALLENGER, Self::GRANDMASTER, Self::MASTER,
Self::DIAMOND, Self::PLATINUM, Self::GOLD,
Self::SILVER, Self::BRONZE, Self::IRON
].iter().copied()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ord() {
assert!(Tier::GOLD < Tier::DIAMOND);
assert!(Tier::UNRANKED < Tier::IRON);
}
#[test]
fn is_apex() {
assert!(Tier::GRANDMASTER.is_apex());
assert!(!Tier::DIAMOND.is_apex());
assert!(!Tier::UNRANKED.is_apex());
}
#[test]
fn is_ranked() {
assert!(Tier::GRANDMASTER.is_ranked());
assert!(Tier::DIAMOND.is_ranked());
assert!(!Tier::UNRANKED.is_ranked());
}
#[test]
fn is_unranked() {
assert!(!Tier::GRANDMASTER.is_unranked());
assert!(!Tier::DIAMOND.is_unranked());
assert!(Tier::UNRANKED.is_unranked());
}
#[test]
fn to_ranked() {
assert_eq!(Some(Tier::GRANDMASTER), Tier::GRANDMASTER.to_ranked());
assert_eq!(Some(Tier::DIAMOND), Tier::DIAMOND.to_ranked());
assert_eq!(None, Tier::UNRANKED.to_ranked());
}
#[test]
fn is_standard() {
assert!(!Tier::GRANDMASTER.is_standard());
assert!(Tier::DIAMOND.is_standard());
assert!(!Tier::UNRANKED.is_standard());
}
#[test]
fn to_string() {
assert_eq!("GRANDMASTER", Tier::GRANDMASTER.as_ref());
assert_eq!("GRANDMASTER", Tier::GRANDMASTER.to_string());
assert_eq!("UNRANKED", Tier::UNRANKED.as_ref());
assert_eq!("UNRANKED", Tier::UNRANKED.to_string());
}
#[test]
fn from_string() {
assert_eq!(Ok(Tier::GRANDMASTER), "GRANDMASTER".parse());
assert_eq!(Ok(Tier::UNRANKED), "UNRANKED".parse());
}
#[test]
fn iter() {
use strum::IntoEnumIterator;
let mut iter = Tier::iter();
assert_eq!(Some(Tier::CHALLENGER), iter.next());
iter.next();
iter.next();
assert_eq!(Some(Tier::DIAMOND), iter.next());
iter.next();
iter.next();
iter.next();
iter.next();
assert_eq!(Some(Tier::IRON), iter.next());
assert_eq!(None, iter.next());
assert_eq!(None, iter.next_back());
let mut iter = Tier::iter().rev();
assert_eq!(Some(Tier::IRON), iter.next());
iter.next();
iter.next();
iter.next();
iter.next();
assert_eq!(Some(Tier::DIAMOND), iter.next());
iter.next();
iter.next();
assert_eq!(Some(Tier::CHALLENGER), iter.next());
assert_eq!(None, iter.next());
assert_eq!(None, iter.next_back());
let mut iter = Tier::iter();
assert_eq!(Some(Tier::CHALLENGER), iter.next());
assert_eq!(Some(Tier::IRON), iter.next_back());
}
}
use strum::IntoEnumIterator;
use num_enum::{ IntoPrimitive, TryFromPrimitive };
use strum_macros::{ EnumString, Display, AsRefStr, IntoStaticStr };
/// LoL and TFT ranked tiers, such as gold, diamond, challenger, etc.
///
/// Sorts from lowest rank to highest rank.
///
/// Repr'd as arbitrary `u8` values.
///
/// Implements [IntoEnumIterator](super::IntoEnumIterator).
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq, Hash, PartialOrd, Ord)]
#[derive(IntoPrimitive, TryFromPrimitive)]
#[derive(EnumString, Display, AsRefStr, IntoStaticStr)]
#[repr(u8)]
pub enum Tier {
/// Challenger, the highest tier, an apex tier. Repr: `220_u8`.
CHALLENGER = 220,
/// Grand Master, an apex tier. Repr: `200_u8`.
GRANDMASTER = 200,
/// Master, an apex tier. Repr: `180_u8`.
MASTER = 180,
/// Diamond, the higest non-apex tier. Repr: `140_u8`.
DIAMOND = 140,
/// Platinum. Repr: `120_u8`.
PLATINUM = 120,
/// Gold. Repr: `100_u8`.
GOLD = 100,
/// Silver. Repr: `80_u8`.
SILVER = 80,
/// Bronze. Repr: `60_u8`.
BRONZE = 60,
/// Iron, the lowest tier. Repr: `40_u8`.
IRON = 40,
/// Unranked, no tier. Repr: `0_u8`.
UNRANKED = 0,
}
serde_string!(Tier);
impl Tier {
/// If this tier is an apex tier: master, grandmaster, or challenger.
/// Returns false for unranked.
///
/// These tiers are NOT queryable by LeagueV4Endpoints::get_league_entries(...).
pub const fn is_apex(self) -> bool {
// Casts needed for const.
(Self::MASTER as u8) <= (self as u8)
}
/// If this tier is a "standard" tier: iron through diamond.
/// Returns false for unranked.
///
/// ONLY these tiers are queryable by LeagueV4Endpoints::get_league_entries(...).
pub fn is_standard(self) -> bool {
// Casts needed for const.
((Self::UNRANKED as u8) < (self as u8)) && ((self as u8) < (Self::MASTER as u8))
}
/// If this tier is ranked. Returns true for iron through challenger, false for unranked.
pub const fn is_ranked(self) -> bool {
// Casts needed for const.
(Self::UNRANKED as u8) < (self as u8)
}
/// If this tier is unranked (`Tier::UNRANKED`).
///
/// UNRANKED is returned by `Participant.highest_achieved_season_tier`.
pub const fn is_unranked(self) -> bool {
// Casts needed for const.
(self as u8) <= (Self::UNRANKED as u8)
}
/// Converts UNRANKED to None and all ranked tiers to Some(...).
pub fn to_ranked(self) -> Option<Self> {
if self.is_unranked() { None } else { Some(self) }
}
}
/// Returns a DoubleEndedIterator of I, II, III, IV.
/// Ordered from high rank (I) to low (IV).
/// Excludes V, which is deprecated.
impl IntoEnumIterator for Tier {
type Iterator = std::iter::Copied<std::slice::Iter<'static, Self>>;
fn iter() -> Self::Iterator {
[
Self::CHALLENGER, Self::GRANDMASTER, Self::MASTER,
Self::DIAMOND, Self::PLATINUM, Self::GOLD,
Self::SILVER, Self::BRONZE, Self::IRON
].iter().copied()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ord() {
assert!(Tier::GOLD < Tier::DIAMOND);
assert!(Tier::UNRANKED < Tier::IRON);
}
#[test]
fn is_apex() {
assert!(Tier::GRANDMASTER.is_apex());
assert!(!Tier::DIAMOND.is_apex());
assert!(!Tier::UNRANKED.is_apex());
}
#[test]
fn is_ranked() {
assert!(Tier::GRANDMASTER.is_ranked());
assert!(Tier::DIAMOND.is_ranked());
assert!(!Tier::UNRANKED.is_ranked());
}
#[test]
fn is_unranked() {
assert!(!Tier::GRANDMASTER.is_unranked());
assert!(!Tier::DIAMOND.is_unranked());
assert!(Tier::UNRANKED.is_unranked());
}
#[test]
fn to_ranked() {
assert_eq!(Some(Tier::GRANDMASTER), Tier::GRANDMASTER.to_ranked());
assert_eq!(Some(Tier::DIAMOND), Tier::DIAMOND.to_ranked());
assert_eq!(None, Tier::UNRANKED.to_ranked());
}
#[test]
fn is_standard() {
assert!(!Tier::GRANDMASTER.is_standard());
assert!(Tier::DIAMOND.is_standard());
assert!(!Tier::UNRANKED.is_standard());
}
#[test]
fn to_string() {
assert_eq!("GRANDMASTER", Tier::GRANDMASTER.as_ref());
assert_eq!("GRANDMASTER", Tier::GRANDMASTER.to_string());
assert_eq!("UNRANKED", Tier::UNRANKED.as_ref());
assert_eq!("UNRANKED", Tier::UNRANKED.to_string());
}
#[test]
fn from_string() {
assert_eq!(Ok(Tier::GRANDMASTER), "GRANDMASTER".parse());
assert_eq!(Ok(Tier::UNRANKED), "UNRANKED".parse());
}
#[test]
fn iter() {
use strum::IntoEnumIterator;
let mut iter = Tier::iter();
assert_eq!(Some(Tier::CHALLENGER), iter.next());
iter.next();
iter.next();
assert_eq!(Some(Tier::DIAMOND), iter.next());
iter.next();
iter.next();
iter.next();
iter.next();
assert_eq!(Some(Tier::IRON), iter.next());
assert_eq!(None, iter.next());
assert_eq!(None, iter.next_back());
let mut iter = Tier::iter().rev();
assert_eq!(Some(Tier::IRON), iter.next());
iter.next();
iter.next();
iter.next();
iter.next();
assert_eq!(Some(Tier::DIAMOND), iter.next());
iter.next();
iter.next();
assert_eq!(Some(Tier::CHALLENGER), iter.next());
assert_eq!(None, iter.next());
assert_eq!(None, iter.next_back());
let mut iter = Tier::iter();
assert_eq!(Some(Tier::CHALLENGER), iter.next());
assert_eq!(Some(Tier::IRON), iter.next_back());
}
}

View File

@ -1,65 +1,65 @@
use std::error::Error as StdError;
use std::fmt;
use reqwest::{ Error, Response, StatusCode };
/// Result containing RiotApiError on failure.
pub type Result<T> = std::result::Result<T, RiotApiError>;
/// An error that occurred while processing a Riot API request.
///
/// Although Riven may make multiple requests due to retries, this will always
/// contain exactly one reqwest::Error for the final request which failed.
#[derive(Debug)]
pub struct RiotApiError {
reqwest_error: Error,
retries: u8,
response: Option<Response>,
status_code: Option<StatusCode>,
}
impl RiotApiError {
pub(crate) fn new(reqwest_error: Error, retries: u8, response: Option<Response>, status_code: Option<StatusCode>) -> Self {
Self {
reqwest_error: reqwest_error,
retries: retries,
response: response,
status_code: status_code,
}
}
/// The reqwest::Error for the final failed request.
pub fn source_reqwest_error(&self) -> &Error {
&self.reqwest_error
}
/// The number of retires attempted. Zero means exactly one request, zero retries.
pub fn retries(&self) -> u8 {
self.retries
}
/// The failed response.
/// `Some(&reqwest::Response)` if the request was sent and failed.
/// `None` if the request was not sent, OR if parsing the response JSON failed.
pub fn response(&self) -> Option<&Response> {
self.response.as_ref()
}
/// The failed response.
/// `Some(reqwest::Response)` if the request was sent and failed.
/// `None` if the request was not sent, OR if parsing the response JSON failed.
pub fn take_response(&mut self) -> Option<Response> {
self.response.take()
}
/// The failed response's HTTP status code.
/// `Some(reqwest::StatusCode)` if the request was sent and failed, OR if parsing the response JSON failed.
/// `None` if the request was not sent.
pub fn status_code(&self) -> Option<StatusCode> {
self.status_code
}
}
impl fmt::Display for RiotApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:#?}", self)
}
}
impl StdError for RiotApiError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(&self.reqwest_error)
}
}
use std::error::Error as StdError;
use std::fmt;
use reqwest::{ Error, Response, StatusCode };
/// Result containing RiotApiError on failure.
pub type Result<T> = std::result::Result<T, RiotApiError>;
/// An error that occurred while processing a Riot API request.
///
/// Although Riven may make multiple requests due to retries, this will always
/// contain exactly one reqwest::Error for the final request which failed.
#[derive(Debug)]
pub struct RiotApiError {
reqwest_error: Error,
retries: u8,
response: Option<Response>,
status_code: Option<StatusCode>,
}
impl RiotApiError {
pub(crate) fn new(reqwest_error: Error, retries: u8, response: Option<Response>, status_code: Option<StatusCode>) -> Self {
Self {
reqwest_error: reqwest_error,
retries: retries,
response: response,
status_code: status_code,
}
}
/// The reqwest::Error for the final failed request.
pub fn source_reqwest_error(&self) -> &Error {
&self.reqwest_error
}
/// The number of retires attempted. Zero means exactly one request, zero retries.
pub fn retries(&self) -> u8 {
self.retries
}
/// The failed response.
/// `Some(&reqwest::Response)` if the request was sent and failed.
/// `None` if the request was not sent, OR if parsing the response JSON failed.
pub fn response(&self) -> Option<&Response> {
self.response.as_ref()
}
/// The failed response.
/// `Some(reqwest::Response)` if the request was sent and failed.
/// `None` if the request was not sent, OR if parsing the response JSON failed.
pub fn take_response(&mut self) -> Option<Response> {
self.response.take()
}
/// The failed response's HTTP status code.
/// `Some(reqwest::StatusCode)` if the request was sent and failed, OR if parsing the response JSON failed.
/// `None` if the request was not sent.
pub fn status_code(&self) -> Option<StatusCode> {
self.status_code
}
}
impl fmt::Display for RiotApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:#?}", self)
}
}
impl StdError for RiotApiError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(&self.reqwest_error)
}
}

View File

@ -1,18 +1,18 @@
//! Module containing rate limiting and requesting types.
mod rate_limit;
pub use rate_limit::*;
mod rate_limit_type;
pub use rate_limit_type::*;
use std::time::Instant; // Hack for token_bucket_test.rs.
mod token_bucket;
pub use token_bucket::*;
mod regional_requester;
pub use regional_requester::*;
#[cfg(test)]
#[path = "token_bucket.test.rs"]
mod token_bucket_test;
//! Module containing rate limiting and requesting types.
mod rate_limit;
pub use rate_limit::*;
mod rate_limit_type;
pub use rate_limit_type::*;
use std::time::Instant; // Hack for token_bucket_test.rs.
mod token_bucket;
pub use token_bucket::*;
mod regional_requester;
pub use regional_requester::*;
#[cfg(test)]
#[path = "token_bucket.test.rs"]
mod token_bucket_test;

View File

@ -1,204 +1,204 @@
use std::cmp;
use std::time::{ Duration, Instant };
use log;
use parking_lot::{ RwLock, RwLockUpgradableReadGuard };
use reqwest::{ StatusCode, Response };
use scan_fmt::scan_fmt;
use crate::RiotApiConfig;
use super::{ TokenBucket, VectorTokenBucket };
use super::RateLimitType;
pub struct RateLimit {
rate_limit_type: RateLimitType,
// Buckets for this rate limit (synchronized).
// Almost always read, written only when rate limit rates are updated
// from API response.
buckets: RwLock<Vec<VectorTokenBucket>>,
// Set to when we can retry if a retry-after header is received.
retry_after: RwLock<Option<Instant>>,
}
impl RateLimit {
/// Header specifying which RateLimitType caused a 429.
const HEADER_XRATELIMITTYPE: &'static str = "X-Rate-Limit-Type";
pub fn new(rate_limit_type: RateLimitType) -> Self {
let initial_bucket = VectorTokenBucket::new(
Duration::from_secs(1), 1, Duration::new(0, 0), 1.0);
RateLimit {
rate_limit_type: rate_limit_type,
// Rate limit before getting from response: 1/s.
buckets: RwLock::new(vec![initial_bucket]),
retry_after: RwLock::new(None),
}
}
pub fn get_both_or_delay(app_rate_limit: &Self, method_rate_limit: &Self) -> Option<Duration> {
// Check retry after.
{
let retry_after_delay = app_rate_limit.get_retry_after_delay()
.and_then(|a| method_rate_limit.get_retry_after_delay().map(|m| cmp::max(a, m)));
if retry_after_delay.is_some() {
return retry_after_delay
}
}
// Check buckets.
let app_buckets = app_rate_limit.buckets.read();
let method_buckets = method_rate_limit.buckets.read();
for bucket in app_buckets.iter().chain(method_buckets.iter()) {
let delay = bucket.get_delay();
if delay.is_some() {
return delay;
}
}
// Success.
for bucket in app_buckets.iter().chain(method_buckets.iter()) {
bucket.get_tokens(1);
}
log::trace!("Tokens obtained, buckets: APP {:?} METHOD {:?}", app_buckets, method_buckets);
None
}
pub fn get_retry_after_delay(&self) -> Option<Duration> {
self.retry_after.read().and_then(|i| Instant::now().checked_duration_since(i))
}
pub fn on_response(&self, config: &RiotApiConfig, response: &Response) {
self.on_response_retry_after(response);
self.on_response_rate_limits(config, response);
}
/// `on_response` helper for retry after check.
#[inline]
fn on_response_retry_after(&self, response: &Response) {
if let Some(retry_after) = || -> Option<Instant> {
// Only care about 429s.
if StatusCode::TOO_MANY_REQUESTS != response.status() {
return None;
}
{
// Check if the header that indicates the relevant RateLimit is present.
let header_opt = response.headers()
.get(RateLimit::HEADER_XRATELIMITTYPE)
.or_else(|| {
log::info!("429 response missing {} header.", RateLimit::HEADER_XRATELIMITTYPE);
None
})
.and_then(|header_value| header_value.to_str()
.map_err(|e| log::info!("429 response, failed to parse {} header as string: {}.",
RateLimit::HEADER_XRATELIMITTYPE, e))
.ok());
match header_opt {
None => {
// Take default responsibility, or ignore.
if !self.rate_limit_type.default_responsibility() {
return None;
}
log::warn!("429 response has missing or invalid {} header, {} rate limit taking responsibility.",
RateLimit::HEADER_XRATELIMITTYPE, self.rate_limit_type.type_name());
}
Some(header_value) => {
// Ignore if header's value does not match us.
if self.rate_limit_type.type_name() != header_value.to_lowercase() {
return None;
}
}
}
}
// Get retry after header. Only care if it exists.
let retry_after_header = response.headers()
.get(reqwest::header::RETRY_AFTER)?.to_str()
.expect("Failed to read retry-after header as string.");
log::info!("429 response, rate limit {}, retry-after {} secs.", self.rate_limit_type.type_name(), retry_after_header);
// Header currently only returns ints, but float is more general. Can be zero.
let retry_after_secs: f32 = retry_after_header.parse()
.expect("Failed to parse retry-after header as f32.");
// Add 0.5 seconds to account for rounding, cases when response is zero.
let delay = Duration::from_secs_f32(0.5 + retry_after_secs);
return Some(Instant::now() + delay);
}() {
*self.retry_after.write() = Some(retry_after);
}
}
#[inline]
fn on_response_rate_limits(&self, config: &RiotApiConfig, response: &Response) {
// Check if rate limits changed.
let headers = response.headers();
let limit_header_opt = headers.get(self.rate_limit_type.limit_header())
.map(|h| h.to_str().expect("Failed to read limit header as string."));
let count_header_opt = headers.get(self.rate_limit_type.count_header())
.map(|h| h.to_str().expect("Failed to read count header as string."));
// https://github.com/rust-lang/rust/issues/53667
if let Some(limit_header) = limit_header_opt {
if let Some(count_header) = count_header_opt {
let buckets = self.buckets.upgradable_read();
if !buckets_require_updating(limit_header, &*buckets) {
return;
}
// Buckets require updating. Upgrade to write lock.
let mut buckets = RwLockUpgradableReadGuard::upgrade(buckets);
*buckets = buckets_from_header(config, limit_header, count_header)
}}
}
}
fn buckets_require_updating(limit_header: &str, buckets: &Vec<VectorTokenBucket>) -> bool {
if buckets.len() != limit_header.split(",").count() {
return true;
}
for (limit_header_entry, bucket) in limit_header.split(",").zip(&*buckets) {
// 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());
if limit_header_entry != bucket_entry {
return true;
}
}
false
}
fn buckets_from_header(config: &RiotApiConfig, limit_header: &str, count_header: &str) -> Vec<VectorTokenBucket> {
// Limits: "20000:10,1200000:600"
// Counts: "7:10,58:600"
let size = limit_header.split(",").count();
debug_assert!(size == count_header.split(",").count());
let mut out = Vec::with_capacity(size);
for (limit_entry, count_entry) in limit_header.split(",").zip(count_header.split(",")) {
let (limit, limit_secs) = scan_fmt!(limit_entry, "{d}:{d}", usize, u64)
.unwrap_or_else(|_| panic!("Failed to parse limit entry \"{}\".", limit_entry));
let (count, count_secs) = scan_fmt!(count_entry, "{d}:{d}", usize, u64)
.unwrap_or_else(|_| panic!("Failed to parse count entry \"{}\".", count_entry));
debug_assert!(limit_secs == count_secs);
let limit_f32 = limit as f32;
let scaled_burst_pct = config.burst_pct * limit_f32 / (limit_f32 + 1.0);
let bucket = VectorTokenBucket::new(Duration::from_secs(limit_secs), limit,
config.duration_overhead, scaled_burst_pct);
bucket.get_tokens(count);
out.push(bucket);
}
log::debug!("Set buckets to {} limit, {} count.", limit_header, count_header);
out
}
#[cfg(test)]
mod tests {
// use super::*;
// fn send_sync() {
// fn is_send_sync<T: Send + Sync>() {}
// is_send_sync::<RateLimit>();
// }
}
use std::cmp;
use std::time::{ Duration, Instant };
use log;
use parking_lot::{ RwLock, RwLockUpgradableReadGuard };
use reqwest::{ StatusCode, Response };
use scan_fmt::scan_fmt;
use crate::RiotApiConfig;
use super::{ TokenBucket, VectorTokenBucket };
use super::RateLimitType;
pub struct RateLimit {
rate_limit_type: RateLimitType,
// Buckets for this rate limit (synchronized).
// Almost always read, written only when rate limit rates are updated
// from API response.
buckets: RwLock<Vec<VectorTokenBucket>>,
// Set to when we can retry if a retry-after header is received.
retry_after: RwLock<Option<Instant>>,
}
impl RateLimit {
/// Header specifying which RateLimitType caused a 429.
const HEADER_XRATELIMITTYPE: &'static str = "X-Rate-Limit-Type";
pub fn new(rate_limit_type: RateLimitType) -> Self {
let initial_bucket = VectorTokenBucket::new(
Duration::from_secs(1), 1, Duration::new(0, 0), 1.0);
RateLimit {
rate_limit_type: rate_limit_type,
// Rate limit before getting from response: 1/s.
buckets: RwLock::new(vec![initial_bucket]),
retry_after: RwLock::new(None),
}
}
pub fn get_both_or_delay(app_rate_limit: &Self, method_rate_limit: &Self) -> Option<Duration> {
// Check retry after.
{
let retry_after_delay = app_rate_limit.get_retry_after_delay()
.and_then(|a| method_rate_limit.get_retry_after_delay().map(|m| cmp::max(a, m)));
if retry_after_delay.is_some() {
return retry_after_delay
}
}
// Check buckets.
let app_buckets = app_rate_limit.buckets.read();
let method_buckets = method_rate_limit.buckets.read();
for bucket in app_buckets.iter().chain(method_buckets.iter()) {
let delay = bucket.get_delay();
if delay.is_some() {
return delay;
}
}
// Success.
for bucket in app_buckets.iter().chain(method_buckets.iter()) {
bucket.get_tokens(1);
}
log::trace!("Tokens obtained, buckets: APP {:?} METHOD {:?}", app_buckets, method_buckets);
None
}
pub fn get_retry_after_delay(&self) -> Option<Duration> {
self.retry_after.read().and_then(|i| Instant::now().checked_duration_since(i))
}
pub fn on_response(&self, config: &RiotApiConfig, response: &Response) {
self.on_response_retry_after(response);
self.on_response_rate_limits(config, response);
}
/// `on_response` helper for retry after check.
#[inline]
fn on_response_retry_after(&self, response: &Response) {
if let Some(retry_after) = || -> Option<Instant> {
// Only care about 429s.
if StatusCode::TOO_MANY_REQUESTS != response.status() {
return None;
}
{
// Check if the header that indicates the relevant RateLimit is present.
let header_opt = response.headers()
.get(RateLimit::HEADER_XRATELIMITTYPE)
.or_else(|| {
log::info!("429 response missing {} header.", RateLimit::HEADER_XRATELIMITTYPE);
None
})
.and_then(|header_value| header_value.to_str()
.map_err(|e| log::info!("429 response, failed to parse {} header as string: {}.",
RateLimit::HEADER_XRATELIMITTYPE, e))
.ok());
match header_opt {
None => {
// Take default responsibility, or ignore.
if !self.rate_limit_type.default_responsibility() {
return None;
}
log::warn!("429 response has missing or invalid {} header, {} rate limit taking responsibility.",
RateLimit::HEADER_XRATELIMITTYPE, self.rate_limit_type.type_name());
}
Some(header_value) => {
// Ignore if header's value does not match us.
if self.rate_limit_type.type_name() != header_value.to_lowercase() {
return None;
}
}
}
}
// Get retry after header. Only care if it exists.
let retry_after_header = response.headers()
.get(reqwest::header::RETRY_AFTER)?.to_str()
.expect("Failed to read retry-after header as string.");
log::info!("429 response, rate limit {}, retry-after {} secs.", self.rate_limit_type.type_name(), retry_after_header);
// Header currently only returns ints, but float is more general. Can be zero.
let retry_after_secs: f32 = retry_after_header.parse()
.expect("Failed to parse retry-after header as f32.");
// Add 0.5 seconds to account for rounding, cases when response is zero.
let delay = Duration::from_secs_f32(0.5 + retry_after_secs);
return Some(Instant::now() + delay);
}() {
*self.retry_after.write() = Some(retry_after);
}
}
#[inline]
fn on_response_rate_limits(&self, config: &RiotApiConfig, response: &Response) {
// Check if rate limits changed.
let headers = response.headers();
let limit_header_opt = headers.get(self.rate_limit_type.limit_header())
.map(|h| h.to_str().expect("Failed to read limit header as string."));
let count_header_opt = headers.get(self.rate_limit_type.count_header())
.map(|h| h.to_str().expect("Failed to read count header as string."));
// https://github.com/rust-lang/rust/issues/53667
if let Some(limit_header) = limit_header_opt {
if let Some(count_header) = count_header_opt {
let buckets = self.buckets.upgradable_read();
if !buckets_require_updating(limit_header, &*buckets) {
return;
}
// Buckets require updating. Upgrade to write lock.
let mut buckets = RwLockUpgradableReadGuard::upgrade(buckets);
*buckets = buckets_from_header(config, limit_header, count_header)
}}
}
}
fn buckets_require_updating(limit_header: &str, buckets: &Vec<VectorTokenBucket>) -> bool {
if buckets.len() != limit_header.split(",").count() {
return true;
}
for (limit_header_entry, bucket) in limit_header.split(",").zip(&*buckets) {
// 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());
if limit_header_entry != bucket_entry {
return true;
}
}
false
}
fn buckets_from_header(config: &RiotApiConfig, limit_header: &str, count_header: &str) -> Vec<VectorTokenBucket> {
// Limits: "20000:10,1200000:600"
// Counts: "7:10,58:600"
let size = limit_header.split(",").count();
debug_assert!(size == count_header.split(",").count());
let mut out = Vec::with_capacity(size);
for (limit_entry, count_entry) in limit_header.split(",").zip(count_header.split(",")) {
let (limit, limit_secs) = scan_fmt!(limit_entry, "{d}:{d}", usize, u64)
.unwrap_or_else(|_| panic!("Failed to parse limit entry \"{}\".", limit_entry));
let (count, count_secs) = scan_fmt!(count_entry, "{d}:{d}", usize, u64)
.unwrap_or_else(|_| panic!("Failed to parse count entry \"{}\".", count_entry));
debug_assert!(limit_secs == count_secs);
let limit_f32 = limit as f32;
let scaled_burst_pct = config.burst_pct * limit_f32 / (limit_f32 + 1.0);
let bucket = VectorTokenBucket::new(Duration::from_secs(limit_secs), limit,
config.duration_overhead, scaled_burst_pct);
bucket.get_tokens(count);
out.push(bucket);
}
log::debug!("Set buckets to {} limit, {} count.", limit_header, count_header);
out
}
#[cfg(test)]
mod tests {
// use super::*;
// fn send_sync() {
// fn is_send_sync<T: Send + Sync>() {}
// is_send_sync::<RateLimit>();
// }
}

View File

@ -1,37 +1,37 @@
#[derive(Copy, Clone)]
pub enum RateLimitType {
Application,
Method,
}
impl RateLimitType {
pub fn type_name(self) -> &'static str {
match self {
Self::Application => "application",
Self::Method => "method",
}
}
pub fn limit_header(self) -> &'static str {
match self {
Self::Application => "X-App-Rate-Limit",
Self::Method => "X-Method-Rate-Limit",
}
}
pub fn count_header(self) -> &'static str {
match self {
Self::Application => "X-App-Rate-Limit-Count",
Self::Method => "X-Method-Rate-Limit-Count",
}
}
/// Return if this RateLimitType should take responsibility for responses
/// which are lacking a "X-Rate-Limit-Type" header.
pub fn default_responsibility(self) -> bool {
match self {
Self::Application => true,
Self::Method => false,
}
}
}
#[derive(Copy, Clone)]
pub enum RateLimitType {
Application,
Method,
}
impl RateLimitType {
pub fn type_name(self) -> &'static str {
match self {
Self::Application => "application",
Self::Method => "method",
}
}
pub fn limit_header(self) -> &'static str {
match self {
Self::Application => "X-App-Rate-Limit",
Self::Method => "X-Method-Rate-Limit",
}
}
pub fn count_header(self) -> &'static str {
match self {
Self::Application => "X-App-Rate-Limit-Count",
Self::Method => "X-Method-Rate-Limit-Count",
}
}
/// Return if this RateLimitType should take responsibility for responses
/// which are lacking a "X-Rate-Limit-Type" header.
pub fn default_responsibility(self) -> bool {
match self {
Self::Application => true,
Self::Method => false,
}
}
}

View File

@ -1,97 +1,97 @@
use std::future::Future;
use std::sync::Arc;
use log;
use reqwest::{ StatusCode, RequestBuilder };
use crate::Result;
use crate::ResponseInfo;
use crate::RiotApiError;
use crate::RiotApiConfig;
use crate::util::InsertOnlyCHashMap;
use super::RateLimit;
use super::RateLimitType;
pub struct RegionalRequester {
/// The app rate limit.
app_rate_limit: RateLimit,
/// Method rate limits.
method_rate_limits: InsertOnlyCHashMap<&'static str, RateLimit>,
}
impl RegionalRequester {
/// HTTP status codes which are considered a success but will results in `None`.
const NONE_STATUS_CODES: [StatusCode; 2] = [
StatusCode::NO_CONTENT, // 204
StatusCode::NOT_FOUND, // 404
];
pub fn new() -> Self {
Self {
app_rate_limit: RateLimit::new(RateLimitType::Application),
method_rate_limits: InsertOnlyCHashMap::new(),
}
}
pub fn execute<'a>(self: Arc<Self>,
config: &'a RiotApiConfig,
method_id: &'static str, request: RequestBuilder)
-> impl Future<Output = Result<ResponseInfo>> + 'a
{
async move {
let mut retries: u8 = 0;
loop {
let method_rate_limit: Arc<RateLimit> = self.method_rate_limits
.get_or_insert_with(method_id, || RateLimit::new(RateLimitType::Method));
// Rate limiting.
while let Some(delay) = RateLimit::get_both_or_delay(&self.app_rate_limit, &*method_rate_limit) {
tokio::time::sleep(delay).await;
}
// Send request.
let request_clone = request.try_clone().expect("Failed to clone request.");
let response = request_clone.send().await
.map_err(|e| RiotApiError::new(e, retries, None, None))?;
// Maybe update rate limits (based on response headers).
self.app_rate_limit.on_response(&config, &response);
method_rate_limit.on_response(&config, &response);
let status = response.status();
// Handle normal success / failure cases.
let status_none = Self::NONE_STATUS_CODES.contains(&status);
// Success case.
if status.is_success() || status_none {
log::trace!("Response {} (retried {} times), success, returning result.", status, retries);
break Ok(ResponseInfo {
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.
// Not-retryable: no more retries or 4xx or ? (3xx, redirects exceeded).
// Retryable: retries remaining, and 429 or 5xx.
if retries >= config.retries ||
(StatusCode::TOO_MANY_REQUESTS != status
&& !status.is_server_error())
{
log::debug!("Response {} (retried {} times), failure, returning error.", status, retries);
break Err(RiotApiError::new(err, retries, Some(response), Some(status)));
}
log::debug!("Response {} (retried {} times), retrying.", status, retries);
retries += 1;
}
}
}
}
#[cfg(test)]
mod tests {
// use super::*;
}
use std::future::Future;
use std::sync::Arc;
use log;
use reqwest::{ StatusCode, RequestBuilder };
use crate::Result;
use crate::ResponseInfo;
use crate::RiotApiError;
use crate::RiotApiConfig;
use crate::util::InsertOnlyCHashMap;
use super::RateLimit;
use super::RateLimitType;
pub struct RegionalRequester {
/// The app rate limit.
app_rate_limit: RateLimit,
/// Method rate limits.
method_rate_limits: InsertOnlyCHashMap<&'static str, RateLimit>,
}
impl RegionalRequester {
/// HTTP status codes which are considered a success but will results in `None`.
const NONE_STATUS_CODES: [StatusCode; 2] = [
StatusCode::NO_CONTENT, // 204
StatusCode::NOT_FOUND, // 404
];
pub fn new() -> Self {
Self {
app_rate_limit: RateLimit::new(RateLimitType::Application),
method_rate_limits: InsertOnlyCHashMap::new(),
}
}
pub fn execute<'a>(self: Arc<Self>,
config: &'a RiotApiConfig,
method_id: &'static str, request: RequestBuilder)
-> impl Future<Output = Result<ResponseInfo>> + 'a
{
async move {
let mut retries: u8 = 0;
loop {
let method_rate_limit: Arc<RateLimit> = self.method_rate_limits
.get_or_insert_with(method_id, || RateLimit::new(RateLimitType::Method));
// Rate limiting.
while let Some(delay) = RateLimit::get_both_or_delay(&self.app_rate_limit, &*method_rate_limit) {
tokio::time::sleep(delay).await;
}
// Send request.
let request_clone = request.try_clone().expect("Failed to clone request.");
let response = request_clone.send().await
.map_err(|e| RiotApiError::new(e, retries, None, None))?;
// Maybe update rate limits (based on response headers).
self.app_rate_limit.on_response(&config, &response);
method_rate_limit.on_response(&config, &response);
let status = response.status();
// Handle normal success / failure cases.
let status_none = Self::NONE_STATUS_CODES.contains(&status);
// Success case.
if status.is_success() || status_none {
log::trace!("Response {} (retried {} times), success, returning result.", status, retries);
break Ok(ResponseInfo {
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.
// Not-retryable: no more retries or 4xx or ? (3xx, redirects exceeded).
// Retryable: retries remaining, and 429 or 5xx.
if retries >= config.retries ||
(StatusCode::TOO_MANY_REQUESTS != status
&& !status.is_server_error())
{
log::debug!("Response {} (retried {} times), failure, returning error.", status, retries);
break Err(RiotApiError::new(err, retries, Some(response), Some(status)));
}
log::debug!("Response {} (retried {} times), retrying.", status, retries);
retries += 1;
}
}
}
}
#[cfg(test)]
mod tests {
// use super::*;
}

View File

@ -1,156 +1,156 @@
use std::fmt;
use std::collections::VecDeque;
use std::time::Duration;
use parking_lot::{Mutex, MutexGuard};
use super::Instant; // Hack for token_bucket_test.rs.
/// A `TokenBucket` keeps track of number of requests allowed per duration of
/// time.
///
/// Respone headers contain descriptions of rate limits such as
/// `"X-App-Rate-Limit": "20:1,100:120"`. Each `TokenBucket` corresponds to a
/// single `"100:120"` (100 requests per 120 seconds).
pub trait TokenBucket {
/// Get the duration til the next available token, or None if a token
/// is available.
/// # Returns
/// Duration or 0 duration.
fn get_delay(&self) -> Option<Duration>;
/// Gets n tokens, regardless of whether they are available.
/// # Parameters
/// * `n` - Number of tokens to take.
/// # Returns
/// True if the tokens were obtained without violating limits, false
/// otherwise.
fn get_tokens(&self, n: usize) -> bool;
/// Get the duration of this bucket.
/// # Returns
/// Duration of the bucket.
fn get_bucket_duration(&self) -> Duration;
/// Get the total limit of this bucket per timespan.
/// # Returns
/// Total limit per timespan.
fn get_total_limit(&self) -> usize;
}
pub struct VectorTokenBucket {
/// Duration of this TokenBucket.
duration: Duration,
// Total tokens available from this TokenBucket.
total_limit: usize,
/// Extra duration to be considered on top of `duration`, to account for
/// varying network latency.
duration_overhead: Duration,
/// Duration considered for burst factor.
burst_duration: Duration,
/// Limit allowed per burst_duration, for burst factor.
burst_limit: usize,
/// Record of timestamps (synchronized).
timestamps: Mutex<VecDeque<Instant>>,
}
impl VectorTokenBucket {
pub fn new(duration: Duration, total_limit: usize,
duration_overhead: Duration, burst_pct: f32) -> Self
{
debug_assert!(0.0 < burst_pct && burst_pct <= 1.0,
"BAD burst_pct {}.", burst_pct);
// Float ops may lose precision, but nothing should be that precise.
// API always uses round numbers, burst_pct is frac of 256.
// Effective duration.
let d_eff = duration + duration_overhead;
let burst_duration = Duration::new(
(d_eff.as_secs() as f32 * burst_pct).ceil() as u64,
(d_eff.subsec_nanos() as f32 * burst_pct).ceil() as u32);
let burst_limit = std::cmp::max(1,
(total_limit as f32 * burst_pct).floor() as usize);
debug_assert!(burst_limit <= total_limit);
VectorTokenBucket {
duration: duration,
total_limit: total_limit,
duration_overhead: duration_overhead,
burst_duration: burst_duration,
burst_limit: burst_limit,
timestamps: Mutex::new(VecDeque::with_capacity(total_limit)),
}
}
fn update_get_timestamps(&self) -> MutexGuard<VecDeque<Instant>> {
let mut timestamps = self.timestamps.lock();
let cutoff = Instant::now() - self.duration - self.duration_overhead;
// We only need to trim the end of the queue to not leak memory.
// We could do it lazily somehow if we wanted to be really fancy.
while timestamps.back().map_or(false, |ts| *ts < cutoff) {
timestamps.pop_back();
}
return timestamps;
}
}
impl TokenBucket for VectorTokenBucket {
fn get_delay(&self) -> Option<Duration> {
let timestamps = self.update_get_timestamps();
// The "?" means:
// `if timestamps.len() < self.total_limit { return None }`
// Timestamp that needs to be popped before
// we can enter another timestamp.
// Full rate limit.
if let Some(ts) = timestamps.get(self.total_limit - 1) {
// Return amount of time needed for timestamp `ts` to go away.
Instant::now().checked_duration_since(*ts)
.and_then(|passed_dur| (self.duration + self.duration_overhead)
.checked_sub(passed_dur))
}
// Otherwise burst rate limit.
else if let Some(ts) = timestamps.get(self.burst_limit - 1) {
// Return amount of time needed for timestamp `ts` to go away.
Instant::now().checked_duration_since(*ts)
.and_then(|passed_dur| self.burst_duration.checked_sub(passed_dur))
}
// No delay needed.
else {
None
}
}
fn get_tokens(&self, n: usize) -> bool {
let mut timestamps = self.update_get_timestamps();
let now = Instant::now();
timestamps.reserve(n);
for _ in 0..n {
timestamps.push_front(now);
}
timestamps.len() <= self.total_limit
}
fn get_bucket_duration(&self) -> Duration {
self.duration
}
fn get_total_limit(&self) -> usize {
self.total_limit
}
}
impl fmt::Debug for VectorTokenBucket {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}/{}:{})", self.timestamps.lock().len(), self.total_limit, self.duration.as_secs())
}
}
use std::fmt;
use std::collections::VecDeque;
use std::time::Duration;
use parking_lot::{Mutex, MutexGuard};
use super::Instant; // Hack for token_bucket_test.rs.
/// A `TokenBucket` keeps track of number of requests allowed per duration of
/// time.
///
/// Respone headers contain descriptions of rate limits such as
/// `"X-App-Rate-Limit": "20:1,100:120"`. Each `TokenBucket` corresponds to a
/// single `"100:120"` (100 requests per 120 seconds).
pub trait TokenBucket {
/// Get the duration til the next available token, or None if a token
/// is available.
/// # Returns
/// Duration or 0 duration.
fn get_delay(&self) -> Option<Duration>;
/// Gets n tokens, regardless of whether they are available.
/// # Parameters
/// * `n` - Number of tokens to take.
/// # Returns
/// True if the tokens were obtained without violating limits, false
/// otherwise.
fn get_tokens(&self, n: usize) -> bool;
/// Get the duration of this bucket.
/// # Returns
/// Duration of the bucket.
fn get_bucket_duration(&self) -> Duration;
/// Get the total limit of this bucket per timespan.
/// # Returns
/// Total limit per timespan.
fn get_total_limit(&self) -> usize;
}
pub struct VectorTokenBucket {
/// Duration of this TokenBucket.
duration: Duration,
// Total tokens available from this TokenBucket.
total_limit: usize,
/// Extra duration to be considered on top of `duration`, to account for
/// varying network latency.
duration_overhead: Duration,
/// Duration considered for burst factor.
burst_duration: Duration,
/// Limit allowed per burst_duration, for burst factor.
burst_limit: usize,
/// Record of timestamps (synchronized).
timestamps: Mutex<VecDeque<Instant>>,
}
impl VectorTokenBucket {
pub fn new(duration: Duration, total_limit: usize,
duration_overhead: Duration, burst_pct: f32) -> Self
{
debug_assert!(0.0 < burst_pct && burst_pct <= 1.0,
"BAD burst_pct {}.", burst_pct);
// Float ops may lose precision, but nothing should be that precise.
// API always uses round numbers, burst_pct is frac of 256.
// Effective duration.
let d_eff = duration + duration_overhead;
let burst_duration = Duration::new(
(d_eff.as_secs() as f32 * burst_pct).ceil() as u64,
(d_eff.subsec_nanos() as f32 * burst_pct).ceil() as u32);
let burst_limit = std::cmp::max(1,
(total_limit as f32 * burst_pct).floor() as usize);
debug_assert!(burst_limit <= total_limit);
VectorTokenBucket {
duration: duration,
total_limit: total_limit,
duration_overhead: duration_overhead,
burst_duration: burst_duration,
burst_limit: burst_limit,
timestamps: Mutex::new(VecDeque::with_capacity(total_limit)),
}
}
fn update_get_timestamps(&self) -> MutexGuard<VecDeque<Instant>> {
let mut timestamps = self.timestamps.lock();
let cutoff = Instant::now() - self.duration - self.duration_overhead;
// We only need to trim the end of the queue to not leak memory.
// We could do it lazily somehow if we wanted to be really fancy.
while timestamps.back().map_or(false, |ts| *ts < cutoff) {
timestamps.pop_back();
}
return timestamps;
}
}
impl TokenBucket for VectorTokenBucket {
fn get_delay(&self) -> Option<Duration> {
let timestamps = self.update_get_timestamps();
// The "?" means:
// `if timestamps.len() < self.total_limit { return None }`
// Timestamp that needs to be popped before
// we can enter another timestamp.
// Full rate limit.
if let Some(ts) = timestamps.get(self.total_limit - 1) {
// Return amount of time needed for timestamp `ts` to go away.
Instant::now().checked_duration_since(*ts)
.and_then(|passed_dur| (self.duration + self.duration_overhead)
.checked_sub(passed_dur))
}
// Otherwise burst rate limit.
else if let Some(ts) = timestamps.get(self.burst_limit - 1) {
// Return amount of time needed for timestamp `ts` to go away.
Instant::now().checked_duration_since(*ts)
.and_then(|passed_dur| self.burst_duration.checked_sub(passed_dur))
}
// No delay needed.
else {
None
}
}
fn get_tokens(&self, n: usize) -> bool {
let mut timestamps = self.update_get_timestamps();
let now = Instant::now();
timestamps.reserve(n);
for _ in 0..n {
timestamps.push_front(now);
}
timestamps.len() <= self.total_limit
}
fn get_bucket_duration(&self) -> Duration {
self.duration
}
fn get_total_limit(&self) -> usize {
self.total_limit
}
}
impl fmt::Debug for VectorTokenBucket {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}/{}:{})", self.timestamps.lock().len(), self.total_limit, self.duration.as_secs())
}
}

View File

@ -1,110 +1,110 @@
#![cfg(test)]
use fake_instant::FakeClock as Instant;
/// This is a hack to test token bucket, substituting FakeClock for Instant.
mod token_bucket {
include!("token_bucket.rs");
mod tests {
use super::*;
use lazy_static::lazy_static;
lazy_static! {
pub static ref D00: Duration = Duration::new(0, 0);
}
#[test]
fn test_basic() {
Instant::set_time(50_000);
let bucket = VectorTokenBucket::new(Duration::from_millis(1000), 100, *D00, 0.95);
assert!(bucket.get_tokens(50), "Should have not violated limit.");
assert_eq!(None, bucket.get_delay(), "Can get stuff.");
assert!(!bucket.get_tokens(51), "Should have violated limit.");
}
#[test]
fn test_internal_constructor() {
let bucket = VectorTokenBucket::new(Duration::from_millis(1000), 100, *D00, 1.0);
assert_eq!(100, bucket.burst_limit);
let bucket = VectorTokenBucket::new(Duration::from_millis(1000), 100, *D00, 1e-6);
assert_eq!(1, bucket.burst_limit);
}
#[test]
fn test_saturated_100_burst() {
let bucket = VectorTokenBucket::new(Duration::from_millis(1000), 100, *D00, 1.00);
Instant::set_time(50_000);
assert!(bucket.get_tokens(100), "All tokens should be immediately available.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
Instant::advance_time(1001); // Extra buffer for Duration(0).
assert!(bucket.get_tokens(100), "All tokens should be available after a bucket duration.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
}
#[test]
fn test_saturated_95_burst() {
let bucket = VectorTokenBucket::new(Duration::from_millis(1000), 100, *D00, 0.50);
Instant::set_time(50_000);
assert!(bucket.get_tokens(95), "95 tokens should be immediately available.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
Instant::advance_time(475); // Total 951.
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
Instant::advance_time(476); // Extra buffer for Duration(0).
assert!(bucket.get_tokens(5), "Last 5 tokens should be available.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
Instant::advance_time(51);
assert!(bucket.get_tokens(95), "95 tokens should be available.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
Instant::advance_time(951);
assert!(bucket.get_tokens(5), "Last 5 tokens should be available.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
}
#[test]
fn test_saturated_50_burst() {
let bucket = VectorTokenBucket::new(Duration::from_millis(1000), 100, *D00, 0.5);
Instant::set_time(50_000);
assert!(bucket.get_tokens(50), "Half the tokens should be immediately available.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
Instant::advance_time(501); // Extra buffer for Duration(0).
assert!(bucket.get_tokens(50), "Half the tokens should be available after a half bucket duration.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
Instant::advance_time(501);
assert!(bucket.get_tokens(50), "Half the tokens should be available after a full bucket duration.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
Instant::advance_time(501);
assert!(bucket.get_tokens(50));
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
}
#[test]
fn test_many() {
Instant::set_time(50_000);
let bucket = VectorTokenBucket::new(Duration::from_millis(1000), 100, *D00, 0.95);
assert!(bucket.get_tokens(50), "Should have not violated limit.");
assert_eq!(None, bucket.get_delay(), "Should not be blocked.");
for _ in 0..20_000 {
Instant::advance_time(501);
assert!(bucket.get_tokens(50), "Should have not violated limit.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
Instant::advance_time(501);
assert!(bucket.get_tokens(50), "Should have not violated limit.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
}
assert!(bucket.timestamps.lock().len() < 110, "Check memory leak.");
}
}
}
#![cfg(test)]
use fake_instant::FakeClock as Instant;
/// This is a hack to test token bucket, substituting FakeClock for Instant.
mod token_bucket {
include!("token_bucket.rs");
mod tests {
use super::*;
use lazy_static::lazy_static;
lazy_static! {
pub static ref D00: Duration = Duration::new(0, 0);
}
#[test]
fn test_basic() {
Instant::set_time(50_000);
let bucket = VectorTokenBucket::new(Duration::from_millis(1000), 100, *D00, 0.95);
assert!(bucket.get_tokens(50), "Should have not violated limit.");
assert_eq!(None, bucket.get_delay(), "Can get stuff.");
assert!(!bucket.get_tokens(51), "Should have violated limit.");
}
#[test]
fn test_internal_constructor() {
let bucket = VectorTokenBucket::new(Duration::from_millis(1000), 100, *D00, 1.0);
assert_eq!(100, bucket.burst_limit);
let bucket = VectorTokenBucket::new(Duration::from_millis(1000), 100, *D00, 1e-6);
assert_eq!(1, bucket.burst_limit);
}
#[test]
fn test_saturated_100_burst() {
let bucket = VectorTokenBucket::new(Duration::from_millis(1000), 100, *D00, 1.00);
Instant::set_time(50_000);
assert!(bucket.get_tokens(100), "All tokens should be immediately available.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
Instant::advance_time(1001); // Extra buffer for Duration(0).
assert!(bucket.get_tokens(100), "All tokens should be available after a bucket duration.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
}
#[test]
fn test_saturated_95_burst() {
let bucket = VectorTokenBucket::new(Duration::from_millis(1000), 100, *D00, 0.50);
Instant::set_time(50_000);
assert!(bucket.get_tokens(95), "95 tokens should be immediately available.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
Instant::advance_time(475); // Total 951.
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
Instant::advance_time(476); // Extra buffer for Duration(0).
assert!(bucket.get_tokens(5), "Last 5 tokens should be available.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
Instant::advance_time(51);
assert!(bucket.get_tokens(95), "95 tokens should be available.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
Instant::advance_time(951);
assert!(bucket.get_tokens(5), "Last 5 tokens should be available.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
}
#[test]
fn test_saturated_50_burst() {
let bucket = VectorTokenBucket::new(Duration::from_millis(1000), 100, *D00, 0.5);
Instant::set_time(50_000);
assert!(bucket.get_tokens(50), "Half the tokens should be immediately available.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
Instant::advance_time(501); // Extra buffer for Duration(0).
assert!(bucket.get_tokens(50), "Half the tokens should be available after a half bucket duration.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
Instant::advance_time(501);
assert!(bucket.get_tokens(50), "Half the tokens should be available after a full bucket duration.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
Instant::advance_time(501);
assert!(bucket.get_tokens(50));
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
}
#[test]
fn test_many() {
Instant::set_time(50_000);
let bucket = VectorTokenBucket::new(Duration::from_millis(1000), 100, *D00, 0.95);
assert!(bucket.get_tokens(50), "Should have not violated limit.");
assert_eq!(None, bucket.get_delay(), "Should not be blocked.");
for _ in 0..20_000 {
Instant::advance_time(501);
assert!(bucket.get_tokens(50), "Should have not violated limit.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
Instant::advance_time(501);
assert!(bucket.get_tokens(50), "Should have not violated limit.");
assert_ne!(None, bucket.get_delay(), "Bucket should have delay.");
}
assert!(bucket.timestamps.lock().len() < 110, "Check memory leak.");
}
}
}

View File

@ -1,180 +1,180 @@
use std::future::Future;
use std::sync::Arc;
use log;
use reqwest::{ Client, RequestBuilder, Method };
use crate::Result;
use crate::ResponseInfo;
use crate::RiotApiConfig;
use crate::RiotApiError;
use crate::req::RegionalRequester;
use crate::util::InsertOnlyCHashMap;
/// For retrieving data from the Riot Games API.
///
/// # Usage
///
/// Construct an instance using [`with_key(api_key)`](RiotApi::with_key) or
/// [`with_config(config)`](RiotApi::with_config).
///
/// An instance provides access to "endpoint handles" which in turn provide access
/// to individual API method calls. For example, getting a summoner by name:
/// ```ignore
/// riot_api.summoner_v4().get_by_summoner_name(Region::NA, "LugnutsK")
/// ```
///
/// # Rate Limiting
///
/// The Riot Game API enforces _dynamic_ rate limiting, meaning that rate limits are
/// specified in response headers and (theoretically) could change at any time.
/// Riven keeps track of changing rate limits seamlessly, preventing you from
/// getting blacklisted.
///
/// Riven's rate limiting is highly efficient, meaning that it can reach the limits
/// of your rate limit without going over.
///
/// To adjust rate limiting, see [RiotApiConfig](crate::RiotApiConfig) and use
/// [`with_config(config)`](RiotApi::with_config) to construct an instance.
pub struct RiotApi {
/// Configuration settings.
config: RiotApiConfig,
/// Client for making requests.
client: Client,
/// Per-region requesters.
regional_requesters: InsertOnlyCHashMap<&'static str, RegionalRequester>,
}
impl RiotApi {
/// Constructs a new instance from the given [RiotApiConfig](crate::RiotApiConfig), consuming it.
pub fn with_config(mut config: RiotApiConfig) -> Self {
let client_builder = config.client_builder.take()
.expect("!NONE CLIENT_BUILDER IN CONFIG.");
Self {
config: config,
client: client_builder.build().expect("Failed to create client from builder."),
regional_requesters: InsertOnlyCHashMap::new(),
}
}
/// Constructs a new instance from the given API key, using default configuration.
///
/// `api_key` should be a Riot Games API key from
/// [https://developer.riotgames.com/](https://developer.riotgames.com/),
/// and should look like `"RGAPI-01234567-89ab-cdef-0123-456789abcdef"`.
pub fn with_key(api_key: impl AsRef<[u8]>) -> Self {
Self::with_config(RiotApiConfig::with_key(api_key))
}
/// This method should generally not be used directly. Consider using endpoint wrappers instead.
///
/// Creates a `RequestBuilder` instance with the given parameters, for use with the `execute*()` methods.
///
/// # Parameters
/// * `method` - The HTTP method for this request.
/// * `region_platform` - The stringified platform, used to create the base URL.
/// * `path` - The URL path, appended to the base URL.
pub fn request(&self, method: Method, region_platform: &str, path: &str) -> RequestBuilder {
let base_url_platform = self.config.base_url.replace("{}", region_platform);
self.client.request(method, format!("{}{}", base_url_platform, path))
}
/// This method should generally not be used directly. Consider using endpoint wrappers instead.
///
/// This sends a request based on the given parameters and returns a parsed result.
///
/// # Parameters
/// * `method_id` - A unique string id representing the endpoint method for per-method rate limiting.
/// * `region_platform` - The stringified platform, used in rate limiting.
/// * `request` - The request information. Use `request()` to obtain a `RequestBuilder` instance.
///
/// # Returns
/// A future resolving to a `Result` containg either a `T` (success) or a `RiotApiError` (failure).
pub async fn execute_val<'a, T: serde::de::DeserializeOwned + 'a>(&'a self,
method_id: &'static str, region_platform: &'static str, request: RequestBuilder)
-> Result<T>
{
let rinfo = self.execute_raw(method_id, region_platform, request).await?;
let retries = rinfo.retries;
let status = rinfo.response.status();
let value = rinfo.response.json::<T>().await;
value.map_err(|e| RiotApiError::new(e, retries, None, Some(status)))
}
/// This method should generally not be used directly. Consider using endpoint wrappers instead.
///
/// This sends a request based on the given parameters and returns an optional parsed result.
///
/// # Parameters
/// * `method_id` - A unique string id representing the endpoint method for per-method rate limiting.
/// * `region_platform` - The stringified platform, used in rate limiting.
/// * `request` - The request information. Use `request()` to obtain a `RequestBuilder` instance.
///
/// # Returns
/// A future resolving to a `Result` containg either a `Option<T>` (success) or a `RiotApiError` (failure).
pub async fn execute_opt<'a, T: serde::de::DeserializeOwned + 'a>(&'a self,
method_id: &'static str, region_platform: &'static str, request: RequestBuilder)
-> Result<Option<T>>
{
let rinfo = self.execute_raw(method_id, region_platform, request).await?;
if rinfo.status_none {
return Ok(None);
}
let retries = rinfo.retries;
let status = rinfo.response.status();
let value = rinfo.response.json::<Option<T>>().await;
value.map_err(|e| RiotApiError::new(e, retries, None, Some(status)))
}
/// This method should generally not be used directly. Consider using endpoint wrappers instead.
///
/// This sends a request based on the given parameters but does not deserialize any response body.
///
/// # Parameters
/// * `method_id` - A unique string id representing the endpoint method for per-method rate limiting.
/// * `region_platform` - The stringified platform, used in rate limiting.
/// * `request` - The request information. Use `request()` to obtain a `RequestBuilder` instance.
///
/// # Returns
/// A future resolving to a `Result` containg either `()` (success) or a `RiotApiError` (failure).
pub async fn execute(&self,
method_id: &'static str, region_platform: &'static str, request: RequestBuilder)
-> Result<()>
{
let rinfo = self.execute_raw(method_id, region_platform, request).await?;
let retries = rinfo.retries;
let status = rinfo.response.status();
rinfo.response.error_for_status()
.map(|_| ())
.map_err(|e| RiotApiError::new(e, retries, None, Some(status)))
}
/// This method should generally not be used directly. Consider using endpoint wrappers instead.
///
/// This sends a 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, used in rate limiting.
/// * `request` - The request information. Use `request()` to obtain a `RequestBuilder` instance.
///
/// # Returns
/// 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)
-> impl Future<Output = Result<ResponseInfo>> + '_
{
self.regional_requester(region_platform)
.execute(&self.config, method_id, request)
}
/// Get or create the RegionalRequester for the given region.
fn regional_requester(&self, region_platform: &'static str) -> Arc<RegionalRequester> {
self.regional_requesters.get_or_insert_with(region_platform, || {
log::debug!("Creating requester for region platform {}.", region_platform);
RegionalRequester::new()
})
}
}
use std::future::Future;
use std::sync::Arc;
use log;
use reqwest::{ Client, RequestBuilder, Method };
use crate::Result;
use crate::ResponseInfo;
use crate::RiotApiConfig;
use crate::RiotApiError;
use crate::req::RegionalRequester;
use crate::util::InsertOnlyCHashMap;
/// For retrieving data from the Riot Games API.
///
/// # Usage
///
/// Construct an instance using [`with_key(api_key)`](RiotApi::with_key) or
/// [`with_config(config)`](RiotApi::with_config).
///
/// An instance provides access to "endpoint handles" which in turn provide access
/// to individual API method calls. For example, getting a summoner by name:
/// ```ignore
/// riot_api.summoner_v4().get_by_summoner_name(Region::NA, "LugnutsK")
/// ```
///
/// # Rate Limiting
///
/// The Riot Game API enforces _dynamic_ rate limiting, meaning that rate limits are
/// specified in response headers and (theoretically) could change at any time.
/// Riven keeps track of changing rate limits seamlessly, preventing you from
/// getting blacklisted.
///
/// Riven's rate limiting is highly efficient, meaning that it can reach the limits
/// of your rate limit without going over.
///
/// To adjust rate limiting, see [RiotApiConfig](crate::RiotApiConfig) and use
/// [`with_config(config)`](RiotApi::with_config) to construct an instance.
pub struct RiotApi {
/// Configuration settings.
config: RiotApiConfig,
/// Client for making requests.
client: Client,
/// Per-region requesters.
regional_requesters: InsertOnlyCHashMap<&'static str, RegionalRequester>,
}
impl RiotApi {
/// Constructs a new instance from the given [RiotApiConfig](crate::RiotApiConfig), consuming it.
pub fn with_config(mut config: RiotApiConfig) -> Self {
let client_builder = config.client_builder.take()
.expect("!NONE CLIENT_BUILDER IN CONFIG.");
Self {
config: config,
client: client_builder.build().expect("Failed to create client from builder."),
regional_requesters: InsertOnlyCHashMap::new(),
}
}
/// Constructs a new instance from the given API key, using default configuration.
///
/// `api_key` should be a Riot Games API key from
/// [https://developer.riotgames.com/](https://developer.riotgames.com/),
/// and should look like `"RGAPI-01234567-89ab-cdef-0123-456789abcdef"`.
pub fn with_key(api_key: impl AsRef<[u8]>) -> Self {
Self::with_config(RiotApiConfig::with_key(api_key))
}
/// This method should generally not be used directly. Consider using endpoint wrappers instead.
///
/// Creates a `RequestBuilder` instance with the given parameters, for use with the `execute*()` methods.
///
/// # Parameters
/// * `method` - The HTTP method for this request.
/// * `region_platform` - The stringified platform, used to create the base URL.
/// * `path` - The URL path, appended to the base URL.
pub fn request(&self, method: Method, region_platform: &str, path: &str) -> RequestBuilder {
let base_url_platform = self.config.base_url.replace("{}", region_platform);
self.client.request(method, format!("{}{}", base_url_platform, path))
}
/// This method should generally not be used directly. Consider using endpoint wrappers instead.
///
/// This sends a request based on the given parameters and returns a parsed result.
///
/// # Parameters
/// * `method_id` - A unique string id representing the endpoint method for per-method rate limiting.
/// * `region_platform` - The stringified platform, used in rate limiting.
/// * `request` - The request information. Use `request()` to obtain a `RequestBuilder` instance.
///
/// # Returns
/// A future resolving to a `Result` containg either a `T` (success) or a `RiotApiError` (failure).
pub async fn execute_val<'a, T: serde::de::DeserializeOwned + 'a>(&'a self,
method_id: &'static str, region_platform: &'static str, request: RequestBuilder)
-> Result<T>
{
let rinfo = self.execute_raw(method_id, region_platform, request).await?;
let retries = rinfo.retries;
let status = rinfo.response.status();
let value = rinfo.response.json::<T>().await;
value.map_err(|e| RiotApiError::new(e, retries, None, Some(status)))
}
/// This method should generally not be used directly. Consider using endpoint wrappers instead.
///
/// This sends a request based on the given parameters and returns an optional parsed result.
///
/// # Parameters
/// * `method_id` - A unique string id representing the endpoint method for per-method rate limiting.
/// * `region_platform` - The stringified platform, used in rate limiting.
/// * `request` - The request information. Use `request()` to obtain a `RequestBuilder` instance.
///
/// # Returns
/// A future resolving to a `Result` containg either a `Option<T>` (success) or a `RiotApiError` (failure).
pub async fn execute_opt<'a, T: serde::de::DeserializeOwned + 'a>(&'a self,
method_id: &'static str, region_platform: &'static str, request: RequestBuilder)
-> Result<Option<T>>
{
let rinfo = self.execute_raw(method_id, region_platform, request).await?;
if rinfo.status_none {
return Ok(None);
}
let retries = rinfo.retries;
let status = rinfo.response.status();
let value = rinfo.response.json::<Option<T>>().await;
value.map_err(|e| RiotApiError::new(e, retries, None, Some(status)))
}
/// This method should generally not be used directly. Consider using endpoint wrappers instead.
///
/// This sends a request based on the given parameters but does not deserialize any response body.
///
/// # Parameters
/// * `method_id` - A unique string id representing the endpoint method for per-method rate limiting.
/// * `region_platform` - The stringified platform, used in rate limiting.
/// * `request` - The request information. Use `request()` to obtain a `RequestBuilder` instance.
///
/// # Returns
/// A future resolving to a `Result` containg either `()` (success) or a `RiotApiError` (failure).
pub async fn execute(&self,
method_id: &'static str, region_platform: &'static str, request: RequestBuilder)
-> Result<()>
{
let rinfo = self.execute_raw(method_id, region_platform, request).await?;
let retries = rinfo.retries;
let status = rinfo.response.status();
rinfo.response.error_for_status()
.map(|_| ())
.map_err(|e| RiotApiError::new(e, retries, None, Some(status)))
}
/// This method should generally not be used directly. Consider using endpoint wrappers instead.
///
/// This sends a 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, used in rate limiting.
/// * `request` - The request information. Use `request()` to obtain a `RequestBuilder` instance.
///
/// # Returns
/// 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)
-> impl Future<Output = Result<ResponseInfo>> + '_
{
self.regional_requester(region_platform)
.execute(&self.config, method_id, request)
}
/// Get or create the RegionalRequester for the given region.
fn regional_requester(&self, region_platform: &'static str) -> Arc<RegionalRequester> {
self.regional_requesters.get_or_insert_with(region_platform, || {
log::debug!("Creating requester for region platform {}.", region_platform);
RegionalRequester::new()
})
}
}

View File

@ -1,36 +1,36 @@
// use std::borrow::Borrow;
use std::collections::HashMap;
use std::hash::Hash;
use std::sync::Arc;
use parking_lot::Mutex;
pub struct InsertOnlyCHashMap<K: Hash + Eq, V> {
base: Mutex<HashMap<K, Arc<V>>>,
}
impl<K: Hash + Eq, V> InsertOnlyCHashMap<K, V> {
#[inline]
pub fn new() -> Self {
Self {
base: Mutex::new(HashMap::new())
}
}
// #[inline]
// pub fn get<Q: ?Sized>(&self, key: &Q) -> Option<Arc<V>>
// where
// K: Borrow<Q>,
// Q: Hash + Eq,
// {
// self.base.lock().get(key).map(|v| Arc::clone(v))
// }
#[inline]
pub fn get_or_insert_with<F: FnOnce() -> V>(&self, key: K, default: F) -> Arc<V>
{
Arc::clone(self.base.lock()
.entry(key)
.or_insert_with(|| Arc::new(default())))
}
}
// use std::borrow::Borrow;
use std::collections::HashMap;
use std::hash::Hash;
use std::sync::Arc;
use parking_lot::Mutex;
pub struct InsertOnlyCHashMap<K: Hash + Eq, V> {
base: Mutex<HashMap<K, Arc<V>>>,
}
impl<K: Hash + Eq, V> InsertOnlyCHashMap<K, V> {
#[inline]
pub fn new() -> Self {
Self {
base: Mutex::new(HashMap::new())
}
}
// #[inline]
// pub fn get<Q: ?Sized>(&self, key: &Q) -> Option<Arc<V>>
// where
// K: Borrow<Q>,
// Q: Hash + Eq,
// {
// self.base.lock().get(key).map(|v| Arc::clone(v))
// }
#[inline]
pub fn get_or_insert_with<F: FnOnce() -> V>(&self, key: K, default: F) -> Arc<V>
{
Arc::clone(self.base.lock()
.entry(key)
.or_insert_with(|| Arc::new(default())))
}
}

View File

@ -1,3 +1,3 @@
mod insert_only_chashmap;
pub use insert_only_chashmap::*;
mod insert_only_chashmap;
pub use insert_only_chashmap::*;

View File

@ -1,158 +1,158 @@
{{
const dotUtils = require('./dotUtils.js');
const champions = require('./.champion.json')
.filter(({ id }) => id > 0)
.sortBy(({ name }) => name);
const constName = name => dotUtils.changeCase.constantCase(name).replace(/[^_A-Z0-9]+/g, '');
const constNamePad = 12;
const hashFactor = 256;
const strHash = (str) => {
let h = 0;
for (let c of str)
h = hashFactor * h + c.charCodeAt(0);
return h;
};
}}{{= dotUtils.preamble() }}
use serde::{ Serialize, Deserialize };
/// League of Legends champions.
///
/// The documentation of each const field specifies:<br>
/// NAME (`IDENTIFIER`, ID).
///
/// Implements [IntoEnumIterator](super::IntoEnumIterator).
#[derive(Serialize, Deserialize)]
#[derive(Copy, Clone)]
#[derive(PartialEq, Eq, PartialOrd, Ord)]
#[serde(transparent)]
#[repr(transparent)]
pub struct Champion(pub i16);
impl Champion {
{{
for (let { id, alias, name } of champions) {
}}
/// {{= name }} (`{{= alias }}`, {{= id }}).
pub const {{= constName(name) }}: Self = Self({{= id }});
{{
}
}}
pub const fn is_known(self) -> bool {
match self {
{{
for (let { name, alias } of champions) {
}}
Self::{{= constName(name).padEnd(constNamePad) }} => true,
{{
}
}}
_ => false,
}
}
/// The champion's name (`en_US` localization).
pub const fn name(self) -> Option<&'static str> {
match self {
{{
for (let { name } of champions) {
}}
Self::{{= constName(name).padEnd(constNamePad) }} => Some("{{= name }}"),
{{
}
}}
_ => None,
}
}
/// The champion's identifier key. Somtimes called "key", "identifier", or "alias".
/// This is mainly used in DDragon paths.
///
/// This is generally the `en_US` name with spaces and punctuation removed,
/// capitalization preserved, however the follow are exceptions:
///
/// Field | Name | Identifier
/// --------|------|-----------
{{
for (let { name, alias } of champions) {
if (name.replace(/[^a-zA-Z0-9]+/, '') !== alias) {
}}
/// `{{= constName(name) }}` | "{{= name }}" | "{{= alias }}"
{{
}
}
}}
pub const fn identifier(self) -> Option<&'static str> {
match self {
{{
for (let { name, alias } of champions) {
}}
Self::{{= constName(name).padEnd(constNamePad) }} => Some("{{= alias }}"),
{{
}
}}
_ => None,
}
}
}
impl std::str::FromStr for Champion {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.chars()
.take(4)
.filter(|c| c.is_ascii_alphanumeric())
.fold(0_u32, |hash, next| hash * {{= hashFactor }} + u32::from(next))
{
{{
const keyStrings = (name, alias) => new Set([].concat(...[ name, alias ].map(s => s.toUpperCase())
.map(s => [
s.replace(/[^A-Z0-9]+/, '').substring(0, 4),
s.split(/[^A-Z0-9]/, 1)[0].substring(0, 4),
s.split(/[^A-Z]/, 1)[0].substring(0, 4),
])));
for (const { id, alias, name } of champions) {
for (const prefix of keyStrings(name, alias)) {
}}
0x{{= strHash(prefix).toString(16).padEnd(8) }} /* {{= prefix.padEnd(4) }} */ => Ok(Champion::{{= constName(name) }}),
{{
}
}
}}
_ => Err(()),
}
}
}
impl std::convert::TryFrom<&str> for Champion {
type Error = <Self as std::str::FromStr>::Err;
fn try_from(value: &str) -> Result<Self, Self::Error> {
<Self as std::str::FromStr>::from_str(value)
}
}
impl std::convert::From<i16> for Champion {
fn from(value: i16) -> Self {
Self(value)
}
}
impl std::convert::From<Champion> for i16 {
fn from(value: Champion) -> Self {
value.0
}
}
impl std::fmt::Display for Champion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl std::fmt::Debug for Champion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Champion({} {})", self.0, self.identifier().unwrap_or("UNKNOWN"))
}
}
{{
const dotUtils = require('./dotUtils.js');
const champions = require('./.champion.json')
.filter(({ id }) => id > 0)
.sortBy(({ name }) => name);
const constName = name => dotUtils.changeCase.constantCase(name).replace(/[^_A-Z0-9]+/g, '');
const constNamePad = 12;
const hashFactor = 256;
const strHash = (str) => {
let h = 0;
for (let c of str)
h = hashFactor * h + c.charCodeAt(0);
return h;
};
}}{{= dotUtils.preamble() }}
use serde::{ Serialize, Deserialize };
/// League of Legends champions.
///
/// The documentation of each const field specifies:<br>
/// NAME (`IDENTIFIER`, ID).
///
/// Implements [IntoEnumIterator](super::IntoEnumIterator).
#[derive(Serialize, Deserialize)]
#[derive(Copy, Clone)]
#[derive(PartialEq, Eq, PartialOrd, Ord)]
#[serde(transparent)]
#[repr(transparent)]
pub struct Champion(pub i16);
impl Champion {
{{
for (let { id, alias, name } of champions) {
}}
/// {{= name }} (`{{= alias }}`, {{= id }}).
pub const {{= constName(name) }}: Self = Self({{= id }});
{{
}
}}
pub const fn is_known(self) -> bool {
match self {
{{
for (let { name, alias } of champions) {
}}
Self::{{= constName(name).padEnd(constNamePad) }} => true,
{{
}
}}
_ => false,
}
}
/// The champion's name (`en_US` localization).
pub const fn name(self) -> Option<&'static str> {
match self {
{{
for (let { name } of champions) {
}}
Self::{{= constName(name).padEnd(constNamePad) }} => Some("{{= name }}"),
{{
}
}}
_ => None,
}
}
/// The champion's identifier key. Somtimes called "key", "identifier", or "alias".
/// This is mainly used in DDragon paths.
///
/// This is generally the `en_US` name with spaces and punctuation removed,
/// capitalization preserved, however the follow are exceptions:
///
/// Field | Name | Identifier
/// --------|------|-----------
{{
for (let { name, alias } of champions) {
if (name.replace(/[^a-zA-Z0-9]+/, '') !== alias) {
}}
/// `{{= constName(name) }}` | "{{= name }}" | "{{= alias }}"
{{
}
}
}}
pub const fn identifier(self) -> Option<&'static str> {
match self {
{{
for (let { name, alias } of champions) {
}}
Self::{{= constName(name).padEnd(constNamePad) }} => Some("{{= alias }}"),
{{
}
}}
_ => None,
}
}
}
impl std::str::FromStr for Champion {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.chars()
.take(4)
.filter(|c| c.is_ascii_alphanumeric())
.fold(0_u32, |hash, next| hash * {{= hashFactor }} + u32::from(next))
{
{{
const keyStrings = (name, alias) => new Set([].concat(...[ name, alias ].map(s => s.toUpperCase())
.map(s => [
s.replace(/[^A-Z0-9]+/, '').substring(0, 4),
s.split(/[^A-Z0-9]/, 1)[0].substring(0, 4),
s.split(/[^A-Z]/, 1)[0].substring(0, 4),
])));
for (const { id, alias, name } of champions) {
for (const prefix of keyStrings(name, alias)) {
}}
0x{{= strHash(prefix).toString(16).padEnd(8) }} /* {{= prefix.padEnd(4) }} */ => Ok(Champion::{{= constName(name) }}),
{{
}
}
}}
_ => Err(()),
}
}
}
impl std::convert::TryFrom<&str> for Champion {
type Error = <Self as std::str::FromStr>::Err;
fn try_from(value: &str) -> Result<Self, Self::Error> {
<Self as std::str::FromStr>::from_str(value)
}
}
impl std::convert::From<i16> for Champion {
fn from(value: i16) -> Self {
Self(value)
}
}
impl std::convert::From<Champion> for i16 {
fn from(value: Champion) -> Self {
value.0
}
}
impl std::fmt::Display for Champion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl std::fmt::Debug for Champion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Champion({} {})", self.0, self.identifier().unwrap_or("UNKNOWN"))
}
}

View File

@ -1,29 +1,29 @@
{{
const dotUtils = require('./dotUtils.js');
const gameModes = require('./.gameModes.json');
}}{{= dotUtils.preamble() }}
use strum_macros::{ EnumString, Display, AsRefStr, IntoStaticStr };
/// League of Legends game mode, such as Classic,
/// ARAM, URF, One For All, Ascension, etc.
#[non_exhaustive]
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq, Hash)]
#[derive(EnumString, Display, AsRefStr, IntoStaticStr)]
#[repr(u8)]
pub enum GameMode {
{{
for (const e of gameModes) {
const desc = e['x-desc'] ? e['x-desc'].split('\n') : [];
}}
{{~ desc :line }}
/// {{= line }}
{{~}}
{{= e['x-name'] }},
{{
}
}}
}
serde_string!(GameMode);
{{
const dotUtils = require('./dotUtils.js');
const gameModes = require('./.gameModes.json');
}}{{= dotUtils.preamble() }}
use strum_macros::{ EnumString, Display, AsRefStr, IntoStaticStr };
/// League of Legends game mode, such as Classic,
/// ARAM, URF, One For All, Ascension, etc.
#[non_exhaustive]
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq, Hash)]
#[derive(EnumString, Display, AsRefStr, IntoStaticStr)]
#[repr(u8)]
pub enum GameMode {
{{
for (const e of gameModes) {
const desc = e['x-desc'] ? e['x-desc'].split('\n') : [];
}}
{{~ desc :line }}
/// {{= line }}
{{~}}
{{= e['x-name'] }},
{{
}
}}
}
serde_string!(GameMode);

View File

@ -1,27 +1,27 @@
{{
const dotUtils = require('./dotUtils.js');
const gameTypes = require('./.gameTypes.json');
}}{{= dotUtils.preamble() }}
use strum_macros::{ EnumString, Display, AsRefStr, IntoStaticStr };
/// League of Legends game type: matched game, custom game, or tutorial game.
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq, Hash)]
#[derive(EnumString, Display, AsRefStr, IntoStaticStr)]
#[repr(u8)]
pub enum GameType {
{{
for (const e of gameTypes) {
const desc = e['x-desc'] ? e['x-desc'].split('\n') : [];
}}
{{~ desc :line }}
/// {{= line }}
{{~}}
{{= e['x-name'] }},
{{
}
}}
}
serde_string!(GameType);
{{
const dotUtils = require('./dotUtils.js');
const gameTypes = require('./.gameTypes.json');
}}{{= dotUtils.preamble() }}
use strum_macros::{ EnumString, Display, AsRefStr, IntoStaticStr };
/// League of Legends game type: matched game, custom game, or tutorial game.
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq, Hash)]
#[derive(EnumString, Display, AsRefStr, IntoStaticStr)]
#[repr(u8)]
pub enum GameType {
{{
for (const e of gameTypes) {
const desc = e['x-desc'] ? e['x-desc'].split('\n') : [];
}}
{{~ desc :line }}
/// {{= line }}
{{~}}
{{= e['x-name'] }},
{{
}
}}
}
serde_string!(GameType);

View File

@ -1,28 +1,28 @@
{{
const dotUtils = require('./dotUtils.js');
const maps = require('./.maps.json');
}}{{= dotUtils.preamble() }}
use serde_repr::{ Serialize_repr, Deserialize_repr };
use num_enum::{ IntoPrimitive, TryFromPrimitive };
/// League of Legends maps.
#[non_exhaustive]
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq, Hash, PartialOrd, Ord)]
#[derive(Serialize_repr, Deserialize_repr)]
#[derive(IntoPrimitive, TryFromPrimitive)]
#[repr(u8)]
pub enum Map {
{{
for (const e of maps) {
const desc = e['x-desc'] ? e['x-desc'].split('\n') : [];
}}
{{~ desc :line }}
/// {{= line }}
{{~}}
{{= e['x-name'] }} = {{= e['x-value'] }},
{{
}
}}
}
{{
const dotUtils = require('./dotUtils.js');
const maps = require('./.maps.json');
}}{{= dotUtils.preamble() }}
use serde_repr::{ Serialize_repr, Deserialize_repr };
use num_enum::{ IntoPrimitive, TryFromPrimitive };
/// League of Legends maps.
#[non_exhaustive]
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq, Hash, PartialOrd, Ord)]
#[derive(Serialize_repr, Deserialize_repr)]
#[derive(IntoPrimitive, TryFromPrimitive)]
#[repr(u8)]
pub enum Map {
{{
for (const e of maps) {
const desc = e['x-desc'] ? e['x-desc'].split('\n') : [];
}}
{{~ desc :line }}
/// {{= line }}
{{~}}
{{= e['x-name'] }} = {{= e['x-value'] }},
{{
}
}}
}

View File

@ -1,32 +1,32 @@
{{
const dotUtils = require('./dotUtils.js');
const queues = require('./.queues.json');
}}{{= dotUtils.preamble() }}
use serde_repr::{ Serialize_repr, Deserialize_repr };
use num_enum::{ IntoPrimitive, TryFromPrimitive };
/// League of Legends matchmaking queue.
#[non_exhaustive]
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq)]
#[derive(Serialize_repr, Deserialize_repr)]
#[derive(IntoPrimitive, TryFromPrimitive)]
#[repr(u16)]
pub enum Queue {
{{
for (const e of queues) {
const desc = e['x-desc'] ? e['x-desc'].split('\n') : [];
}}
{{~ desc :line }}
/// {{= line }}
{{~}}
{{? e['x-deprecated'] }}
/// {{= e.notes }}
#[deprecated(note="{{= e.notes }}")]
{{?}}
{{= e['x-name'] }} = {{= e['x-value'] }},
{{
}
}}
{{
const dotUtils = require('./dotUtils.js');
const queues = require('./.queues.json');
}}{{= dotUtils.preamble() }}
use serde_repr::{ Serialize_repr, Deserialize_repr };
use num_enum::{ IntoPrimitive, TryFromPrimitive };
/// League of Legends matchmaking queue.
#[non_exhaustive]
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq)]
#[derive(Serialize_repr, Deserialize_repr)]
#[derive(IntoPrimitive, TryFromPrimitive)]
#[repr(u16)]
pub enum Queue {
{{
for (const e of queues) {
const desc = e['x-desc'] ? e['x-desc'].split('\n') : [];
}}
{{~ desc :line }}
/// {{= line }}
{{~}}
{{? e['x-deprecated'] }}
/// {{= e.notes }}
#[deprecated(note="{{= e.notes }}")]
{{?}}
{{= e['x-name'] }} = {{= e['x-value'] }},
{{
}
}}
}

View File

@ -1,28 +1,28 @@
{{
const dotUtils = require('./dotUtils.js');
const seasons = require('./.seasons.json');
}}{{= dotUtils.preamble() }}
use serde_repr::{ Serialize_repr, Deserialize_repr };
use num_enum::{ IntoPrimitive, TryFromPrimitive };
/// League of Legends matchmaking seasons.
#[non_exhaustive]
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq, Hash, PartialOrd, Ord)]
#[derive(Serialize_repr, Deserialize_repr)]
#[derive(IntoPrimitive, TryFromPrimitive)]
#[repr(u8)]
pub enum Season {
{{
for (const e of seasons) {
const desc = e['x-desc'] ? e['x-desc'].split('\n') : [];
}}
{{~ desc :line }}
/// {{= line }}
{{~}}
{{= e['x-name'] }} = {{= e['x-value'] }},
{{
}
}}
}
{{
const dotUtils = require('./dotUtils.js');
const seasons = require('./.seasons.json');
}}{{= dotUtils.preamble() }}
use serde_repr::{ Serialize_repr, Deserialize_repr };
use num_enum::{ IntoPrimitive, TryFromPrimitive };
/// League of Legends matchmaking seasons.
#[non_exhaustive]
#[derive(Debug, Copy, Clone)]
#[derive(Eq, PartialEq, Hash, PartialOrd, Ord)]
#[derive(Serialize_repr, Deserialize_repr)]
#[derive(IntoPrimitive, TryFromPrimitive)]
#[repr(u8)]
pub enum Season {
{{
for (const e of seasons) {
const desc = e['x-desc'] ? e['x-desc'].split('\n') : [];
}}
{{~ desc :line }}
/// {{= line }}
{{~}}
{{= e['x-name'] }} = {{= e['x-value'] }},
{{
}
}}
}

View File

@ -1,160 +1,160 @@
const changeCase = require('change-case');
// flatMap: https://gist.github.com/samgiles/762ee337dff48623e729
// [B](f: (A) ⇒ [B]): [B] ; Although the types in the arrays aren't strict (:
Array.prototype.flatMap = function(lambda) {
return Array.prototype.concat.apply([], this.map(lambda));
};
Array.prototype.groupBy = function(lambda) {
return Object.entries(this.reduce((agg, x) => {
const k = lambda(x);
(agg[k] = agg[k] || []).push(x);
return agg;
}, {}));
};
Array.prototype.sortBy = function(lambda) {
return this.sort((a, b) => {
const va = lambda(a);
const vb = lambda(b);
if ((typeof va) !== (typeof vb))
throw Error(`Mismatched sort types: ${typeof va}, ${typeof vb}.`);
if (typeof va === 'number')
return va - vb;
if (typeof va === 'string')
return va.localeCompare(vb);
throw Error(`Unknown sort type: ${typeof va}.`);
});
};
function preamble() {
return `\
///////////////////////////////////////////////
// //
// ! //
// This file is automatically generated! //
// Do not directly edit! //
// //
///////////////////////////////////////////////`;
}
function capitalize(input) {
return input[0].toUpperCase() + input.slice(1);
}
function decapitalize(input) {
return input[0].toLowerCase() + input.slice(1);
}
function normalizeSchemaName(name) {
return name.replace(/DTO/ig, '');
}
function normalizeArgName(name) {
const tokens = name.split('_');
const argName = decapitalize(tokens.map(capitalize).join(''));
return 'base' === argName ? 'Base' : argName;
}
function normalizePropName(propName) {
let out = changeCase.snakeCase(propName);
if (/^\d/.test(out)) // No leading digits.
out = 'x' + out;
if ('type' === out)
return 'r#' + out;
return out;
}
function stringifyType(prop, { endpoint = null, optional = false, fullpath = true, owned = true }) {
if (prop.anyOf) {
prop = prop.anyOf[0];
}
if (optional) {
return `Option<${stringifyType(prop, { endpoint, fullpath, owned })}>`;
}
let enumType = prop['x-enum'];
if (enumType && 'locale' !== enumType)
return 'crate::consts::' + changeCase.pascalCase(enumType);
let refType = prop['$ref'];
if (refType) {
return (!endpoint ? '' : changeCase.snakeCase(endpoint) + '::') +
normalizeSchemaName(refType.slice(refType.indexOf('.') + 1));
}
switch (prop.type) {
case 'boolean': return 'bool';
case 'integer': return ('int32' === prop.format ? 'i32' : 'i64');
case 'number': return ('float' === prop.format ? 'f32' : 'f64');
case 'array':
const subprop = stringifyType(prop.items, { endpoint, optional, fullpath, owned });
return (owned ? (fullpath ? 'std::vec::' : '') + `Vec<${subprop}>` : `&[${subprop}]`);
case 'string': return (owned ? 'String' : '&str');
case 'object':
return 'std::collections::HashMap<' + stringifyType(prop['x-key'], { endpoint, optional, fullpath, owned }) + ', ' +
stringifyType(prop.additionalProperties, { endpoint, optional, fullpath, owned }) + '>';
default: return prop.type;
}
}
function formatJsonProperty(name) {
return `#[serde(rename = "${name}")]`;
}
function formatAddQueryParam(param) {
const k = `"${param.name}"`;
const name = normalizePropName(param.name);
const condStart = param.required ? '' : `mut request = request; if let Some(${name}) = ${name} { `;
const condEnd = param.required ? '' : ' }'
const prop = param.schema;
switch (prop.type) {
case 'array': return `let ${condStart}request = request.query(&*${name}.iter()`
+ `.map(|w| ( ${k}, w )).collect::<Vec<_>>());${condEnd}`;
case 'object':
throw 'unsupported';
default:
return `let ${condStart}request = request.query(&[ (${k}, ${name}) ]);${condEnd}`;
}
}
function formatAddHeaderParam(param) {
const k = `"${param.name}"`;
const name = changeCase.snakeCase(param.name);
const condStart = param.required ? '' : `mut request = request; if let Some(${name}) = ${name} { `;
const condEnd = param.required ? '' : ' }'
const prop = param.schema;
switch (prop.type) {
case 'string':
return `let ${condStart}request = request.header(${k}, ${name});${condEnd}`;
case 'object':
throw 'unsupported';
default:
return `let ${condStart}request = request.header(${k}, ${name}.to_string());${condEnd}`;
}
}
function formatRouteArgument(route, pathParams = []) {
if (!pathParams.length)
return `"${route}"`;
route = route.replace(/\{\S+?\}/g, '{}');
const args = pathParams
.map(({name}) => name)
.map(changeCase.snakeCase)
.join(', ');
return `&format!("${route}", ${args})`;
}
module.exports = {
changeCase,
preamble,
capitalize,
decapitalize,
normalizeSchemaName,
normalizeArgName,
normalizePropName,
stringifyType,
formatJsonProperty,
formatAddQueryParam,
formatAddHeaderParam,
formatRouteArgument,
const changeCase = require('change-case');
// flatMap: https://gist.github.com/samgiles/762ee337dff48623e729
// [B](f: (A) ⇒ [B]): [B] ; Although the types in the arrays aren't strict (:
Array.prototype.flatMap = function(lambda) {
return Array.prototype.concat.apply([], this.map(lambda));
};
Array.prototype.groupBy = function(lambda) {
return Object.entries(this.reduce((agg, x) => {
const k = lambda(x);
(agg[k] = agg[k] || []).push(x);
return agg;
}, {}));
};
Array.prototype.sortBy = function(lambda) {
return this.sort((a, b) => {
const va = lambda(a);
const vb = lambda(b);
if ((typeof va) !== (typeof vb))
throw Error(`Mismatched sort types: ${typeof va}, ${typeof vb}.`);
if (typeof va === 'number')
return va - vb;
if (typeof va === 'string')
return va.localeCompare(vb);
throw Error(`Unknown sort type: ${typeof va}.`);
});
};
function preamble() {
return `\
///////////////////////////////////////////////
// //
// ! //
// This file is automatically generated! //
// Do not directly edit! //
// //
///////////////////////////////////////////////`;
}
function capitalize(input) {
return input[0].toUpperCase() + input.slice(1);
}
function decapitalize(input) {
return input[0].toLowerCase() + input.slice(1);
}
function normalizeSchemaName(name) {
return name.replace(/DTO/ig, '');
}
function normalizeArgName(name) {
const tokens = name.split('_');
const argName = decapitalize(tokens.map(capitalize).join(''));
return 'base' === argName ? 'Base' : argName;
}
function normalizePropName(propName) {
let out = changeCase.snakeCase(propName);
if (/^\d/.test(out)) // No leading digits.
out = 'x' + out;
if ('type' === out)
return 'r#' + out;
return out;
}
function stringifyType(prop, { endpoint = null, optional = false, fullpath = true, owned = true }) {
if (prop.anyOf) {
prop = prop.anyOf[0];
}
if (optional) {
return `Option<${stringifyType(prop, { endpoint, fullpath, owned })}>`;
}
let enumType = prop['x-enum'];
if (enumType && 'locale' !== enumType)
return 'crate::consts::' + changeCase.pascalCase(enumType);
let refType = prop['$ref'];
if (refType) {
return (!endpoint ? '' : changeCase.snakeCase(endpoint) + '::') +
normalizeSchemaName(refType.slice(refType.indexOf('.') + 1));
}
switch (prop.type) {
case 'boolean': return 'bool';
case 'integer': return ('int32' === prop.format ? 'i32' : 'i64');
case 'number': return ('float' === prop.format ? 'f32' : 'f64');
case 'array':
const subprop = stringifyType(prop.items, { endpoint, optional, fullpath, owned });
return (owned ? (fullpath ? 'std::vec::' : '') + `Vec<${subprop}>` : `&[${subprop}]`);
case 'string': return (owned ? 'String' : '&str');
case 'object':
return 'std::collections::HashMap<' + stringifyType(prop['x-key'], { endpoint, optional, fullpath, owned }) + ', ' +
stringifyType(prop.additionalProperties, { endpoint, optional, fullpath, owned }) + '>';
default: return prop.type;
}
}
function formatJsonProperty(name) {
return `#[serde(rename = "${name}")]`;
}
function formatAddQueryParam(param) {
const k = `"${param.name}"`;
const name = normalizePropName(param.name);
const condStart = param.required ? '' : `mut request = request; if let Some(${name}) = ${name} { `;
const condEnd = param.required ? '' : ' }'
const prop = param.schema;
switch (prop.type) {
case 'array': return `let ${condStart}request = request.query(&*${name}.iter()`
+ `.map(|w| ( ${k}, w )).collect::<Vec<_>>());${condEnd}`;
case 'object':
throw 'unsupported';
default:
return `let ${condStart}request = request.query(&[ (${k}, ${name}) ]);${condEnd}`;
}
}
function formatAddHeaderParam(param) {
const k = `"${param.name}"`;
const name = changeCase.snakeCase(param.name);
const condStart = param.required ? '' : `mut request = request; if let Some(${name}) = ${name} { `;
const condEnd = param.required ? '' : ' }'
const prop = param.schema;
switch (prop.type) {
case 'string':
return `let ${condStart}request = request.header(${k}, ${name});${condEnd}`;
case 'object':
throw 'unsupported';
default:
return `let ${condStart}request = request.header(${k}, ${name}.to_string());${condEnd}`;
}
}
function formatRouteArgument(route, pathParams = []) {
if (!pathParams.length)
return `"${route}"`;
route = route.replace(/\{\S+?\}/g, '{}');
const args = pathParams
.map(({name}) => name)
.map(changeCase.snakeCase)
.join(', ');
return `&format!("${route}", ${args})`;
}
module.exports = {
changeCase,
preamble,
capitalize,
decapitalize,
normalizeSchemaName,
normalizeArgName,
normalizePropName,
stringifyType,
formatJsonProperty,
formatAddQueryParam,
formatAddHeaderParam,
formatRouteArgument,
};

View File

@ -1,81 +1,81 @@
const util = require('util');
const fs = require('fs');
fs.readFileAsync = util.promisify(fs.readFile);
fs.writeFileAsync = util.promisify(fs.writeFile);
const req = require("request-promise-native");
process.chdir(__dirname);
const files = [
[
'http://raw.communitydragon.org/pbe/plugins/rcp-be-lol-game-data/global/default/v1/champion-summary.json',
'.champion.json'
],
[
'http://www.mingweisamuel.com/riotapi-schema/openapi-3.0.0.json',
'.spec.json'
],
[
'http://www.mingweisamuel.com/riotapi-schema/enums/seasons.json',
'.seasons.json'
],
[
'http://www.mingweisamuel.com/riotapi-schema/enums/queues.json',
'.queues.json'
],
[
'http://www.mingweisamuel.com/riotapi-schema/enums/gameTypes.json',
'.gameTypes.json'
],
[
'http://www.mingweisamuel.com/riotapi-schema/enums/gameModes.json',
'.gameModes.json'
],
[
'http://www.mingweisamuel.com/riotapi-schema/enums/maps.json',
'.maps.json'
]
]
const downloadFilesPromise = Promise.all(files.map(([url, file]) => req(url)
.then(body => fs.writeFileAsync(file, body, "utf8"))));
const doT = require('dot');
const glob = require('glob-promise');
const log = a => { console.log(a); return a; };
const suffix = '.dt';
doT.templateSettings = {
evaluate: /\r?\n?\{\{([\s\S]+?)\}\}/g,
interpolate: /\r?\n?\{\{=([\s\S]+?)\}\}/g,
encode: /\r?\n?\{\{!([\s\S]+?)\}\}/g,
use: /\r?\n?\{\{#([\s\S]+?)\}\}/g,
define: /\r?\n?\{\{##\s*([\w\.$]+)\s*(\:|=)([\s\S]+?)#\}\}/g,
conditional: /\r?\n?\{\{\?(\?)?\s*([\s\S]*?)\s*\}\}/g,
iterate: /\r?\n?\{\{~\s*(?:\}\}|([\s\S]+?)\s*\:\s*([\w$]+)\s*(?:\:\s*([\w$]+))?\s*\}\})/g,
varname: 'it',
strip: false,
append: false,
selfcontained: false
};
global.require = require;
downloadFilesPromise.then(() => glob.promise("**/*" + suffix, { ignore: ["**/node_modules/**"] }))
.then(files => Promise.all(files
.map(log)
.map(file => fs.readFileAsync(file, "utf8")
.then(input => {
try {
return doT.template(input)({});
}
catch (e) {
console.error(`Error thrown while running "${file}":`, e);
throw e;
}
})
.then(output => fs.writeFileAsync("../src/" + file.slice(0, -suffix.length), output, "utf8"))
)
))
const util = require('util');
const fs = require('fs');
fs.readFileAsync = util.promisify(fs.readFile);
fs.writeFileAsync = util.promisify(fs.writeFile);
const req = require("request-promise-native");
process.chdir(__dirname);
const files = [
[
'http://raw.communitydragon.org/pbe/plugins/rcp-be-lol-game-data/global/default/v1/champion-summary.json',
'.champion.json'
],
[
'http://www.mingweisamuel.com/riotapi-schema/openapi-3.0.0.json',
'.spec.json'
],
[
'http://www.mingweisamuel.com/riotapi-schema/enums/seasons.json',
'.seasons.json'
],
[
'http://www.mingweisamuel.com/riotapi-schema/enums/queues.json',
'.queues.json'
],
[
'http://www.mingweisamuel.com/riotapi-schema/enums/gameTypes.json',
'.gameTypes.json'
],
[
'http://www.mingweisamuel.com/riotapi-schema/enums/gameModes.json',
'.gameModes.json'
],
[
'http://www.mingweisamuel.com/riotapi-schema/enums/maps.json',
'.maps.json'
]
]
const downloadFilesPromise = Promise.all(files.map(([url, file]) => req(url)
.then(body => fs.writeFileAsync(file, body, "utf8"))));
const doT = require('dot');
const glob = require('glob-promise');
const log = a => { console.log(a); return a; };
const suffix = '.dt';
doT.templateSettings = {
evaluate: /\r?\n?\{\{([\s\S]+?)\}\}/g,
interpolate: /\r?\n?\{\{=([\s\S]+?)\}\}/g,
encode: /\r?\n?\{\{!([\s\S]+?)\}\}/g,
use: /\r?\n?\{\{#([\s\S]+?)\}\}/g,
define: /\r?\n?\{\{##\s*([\w\.$]+)\s*(\:|=)([\s\S]+?)#\}\}/g,
conditional: /\r?\n?\{\{\?(\?)?\s*([\s\S]*?)\s*\}\}/g,
iterate: /\r?\n?\{\{~\s*(?:\}\}|([\s\S]+?)\s*\:\s*([\w$]+)\s*(?:\:\s*([\w$]+))?\s*\}\})/g,
varname: 'it',
strip: false,
append: false,
selfcontained: false
};
global.require = require;
downloadFilesPromise.then(() => glob.promise("**/*" + suffix, { ignore: ["**/node_modules/**"] }))
.then(files => Promise.all(files
.map(log)
.map(file => fs.readFileAsync(file, "utf8")
.then(input => {
try {
return doT.template(input)({});
}
catch (e) {
console.error(`Error thrown while running "${file}":`, e);
throw e;
}
})
.then(output => fs.writeFileAsync("../src/" + file.slice(0, -suffix.length), output, "utf8"))
)
))
.catch(console.error);

View File

@ -1,20 +1,20 @@
{
"name": "dot-gen",
"version": "0.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"change-case": "^3.1.0",
"dot": "^1.1.3",
"glob": "^7.1.2",
"glob-promise": "^3.3.0",
"request": "^2.88.0",
"request-promise-native": "^1.0.7"
},
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "GPL-2.0"
}
{
"name": "dot-gen",
"version": "0.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"change-case": "^3.1.0",
"dot": "^1.1.3",
"glob": "^7.1.2",
"glob-promise": "^3.3.0",
"request": "^2.88.0",
"request-promise-native": "^1.0.7"
},
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "GPL-2.0"
}

View File

@ -1,9 +1,9 @@
Tests are divided up by region. This is because tests cannot share state, and therefore
cannot share the rate limiting done within a `RiotApi` instance. However, rate limiting
is separate for each region, so it is safe to send requests to different regions from
different instances.
The tests within an individual file do share their `RiotApi` instance thanks to custom
test runners and some macros I hacked together which are located in `async_tests.rs`.
They are set up in a way to look like normal test output for fun and probably to
confuse people.
Tests are divided up by region. This is because tests cannot share state, and therefore
cannot share the rate limiting done within a `RiotApi` instance. However, rate limiting
is separate for each region, so it is safe to send requests to different regions from
different instances.
The tests within an individual file do share their `RiotApi` instance thanks to custom
test runners and some macros I hacked together which are located in `async_tests.rs`.
They are set up in a way to look like normal test output for fun and probably to
confuse people.

View File

@ -1,87 +1,87 @@
/// This is just a huge hack to make a test runner (with no test cases)
/// look as if it's running a bunch of (async) test cases.
#[macro_export]
macro_rules! async_tests {
( $runner:ident { $( $name:ident : async $eval:block, )* } ) => {
#[allow(dead_code)]
fn $runner(_: &[()]) {
env_logger::init();
std::process::exit({
let rt = tokio::runtime::Runtime::new()
.expect("Failed to create runtime.");
let (_, errs) = rt.block_on(async {
println!();
println!("running tests");
println!();
let mut oks: u32 = 0;
let mut errs: u32 = 0;
$(
let $name = async {
let result: std::result::Result<(), String> = async {
$eval
}.await;
result
};
let $name = tokio::spawn($name);
)*
$(
let $name = $name.await.expect("Failed to spawn.");
)*
$(
print!("test {} ... ", stringify!($name));
match $name {
Ok(_) => {
println!("{}", "ok".green());
oks += 1;
}
Err(msg) => {
println!("{}", "error".bright_red());
println!("{}", msg);
errs += 1;
}
}
)*
println!();
print!("test result: {}. ", if errs > 0 { "error".bright_red() } else { "ok".green() });
println!("{} passed; {} failed; 0 ignored; 0 measured; 0 filtered out", oks, errs);
println!();
(oks, errs)
});
// Just returns #errs as exit code.
errs as i32
});
}
};
}
#[macro_export]
macro_rules! rassert {
( $x:expr ) => {
{
if $x { Ok(()) } else { Err(stringify!($x)) }?
}
};
( $x:expr, $format:expr $(, $arg:expr)* ) => {
{
if $x { Ok(()) } else { Err( format!($format, $( $arg )* ) ) }?
}
};
}
#[macro_export]
macro_rules! rassert_eq {
( $a:expr, $b:expr ) => { rassert!($a == $b) };
( $a:expr, $b:expr, $format:expr $(, $arg:expr)* ) => {
rassert!($a == $b, $format $(, $arg )* )
};
}
#[macro_export]
macro_rules! rassert_ne {
( $a:expr, $b:expr ) => { rassert!($a != $b) };
( $a:expr, $b:expr, $format:expr $(, $arg:expr)* ) => {
rassert!($a != $b, $format $(, $arg )* )
};
}
/// This is just a huge hack to make a test runner (with no test cases)
/// look as if it's running a bunch of (async) test cases.
#[macro_export]
macro_rules! async_tests {
( $runner:ident { $( $name:ident : async $eval:block, )* } ) => {
#[allow(dead_code)]
fn $runner(_: &[()]) {
env_logger::init();
std::process::exit({
let rt = tokio::runtime::Runtime::new()
.expect("Failed to create runtime.");
let (_, errs) = rt.block_on(async {
println!();
println!("running tests");
println!();
let mut oks: u32 = 0;
let mut errs: u32 = 0;
$(
let $name = async {
let result: std::result::Result<(), String> = async {
$eval
}.await;
result
};
let $name = tokio::spawn($name);
)*
$(
let $name = $name.await.expect("Failed to spawn.");
)*
$(
print!("test {} ... ", stringify!($name));
match $name {
Ok(_) => {
println!("{}", "ok".green());
oks += 1;
}
Err(msg) => {
println!("{}", "error".bright_red());
println!("{}", msg);
errs += 1;
}
}
)*
println!();
print!("test result: {}. ", if errs > 0 { "error".bright_red() } else { "ok".green() });
println!("{} passed; {} failed; 0 ignored; 0 measured; 0 filtered out", oks, errs);
println!();
(oks, errs)
});
// Just returns #errs as exit code.
errs as i32
});
}
};
}
#[macro_export]
macro_rules! rassert {
( $x:expr ) => {
{
if $x { Ok(()) } else { Err(stringify!($x)) }?
}
};
( $x:expr, $format:expr $(, $arg:expr)* ) => {
{
if $x { Ok(()) } else { Err( format!($format, $( $arg )* ) ) }?
}
};
}
#[macro_export]
macro_rules! rassert_eq {
( $a:expr, $b:expr ) => { rassert!($a == $b) };
( $a:expr, $b:expr, $format:expr $(, $arg:expr)* ) => {
rassert!($a == $b, $format $(, $arg )* )
};
}
#[macro_export]
macro_rules! rassert_ne {
( $a:expr, $b:expr ) => { rassert!($a != $b) };
( $a:expr, $b:expr, $format:expr $(, $arg:expr)* ) => {
rassert!($a != $b, $format $(, $arg )* )
};
}

View File

@ -1,76 +1,76 @@
#![cfg_attr(feature = "nightly", feature(custom_test_frameworks))]
#![cfg_attr(feature = "nightly", test_runner(my_runner))]
mod async_tests;
mod testutils;
use testutils::*;
use colored::*;
use riven::consts::*;
const ROUTE: PlatformRoute = PlatformRoute::EUW1;
async_tests!{
my_runner {
// Champion Mastery tests.
championmastery_getscore_ma5tery: async {
let sum = RIOT_API.summoner_v4().get_by_summoner_name(ROUTE, "ma5tery");
let sum = sum.await.map_err(|e| e.to_string())?.ok_or("Failed to get summoner".to_owned())?;
let p = RIOT_API.champion_mastery_v4().get_champion_mastery_score(ROUTE, &*sum.id);
let s = p.await.map_err(|e| e.to_string())?;
rassert!(969 <= s && s <= 1000, "Unexpected ma5tery score: {}.", s);
Ok(())
},
championmastery_getall_ma5tery: async {
let sum = RIOT_API.summoner_v4().get_by_summoner_name(ROUTE, "ma5tery");
let sum = sum.await.map_err(|e| e.to_string())?.ok_or("Failed to get summoner".to_owned())?;
let p = RIOT_API.champion_mastery_v4().get_all_champion_masteries(ROUTE, &*sum.id);
let s = p.await.map_err(|e| e.to_string())?;
rassert!(s.len() >= 142, "Expected masteries: {}.", s.len());
Ok(())
},
spectator_combo: async {
let featured_p = RIOT_API.spectator_v4().get_featured_games(ROUTE);
let featured = featured_p.await.map_err(|e| e.to_string())?;
rassert!(featured.game_list.len() > 0);
let summoner_name = &featured.game_list[0].participants[0].summoner_name;
let summoner_p = RIOT_API.summoner_v4().get_by_summoner_name(ROUTE, summoner_name);
let summoner = summoner_p.await.map_err(|e| e.to_string())?.ok_or("Failed to get summoner".to_owned())?;
let livegame_p = RIOT_API.spectator_v4().get_current_game_info_by_summoner(ROUTE, &summoner.id);
let livegame_o = livegame_p.await.map_err(|e| e.to_string())?;
if let Some(livegame) = livegame_o {
let participant_match = livegame.participants.iter().find(|p| p.summoner_name == *summoner_name);
rassert!(participant_match.is_some(), "Failed to find summoner in match: {}.", summoner_name);
}
Ok(())
},
// // TFT tests.
// tftleaguev1_getchallengerleague: async {
// let p = RIOT_API.tft_league_v1().get_challenger_league(Region::EUW);
// let l = p.await.map_err(|e| e.to_string())?;
// rassert!(l.entries.len() > 10, "Expected a few challenger players, got: {}.", l.entries.len());
// Ok(())
// },
// tftmatchv1_getmatch: async {
// let p = RIOT_API.tft_match_v1().get_match(Region::EUROPE, "EUW1_4568680990");
// let _m = p.await.map_err(|e| e.to_string())?.ok_or("Failed to get TFT match.".to_owned())?;
// Ok(())
// },
// tftsummonerv1_getbyname: async {
// let p = RIOT_API.tft_summoner_v1().get_by_summoner_name(Region::EUW, "相当猥琐");
// let _s = p.await.map_err(|e| e.to_string())?.ok_or("Failed to get TFT summoner.".to_owned())?;
// Ok(())
// },
// tftsummonerv1_getbyname_none: async {
// let p = RIOT_API.tft_summoner_v1().get_by_summoner_name(Region::EUW, "this summoner does not exist");
// rassert!(p.await.map_err(|e| e.to_string())?.is_none());
// Ok(())
// },
}
}
#![cfg_attr(feature = "nightly", feature(custom_test_frameworks))]
#![cfg_attr(feature = "nightly", test_runner(my_runner))]
mod async_tests;
mod testutils;
use testutils::*;
use colored::*;
use riven::consts::*;
const ROUTE: PlatformRoute = PlatformRoute::EUW1;
async_tests!{
my_runner {
// Champion Mastery tests.
championmastery_getscore_ma5tery: async {
let sum = RIOT_API.summoner_v4().get_by_summoner_name(ROUTE, "ma5tery");
let sum = sum.await.map_err(|e| e.to_string())?.ok_or("Failed to get summoner".to_owned())?;
let p = RIOT_API.champion_mastery_v4().get_champion_mastery_score(ROUTE, &*sum.id);
let s = p.await.map_err(|e| e.to_string())?;
rassert!(969 <= s && s <= 1000, "Unexpected ma5tery score: {}.", s);
Ok(())
},
championmastery_getall_ma5tery: async {
let sum = RIOT_API.summoner_v4().get_by_summoner_name(ROUTE, "ma5tery");
let sum = sum.await.map_err(|e| e.to_string())?.ok_or("Failed to get summoner".to_owned())?;
let p = RIOT_API.champion_mastery_v4().get_all_champion_masteries(ROUTE, &*sum.id);
let s = p.await.map_err(|e| e.to_string())?;
rassert!(s.len() >= 142, "Expected masteries: {}.", s.len());
Ok(())
},
spectator_combo: async {
let featured_p = RIOT_API.spectator_v4().get_featured_games(ROUTE);
let featured = featured_p.await.map_err(|e| e.to_string())?;
rassert!(featured.game_list.len() > 0);
let summoner_name = &featured.game_list[0].participants[0].summoner_name;
let summoner_p = RIOT_API.summoner_v4().get_by_summoner_name(ROUTE, summoner_name);
let summoner = summoner_p.await.map_err(|e| e.to_string())?.ok_or("Failed to get summoner".to_owned())?;
let livegame_p = RIOT_API.spectator_v4().get_current_game_info_by_summoner(ROUTE, &summoner.id);
let livegame_o = livegame_p.await.map_err(|e| e.to_string())?;
if let Some(livegame) = livegame_o {
let participant_match = livegame.participants.iter().find(|p| p.summoner_name == *summoner_name);
rassert!(participant_match.is_some(), "Failed to find summoner in match: {}.", summoner_name);
}
Ok(())
},
// // TFT tests.
// tftleaguev1_getchallengerleague: async {
// let p = RIOT_API.tft_league_v1().get_challenger_league(Region::EUW);
// let l = p.await.map_err(|e| e.to_string())?;
// rassert!(l.entries.len() > 10, "Expected a few challenger players, got: {}.", l.entries.len());
// Ok(())
// },
// tftmatchv1_getmatch: async {
// let p = RIOT_API.tft_match_v1().get_match(Region::EUROPE, "EUW1_4568680990");
// let _m = p.await.map_err(|e| e.to_string())?.ok_or("Failed to get TFT match.".to_owned())?;
// Ok(())
// },
// tftsummonerv1_getbyname: async {
// let p = RIOT_API.tft_summoner_v1().get_by_summoner_name(Region::EUW, "相当猥琐");
// let _s = p.await.map_err(|e| e.to_string())?.ok_or("Failed to get TFT summoner.".to_owned())?;
// Ok(())
// },
// tftsummonerv1_getbyname_none: async {
// let p = RIOT_API.tft_summoner_v1().get_by_summoner_name(Region::EUW, "this summoner does not exist");
// rassert!(p.await.map_err(|e| e.to_string())?.is_none());
// Ok(())
// },
}
}

View File

@ -1,71 +1,71 @@
#![cfg_attr(feature = "nightly", feature(custom_test_frameworks))]
#![cfg_attr(feature = "nightly", test_runner(my_runner))]
mod async_tests;
mod testutils;
use testutils::*;
use colored::*;
use riven::consts::*;
const ROUTE: PlatformRoute = PlatformRoute::JP1;
async_tests!{
my_runner {
// Summoner tests.
summoner_get_kanjikana: async {
let p = RIOT_API.summoner_v4().get_by_summoner_name(ROUTE, "私の 頭が かたい");
let s = p.await.map_err(|e| e.to_string())?.ok_or("Failed to get myheadhard".to_owned())?;
rassert_eq!("私の頭がかたい", s.name);
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 request = RIOT_API.request(reqwest::Method::GET, ROUTE.into(), &path_string);
let p = RIOT_API.execute_val::<riven::models::summoner_v4::Summoner>(
"summoner-v4.getBySummonerName", ROUTE.into(), request);
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(ROUTE.to_regional(), "INVALID_CODE");
let r = p.await;
rassert!(r.is_err());
rassert_eq!(Some(reqwest::StatusCode::FORBIDDEN), r.unwrap_err().status_code());
Ok(())
},
// tft-league-v1.getLeagueEntriesForSummoner
// https://github.com/MingweiSamuel/Riven/issues/25
tft_league_getleagueentriesforsummoner: async {
let sp = RIOT_API.summoner_v4().get_by_summoner_name(ROUTE, "Caihonbbt");
let sr = sp.await.map_err(|e| e.to_string())?.ok_or("Failed to get \"Caihonbbt\"".to_owned())?;
let lp = RIOT_API.tft_league_v1().get_league_entries_for_summoner(ROUTE, &sr.id);
let lr = lp.await.map_err(|e| e.to_string())?;
rassert!(0 < lr.len());
Ok(())
},
// tft-league-v1.getTopRatedLadder
// https://github.com/MingweiSamuel/Riven/issues/24
tft_league_gettopratedladder: async {
let lp = RIOT_API.tft_league_v1().get_top_rated_ladder(ROUTE, QueueType::RANKED_TFT_TURBO);
let lr = lp.await.map_err(|e| e.to_string())?;
rassert!(0 < lr.len());
Ok(())
},
}
}
#![cfg_attr(feature = "nightly", feature(custom_test_frameworks))]
#![cfg_attr(feature = "nightly", test_runner(my_runner))]
mod async_tests;
mod testutils;
use testutils::*;
use colored::*;
use riven::consts::*;
const ROUTE: PlatformRoute = PlatformRoute::JP1;
async_tests!{
my_runner {
// Summoner tests.
summoner_get_kanjikana: async {
let p = RIOT_API.summoner_v4().get_by_summoner_name(ROUTE, "私の 頭が かたい");
let s = p.await.map_err(|e| e.to_string())?.ok_or("Failed to get myheadhard".to_owned())?;
rassert_eq!("私の頭がかたい", s.name);
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 request = RIOT_API.request(reqwest::Method::GET, ROUTE.into(), &path_string);
let p = RIOT_API.execute_val::<riven::models::summoner_v4::Summoner>(
"summoner-v4.getBySummonerName", ROUTE.into(), request);
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(ROUTE.to_regional(), "INVALID_CODE");
let r = p.await;
rassert!(r.is_err());
rassert_eq!(Some(reqwest::StatusCode::FORBIDDEN), r.unwrap_err().status_code());
Ok(())
},
// tft-league-v1.getLeagueEntriesForSummoner
// https://github.com/MingweiSamuel/Riven/issues/25
tft_league_getleagueentriesforsummoner: async {
let sp = RIOT_API.summoner_v4().get_by_summoner_name(ROUTE, "Caihonbbt");
let sr = sp.await.map_err(|e| e.to_string())?.ok_or("Failed to get \"Caihonbbt\"".to_owned())?;
let lp = RIOT_API.tft_league_v1().get_league_entries_for_summoner(ROUTE, &sr.id);
let lr = lp.await.map_err(|e| e.to_string())?;
rassert!(0 < lr.len());
Ok(())
},
// tft-league-v1.getTopRatedLadder
// https://github.com/MingweiSamuel/Riven/issues/24
tft_league_gettopratedladder: async {
let lp = RIOT_API.tft_league_v1().get_top_rated_ladder(ROUTE, QueueType::RANKED_TFT_TURBO);
let lr = lp.await.map_err(|e| e.to_string())?;
rassert!(0 < lr.len());
Ok(())
},
}
}

View File

@ -1,60 +1,60 @@
#![cfg_attr(feature = "nightly", feature(custom_test_frameworks))]
#![feature(async_closure)]
#![cfg_attr(feature = "nightly", test_runner(my_runner))]
mod async_tests;
mod testutils;
use testutils::*;
use futures_util::future;
use colored::*;
use riven::consts::*;
use riven::models::summoner_v4::Summoner;
const REGION: Region = Region::KR;
async_tests!{
my_runner {
league_summoner_bulk_test: async {
let leagues = (1..10)
.map(async move |i| {
let leaguelist = RIOT_API.league_v4().get_league_entries(REGION,
QueueType::RANKED_SOLO_5x5, Tier::GOLD, Division::III, Some(i));
let leaguelist = leaguelist.await
.map_err(|e| e.to_string())?
.ok_or("Failed to get challenger league".to_owned())?;
println!("League list {}: {} items.", i, leaguelist.len());
let summoners = leaguelist
.iter()
.map(async move |leagueentry| {
let summonerfuture = RIOT_API.summoner_v4().get_by_summoner_id(
REGION, &leagueentry.summoner_id);
summonerfuture.await
.map_err(|e| e.to_string())?
.ok_or(format!("Failed to get summoner_id {}.",
leagueentry.summoner_id))
});
future::join_all(summoners).await
.into_iter()
// I'm not sure where this result goes.
.collect::<Result<Vec<Summoner>, String>>()
});
let all_summoners = future::join_all(leagues).await
.into_iter()
.flat_map(|league| league)
.flat_map(|summoner| summoner);
for (i, summoner) in all_summoners.enumerate() {
println!("{}: {}", i + 1, summoner.name);
}
Ok(())
},
}
}
#![cfg_attr(feature = "nightly", feature(custom_test_frameworks))]
#![feature(async_closure)]
#![cfg_attr(feature = "nightly", test_runner(my_runner))]
mod async_tests;
mod testutils;
use testutils::*;
use futures_util::future;
use colored::*;
use riven::consts::*;
use riven::models::summoner_v4::Summoner;
const REGION: Region = Region::KR;
async_tests!{
my_runner {
league_summoner_bulk_test: async {
let leagues = (1..10)
.map(async move |i| {
let leaguelist = RIOT_API.league_v4().get_league_entries(REGION,
QueueType::RANKED_SOLO_5x5, Tier::GOLD, Division::III, Some(i));
let leaguelist = leaguelist.await
.map_err(|e| e.to_string())?
.ok_or("Failed to get challenger league".to_owned())?;
println!("League list {}: {} items.", i, leaguelist.len());
let summoners = leaguelist
.iter()
.map(async move |leagueentry| {
let summonerfuture = RIOT_API.summoner_v4().get_by_summoner_id(
REGION, &leagueentry.summoner_id);
summonerfuture.await
.map_err(|e| e.to_string())?
.ok_or(format!("Failed to get summoner_id {}.",
leagueentry.summoner_id))
});
future::join_all(summoners).await
.into_iter()
// I'm not sure where this result goes.
.collect::<Result<Vec<Summoner>, String>>()
});
let all_summoners = future::join_all(leagues).await
.into_iter()
.flat_map(|league| league)
.flat_map(|summoner| summoner);
for (i, summoner) in all_summoners.enumerate() {
println!("{}: {}", i + 1, summoner.name);
}
Ok(())
},
}
}

View File

@ -1,154 +1,154 @@
#![cfg_attr(feature = "nightly", feature(custom_test_frameworks))]
#![cfg_attr(feature = "nightly", test_runner(my_runner))]
mod async_tests;
mod testutils;
use testutils::*;
use colored::*;
use riven::consts::*;
use riven::models::summoner_v4::*;
fn validate_summoners(s1: Summoner, s2: Summoner) -> Result<(), String> {
rassert_eq!(s1.name, s2.name, "Names 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 {}.", "");
Ok(())
}
const ROUTE: PlatformRoute = PlatformRoute::NA1;
async_tests!{
my_runner {
// Summoner tests.
summoner_double: async {
let l1p = RIOT_API.summoner_v4().get_by_summoner_name(ROUTE, "lug nuts k");
let l2p = RIOT_API.summoner_v4().get_by_summoner_name(ROUTE, "lugnuts k");
let l1 = l1p.await.map_err(|e| e.to_string())?.ok_or("Failed to get l1".to_owned())?;
let l2 = l2p.await.map_err(|e| e.to_string())?.ok_or("Failed to get l2".to_owned())?;
validate_summoners(l1, l2)?;
Ok(())
},
champion_getrotation: async {
let p = RIOT_API.champion_v3().get_champion_info(ROUTE);
let d = p.await.map_err(|e| e.to_string())?;
let new_len = d.free_champion_ids_for_new_players.len();
let free_len = d.free_champion_ids.len();
let level = d.max_new_player_level;
rassert!(new_len >= 10, "New len: {}", new_len);
rassert!(free_len >= 15, "Free len: {}", free_len);
rassert_eq!(10, level, "New player level: {}", level);
Ok(())
},
leagueexp_get: async {
let p = RIOT_API.league_exp_v4().get_league_entries(ROUTE, QueueType::RANKED_SOLO_5x5, Tier::CHALLENGER, Division::I, None);
let d = p.await.map_err(|e| e.to_string())?;
rassert!(!d.is_empty(), "Challenger shouldn't be empty.");
Ok(())
},
matchlist_get: async {
let sp = RIOT_API.summoner_v4().get_by_summoner_name(ROUTE, "haha yes");
let s = sp.await.map_err(|e| e.to_string())?.ok_or("Failed to get \"haha yes\"".to_owned())?;
let mp = RIOT_API.match_v4().get_matchlist(ROUTE, &s.account_id, None, Some(2500), None, None, Some(2600), None, None);
let m = mp.await.map_err(|e| e.to_string())?.ok_or("Failed to get matchlist".to_owned())?;
rassert!(m.matches.len() > 0, "Matchlist should not be empty");
Ok(())
},
matchlist_get2: async {
let sp = RIOT_API.summoner_v4().get_by_summoner_name(ROUTE, "haha yes");
let s = sp.await.map_err(|e| e.to_string())?.ok_or("Failed to get \"haha yes\"".to_owned())?;
let mp = RIOT_API.match_v4().get_matchlist(ROUTE, &s.account_id, None, None, Some(&[ Champion::SION, Champion::SIVIR, Champion::CASSIOPEIA ]), None, None, None, None);
let m = mp.await.map_err(|e| e.to_string())?.ok_or("Failed to get matchlist".to_owned())?;
rassert!(m.matches.len() > 0, "Matchlist should not be empty");
Ok(())
},
match_get: async {
let p = RIOT_API.match_v4().get_match(ROUTE, 3190191338);
let m = p.await.map_err(|e| e.to_string())?.ok_or("Match not found.".to_owned())?;
rassert!(!m.participants.is_empty(), "Match should have participants.");
Ok(())
},
match_get_bots: async {
let p = RIOT_API.match_v4().get_match(ROUTE, 3251803350);
let m = p.await.map_err(|e| e.to_string())?.ok_or("Match not found.".to_owned())?;
rassert!(!m.participants.is_empty(), "Match should have participants.");
Ok(())
},
match_get_odyssey: async {
let p = RIOT_API.match_v4().get_match(ROUTE, 2881976826);
let m = p.await.map_err(|e| e.to_string())?.ok_or("Match not found.".to_owned())?;
rassert!(!m.participants.is_empty(), "Match should have participants.");
Ok(())
},
match_get_aram: async {
let p = RIOT_API.match_v4().get_match(ROUTE, 2961635718);
let m = p.await.map_err(|e| e.to_string())?.ok_or("Failed to get match.".to_owned())?;
rassert!(!m.participants.is_empty(), "Match should have participants.");
Ok(())
},
match_get_aram2: async {
let p = RIOT_API.match_v4().get_match(ROUTE, 3596184782);
let m = p.await.map_err(|e| e.to_string())?.ok_or("Match not found.".to_owned())?;
rassert!(!m.participants.is_empty(), "Match should have participants.");
Ok(())
},
match_get_urf900: async {
let p = RIOT_API.match_v4().get_match(ROUTE, 2963663381);
let m = p.await.map_err(|e| e.to_string())?.ok_or("Failed to get match.".to_owned())?;
rassert!(!m.participants.is_empty(), "Match should have participants.");
Ok(())
},
match_get_tutorial1: async {
let p = RIOT_API.match_v4().get_match(ROUTE, 3432145099);
let m = p.await.map_err(|e| e.to_string())?.ok_or("Failed to get match.".to_owned())?;
rassert!(!m.participants.is_empty(), "Match should have participants.");
Ok(())
},
match_get_tutorial2: async {
let p = RIOT_API.match_v4().get_match(ROUTE, 3432116214);
let m = p.await.map_err(|e| e.to_string())?.ok_or("Failed to get match.".to_owned())?;
rassert!(!m.participants.is_empty(), "Match should have participants.");
Ok(())
},
match_get_tutorial3: async {
let p = RIOT_API.match_v4().get_match(ROUTE, 3432156790);
let m = p.await.map_err(|e| e.to_string())?.ok_or("Failed to get match.".to_owned())?;
rassert!(!m.participants.is_empty(), "Match should have participants.");
Ok(())
},
match_gettimeline: async {
let p = RIOT_API.match_v4().get_match_timeline(ROUTE, 3190191338);
let m = p.await.map_err(|e| e.to_string())?.ok_or("Match timeline not found.".to_owned())?;
rassert!(!m.frames.is_empty(), "Match timeline should have frames.");
Ok(())
},
// Commented out, requires special API key.
// // LOR
// lor_ranked_get_leaderboards: async {
// let future = RIOT_API.lor_ranked_v1().get_leaderboards(Region::AMERICAS);
// let _leaderboard = future.await.map_err(|e| e.to_string())?;
// Ok(())
// },
// CLASH
clash_get_tournaments: async {
let p = RIOT_API.clash_v1().get_tournaments(ROUTE);
let tours = p.await.map_err(|e| e.to_string())?;
if let Some(tour0) = tours.first() {
let p = RIOT_API.clash_v1().get_tournament_by_id(ROUTE, tour0.id);
let tour1 = p.await.map_err(|e| e.to_string())?;
assert_eq!(Some(tour0.id), tour1.map(|t| t.id));
}
Ok(())
},
clash_get_team_by_id: async {
let p = RIOT_API.clash_v1().get_team_by_id(ROUTE, "00000000-0000-0000-0000-000000000000");
let team = p.await.map_err(|e| e.to_string())?;
assert!(team.is_none());
Ok(())
},
}
}
#![cfg_attr(feature = "nightly", feature(custom_test_frameworks))]
#![cfg_attr(feature = "nightly", test_runner(my_runner))]
mod async_tests;
mod testutils;
use testutils::*;
use colored::*;
use riven::consts::*;
use riven::models::summoner_v4::*;
fn validate_summoners(s1: Summoner, s2: Summoner) -> Result<(), String> {
rassert_eq!(s1.name, s2.name, "Names 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 {}.", "");
Ok(())
}
const ROUTE: PlatformRoute = PlatformRoute::NA1;
async_tests!{
my_runner {
// Summoner tests.
summoner_double: async {
let l1p = RIOT_API.summoner_v4().get_by_summoner_name(ROUTE, "lug nuts k");
let l2p = RIOT_API.summoner_v4().get_by_summoner_name(ROUTE, "lugnuts k");
let l1 = l1p.await.map_err(|e| e.to_string())?.ok_or("Failed to get l1".to_owned())?;
let l2 = l2p.await.map_err(|e| e.to_string())?.ok_or("Failed to get l2".to_owned())?;
validate_summoners(l1, l2)?;
Ok(())
},
champion_getrotation: async {
let p = RIOT_API.champion_v3().get_champion_info(ROUTE);
let d = p.await.map_err(|e| e.to_string())?;
let new_len = d.free_champion_ids_for_new_players.len();
let free_len = d.free_champion_ids.len();
let level = d.max_new_player_level;
rassert!(new_len >= 10, "New len: {}", new_len);
rassert!(free_len >= 15, "Free len: {}", free_len);
rassert_eq!(10, level, "New player level: {}", level);
Ok(())
},
leagueexp_get: async {
let p = RIOT_API.league_exp_v4().get_league_entries(ROUTE, QueueType::RANKED_SOLO_5x5, Tier::CHALLENGER, Division::I, None);
let d = p.await.map_err(|e| e.to_string())?;
rassert!(!d.is_empty(), "Challenger shouldn't be empty.");
Ok(())
},
matchlist_get: async {
let sp = RIOT_API.summoner_v4().get_by_summoner_name(ROUTE, "haha yes");
let s = sp.await.map_err(|e| e.to_string())?.ok_or("Failed to get \"haha yes\"".to_owned())?;
let mp = RIOT_API.match_v4().get_matchlist(ROUTE, &s.account_id, None, Some(2500), None, None, Some(2600), None, None);
let m = mp.await.map_err(|e| e.to_string())?.ok_or("Failed to get matchlist".to_owned())?;
rassert!(m.matches.len() > 0, "Matchlist should not be empty");
Ok(())
},
matchlist_get2: async {
let sp = RIOT_API.summoner_v4().get_by_summoner_name(ROUTE, "haha yes");
let s = sp.await.map_err(|e| e.to_string())?.ok_or("Failed to get \"haha yes\"".to_owned())?;
let mp = RIOT_API.match_v4().get_matchlist(ROUTE, &s.account_id, None, None, Some(&[ Champion::SION, Champion::SIVIR, Champion::CASSIOPEIA ]), None, None, None, None);
let m = mp.await.map_err(|e| e.to_string())?.ok_or("Failed to get matchlist".to_owned())?;
rassert!(m.matches.len() > 0, "Matchlist should not be empty");
Ok(())
},
match_get: async {
let p = RIOT_API.match_v4().get_match(ROUTE, 3190191338);
let m = p.await.map_err(|e| e.to_string())?.ok_or("Match not found.".to_owned())?;
rassert!(!m.participants.is_empty(), "Match should have participants.");
Ok(())
},
match_get_bots: async {
let p = RIOT_API.match_v4().get_match(ROUTE, 3251803350);
let m = p.await.map_err(|e| e.to_string())?.ok_or("Match not found.".to_owned())?;
rassert!(!m.participants.is_empty(), "Match should have participants.");
Ok(())
},
match_get_odyssey: async {
let p = RIOT_API.match_v4().get_match(ROUTE, 2881976826);
let m = p.await.map_err(|e| e.to_string())?.ok_or("Match not found.".to_owned())?;
rassert!(!m.participants.is_empty(), "Match should have participants.");
Ok(())
},
match_get_aram: async {
let p = RIOT_API.match_v4().get_match(ROUTE, 2961635718);
let m = p.await.map_err(|e| e.to_string())?.ok_or("Failed to get match.".to_owned())?;
rassert!(!m.participants.is_empty(), "Match should have participants.");
Ok(())
},
match_get_aram2: async {
let p = RIOT_API.match_v4().get_match(ROUTE, 3596184782);
let m = p.await.map_err(|e| e.to_string())?.ok_or("Match not found.".to_owned())?;
rassert!(!m.participants.is_empty(), "Match should have participants.");
Ok(())
},
match_get_urf900: async {
let p = RIOT_API.match_v4().get_match(ROUTE, 2963663381);
let m = p.await.map_err(|e| e.to_string())?.ok_or("Failed to get match.".to_owned())?;
rassert!(!m.participants.is_empty(), "Match should have participants.");
Ok(())
},
match_get_tutorial1: async {
let p = RIOT_API.match_v4().get_match(ROUTE, 3432145099);
let m = p.await.map_err(|e| e.to_string())?.ok_or("Failed to get match.".to_owned())?;
rassert!(!m.participants.is_empty(), "Match should have participants.");
Ok(())
},
match_get_tutorial2: async {
let p = RIOT_API.match_v4().get_match(ROUTE, 3432116214);
let m = p.await.map_err(|e| e.to_string())?.ok_or("Failed to get match.".to_owned())?;
rassert!(!m.participants.is_empty(), "Match should have participants.");
Ok(())
},
match_get_tutorial3: async {
let p = RIOT_API.match_v4().get_match(ROUTE, 3432156790);
let m = p.await.map_err(|e| e.to_string())?.ok_or("Failed to get match.".to_owned())?;
rassert!(!m.participants.is_empty(), "Match should have participants.");
Ok(())
},
match_gettimeline: async {
let p = RIOT_API.match_v4().get_match_timeline(ROUTE, 3190191338);
let m = p.await.map_err(|e| e.to_string())?.ok_or("Match timeline not found.".to_owned())?;
rassert!(!m.frames.is_empty(), "Match timeline should have frames.");
Ok(())
},
// Commented out, requires special API key.
// // LOR
// lor_ranked_get_leaderboards: async {
// let future = RIOT_API.lor_ranked_v1().get_leaderboards(Region::AMERICAS);
// let _leaderboard = future.await.map_err(|e| e.to_string())?;
// Ok(())
// },
// CLASH
clash_get_tournaments: async {
let p = RIOT_API.clash_v1().get_tournaments(ROUTE);
let tours = p.await.map_err(|e| e.to_string())?;
if let Some(tour0) = tours.first() {
let p = RIOT_API.clash_v1().get_tournament_by_id(ROUTE, tour0.id);
let tour1 = p.await.map_err(|e| e.to_string())?;
assert_eq!(Some(tour0.id), tour1.map(|t| t.id));
}
Ok(())
},
clash_get_team_by_id: async {
let p = RIOT_API.clash_v1().get_team_by_id(ROUTE, "00000000-0000-0000-0000-000000000000");
let team = p.await.map_err(|e| e.to_string())?;
assert!(team.is_none());
Ok(())
},
}
}

View File

@ -1,40 +1,40 @@
#![cfg_attr(feature = "nightly", feature(custom_test_frameworks))]
#![cfg_attr(feature = "nightly", test_runner(my_runner))]
mod async_tests;
mod testutils;
use testutils::RIOT_API;
use colored::*;
use riven::consts::*;
use riven::models::summoner_v4::Summoner;
const ROUTE: PlatformRoute = PlatformRoute::TR1;
async_tests!{
my_runner {
league_summoner_bulk_test: async {
let p = RIOT_API.league_v4().get_challenger_league(ROUTE, QueueType::RANKED_SOLO_5x5);
// let p = future_start(p);
let ll = p.await.map_err(|e| e.to_string())?;
println!("{:?} Challenger {} entries.", ROUTE, ll.entries.len());
let sl = ll.entries.iter().take(50)
.map(|entry| RIOT_API.summoner_v4().get_by_summoner_id(ROUTE, &entry.summoner_id))
.map(tokio::spawn)
.collect::<Vec<_>>();
for (i, s) in sl.into_iter().enumerate() {
let summoner: Summoner = s.await
.expect("tokio::spawn join error")
.map_err(|e| e.to_string())?;
println!("{}: {}", i + 1, summoner.name);
}
Ok(())
},
}
}
#![cfg_attr(feature = "nightly", feature(custom_test_frameworks))]
#![cfg_attr(feature = "nightly", test_runner(my_runner))]
mod async_tests;
mod testutils;
use testutils::RIOT_API;
use colored::*;
use riven::consts::*;
use riven::models::summoner_v4::Summoner;
const ROUTE: PlatformRoute = PlatformRoute::TR1;
async_tests!{
my_runner {
league_summoner_bulk_test: async {
let p = RIOT_API.league_v4().get_challenger_league(ROUTE, QueueType::RANKED_SOLO_5x5);
// let p = future_start(p);
let ll = p.await.map_err(|e| e.to_string())?;
println!("{:?} Challenger {} entries.", ROUTE, ll.entries.len());
let sl = ll.entries.iter().take(50)
.map(|entry| RIOT_API.summoner_v4().get_by_summoner_id(ROUTE, &entry.summoner_id))
.map(tokio::spawn)
.collect::<Vec<_>>();
for (i, s) in sl.into_iter().enumerate() {
let summoner: Summoner = s.await
.expect("tokio::spawn join error")
.map_err(|e| e.to_string())?;
println!("{}: {}", i + 1, summoner.name);
}
Ok(())
},
}
}

View File

@ -1,23 +1,23 @@
#![allow(dead_code)]
use lazy_static::lazy_static;
use riven::{ RiotApi, RiotApiConfig };
lazy_static! {
pub static ref RIOT_API: RiotApi = {
let api_key = std::env::var("RGAPI_KEY").ok()
.or_else(|| std::fs::read_to_string("apikey.txt").ok())
.expect("Failed to find RGAPI_KEY env var or apikey.txt.");
RiotApi::with_config(RiotApiConfig::with_key(api_key.trim())
.preconfig_burst())
};
}
pub mod ids {
pub const SUMMONER_ID_LUGNUTSK: &'static str = "SBM8Ubipo4ge2yj7bhEzL7yvV0C9Oc1XA2l6v5okGMA_nCw";
pub const SUMMONER_ID_MA5TERY: &'static str = "IbC4uyFEEW3ZkZw6FZF4bViw3P1EynclAcI6-p-vCpI99Ec";
pub const SUMMONER_ID_C9SNEAKY: &'static str = "ghHSdADqgxKwcRl_vWndx6wKiyZx0xKQv-LOhOcU5LU";
pub const ACCOUNT_ID_C9SNEAKY: &'static str = "ML_CcLT94UUHp1iDvXOXCidfmzzPrk_Jbub1f_INhw";
pub const ACCOUNT_ID_LUGNUTSK: &'static str = "iheZF2uJ50S84Hfq6Ob8GNlJAUmBmac-EsEEWBJjD01q1jQ";
}
#![allow(dead_code)]
use lazy_static::lazy_static;
use riven::{ RiotApi, RiotApiConfig };
lazy_static! {
pub static ref RIOT_API: RiotApi = {
let api_key = std::env::var("RGAPI_KEY").ok()
.or_else(|| std::fs::read_to_string("apikey.txt").ok())
.expect("Failed to find RGAPI_KEY env var or apikey.txt.");
RiotApi::with_config(RiotApiConfig::with_key(api_key.trim())
.preconfig_burst())
};
}
pub mod ids {
pub const SUMMONER_ID_LUGNUTSK: &'static str = "SBM8Ubipo4ge2yj7bhEzL7yvV0C9Oc1XA2l6v5okGMA_nCw";
pub const SUMMONER_ID_MA5TERY: &'static str = "IbC4uyFEEW3ZkZw6FZF4bViw3P1EynclAcI6-p-vCpI99Ec";
pub const SUMMONER_ID_C9SNEAKY: &'static str = "ghHSdADqgxKwcRl_vWndx6wKiyZx0xKQv-LOhOcU5LU";
pub const ACCOUNT_ID_C9SNEAKY: &'static str = "ML_CcLT94UUHp1iDvXOXCidfmzzPrk_Jbub1f_INhw";
pub const ACCOUNT_ID_LUGNUTSK: &'static str = "iheZF2uJ50S84Hfq6Ob8GNlJAUmBmac-EsEEWBJjD01q1jQ";
}