From d35534c0795caeda46e57fc515b74eba701110a2 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Fri, 6 Dec 2019 18:00:04 +0100 Subject: initial commit --- src/api/mod.rs | 205 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 src/api/mod.rs (limited to 'src/api/mod.rs') diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..6e1b457 --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,205 @@ +//! 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 professions; +pub mod skills; +pub mod specializations; +pub mod traits; + +pub use self::{ + 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) + } + + /// 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)?) + } +} -- cgit v1.2.3