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
use std::future::Future;
use std::sync::Arc;

use reqwest::{Client, Method, RequestBuilder};
#[cfg(feature = "tracing")]
use tracing as log;

use crate::req::RegionalRequester;
use crate::util::InsertOnlyCHashMap;
use crate::{ResponseInfo, Result, RiotApiConfig, RiotApiError};

/// For retrieving data from the Riot Games API.
///
/// # Usage
///
/// Construct an instance using [`RiotApi::new(api_key or config)`](RiotApi::new).
/// The parameter may be a Riot API key string or a [`RiotApiConfig`]. Riot API
/// keys are obtained from the [Riot Developer Portal](https://developer.riotgames.com/)
/// and look like `"RGAPI-01234567-89ab-cdef-0123-456789abcdef"`.
///
/// An instance provides access to "endpoint handles" which in turn provide
/// access to individual API method calls. For example, to get a summoner by
/// name we first access the [`summoner_v4()`](RiotApi::summoner_v4) endpoints
/// then call the [`get_by_summoner_name()`](crate::endpoints::SummonerV4::get_by_summoner_name)
/// method:
/// ```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 can change at any time.
/// Riven keeps track of changing rate limits seamlessly, preventing you from
/// getting blacklisted.
///
/// Riven's rate limiting is highly efficient; it can use the full throughput
/// of your rate limit without triggering 429 errors.
///
/// To adjust rate limiting, see [RiotApiConfig] and use
/// [`RiotApi::new(config)`](RiotApi::new) 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 an API key (e.g. `"RGAPI-01234567-89ab-cdef-0123-456789abcdef"`) or a [RiotApiConfig].
    pub fn new(config: impl Into<RiotApiConfig>) -> Self {
        let mut config = config.into();
        let client_builder = config
            .client_builder
            .take()
            .expect("CLIENT_BUILDER IN CONFIG SHOULD NOT BE NONE.");
        Self {
            config,
            client: client_builder
                .build()
                .expect("Failed to create client from builder."),
            regional_requesters: InsertOnlyCHashMap::new(),
        }
    }

    /// 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 an `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()
            })
    }
}