riven/riot_api.rs
1use core::panic;
2use std::future::Future;
3
4use memo_map::MemoMap;
5use reqwest::{Client, Method, RequestBuilder};
6#[cfg(feature = "tracing")]
7use tracing as log;
8
9use crate::req::RegionalRequester;
10use crate::{ResponseInfo, Result, RiotApiConfig, RiotApiError, TryRequestError, TryRequestResult};
11
12/// For retrieving data from the Riot Games API.
13///
14/// # Usage
15///
16/// Construct an instance using [`RiotApi::new(api_key or config)`](RiotApi::new).
17/// The parameter may be a Riot API key string or a [`RiotApiConfig`]. Riot API
18/// keys are obtained from the [Riot Developer Portal](https://developer.riotgames.com/)
19/// and look like `"RGAPI-01234567-89ab-cdef-0123-456789abcdef"`.
20///
21/// An instance provides access to "endpoint handles" which in turn provide
22/// access to individual API method calls. For example, to get a user by Riot
23/// ID we first access the [`account_v1()`](RiotApi::account_v1) endpoints
24/// then call the [`get_by_riot_id()`](crate::endpoints::AccountV1::get_by_riot_id)
25/// method:
26/// ```ignore
27/// riot_api.account_v1().get_by_riot_id(Region::NA, "LugnutsK", "000")
28/// ```
29///
30/// # Rate Limiting
31///
32/// The Riot Game API enforces _dynamic_ rate limiting, meaning that rate limits are
33/// specified in response headers and can change at any time.
34/// Riven keeps track of changing rate limits seamlessly, preventing you from
35/// getting blacklisted.
36///
37/// Riven's rate limiting is highly efficient; it can use the full throughput
38/// of your rate limit without triggering 429 errors.
39///
40/// To adjust rate limiting, see [RiotApiConfig] and use
41/// [`RiotApi::new(config)`](RiotApi::new) to construct an instance.
42pub struct RiotApi {
43 /// Configuration settings.
44 config: RiotApiConfig,
45 /// Client for making requests.
46 client: Client,
47
48 /// Per-region requesters.
49 regional_requesters: MemoMap<&'static str, RegionalRequester>,
50}
51
52impl RiotApi {
53 /// Constructs a new instance from an API key (e.g. `"RGAPI-01234567-89ab-cdef-0123-456789abcdef"`) or a [RiotApiConfig].
54 pub fn new(config: impl Into<RiotApiConfig>) -> Self {
55 let mut config = config.into();
56 let client_builder = config
57 .client_builder
58 .take()
59 .expect("CLIENT_BUILDER IN CONFIG SHOULD NOT BE NONE.");
60 Self {
61 config,
62 client: client_builder
63 .build()
64 .expect("Failed to create client from builder."),
65 regional_requesters: MemoMap::new(),
66 }
67 }
68
69 /// This method should generally not be used directly. Consider using endpoint wrappers instead.
70 ///
71 /// Creates a `RequestBuilder` instance with the given parameters, for use with the `execute*()` methods.
72 ///
73 /// # Parameters
74 /// * `method` - The HTTP method for this request.
75 /// * `region_platform` - The stringified platform, used to create the base URL.
76 /// * `path` - The URL path, appended to the base URL.
77 pub fn request(&self, method: Method, region_platform: &str, path: &str) -> RequestBuilder {
78 let base_url_platform = self.config.base_url.replace("{}", region_platform);
79 self.client
80 .request(method, format!("{}{}", base_url_platform, path))
81 }
82
83 /// This method should generally not be used directly. Consider using endpoint wrappers instead.
84 ///
85 /// This sends a request based on the given parameters and returns a parsed result.
86 ///
87 /// # Parameters
88 /// * `method_id` - A unique string id representing the endpoint method for per-method rate limiting.
89 /// * `region_platform` - The stringified platform, used in rate limiting.
90 /// * `request` - The request information. Use `request()` to obtain a `RequestBuilder` instance.
91 ///
92 /// # Returns
93 /// A future resolving to a `Result` containg either a `T` (success) or a `RiotApiError` (failure).
94 pub async fn execute_val<'a, T: for<'de> crate::de::Deserialize<'de> + 'a>(
95 &'a self,
96 method_id: &'static str,
97 region_platform: &'static str,
98 request: RequestBuilder,
99 ) -> Result<T> {
100 let rinfo = self
101 .execute_raw(method_id, region_platform, request)
102 .await?;
103 rinfo.json::<T>().await
104 }
105
106 /// This method should generally not be used directly. Consider using endpoint wrappers instead.
107 ///
108 /// This sends a request based on the given parameters and returns an optional parsed result.
109 ///
110 /// # Parameters
111 /// * `method_id` - A unique string id representing the endpoint method for per-method rate limiting.
112 /// * `region_platform` - The stringified platform, used in rate limiting.
113 /// * `request` - The request information. Use `request()` to obtain a `RequestBuilder` instance.
114 ///
115 /// # Returns
116 /// A future resolving to a `Result` containg either an `Option<T>` (success) or a `RiotApiError` (failure).
117 pub async fn execute_opt<'a, T: for<'de> crate::de::Deserialize<'de> + 'a>(
118 &'a self,
119 method_id: &'static str,
120 region_platform: &'static str,
121 request: RequestBuilder,
122 ) -> Result<Option<T>> {
123 let rinfo = self
124 .execute_raw(method_id, region_platform, request)
125 .await?;
126 if rinfo.status_none {
127 return Ok(None);
128 }
129 rinfo.json::<Option<T>>().await
130 }
131
132 /// This method should generally not be used directly. Consider using endpoint wrappers instead.
133 ///
134 /// This sends a request based on the given parameters but does not deserialize any response body.
135 ///
136 /// # Parameters
137 /// * `method_id` - A unique string id representing the endpoint method for per-method rate limiting.
138 /// * `region_platform` - The stringified platform, used in rate limiting.
139 /// * `request` - The request information. Use `request()` to obtain a `RequestBuilder` instance.
140 ///
141 /// # Returns
142 /// A future resolving to a `Result` containg either `()` (success) or a `RiotApiError` (failure).
143 pub async fn execute(
144 &self,
145 method_id: &'static str,
146 region_platform: &'static str,
147 request: RequestBuilder,
148 ) -> Result<()> {
149 let rinfo = self
150 .execute_raw(method_id, region_platform, request)
151 .await?;
152 let retries = rinfo.retries;
153 let status = rinfo.response.status();
154 if status.is_client_error() || status.is_server_error() {
155 Err(RiotApiError::new(
156 rinfo.reqwest_errors,
157 None,
158 retries,
159 Some(rinfo.response),
160 Some(status),
161 ))
162 } else {
163 Ok(())
164 }
165 }
166
167 /// This method should generally not be used directly. Consider using endpoint wrappers instead.
168 ///
169 /// This sends a request based on the given parameters and returns a parsed result.
170 ///
171 /// # Parameters
172 /// * `method_id` - A unique string id representing the endpoint method for per-method rate limiting.
173 /// * `region_platform` - The stringified platform, used in rate limiting.
174 /// * `request` - The request information. Use `request()` to obtain a `RequestBuilder` instance.
175 /// * `min_capacity` - Minimum capacity required as a float from 1.0 (all capacity) to 0.0 (no capacity) excluding burst
176 ///
177 /// # Returns
178 /// None if min_capacity is not met, otherwise a future resolving to a `Result` containg either a `T` (success) or a `RiotApiError` (failure).
179 pub async fn try_execute_val<'a, T: for<'de> crate::de::Deserialize<'de> + 'a>(
180 &'a self,
181 method_id: &'static str,
182 region_platform: &'static str,
183 request: RequestBuilder,
184 min_capacity: f32,
185 ) -> TryRequestResult<T> {
186 let rinfo = self
187 .try_execute_raw(method_id, region_platform, request, min_capacity)
188 .await?;
189 Ok(rinfo.json::<T>().await?)
190 }
191
192 /// This method should generally not be used directly. Consider using endpoint wrappers instead.
193 ///
194 /// This sends a request based on the given parameters and returns an optional parsed result.
195 ///
196 /// # Parameters
197 /// * `method_id` - A unique string id representing the endpoint method for per-method rate limiting.
198 /// * `region_platform` - The stringified platform, used in rate limiting.
199 /// * `request` - The request information. Use `request()` to obtain a `RequestBuilder` instance.
200 /// * `min_capacity` - Minimum capacity required as a float from 1.0 (all capacity) to 0.0 (no capacity) excluding burst
201 ///
202 /// # Returns
203 /// None if min_capacity is not met, otherwise a future resolving to a `Result` containg either an `Option<T>` (success) or a `RiotApiError` (failure).
204 pub async fn try_execute_opt<'a, T: for<'de> crate::de::Deserialize<'de> + 'a>(
205 &'a self,
206 method_id: &'static str,
207 region_platform: &'static str,
208 request: RequestBuilder,
209 min_capacity: f32,
210 ) -> TryRequestResult<Option<T>> {
211 let rinfo = self
212 .try_execute_raw(method_id, region_platform, request, min_capacity)
213 .await?;
214 if rinfo.status_none {
215 return Ok(None);
216 }
217 Ok(rinfo.json::<Option<T>>().await?)
218 }
219
220 /// This method should generally not be used directly. Consider using endpoint wrappers instead.
221 ///
222 /// This sends a request based on the given parameters but does not deserialize any response body.
223 ///
224 /// # Parameters
225 /// * `method_id` - A unique string id representing the endpoint method for per-method rate limiting.
226 /// * `region_platform` - The stringified platform, used in rate limiting.
227 /// * `request` - The request information. Use `request()` to obtain a `RequestBuilder` instance.
228 ///
229 /// # Returns
230 /// None if min_capacity is not met, otherwise a future resolving to a `Result` containg either `()` (success) or a `RiotApiError` (failure).
231 pub async fn try_execute(
232 &self,
233 method_id: &'static str,
234 region_platform: &'static str,
235 request: RequestBuilder,
236 min_capacity: f32,
237 ) -> TryRequestResult<()> {
238 let rinfo = self
239 .try_execute_raw(method_id, region_platform, request, min_capacity)
240 .await?;
241 let retries = rinfo.retries;
242 let status = rinfo.response.status();
243 if status.is_client_error() || status.is_server_error() {
244 Err(TryRequestError::RiotApiError(RiotApiError::new(
245 rinfo.reqwest_errors,
246 None,
247 retries,
248 Some(rinfo.response),
249 Some(status),
250 )))
251 } else {
252 Ok(())
253 }
254 }
255
256 /// This method should generally not be used directly. Consider using endpoint wrappers instead.
257 ///
258 /// This sends a request based on the given parameters and returns a raw `ResponseInfo`.
259 ///
260 /// This can be used to implement a Riot API proxy without needing to deserialize and reserialize JSON responses.
261 ///
262 /// # Parameters
263 /// * `method_id` - A unique string id representing the endpoint method for per-method rate limiting.
264 /// * `region_platform` - The stringified platform, used in rate limiting.
265 /// * `request` - The request information. Use `request()` to obtain a `RequestBuilder` instance.
266 ///
267 /// # Returns
268 /// A future resolving to a `Result` containg either a `ResponseInfo` (success) or a `RiotApiError` (failure).
269 pub async fn execute_raw(
270 &self,
271 method_id: &'static str,
272 region_platform: &'static str,
273 request: RequestBuilder,
274 ) -> Result<ResponseInfo> {
275 self.regional_requester(region_platform)
276 .execute(&self.config, method_id, request, None)
277 .await
278 .map_err(|e| match e {
279 TryRequestError::NotEnoughCapacity => {
280 panic!("execute called with no capacity requirement, should not happen.")
281 }
282 TryRequestError::RiotApiError(e) => e,
283 })
284 }
285
286 /// This method should generally not be used directly. Consider using endpoint wrappers instead.
287 ///
288 /// A variabion of `execute_raw` that allows for a minimum capacity to be specified for load shedding.
289 ///
290 /// # Parameters
291 /// * `method_id` - A unique string id representing the endpoint method for per-method rate limiting.
292 /// * `region_platform` - The stringified platform, used in rate limiting.
293 /// * `request` - The request information. Use `request()` to obtain a `RequestBuilder` instance.
294 /// * `min_capacity` - Minimum capacity required as a float from 1.0 (all capacity) to 0.0 (no capacity) excluding burst
295 ///
296 /// # Returns
297 /// None if there is not enough capacity to make the request, otherwise a future resolving to a `Result` containg
298 /// either a `ResponseInfo` (success) or a `RiotApiError` (failure).
299 pub fn try_execute_raw(
300 &self,
301 method_id: &'static str,
302 region_platform: &'static str,
303 request: RequestBuilder,
304 min_capacity: f32,
305 ) -> impl Future<Output = TryRequestResult<ResponseInfo>> + '_ {
306 self.regional_requester(region_platform).execute(
307 &self.config,
308 method_id,
309 request,
310 Some(min_capacity),
311 )
312 }
313
314 /// Gets the [`RiotApiConfig::rso_clear_header`] for use in RSO endpoints.
315 pub(crate) fn get_rso_clear_header(&self) -> Option<&str> {
316 self.config.rso_clear_header.as_deref()
317 }
318
319 /// Get or create the RegionalRequester for the given region.
320 fn regional_requester(&self, region_platform: &'static str) -> &RegionalRequester {
321 self.regional_requesters
322 .get_or_insert(®ion_platform, || {
323 log::debug!(
324 "Creating requester for region platform {}.",
325 region_platform
326 );
327 RegionalRequester::new()
328 })
329 }
330}