Skip to main content

usesend_api/
config.rs

1use governor::clock::DefaultClock;
2use governor::middleware::NoOpMiddleware;
3use governor::state::{InMemoryState, NotKeyed};
4use governor::{Jitter, Quota, RateLimiter};
5use reqwest::{Client, RequestBuilder, Response, StatusCode};
6use serde::de::DeserializeOwned;
7use std::num::NonZeroU32;
8use std::sync::Arc;
9use std::time::Duration;
10
11use crate::error::{ApiError, ApiResult};
12use crate::types::ErrorResponse;
13
14const DEFAULT_BASE_URL: &str = "https://app.usesend.com/api";
15const DEFAULT_RATE_LIMIT: u32 = 9;
16
17type DirectRateLimiter = RateLimiter<NotKeyed, InMemoryState, DefaultClock, NoOpMiddleware>;
18
19/// Shared configuration for all API services.
20pub struct Config {
21    pub(crate) client: Client,
22    pub(crate) base_url: String,
23    pub(crate) api_key: String,
24    pub(crate) limiter: Arc<DirectRateLimiter>,
25}
26
27impl std::fmt::Debug for Config {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        f.debug_struct("Config")
30            .field("base_url", &self.base_url)
31            .field("api_key", &"[REDACTED]")
32            .finish()
33    }
34}
35
36impl Config {
37    pub fn new(api_key: impl Into<String>) -> Self {
38        Self {
39            client: Client::new(),
40            base_url: DEFAULT_BASE_URL.to_string(),
41            api_key: api_key.into(),
42            limiter: Arc::new(Self::build_limiter(DEFAULT_RATE_LIMIT)),
43        }
44    }
45
46    pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
47        self.base_url = base_url.into();
48        self
49    }
50
51    pub fn with_client(mut self, client: Client) -> Self {
52        self.client = client;
53        self
54    }
55
56    /// Set the maximum number of requests per second (default: 9).
57    pub fn with_rate_limit(mut self, per_second: u32) -> Self {
58        self.limiter = Arc::new(Self::build_limiter(per_second));
59        self
60    }
61
62    fn build_limiter(per_second: u32) -> DirectRateLimiter {
63        let quota = Quota::with_period(Duration::from_millis(1100))
64            .expect("valid quota")
65            .allow_burst(NonZeroU32::new(per_second).expect("rate limit must be > 0"));
66        RateLimiter::direct(quota)
67    }
68
69    pub(crate) fn url(&self, path: &str) -> String {
70        format!("{}{}", self.base_url, path)
71    }
72
73    pub(crate) fn auth(&self, req: RequestBuilder) -> RequestBuilder {
74        req.bearer_auth(&self.api_key)
75    }
76
77    pub(crate) async fn send_and_parse<T: DeserializeOwned>(
78        &self,
79        req: RequestBuilder,
80    ) -> ApiResult<T> {
81        let jitter = Jitter::new(Duration::from_millis(10), Duration::from_millis(50));
82        self.limiter.until_ready_with_jitter(jitter).await;
83
84        let resp: Response = req.send().await?;
85        handle_response(resp).await
86    }
87}
88
89pub(crate) async fn handle_response<T: DeserializeOwned>(resp: Response) -> ApiResult<T> {
90    let status = resp.status();
91    if status.is_success() {
92        let body = resp.text().await?;
93        serde_json::from_str::<T>(&body).map_err(|source| ApiError::Deserialize {
94            status,
95            body,
96            source,
97        })
98    } else if status == StatusCode::CONFLICT {
99        let text = resp.text().await.unwrap_or_default();
100        Err(ApiError::Conflict { message: text })
101    } else if status == StatusCode::TOO_MANY_REQUESTS {
102        let retry_after = resp
103            .headers()
104            .get("ratelimit-reset")
105            .and_then(|v| v.to_str().ok())
106            .and_then(|v| v.parse::<u64>().ok());
107        Err(ApiError::RateLimit { retry_after })
108    } else {
109        let text = resp.text().await.unwrap_or_default();
110        match serde_json::from_str::<ErrorResponse>(&text) {
111            Ok(body) => Err(ApiError::Api { status, body }),
112            Err(_) => Err(ApiError::Unexpected { status, text }),
113        }
114    }
115}
116
117/// Wraps `Arc<Config>` for sharing across services.
118#[derive(Debug, Clone)]
119pub(crate) struct SharedConfig(pub(crate) Arc<Config>);
120
121impl SharedConfig {
122    pub(crate) fn new(config: Config) -> Self {
123        Self(Arc::new(config))
124    }
125}
126
127impl std::ops::Deref for SharedConfig {
128    type Target = Config;
129    fn deref(&self) -> &Config {
130        &self.0
131    }
132}