Skip to main content

unirate_api/
client.rs

1//! HTTP client and builder.
2
3use std::time::Duration;
4
5use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, USER_AGENT};
6
7use crate::VERSION;
8
9/// Default request timeout (30 seconds) — matches the other UniRate clients.
10pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
11
12/// Async client for the UniRate API.
13///
14/// Construct via [`Client::new`] for the common case, or [`Client::builder`]
15/// when you need to customize the timeout, base URL, or underlying
16/// [`reqwest::Client`] (the customization path used by the mock test suite).
17#[derive(Debug, Clone)]
18pub struct Client {
19    pub(crate) api_key: String,
20    pub(crate) base_url: String,
21    pub(crate) http: reqwest::Client,
22}
23
24impl Client {
25    /// Create a new client with default settings.
26    ///
27    /// Points at `https://api.unirateapi.com` with a 30-second timeout.
28    pub fn new(api_key: impl Into<String>) -> Self {
29        ClientBuilder::new(api_key)
30            .build()
31            .expect("default reqwest::Client should always build")
32    }
33
34    /// Begin a fluent [`ClientBuilder`].
35    pub fn builder(api_key: impl Into<String>) -> ClientBuilder {
36        ClientBuilder::new(api_key)
37    }
38
39    /// Borrow the API key currently in use.
40    pub fn api_key(&self) -> &str {
41        &self.api_key
42    }
43
44    /// Borrow the base URL currently in use.
45    pub fn base_url(&self) -> &str {
46        &self.base_url
47    }
48}
49
50/// Builder for [`Client`] — set a custom timeout, base URL, or HTTP client.
51#[derive(Debug)]
52pub struct ClientBuilder {
53    api_key: String,
54    base_url: String,
55    timeout: Duration,
56    http: Option<reqwest::Client>,
57}
58
59impl ClientBuilder {
60    /// Start a new builder with the given API key.
61    pub fn new(api_key: impl Into<String>) -> Self {
62        Self {
63            api_key: api_key.into(),
64            base_url: crate::DEFAULT_BASE_URL.to_string(),
65            timeout: DEFAULT_TIMEOUT,
66            http: None,
67        }
68    }
69
70    /// Override the base URL (useful for tests pointing at a `wiremock::MockServer`).
71    pub fn base_url(mut self, url: impl Into<String>) -> Self {
72        self.base_url = url.into();
73        self
74    }
75
76    /// Override the request timeout. Ignored when a fully-configured
77    /// [`reqwest::Client`] is supplied via [`ClientBuilder::http_client`].
78    pub fn timeout(mut self, timeout: Duration) -> Self {
79        self.timeout = timeout;
80        self
81    }
82
83    /// Supply a pre-configured [`reqwest::Client`]. When set, [`ClientBuilder::timeout`]
84    /// is ignored — configure the timeout on the supplied client instead.
85    pub fn http_client(mut self, http: reqwest::Client) -> Self {
86        self.http = Some(http);
87        self
88    }
89
90    /// Finalize the builder and produce a [`Client`].
91    pub fn build(self) -> Result<Client, reqwest::Error> {
92        let http = match self.http {
93            Some(h) => h,
94            None => {
95                let mut headers = HeaderMap::new();
96                headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
97                headers.insert(
98                    USER_AGENT,
99                    HeaderValue::from_str(&format!("unirate-rust/{VERSION}"))
100                        .expect("user agent must be ASCII"),
101                );
102                reqwest::Client::builder()
103                    .timeout(self.timeout)
104                    .default_headers(headers)
105                    .build()?
106            }
107        };
108
109        // Normalize base_url — strip trailing slash so path joins remain stable.
110        let base_url = self.base_url.trim_end_matches('/').to_string();
111
112        Ok(Client {
113            api_key: self.api_key,
114            base_url,
115            http,
116        })
117    }
118}