diff options
Diffstat (limited to 'src/api')
-rw-r--r-- | src/api/mod.rs | 205 | ||||
-rw-r--r-- | src/api/professions.rs | 36 | ||||
-rw-r--r-- | src/api/skills.rs | 24 | ||||
-rw-r--r-- | src/api/specializations.rs | 34 | ||||
-rw-r--r-- | src/api/traits.rs | 40 |
5 files changed, 339 insertions, 0 deletions
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<dyn Cache>, + 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<C: 'static + Cache>(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<T, P>(&mut self, name: P) -> Result<Option<T>, ApiError> + where + T: DeserializeOwned, + P: AsRef<Path>, + { + 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<T, P>(&mut self, name: P, value: &T) -> Result<(), ApiError> + where + T: Serialize, + P: AsRef<Path>, + { + 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<R>( + &mut self, + endpoint: &str, + cache_prefix: &str, + ids: &[R::Id], + ) -> Result<Vec<R>, ApiError> + where + R: HasId + DeserializeOwned + Serialize, + { + let mut result: Vec<R> = Vec::new(); + let mut api_ids: Vec<R::Id> = 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<R> = 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<Vec<String>, 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<Vec<Profession>, 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<Vec<Skill>, 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<Vec<Specialization>, 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<Vec<Trait>, 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<DynamicImage, ApiError> { + 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)?) + } +} diff --git a/src/api/professions.rs b/src/api/professions.rs new file mode 100644 index 0000000..2716a1a --- /dev/null +++ b/src/api/professions.rs @@ -0,0 +1,36 @@ +//! Struct definitions for the professions API endpoint. +//! +//! * [Example](https://api.guildwars2.com/v2/professions/Engineer) +//! * [Wiki](https://wiki.guildwars2.com/wiki/API:2/professions) + +use super::HasId; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct Profession { + /// The profession id. + pub id: String, + /// The name of the profession. + pub name: String, + /// List of specialization ids. + pub specializations: Vec<u32>, + /// List of skills. + pub skills: Vec<Skill>, +} + +impl HasId for Profession { + type Id = String; + fn get_id(&self) -> String { + self.id.clone() + } +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct Skill { + /// The id of the skill. + pub id: u32, + /// The skill bar slot that this skill can be used in. + pub slot: String, + #[serde(rename = "type")] + pub typ: String, +} diff --git a/src/api/skills.rs b/src/api/skills.rs new file mode 100644 index 0000000..9c692e1 --- /dev/null +++ b/src/api/skills.rs @@ -0,0 +1,24 @@ +//! Struct definitions for the skills API endpoint. +//! +//! * [Example](https://api.guildwars2.com/v2/skills/14375) +//! * [Wiki](https://wiki.guildwars2.com/wiki/API:2/skills) + +use super::HasId; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct Skill { + /// The skill id. + pub id: u32, + /// The skill name. + pub name: String, + /// A URL to an icon of the skill. + pub icon: String, +} + +impl HasId for Skill { + type Id = u32; + fn get_id(&self) -> u32 { + self.id + } +} diff --git a/src/api/specializations.rs b/src/api/specializations.rs new file mode 100644 index 0000000..97fc289 --- /dev/null +++ b/src/api/specializations.rs @@ -0,0 +1,34 @@ +//! Struct definitions for the specializations API endpoint. +//! +//! * [Example](https://api.guildwars2.com/v2/specializations/1) +//! * [Wiki](https://wiki.guildwars2.com/wiki/API:2/specializations) + +use super::HasId; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct Specialization { + /// The specialization's ID. + pub id: u32, + /// The name of the specialization. + pub name: String, + /// The profession that this specialization belongs to. + pub profession: String, + /// `true` if this specialization is an elite specialization, `false` otherwise. + pub elite: bool, + /// A URL to an icon of the specialization. + pub icon: String, + /// An URL to the background image of the specialization. + pub background: String, + /// Contains a list of IDs specifying the minor traits in the specialization. + pub minor_traits: Vec<u32>, + /// Contains a list of IDs specifying the major traits in the specialization. + pub major_traits: Vec<u32>, +} + +impl HasId for Specialization { + type Id = u32; + fn get_id(&self) -> u32 { + self.id + } +} diff --git a/src/api/traits.rs b/src/api/traits.rs new file mode 100644 index 0000000..194d061 --- /dev/null +++ b/src/api/traits.rs @@ -0,0 +1,40 @@ +//! Struct definitions for the traits API endpoint. +//! +//! * [Example](https://api.guildwars2.com/v2/traits/214) +//! * [Wiki](https://wiki.guildwars2.com/wiki/API:2/traits) + +use super::HasId; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct Trait { + /// The trait id. + pub id: u32, + /// The trait name. + pub name: String, + /// The trait's icon URL. + pub icon: String, + /// The trait description. + pub description: String, + /// The id of the specialization this trait belongs to. + pub specialization: u32, + /// The trait's tier, as a value from 1-3. + /// + /// Elite specializations also contain a tier 0 minor trait, describing which weapon the elite + /// specialization gains access to. + pub tier: u32, + pub slot: Slot, +} + +impl HasId for Trait { + type Id = u32; + fn get_id(&self) -> u32 { + self.id + } +} + +#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Hash, Copy, Clone)] +pub enum Slot { + Major, + Minor, +} |