1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
//! Configuration of RiotApi.
use reqwest::header::{HeaderMap, HeaderValue};
use reqwest::ClientBuilder;
use crate::time::Duration;
/// Configuration for instantiating RiotApi.
#[derive(Debug)]
pub struct RiotApiConfig {
pub(crate) base_url: String,
pub(crate) retries: u8,
pub(crate) app_rate_usage_factor: f32,
pub(crate) method_rate_usage_factor: f32,
pub(crate) burst_factor: f32,
pub(crate) duration_overhead: Duration,
pub(crate) client_builder: Option<ClientBuilder>,
pub(crate) rso_clear_header: Option<String>,
}
impl RiotApiConfig {
/// Request header name for the Riot API key, `"X-Riot-Token"`.
///
/// When using `set_client_builder`, the supplied builder should include
/// this default header with the Riot API key as the value.
pub 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;
/// `1.0`
///
/// Default rate limit usage factor.
pub const DEFAULT_RATE_USAGE_FACTOR: f32 = 1.0;
/// `0.99`
///
/// Default `burst_factor`, also used by `preconfig_burst`.
pub const PRECONFIG_BURST_BURST_FACTOR: 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_factor` used by `preconfig_throughput`.
pub const PRECONFIG_THROUGHPUT_BURST_FACTOR: 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`).
/// * `burst_factor = 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,
app_rate_usage_factor: Self::DEFAULT_RATE_USAGE_FACTOR,
method_rate_usage_factor: Self::DEFAULT_RATE_USAGE_FACTOR,
burst_factor: Self::PRECONFIG_BURST_BURST_FACTOR,
duration_overhead: Self::PRECONFIG_BURST_DURATION_OVERHEAD,
client_builder: Some(ClientBuilder::new().default_headers(default_headers)),
rso_clear_header: Some(Self::RIOT_KEY_HEADER.to_owned()),
}
}
/// Creates a new `RiotApiConfig` with the given client builder.
///
/// The client builder default headers should include a value for
/// [`RiotApiConfig::RIOT_KEY_HEADER`] (`"X-Riot-Token"`), otherwise authentication will fail.
///
/// * `retries = 3` (`RiotApiConfig::DEFAULT_RETRIES`).
/// * `burst_factor = 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,
app_rate_usage_factor: Self::DEFAULT_RATE_USAGE_FACTOR,
method_rate_usage_factor: Self::DEFAULT_RATE_USAGE_FACTOR,
burst_factor: Self::PRECONFIG_BURST_BURST_FACTOR,
duration_overhead: Self::PRECONFIG_BURST_DURATION_OVERHEAD,
client_builder: Some(client_builder),
rso_clear_header: Some(Self::RIOT_KEY_HEADER.to_owned()),
}
}
/// Sets rate limiting settings to preconfigured values optimized for burst,
/// low latency:
///
/// * `burst_factor = 0.99` (`PRECONFIG_BURST_BURST_FACTOR`).
/// * `duration_overhead = 989 ms` (`PRECONFIG_BURST_DURATION_OVERHEAD_MILLIS`).
///
/// # Returns
/// `self`, for chaining.
pub fn preconfig_burst(mut self) -> Self {
self.burst_factor = Self::PRECONFIG_BURST_BURST_FACTOR;
self.duration_overhead = Self::PRECONFIG_BURST_DURATION_OVERHEAD;
self
}
/// Sets the rate limiting settings to preconfigured values optimized for
/// high throughput:
///
/// * `burst_factor = 0.47` (`PRECONFIG_THROUGHPUT_BURST_FACTOR`).
/// * `duration_overhead = 10 ms` (`PRECONFIG_THROUGHPUT_DURATION_OVERHEAD_MILLIS`).
///
/// # Returns
/// `self`, for chaining.
pub fn preconfig_throughput(mut self) -> Self {
self.burst_factor = Self::PRECONFIG_THROUGHPUT_BURST_FACTOR;
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
}
/// The rate limit usage percentage controls how much of the API key's rate
/// limit will be used. The default value of `1.0` means the entirety of
/// the rate limit may be used if it is needed. This applies to both the
/// API key's rate limit (per route) _and_ to endpoint method rate limits.
///
/// Setting a value lower than `1.0` can be useful if you are running
/// multiple API instances on the same API key.
///
/// For example, four instances, possibly running on different machines,
/// could each have a value of `0.25` to share an API key's rate limit
/// evenly.
///
/// Note that if you have multiple instances hitting _different_ methods,
/// you should use [Self::set_app_rate_usage_factor()] and [Self::set_method_rate_usage_factor()]
/// separately, as this sets both.
///
/// This also can be used to reduce the chance of hitting 429s, although
/// 429s should be rare even with this set to `1.0`.
///
/// # Panics
/// If `rate_usage_factor` is not in range (0, 1].
///
/// # Returns
/// `self`, for chaining.
pub fn set_rate_usage_factor(mut self, rate_usage_factor: f32) -> Self {
// Use inverted check to handle NaN.
if 0.0 < rate_usage_factor && rate_usage_factor <= 1.0 {
self.app_rate_usage_factor = rate_usage_factor;
self.method_rate_usage_factor = rate_usage_factor;
return self;
}
panic!(
"rate_usage_factor \"{}\" not in range (0, 1].",
rate_usage_factor
);
}
/// See [Self::set_rate_usage_factor]. Setting this is useful if you have multiple
/// instances sharing the app rate limit, but are hitting distinct methods
/// and therefore do not need their method usage decreased.
///
/// # Panics
/// If `app_rate_usage_factor` is not in range (0, 1\].
///
/// # Returns
/// `self`, for chaining.
pub fn set_app_rate_usage_factor(mut self, app_rate_usage_factor: f32) -> Self {
// Use inverted check to handle NaN.
if 0.0 < app_rate_usage_factor && app_rate_usage_factor <= 1.0 {
self.app_rate_usage_factor = app_rate_usage_factor;
return self;
}
panic!(
"app_rate_usage_factor \"{}\" not in range (0, 1].",
app_rate_usage_factor
);
}
/// See [Self::set_rate_usage_factor] and [Self::set_app_rate_usage_factor].
/// This method is mainly provided for completeness, though it may be
/// useful in advanced use cases.
///
/// # Panics
/// If `method_rate_usage_factor` is not in range (0, 1\].
///
/// # Returns
/// `self`, for chaining.
pub fn set_method_rate_usage_factor(mut self, method_rate_usage_factor: f32) -> Self {
// Use inverted check to handle NaN.
if 0.0 < method_rate_usage_factor && method_rate_usage_factor <= 1.0 {
self.method_rate_usage_factor = method_rate_usage_factor;
return self;
}
panic!(
"method_rate_usage_factor \"{}\" not in range (0, 1].",
method_rate_usage_factor
);
}
/// 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_factor` is not in range (0, 1\].
///
/// # Returns
/// `self`, for chaining.
pub fn set_burst_factor(mut self, burst_factor: f32) -> Self {
// Use inverted check to handle NaN.
if 0.0 < burst_factor && burst_factor <= 1.0 {
self.burst_factor = burst_factor;
return self;
}
panic!("burst_factor \"{}\" not in range (0, 1].", burst_factor);
}
/// 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
}
/// Sets the header to clear for RSO requests (if `Some`), or will not override any headers (if
/// `None`).
///
/// This is a bit of a hack. The client used by Riven is expected to include the API key as a
/// default header. However, if the API key is included in an [RSO](https://developer.riotgames.com/docs/lol#rso-integration)
/// request the server responds with a 400 "Bad request - Invalid authorization specified"
/// error. To avoid this the `rso_clear_header` header is overridden to be empty for RSO
/// requests.
///
/// This is set to `Some(`[`Self::RIOT_KEY_HEADER`]`)` by default.
///
/// # Returns
/// `self`, for chaining.
pub fn set_rso_clear_header(mut self, rso_clear_header: Option<String>) -> Self {
self.rso_clear_header = rso_clear_header;
self
}
}
impl<T: AsRef<[u8]>> From<T> for RiotApiConfig {
fn from(api_key: T) -> Self {
Self::with_key(api_key)
}
}