//! This module is responsible for accessing the Guild Wars 2 API. //! //! It contains `Deserialize`able definitions for the required API responses, as well as a wrapper //! around the HTTP library. Note that only the required fields are modelled here, which means this //! does not provide a full-featured mapping for all API types. pub mod legends; pub mod professions; pub mod skills; pub mod specializations; pub mod traits; pub use self::{ legends::Legend, professions::Profession, skills::Skill, specializations::Specialization, traits::Trait, }; use image::DynamicImage; use itertools::Itertools; use reqwest::{Client, Url}; use serde::{de::DeserializeOwned, Serialize}; use std::path::Path; use super::cache::{Cache, CacheError}; /// The base URL of the official Guild Wars 2 API. const BASE_URL: &str = "https://api.guildwars2.com/v2/"; quick_error! { #[derive(Debug)] pub enum ApiError { SerializationError(err: serde_json::Error) { cause(err) from() } CacheError(err: CacheError) { cause(err) from() } HttpError(err: reqwest::Error) { cause(err) from() } ImageError(err: image::ImageError) { cause(err) from() } } } /// Trait for API objects that have an ID. /// /// This is used by [`Api`](struct.Api.html) to properly retrieve and cache objects. trait HasId { type Id: ToString + Eq + Clone; fn get_id(&self) -> Self::Id; } /// The main API access struct. /// /// This takes care of caching given the provided cache implementation, as well as keeping a HTTP /// connection pool around to re-use. pub struct Api { cache: Box, base_url: Url, client: Client, } /// API access for the GW2 api. impl Api { /// Create a new API instance with the given cache underlying. pub fn new(cache: C) -> Api { Api { cache: Box::new(cache), base_url: Url::parse(BASE_URL).unwrap(), client: Client::new(), } } /// Combines the given endpoint with the `base_url` of this API. fn make_url(&self, endpoint: &str) -> Url { self.base_url.join(endpoint).expect("Invalid API endpoint") } /// Get and deserialize a cached value. fn get_cached(&mut self, name: P) -> Result, ApiError> where T: DeserializeOwned, P: AsRef, { match self.cache.get(name.as_ref())? { Some(data) => Ok(serde_json::from_slice(&data)?), None => Ok(None), } } /// Serialize and store a value in the cache. fn save_cached(&mut self, name: P, value: &T) -> Result<(), ApiError> where T: Serialize, P: AsRef, { self.cache .store(name.as_ref(), &serde_json::to_vec(value)?)?; Ok(()) } /// Retrieves a list of elements by their ID. /// /// This function first checks the cache if elements can be found there, and otherwise hits the /// API. fn get_multiple_cached( &mut self, endpoint: &str, cache_prefix: &str, ids: &[R::Id], ) -> Result, ApiError> where R: HasId + DeserializeOwned + Serialize, { let mut result: Vec = Vec::new(); let mut api_ids: Vec = Vec::new(); for id in ids { let cache_path = format!("{}{}", cache_prefix, id.to_string()); match self.get_cached(cache_path)? { Some(cached) => result.push(cached), None => api_ids.push(id.clone()), } } if api_ids.is_empty() { return Ok(result); } let url = self.make_url(endpoint); let api_arg = api_ids.iter().map(ToString::to_string).join(","); let resp: Vec = self .client .get(url) .query(&[("ids", api_arg)]) .send()? .json()?; for result in &resp { let cache_path = format!("{}{}", cache_prefix, result.get_id().to_string()); self.save_cached(cache_path, result)?; } result.extend(resp.into_iter()); Ok(result) } /// Retrieve a list of all professions using the professions endpoint. pub fn get_profession_ids(&mut self) -> Result, ApiError> { if let Some(cached) = self.get_cached("profession_ids")? { return Ok(cached); } let url = self.make_url("professions"); let resp = self.client.get(url).send()?.json()?; self.save_cached("profession_ids", &resp)?; Ok(resp) } /// Retrieve detailed information about the given professions. /// /// Professions that are found in the cache are taken from there. Therefore, the order of the /// return vector is not guaranteed to be the same as the input order. pub fn get_professions(&mut self, ids: &[String]) -> Result, ApiError> { self.get_multiple_cached("professions", "professions/", ids) } /// Retrieve detailed information about the given skills. /// /// Skills that are found in the cache are taken from there. pub fn get_skills(&mut self, ids: &[u32]) -> Result, ApiError> { self.get_multiple_cached("skills", "skills/", ids) } /// Retrieve detailed information about the given specializations. /// /// Specializations that are found in the cache are taken from there. pub fn get_specializations(&mut self, ids: &[u32]) -> Result, ApiError> { self.get_multiple_cached("specializations", "specializations/", ids) } /// Retrieve detailed information about the given traits. /// /// Traits that are found in the cache are taken from there. pub fn get_traits(&mut self, ids: &[u32]) -> Result, ApiError> { self.get_multiple_cached("traits", "traits/", ids) } /// Retrieve detailed information about the given legends. /// /// Traits that are found in the cache are taken from there. pub fn get_legends(&mut self, ids: &[String]) -> Result, ApiError> { self.get_multiple_cached("legends", "legends/", ids) } /// Loads the image from the given URL. /// /// This automatically caches and also decodes the resulting data. pub fn get_image(&mut self, url: &str) -> Result { let hashed_url = format!("images/{:x}", md5::compute(url.as_bytes())); if let Some(data) = self.cache.get(Path::new(&hashed_url))? { return Ok(image::load_from_memory(&data)?); } let mut img = Vec::new(); self.client.get(url).send()?.copy_to(&mut img)?; self.cache.store(Path::new(&hashed_url), &img)?; Ok(image::load_from_memory(&img)?) } }