Skip to main content

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(&region_platform, || {
323                log::debug!(
324                    "Creating requester for region platform {}.",
325                    region_platform
326                );
327                RegionalRequester::new()
328            })
329    }
330}