gw2api_rs/
lib.rs

1//! A wrapper for the official Guild Wars 2 API
2//!
3//! # Usage
4//!
5//! ```
6//! use gw2api_rs::v2::build::Build;
7//! use gw2api_rs::Client;
8//!
9//! #[tokio::main]
10//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
11//!     let client = Client::new();
12//!     let build = Build::get(&client).await?;
13//!
14//!     println!("{}", build.id);
15//!     Ok(())
16//! }
17//! ```
18
19pub mod v2;
20
21#[cfg(feature = "blocking")]
22pub mod blocking;
23
24use hyper::{client::connect::HttpConnector, header::AUTHORIZATION, Body, Request};
25use hyper_tls::HttpsConnector;
26use serde::de::DeserializeOwned;
27use serde::Deserialize;
28use thiserror::Error;
29
30use std::borrow::Cow;
31use std::fmt::{self, Display, Formatter};
32use std::future::Future;
33use std::marker::PhantomData;
34use std::pin::Pin;
35use std::task::{Context, Poll};
36
37const SCHEMA_VERSION: &str = "2022-03-23T19:00:00.000Z";
38
39/// The Client for making requests.
40#[derive(Clone, Debug)]
41pub struct Client {
42    client: hyper::Client<HttpsConnector<HttpConnector>>,
43    access_token: Option<String>,
44    language: Language,
45}
46
47impl Client {
48    /// Creates a new `Client`.
49    pub fn new() -> Self {
50        let client = hyper::Client::builder().build(HttpsConnector::new());
51
52        Self {
53            client,
54            access_token: None,
55            language: Language::default(),
56        }
57    }
58
59    /// Creates a new [`Builder`] for a client.
60    #[inline]
61    pub fn builder() -> Builder {
62        Builder::default()
63    }
64}
65
66impl Default for Client {
67    #[inline]
68    fn default() -> Self {
69        Self::new()
70    }
71}
72
73#[derive(Clone, Debug, Default)]
74pub struct Builder {
75    access_token: Option<String>,
76    language: Language,
77}
78
79impl Builder {
80    /// Creates a new `Builder`.
81    pub fn new() -> Self {
82        Self::default()
83    }
84
85    pub fn access_token<T>(mut self, access_token: T) -> Self
86    where
87        T: ToString,
88    {
89        self.access_token = Some(access_token.to_string());
90        self
91    }
92
93    /// Sets the prefered [`Language`] for this `Client`.
94    #[inline]
95    pub fn language(mut self, language: Language) -> Self {
96        self.language = language;
97        self
98    }
99}
100
101/// A client used to make requests to the API.
102///
103/// This trait is sealed and cannot be implemented.
104pub trait ClientExecutor<T>: private::Sealed
105where
106    T: DeserializeOwned,
107{
108    /// The return type of this client.
109    type Result;
110
111    /// Sends a requests returning the client's return type.
112    fn send(&self, request: RequestBuilder) -> Self::Result;
113}
114
115pub(crate) mod private {
116    pub trait Sealed {}
117}
118
119impl From<Builder> for Client {
120    fn from(builder: Builder) -> Self {
121        let mut client = Client::new();
122        client.access_token = builder.access_token;
123        client.language = builder.language;
124        client
125    }
126}
127
128/// An alias for `Result<T, Error>`.
129pub type Result<T> = std::result::Result<T, Error>;
130
131/// An error that may occur when making API requests.
132#[derive(Debug, Error)]
133#[error(transparent)]
134pub struct Error {
135    kind: ErrorKind,
136}
137
138impl Error {
139    /// Returns `true` if this error occured while making a HTTP request.
140    #[inline]
141    pub fn is_http(&self) -> bool {
142        matches!(self.kind, ErrorKind::Http(_))
143    }
144
145    /// Returns `true` if this error occured while deserializing json.
146    #[inline]
147    pub fn is_json(&self) -> bool {
148        matches!(self.kind, ErrorKind::Json(_))
149    }
150}
151
152impl Error {
153    fn from<T>(err: T) -> Self
154    where
155        T: Into<ErrorKind>,
156    {
157        Self { kind: err.into() }
158    }
159}
160
161#[derive(Debug, Error)]
162enum ErrorKind {
163    #[error(transparent)]
164    Api(#[from] ApiError),
165    #[error(transparent)]
166    Http(#[from] hyper::Error),
167    #[error(transparent)]
168    Json(#[from] serde_json::Error),
169    #[error("no access token")]
170    NoAccessToken,
171}
172
173#[derive(Clone, Debug, Error, Deserialize)]
174#[error("api error: {text}")]
175struct ApiError {
176    text: String,
177}
178
179/// A builder for creating endpoint requests.
180pub struct RequestBuilder {
181    uri: Cow<'static, str>,
182    authentication: Authentication,
183    localized: bool,
184}
185
186impl RequestBuilder {
187    pub(crate) fn new<T>(uri: T) -> Self
188    where
189        T: Into<Cow<'static, str>>,
190    {
191        Self {
192            uri: uri.into(),
193            authentication: Authentication::None,
194            localized: false,
195        }
196    }
197
198    pub(crate) fn authenticated(mut self, v: Authentication) -> Self {
199        self.authentication = v;
200        self
201    }
202
203    pub(crate) fn localized(mut self, v: bool) -> Self {
204        self.localized = v;
205        self
206    }
207}
208
209#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
210pub(crate) enum Authentication {
211    None,
212    Required,
213}
214
215impl Authentication {
216    #[inline]
217    pub fn is_none(&self) -> bool {
218        matches!(self, Self::None)
219    }
220}
221
222/// All possible api languages. The default language is `En`.
223#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
224pub enum Language {
225    En,
226    Es,
227    De,
228    Fr,
229    Zh,
230}
231
232impl Default for Language {
233    #[inline]
234    fn default() -> Self {
235        Self::En
236    }
237}
238
239impl Display for Language {
240    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
241        let string = match self {
242            Self::En => "en",
243            Self::Es => "es",
244            Self::De => "de",
245            Self::Fr => "fr",
246            Self::Zh => "zh",
247        };
248
249        write!(f, "{}", string)
250    }
251}
252
253/// A wrapper around a future returned by the async client.
254#[must_use = "futures do nothing unless polled"]
255pub struct ResponseFuture<T>
256where
257    T: DeserializeOwned,
258{
259    state: State<T>,
260    _marker: PhantomData<T>,
261    is_error: bool,
262}
263
264impl<T> ResponseFuture<T>
265where
266    T: DeserializeOwned,
267{
268    fn new(fut: hyper::client::ResponseFuture) -> Self {
269        Self {
270            state: State::Response(fut),
271            _marker: PhantomData,
272            is_error: false,
273        }
274    }
275
276    fn result(res: Result<T>) -> Self {
277        Self {
278            state: State::Result(Some(res)),
279            _marker: PhantomData,
280            is_error: false,
281        }
282    }
283}
284
285enum State<T>
286where
287    T: DeserializeOwned,
288{
289    Response(hyper::client::ResponseFuture),
290    Body(Pin<Box<dyn Future<Output = hyper::Result<hyper::body::Bytes>> + Send + Sync + 'static>>),
291    Result(Option<Result<T>>),
292}
293
294impl<T> Future for ResponseFuture<T>
295where
296    T: DeserializeOwned,
297{
298    type Output = Result<T>;
299
300    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
301        match &mut self.state {
302            State::Response(_) => {
303                let fut = unsafe {
304                    self.as_mut()
305                        .map_unchecked_mut(|this| match &mut this.state {
306                            State::Response(resp) => resp,
307                            _ => unreachable!(),
308                        })
309                };
310
311                match fut.poll(cx) {
312                    Poll::Pending => Poll::Pending,
313                    Poll::Ready(Err(err)) => Poll::Ready(Err(Error::from(err))),
314                    Poll::Ready(Ok(resp)) => {
315                        if !resp.status().is_success() {
316                            self.is_error = true;
317                        }
318                        let is_error = self.is_error;
319
320                        self.state =
321                            State::Body(Box::pin(async move { hyper::body::to_bytes(resp).await }));
322
323                        let fut = unsafe {
324                            self.map_unchecked_mut(|this| match &mut this.state {
325                                State::Body(body) => body,
326                                _ => unreachable!(),
327                            })
328                        };
329
330                        match fut.poll(cx) {
331                            Poll::Pending => Poll::Pending,
332                            Poll::Ready(Err(err)) => Poll::Ready(Err(Error::from(err))),
333                            Poll::Ready(Ok(buf)) => {
334                                if is_error {
335                                    return match serde_json::from_slice::<ApiError>(&buf) {
336                                        Ok(st) => Poll::Ready(Err(Error::from(st))),
337                                        Err(err) => Poll::Ready(Err(Error::from(err))),
338                                    };
339                                }
340
341                                match serde_json::from_slice(&buf) {
342                                    Ok(st) => Poll::Ready(Ok(st)),
343                                    Err(err) => Poll::Ready(Err(Error::from(err))),
344                                }
345                            }
346                        }
347                    }
348                }
349            }
350            State::Body(fut) => {
351                let fut = fut.as_mut();
352                match fut.poll(cx) {
353                    Poll::Pending => Poll::Pending,
354                    Poll::Ready(Err(err)) => Poll::Ready(Err(Error::from(err))),
355                    Poll::Ready(Ok(buf)) => {
356                        if self.is_error {
357                            return match serde_json::from_slice::<ApiError>(&buf) {
358                                Ok(st) => Poll::Ready(Err(Error::from(st))),
359                                Err(err) => Poll::Ready(Err(Error::from(err))),
360                            };
361                        }
362
363                        match serde_json::from_slice(&buf) {
364                            Ok(st) => Poll::Ready(Ok(st)),
365                            Err(err) => Poll::Ready(Err(Error::from(err))),
366                        }
367                    }
368                }
369            }
370            State::Result(res) => Poll::Ready(res.take().unwrap()),
371        }
372    }
373}
374
375impl<T> Unpin for ResponseFuture<T> where T: DeserializeOwned {}
376
377impl<T> ClientExecutor<T> for Client
378where
379    T: DeserializeOwned,
380{
381    type Result = ResponseFuture<T>;
382
383    fn send(&self, builder: RequestBuilder) -> Self::Result {
384        let mut req = Request::builder().uri(format!("https://api.guildwars2.com{}", builder.uri));
385        req = req.header("X-Schema-Version", SCHEMA_VERSION);
386
387        if !builder.authentication.is_none() {
388            let access_token = match &self.access_token {
389                Some(access_token) => access_token,
390                None => return ResponseFuture::result(Err(Error::from(ErrorKind::NoAccessToken))),
391            };
392
393            req = req.header(AUTHORIZATION, format!("Bearer {}", access_token));
394        }
395        let req = req.body(Body::empty()).unwrap();
396
397        let fut = self.client.request(req);
398        ResponseFuture::new(fut)
399    }
400}
401
402#[doc(hidden)]
403impl private::Sealed for Client {}
404
405macro_rules! endpoint {
406    // Basic endpoint (single path, no ids)
407    ($target:ty, $path:expr ) => {
408        impl $target {
409            pub fn get<C>(client: &C) -> C::Result
410            where
411                C: crate::ClientExecutor<Self>,
412            {
413                let builder = crate::RequestBuilder::new($path);
414                client.send(builder)
415            }
416        }
417    };
418    ($target:ty, $path:expr, $id:ty $(,$get_all:tt)?) => {
419        impl $target {
420            /// Returns the item with the given `id`.
421            pub fn get<C>(client: &C, id: $id) -> C::Result
422            where
423                C: crate::ClientExecutor<Self>,
424            {
425                let uri = format!("{}?id={}", $path, id);
426                client.send(crate::RequestBuilder::new(uri))
427            }
428
429            $(
430
431            /// Returns all items.
432            pub fn get_all<C>(client: &C) -> C::Result
433            where
434                C: crate::ClientExecutor<Vec<Self>>,
435            {
436                stringify!($get_all);
437
438                let uri = format!("{}?ids=all", $path);
439                client.send(crate::RequestBuilder::new(uri))
440            }
441
442            )?
443
444            /// Returns a list of all item ids.
445            ///
446            /// # Examples
447            ///
448            /// ```ignore
449            /// # use gw2api_rs::{Client, Result};
450            /// #
451            /// # async fn async_main() -> Result<()> {
452            /// let client = Client::new();
453            #[doc = concat!("let ids: Vec<", stringify!($id) , "> = ", stringify!($target), "::ids(&client).await?;")]
454            /// println!("{:?}", ids);
455            /// #
456            /// # Ok(())
457            /// # }
458            /// ```
459            ///
460            /// Using the [`blocking`] client:
461            /// ```ignore
462            /// # use gw2api_rs::Result;
463            /// # use gw2api_rs::blocking::Client;
464            /// #
465            /// # fn main() -> Result<()> {
466            /// let client = Client::new();
467            #[doc = concat!("let ids: Vec<", stringify!($id), "> = ", stringify!($target), "::ids(&client)?;")]
468            /// println!("{:?}", ids);
469            /// #
470            /// # Ok(())
471            /// # }
472            /// ```
473            ///
474            /// [`blocking`]: crate::blocking
475            pub fn ids<C>(client: &C) -> C::Result
476            where
477                C: crate::ClientExecutor<Vec<$id>>,
478            {
479                client.send(crate::RequestBuilder::new($path))
480            }
481        }
482    };
483}
484
485pub(crate) use endpoint;