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
use ureq::{Header, Response};
use crate::error::ApiError;
use serde::de::DeserializeOwned;

// Base url to the GW2 API.
pub const BASE_URL: &str = "https://api.guildwars2.com";
// Wait max 10 seconds for a response from the server.
pub const TIMEOUT: u64 = 10_000;

/// All available localisations that are supported by the official Guild Wars 2 API.
#[derive(Debug, PartialEq)]
pub enum Localisation {
    English,
    Spanish,
    German,
    French,
    Chinese,
}

impl ToString for Localisation {
    /// Converts the `Localisation` to a valid localisation suffix `String` for the Guild Wars 2 API.
    fn to_string(&self) -> String {
        match self {
            Localisation::English => "en".to_string(),
            Localisation::Spanish => "es".to_string(),
            Localisation::German => "de".to_string(),
            Localisation::French => "fr".to_string(),
            Localisation::Chinese => "zh".to_string(),
        }
    }
}

/// Client that performs requests to the API
pub struct Client {
    /// The API key used for endpoints that require authentication.
    api_key: Option<String>,
    /// The language that the response will be in. Defaults to English if left empty as per the
    /// official Guild Wars 2 API behvaiour.
    lang: Option<Localisation>,
}

impl Client {
    /// Creates a new `Client` to interface with the Guild Wars 2 API.
    pub fn new() -> Client {
        Client {
            api_key: None,
            lang: None,
        }
    }

    /// Sets the API key of the client with a valid Guild Wars 2 API key.
    pub fn set_api_key(mut self, api_key: String) -> Client {
        self.api_key = Some(api_key);
        self
    }

    /// Sets the language to be used in responses, applies to item names and what not.
        pub fn set_lang(mut self, lang: Localisation) -> Client {
        self.lang = Some(lang);
        self
    }

    /// Creates a language HTTP header from the client's given language, if no language is given it
    /// will default to English.
    fn create_lang_header(&self) -> Header {
        let lang = self.lang().unwrap_or(&Localisation::English).to_string();
        let header = Header::new("Accept-Language", &lang);
        header
    }

    /// Creates a HTTP authorization header from the client's given API key, if no key is set it
    /// will panic.
    fn create_auth_header(&self) -> Header {
        let api_key = self.api_key().expect("Guild Wars 2 API key is not set").to_owned();
        let header = Header::new("Authorization", &format!("Bearer {}", api_key));
        header
    }

    /// Make a request to the Guild Wars 2 API with the given url (which has to include version)
    /// as endpoint.
    pub fn request<T>(&self, url: &str) -> Result<T, ApiError>
    where T: DeserializeOwned {
        let full_url = format!("{base_url}/{url}", base_url=BASE_URL, url=url);
        let lang_header = self.create_lang_header();
        let response = ureq::get(&full_url)
            .set(lang_header.name(), lang_header.value())
            .timeout_connect(TIMEOUT)
            .timeout_read(TIMEOUT)
            .call();
        Client::handle_response(response)
    }

    /// Make an authenticated request to the Guild Wars 2 API with the given url (which has to
    /// include version) as endpoint. This requires that the `api_key` field of the client is set,
    /// otherwise it panics.
    ///
    /// This function may fail depending on what the settings of the API key itself are, since you
    /// can limit what resources a certain key may access. In that case the function will return
    /// an error.
    pub fn authenticated_request<T>(&self, url: &str) -> Result<T, ApiError>
    where T: DeserializeOwned {
        let full_url = format!("{base_url}/{url}", base_url=BASE_URL, url=url);
        let lang_header = self.create_lang_header();
        let auth_header = self.create_auth_header();

        let response = ureq::get(&full_url)
            .set(lang_header.name(), lang_header.value())
            .set(auth_header.name(), auth_header.value())
            .timeout_connect(TIMEOUT)
            .timeout_read(TIMEOUT)
            .call();
        Client::handle_response(response)
    }

    /// Handles the initial response of a request by looking at the status codes or if the request
    /// timed out. Returns the deserialized type or raises an `ApiError` upon a receiving an error,
    /// respectively.
    fn handle_response<T>(response: Response) -> Result<T, ApiError>
    where T: DeserializeOwned {
        if response.ok() {
            return Ok(response.into_json_deserialize::<T>().unwrap());
        } else {
            match response.status() {
                // Forbidden
                403 => Err(ApiError::new(response.into_json().unwrap())),
                // Not Found
                404 => Err(ApiError::new(response.into_json().unwrap())),
                // Timeout
                408 =>
                Err(ApiError::new("Client timed out. Probably due to the official API being down.".to_string())),
                _ => Err(ApiError::new(response.into_json().unwrap())),
            }
        }
    }

    /// Returns an `Option` containing a string slice of the Guild Wars 2 API key for the
    /// Client object if it exists, otherwise None is returned in the Option.
    pub fn api_key(&self) -> Option<&str> {
        match &self.api_key {
            Some(key) => Some(&key),
            None => None,
        }
    }

    /// Returns an `Option` to a reference of the `Localisation` enum object if given,
    /// otherwise None is returned in the Option.
    pub fn lang(&self) -> Option<&Localisation> {
        match &self.lang {
            Some(lang) => Some(&lang),
            None => None,
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::client::*;
    //TODO: Make tests for:
    //  * timeout
    //  * requesting any endpoint
    //  * errors - parsing and otherwise

    #[test]
    fn create_client() {
        let api_key = "ABCDEFGH-1324-5678-9012-IJKLMNOPQRSTUVXYZABC-1234-5678-9012-ABCDEFGHIJKL"
            .to_string();
        let client = Client::new().set_api_key(api_key.clone()).set_lang(Localisation::French);
        assert_eq!(&api_key, client.api_key().unwrap());
        assert_eq!(&Localisation::French, client.lang().unwrap());
    }
}