Skip to main content

gw2api/
client.rs

1use ureq::{Header, Response};
2use crate::error::ApiError;
3use serde::de::DeserializeOwned;
4
5// Base url to the GW2 API.
6pub const BASE_URL: &str = "https://api.guildwars2.com";
7// Wait max 10 seconds for a response from the server.
8pub const TIMEOUT: u64 = 10_000;
9
10/// All available localisations that are supported by the official Guild Wars 2 API.
11#[derive(Debug, PartialEq)]
12pub enum Localisation {
13    English,
14    Spanish,
15    German,
16    French,
17    Chinese,
18}
19
20impl ToString for Localisation {
21    /// Converts the `Localisation` to a valid localisation suffix `String` for the Guild Wars 2 API.
22    fn to_string(&self) -> String {
23        match self {
24            Localisation::English => "en".to_string(),
25            Localisation::Spanish => "es".to_string(),
26            Localisation::German => "de".to_string(),
27            Localisation::French => "fr".to_string(),
28            Localisation::Chinese => "zh".to_string(),
29        }
30    }
31}
32
33/// Client that performs requests to the API
34pub struct Client {
35    /// The API key used for endpoints that require authentication.
36    api_key: Option<String>,
37    /// The language that the response will be in. Defaults to English if left empty as per the
38    /// official Guild Wars 2 API behvaiour.
39    lang: Option<Localisation>,
40}
41
42impl Client {
43    /// Creates a new `Client` to interface with the Guild Wars 2 API.
44    pub fn new() -> Client {
45        Client {
46            api_key: None,
47            lang: None,
48        }
49    }
50
51    /// Sets the API key of the client with a valid Guild Wars 2 API key.
52    pub fn set_api_key(mut self, api_key: String) -> Client {
53        self.api_key = Some(api_key);
54        self
55    }
56
57    /// Sets the language to be used in responses, applies to item names and what not.
58        pub fn set_lang(mut self, lang: Localisation) -> Client {
59        self.lang = Some(lang);
60        self
61    }
62
63    /// Creates a language HTTP header from the client's given language, if no language is given it
64    /// will default to English.
65    fn create_lang_header(&self) -> Header {
66        let lang = self.lang().unwrap_or(&Localisation::English).to_string();
67        let header = Header::new("Accept-Language", &lang);
68        header
69    }
70
71    /// Creates a HTTP authorization header from the client's given API key, if no key is set it
72    /// will panic.
73    fn create_auth_header(&self) -> Header {
74        let api_key = self.api_key().expect("Guild Wars 2 API key is not set").to_owned();
75        let header = Header::new("Authorization", &format!("Bearer {}", api_key));
76        header
77    }
78
79    /// Make a request to the Guild Wars 2 API with the given url (which has to include version)
80    /// as endpoint.
81    pub fn request<T>(&self, url: &str) -> Result<T, ApiError>
82    where T: DeserializeOwned {
83        let full_url = format!("{base_url}/{url}", base_url=BASE_URL, url=url);
84        let lang_header = self.create_lang_header();
85        let response = ureq::get(&full_url)
86            .set(lang_header.name(), lang_header.value())
87            .timeout_connect(TIMEOUT)
88            .timeout_read(TIMEOUT)
89            .call();
90        Client::handle_response(response)
91    }
92
93    /// Make an authenticated request to the Guild Wars 2 API with the given url (which has to
94    /// include version) as endpoint. This requires that the `api_key` field of the client is set,
95    /// otherwise it panics.
96    ///
97    /// This function may fail depending on what the settings of the API key itself are, since you
98    /// can limit what resources a certain key may access. In that case the function will return
99    /// an error.
100    pub fn authenticated_request<T>(&self, url: &str) -> Result<T, ApiError>
101    where T: DeserializeOwned {
102        let full_url = format!("{base_url}/{url}", base_url=BASE_URL, url=url);
103        let lang_header = self.create_lang_header();
104        let auth_header = self.create_auth_header();
105
106        let response = ureq::get(&full_url)
107            .set(lang_header.name(), lang_header.value())
108            .set(auth_header.name(), auth_header.value())
109            .timeout_connect(TIMEOUT)
110            .timeout_read(TIMEOUT)
111            .call();
112        Client::handle_response(response)
113    }
114
115    /// Handles the initial response of a request by looking at the status codes or if the request
116    /// timed out. Returns the deserialized type or raises an `ApiError` upon a receiving an error,
117    /// respectively.
118    fn handle_response<T>(response: Response) -> Result<T, ApiError>
119    where T: DeserializeOwned {
120        if response.ok() {
121            return Ok(response.into_json_deserialize::<T>().unwrap());
122        } else {
123            match response.status() {
124                // Forbidden
125                403 => Err(ApiError::new(response.into_json().unwrap())),
126                // Not Found
127                404 => Err(ApiError::new(response.into_json().unwrap())),
128                // Timeout
129                408 =>
130                Err(ApiError::new("Client timed out. Probably due to the official API being down.".to_string())),
131                _ => Err(ApiError::new(response.into_json().unwrap())),
132            }
133        }
134    }
135
136    /// Returns an `Option` containing a string slice of the Guild Wars 2 API key for the
137    /// Client object if it exists, otherwise None is returned in the Option.
138    pub fn api_key(&self) -> Option<&str> {
139        match &self.api_key {
140            Some(key) => Some(&key),
141            None => None,
142        }
143    }
144
145    /// Returns an `Option` to a reference of the `Localisation` enum object if given,
146    /// otherwise None is returned in the Option.
147    pub fn lang(&self) -> Option<&Localisation> {
148        match &self.lang {
149            Some(lang) => Some(&lang),
150            None => None,
151        }
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use crate::client::*;
158    //TODO: Make tests for:
159    //  * timeout
160    //  * requesting any endpoint
161    //  * errors - parsing and otherwise
162
163    #[test]
164    fn create_client() {
165        let api_key = "ABCDEFGH-1324-5678-9012-IJKLMNOPQRSTUVXYZABC-1234-5678-9012-ABCDEFGHIJKL"
166            .to_string();
167        let client = Client::new().set_api_key(api_key.clone()).set_lang(Localisation::French);
168        assert_eq!(&api_key, client.api_key().unwrap());
169        assert_eq!(&Localisation::French, client.lang().unwrap());
170    }
171}