Skip to main content

wc_data/
transport.rs

1//! Shared HTTP transport used by all backends.
2//!
3//! A thin wrapper over a single [`reqwest::Client`] (rustls) that performs GET
4//! requests, maps transport and status failures into [`DataError`], and decodes
5//! JSON. Backends add their own auth headers via the `headers` argument.
6
7use std::time::Duration;
8
9use serde::de::DeserializeOwned;
10
11use crate::error::{DataError, Result};
12
13/// Default request timeout.
14const TIMEOUT: Duration = Duration::from_secs(15);
15
16/// User-Agent sent with every request.
17const USER_AGENT: &str = concat!("wc-data/", env!("CARGO_PKG_VERSION"));
18
19/// A cheap-to-clone HTTP client shared across backends.
20#[derive(Debug, Clone)]
21pub struct Http {
22    client: reqwest::Client,
23}
24
25impl Http {
26    /// Build a client with sensible defaults (timeout, User-Agent).
27    ///
28    /// # Errors
29    /// Returns [`DataError::Transport`] if the underlying client cannot be built.
30    pub fn new() -> Result<Self> {
31        let client = reqwest::Client::builder()
32            .timeout(TIMEOUT)
33            .user_agent(USER_AGENT)
34            .build()
35            .map_err(|err| DataError::Transport(err.to_string()))?;
36        Ok(Self { client })
37    }
38
39    /// Wrap an already-configured client (useful in tests).
40    #[must_use]
41    pub fn with_client(client: reqwest::Client) -> Self {
42        Self { client }
43    }
44
45    /// Fetch `url` and deserialize the JSON body into `T`.
46    ///
47    /// `headers` is a list of `(name, value)` pairs added to the request, used
48    /// by backends for API-key auth.
49    ///
50    /// # Errors
51    /// - [`DataError::Transport`] on connection/timeout failures.
52    /// - [`DataError::RateLimited`] on HTTP 429.
53    /// - [`DataError::Status`] on other non-success responses.
54    /// - [`DataError::Decode`] if the body is not valid JSON for `T`.
55    pub async fn get_json<T: DeserializeOwned>(
56        &self,
57        url: &str,
58        headers: &[(&str, &str)],
59    ) -> Result<T> {
60        let bytes = self.get_bytes(url, headers).await?;
61        serde_json::from_slice(&bytes).map_err(|err| DataError::Decode(err.to_string()))
62    }
63
64    /// Fetch `url` and return the raw response body as bytes, applying the same
65    /// status-code handling as [`Self::get_json`].
66    ///
67    /// # Errors
68    /// See [`Self::get_json`].
69    pub async fn get_bytes(&self, url: &str, headers: &[(&str, &str)]) -> Result<Vec<u8>> {
70        let mut req = self.client.get(url);
71        for (name, value) in headers {
72            req = req.header(*name, *value);
73        }
74        let resp = req
75            .send()
76            .await
77            .map_err(|err| DataError::Transport(err.to_string()))?;
78
79        let status = resp.status();
80        if status.as_u16() == 429 {
81            let retry_after = resp
82                .headers()
83                .get(reqwest::header::RETRY_AFTER)
84                .and_then(|v| v.to_str().ok())
85                .and_then(|v| v.parse::<u64>().ok());
86            return Err(DataError::RateLimited { retry_after });
87        }
88        if !status.is_success() {
89            let message = resp
90                .text()
91                .await
92                .ok()
93                .map(|body| body.chars().take(200).collect::<String>())
94                .filter(|s| !s.is_empty())
95                .unwrap_or_else(|| status.canonical_reason().unwrap_or("error").to_owned());
96            return Err(DataError::Status {
97                status: status.as_u16(),
98                message,
99            });
100        }
101
102        resp.bytes()
103            .await
104            .map(|b| b.to_vec())
105            .map_err(|err| DataError::Transport(err.to_string()))
106    }
107}