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}