1pub 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#[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 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 #[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 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 #[inline]
95 pub fn language(mut self, language: Language) -> Self {
96 self.language = language;
97 self
98 }
99}
100
101pub trait ClientExecutor<T>: private::Sealed
105where
106 T: DeserializeOwned,
107{
108 type Result;
110
111 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
128pub type Result<T> = std::result::Result<T, Error>;
130
131#[derive(Debug, Error)]
133#[error(transparent)]
134pub struct Error {
135 kind: ErrorKind,
136}
137
138impl Error {
139 #[inline]
141 pub fn is_http(&self) -> bool {
142 matches!(self.kind, ErrorKind::Http(_))
143 }
144
145 #[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
179pub 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#[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#[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 ($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 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 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 #[doc = concat!("let ids: Vec<", stringify!($id) , "> = ", stringify!($target), "::ids(&client).await?;")]
454 #[doc = concat!("let ids: Vec<", stringify!($id), "> = ", stringify!($target), "::ids(&client)?;")]
468 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;