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

use log;
use reqwest::{ Client, StatusCode, Url };
use tokio::timer::delay_for;

use crate::Result;
use crate::RiotApiError;
use crate::RiotApiConfig;
use crate::util::InsertOnlyCHashMap;

use super::RateLimit;
use super::RateLimitType;

pub struct RegionalRequester {
    /// Represents the app rate limit.
    app_rate_limit: RateLimit,
    /// Represents method rate limits.
    method_rate_limits: InsertOnlyCHashMap<&'static str, RateLimit>,
}

impl RegionalRequester {
    /// Request header name for the Riot API key.
    const RIOT_KEY_HEADER: &'static str = "X-Riot-Token";

    /// HttpStatus codes that are considered a success, but will return None.
    const NONE_STATUS_CODES: [u16; 3] = [ 204, 404, 422 ];


    pub fn new() -> Self {
        Self {
            app_rate_limit: RateLimit::new(RateLimitType::Application),
            method_rate_limits: InsertOnlyCHashMap::new(),
        }
    }

    pub fn get<'a, T: serde::de::DeserializeOwned>(self: Arc<Self>,
        config: &'a RiotApiConfig, client: &'a Client,
        method_id: &'static str, region_platform: &'a str, path: String, query: Option<String>)
        -> impl Future<Output = Result<Option<T>>> + 'a
    {
        async move {
            let query = query.as_deref();

            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) {
                    delay_for(delay).await;
                }

                // Send request.
                let url_base = format!("https://{}.api.riotgames.com", region_platform);
                let mut url = Url::parse(&*url_base)
                    .unwrap_or_else(|_| panic!("Failed to parse url_base: \"{}\".", url_base));
                url.set_path(&*path);
                url.set_query(query);

                let response = client.get(url)
                    .header(Self::RIOT_KEY_HEADER, &*config.api_key)
                    .send()
                    .await
                    .map_err(|e| RiotApiError::new(e, retries, None))?;

                // Maybe update rate limits (based on response headers).
                self.app_rate_limit.on_response(&config, &response);
                method_rate_limit.on_response(&config, &response);

                // Handle response.
                let status = response.status();
                // Special "none success" cases, return None.
                if Self::is_none_status_code(&status) {
                    log::trace!("Response {} (retried {} times), None result.", status, retries);
                    break Ok(None);
                }
                // Handle normal success / failure cases.
                match response.error_for_status_ref() {
                    // Success.
                    Ok(_) => {
                        log::trace!("Response {} (retried {} times), parsed result.", status, retries);
                        let value = response.json::<T>().await;
                        break value.map(|v| Some(v))
                            .map_err(|e| RiotApiError::new(e, retries, None));
                    },
                    // Failure, may or may not be retryable.
                    Err(err) => {
                        // 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), returning error.", status, retries);
                            break Err(RiotApiError::new(err, retries, Some(response)));
                        }
                        log::debug!("Response {} (retried {} times), retrying.", status, retries);
                    },
                };

                retries += 1;
            }
        }
    }

    fn is_none_status_code(status: &StatusCode) -> bool {
        Self::NONE_STATUS_CODES.contains(&status.as_u16())
    }
}

#[cfg(test)]
mod tests {
    // use super::*;
}