use std::sync::Arc;
use std::time::Duration;
use crate::client::Api2Convert;
use crate::errors::{Api2ConvertError, Result};
use crate::transport::{
DefaultRng, HttpSender, ReqwestSender, Rng, Sleeper, ThreadSleeper, Transport,
};
pub const DEFAULT_BASE_URL: &str = "https://api.api2convert.com/v2";
pub const API_KEY_ENV: &str = "API2CONVERT_API_KEY";
const MIN_POLL_INTERVAL: Duration = Duration::from_millis(500);
const MAX_POLL_TIMEOUT: Duration = Duration::from_secs(14400);
const MIN_TIMEOUT: Duration = Duration::from_secs(1);
pub struct ClientBuilder {
api_key: Option<String>,
base_url: String,
timeout: Duration,
max_retries: u32,
poll_interval: Duration,
poll_max_interval: Duration,
poll_timeout: Duration,
max_download_bytes: u64,
sender: Option<Arc<dyn HttpSender>>,
sleeper: Option<Arc<dyn Sleeper>>,
rng: Option<Arc<dyn Rng>>,
}
impl Default for ClientBuilder {
fn default() -> Self {
ClientBuilder {
api_key: None,
base_url: DEFAULT_BASE_URL.to_string(),
timeout: Duration::from_secs(30),
max_retries: 2,
poll_interval: Duration::from_secs(1),
poll_max_interval: Duration::from_secs(5),
poll_timeout: Duration::from_secs(300),
max_download_bytes: 0,
sender: None,
sleeper: None,
rng: None,
}
}
}
impl ClientBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn api_key(mut self, key: impl Into<String>) -> Self {
self.api_key = Some(key.into());
self
}
pub fn base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = url.into();
self
}
pub fn timeout(mut self, d: Duration) -> Self {
self.timeout = d;
self
}
pub fn max_retries(mut self, n: u32) -> Self {
self.max_retries = n;
self
}
pub fn poll_interval(mut self, d: Duration) -> Self {
self.poll_interval = d;
self
}
pub fn poll_max_interval(mut self, d: Duration) -> Self {
self.poll_max_interval = d;
self
}
pub fn poll_timeout(mut self, d: Duration) -> Self {
self.poll_timeout = d;
self
}
pub fn max_download_bytes(mut self, n: u64) -> Self {
self.max_download_bytes = n;
self
}
pub fn http_sender(mut self, sender: Arc<dyn HttpSender>) -> Self {
self.sender = Some(sender);
self
}
pub fn sleeper(mut self, sleeper: Arc<dyn Sleeper>) -> Self {
self.sleeper = Some(sleeper);
self
}
pub fn rng(mut self, rng: Arc<dyn Rng>) -> Self {
self.rng = Some(rng);
self
}
pub fn build(self) -> Result<Api2Convert> {
let api_key = self
.api_key
.filter(|k| !k.is_empty())
.or_else(|| std::env::var(API_KEY_ENV).ok().filter(|k| !k.is_empty()))
.ok_or_else(|| {
Api2ConvertError::Config(format!(
"an API key is required: pass one to the builder or set {API_KEY_ENV}"
))
})?;
let timeout = clamp_timeout(self.timeout);
let poll_interval = clamp_poll_interval(self.poll_interval);
let poll_max_interval = self.poll_max_interval.max(poll_interval);
let poll_timeout = clamp_poll_timeout(self.poll_timeout);
let sender: Arc<dyn HttpSender> = match self.sender {
Some(s) => s,
None => Arc::new(ReqwestSender::new(timeout)?),
};
let sleeper: Arc<dyn Sleeper> = self.sleeper.unwrap_or_else(|| Arc::new(ThreadSleeper));
let rng: Arc<dyn Rng> = self.rng.unwrap_or_else(|| Arc::new(DefaultRng::new()));
let transport = Transport::new(
api_key,
self.base_url.trim_end_matches('/').to_string(),
timeout,
self.max_retries,
poll_interval,
poll_max_interval,
poll_timeout,
self.max_download_bytes,
sender,
sleeper,
rng,
);
Ok(Api2Convert::from_transport(Arc::new(transport)))
}
}
fn clamp_timeout(d: Duration) -> Duration {
d.max(MIN_TIMEOUT)
}
fn clamp_poll_interval(d: Duration) -> Duration {
d.max(MIN_POLL_INTERVAL)
}
fn clamp_poll_timeout(d: Duration) -> Duration {
d.min(MAX_POLL_TIMEOUT)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn poll_interval_is_floored() {
assert_eq!(
clamp_poll_interval(Duration::from_millis(1)),
MIN_POLL_INTERVAL
);
assert_eq!(clamp_poll_interval(Duration::ZERO), MIN_POLL_INTERVAL);
assert_eq!(
clamp_poll_interval(Duration::from_secs(3)),
Duration::from_secs(3)
);
}
#[test]
fn poll_timeout_is_capped() {
assert_eq!(
clamp_poll_timeout(Duration::from_secs(100_000)),
MAX_POLL_TIMEOUT
);
assert_eq!(
clamp_poll_timeout(Duration::from_secs(42)),
Duration::from_secs(42)
);
}
#[test]
fn timeout_has_a_floor() {
assert_eq!(clamp_timeout(Duration::ZERO), MIN_TIMEOUT);
assert_eq!(
clamp_timeout(Duration::from_secs(10)),
Duration::from_secs(10)
);
}
#[test]
fn missing_api_key_is_a_config_error() {
std::env::remove_var(API_KEY_ENV);
let result = ClientBuilder::new().build();
assert!(matches!(result, Err(Api2ConvertError::Config(_))));
}
#[test]
fn base_url_trailing_slash_is_trimmed() {
let client = ClientBuilder::new()
.api_key("test-key")
.base_url("https://example.test/v2/")
.build()
.unwrap();
assert_eq!(client.debug_base_url(), "https://example.test/v2");
}
}