aboutsummaryrefslogtreecommitdiff
path: root/src/api/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/api/mod.rs')
-rw-r--r--src/api/mod.rs205
1 files changed, 205 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)?)
+ }
+}