Skip to main content

ygo_core/
http.rs

1use crate::card::Card;
2use crate::error::{Error, Result};
3use crate::query::CardQuery;
4use http::StatusCode;
5use http::header::CONTENT_TYPE;
6use reqwest::Client;
7use serde::de::{self, MapAccess, Visitor};
8use serde::{Deserialize, Deserializer};
9use std::borrow::Cow;
10use std::error::Error as _;
11use std::fmt;
12use std::sync::LazyLock;
13
14static HTTP: LazyLock<Client> = LazyLock::new(|| {
15  let user_agent = format!("ygo-rs/{}", env!("CARGO_PKG_VERSION"));
16  Client::builder()
17    .use_rustls_tls()
18    .https_only(true)
19    .user_agent(user_agent)
20    .build()
21    .expect("failed to create http client")
22});
23
24pub enum Response {
25  Data { data: Vec<Card> },
26  Error { error: String },
27}
28
29impl<'de> Deserialize<'de> for Response {
30  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
31  where
32    D: Deserializer<'de>,
33  {
34    deserializer.deserialize_map(VisitResponse)
35  }
36}
37
38struct VisitResponse;
39
40impl<'de> Visitor<'de> for VisitResponse {
41  type Value = Response;
42
43  fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
44    formatter.write_str("enum Response")
45  }
46
47  fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
48  where
49    V: MapAccess<'de>,
50  {
51    let mut data = None;
52    let mut error = None;
53
54    while let Some(key) = map.next_key::<Cow<'static, str>>()? {
55      match key.as_ref() {
56        "data" => data = Some(map.next_value()?),
57        "error" => error = Some(map.next_value()?),
58        _ => return Err(de::Error::unknown_field(&key, &["data", "error"])),
59      }
60    }
61
62    match (data, error) {
63      (Some(data), None) => Ok(Response::Data { data }),
64      (None, Some(error)) => Ok(Response::Error { error }),
65      _ => Err(de::Error::custom(
66        "expected exactly one of `data` or `error`",
67      )),
68    }
69  }
70}
71
72pub(crate) async fn send(query: CardQuery) -> Result<Vec<Card>> {
73  let (status, raw) = HTTP
74    .get(query.into_url())
75    .send()
76    .await
77    .map(|raw| (raw.status(), raw))?;
78
79  if status.is_success()
80    || (matches!(status, StatusCode::BAD_REQUEST | StatusCode::NOT_FOUND)
81      && raw
82        .headers()
83        .get(CONTENT_TYPE)
84        .and_then(|it| it.to_str().ok())
85        .map(str::to_ascii_lowercase)
86        .is_some_and(|it| it.contains("application/json")))
87  {
88    match raw.json::<Response>().await {
89      Ok(Response::Data { data }) => Ok(data),
90      Ok(Response::Error { error }) => Err(Error::BadRequest(error)),
91      Err(err) => {
92        let reason = match err.source() {
93          Some(source) => source.to_string(),
94          None => err.to_string(),
95        };
96
97        Err(Error::RequestFailed { status: Some(status), reason })
98      }
99    }
100  } else {
101    Err(Error::RequestFailed {
102      status: Some(status),
103      reason: raw.text().await?,
104    })
105  }
106}