aboutsummaryrefslogtreecommitdiff
path: root/src/api
diff options
context:
space:
mode:
Diffstat (limited to 'src/api')
-rw-r--r--src/api/mod.rs205
-rw-r--r--src/api/professions.rs36
-rw-r--r--src/api/skills.rs24
-rw-r--r--src/api/specializations.rs34
-rw-r--r--src/api/traits.rs40
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,
+}