use std::time::Duration;
use crate::secret::Secret;
pub const DEFAULT_BASE_URL: &str = "https://backend.blooio.com/v2/api";
#[derive(Clone, Debug)]
pub struct ClientConfig {
pub base_url: String,
pub api_key: Secret<String>,
pub timeout: Duration,
pub user_agent: String,
}
impl ClientConfig {
pub fn new(api_key: impl Into<Secret<String>>) -> Self {
Self {
base_url: DEFAULT_BASE_URL.to_owned(),
api_key: api_key.into(),
timeout: Duration::from_secs(30),
user_agent: concat!("blooio-rs/", env!("CARGO_PKG_VERSION")).to_owned(),
}
}
#[must_use]
pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
let mut url = base_url.into();
while url.ends_with('/') {
url.pop();
}
self.base_url = url;
self
}
#[must_use]
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
#[must_use]
pub fn with_user_agent(mut self, user_agent: impl Into<String>) -> Self {
self.user_agent = user_agent.into();
self
}
pub(crate) fn url_for(&self, path: &str) -> String {
format!("{}{}", self.base_url, path)
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::print_stdout,
clippy::unreadable_literal
)]
mod tests {
use super::*;
#[test]
fn new_uses_production_defaults() {
let cfg = ClientConfig::new("k");
assert_eq!(cfg.base_url, DEFAULT_BASE_URL);
assert_eq!(cfg.timeout, Duration::from_secs(30));
assert!(cfg.user_agent.starts_with("blooio-rs/"));
}
#[test]
fn with_base_url_trims_trailing_slashes() {
let one = ClientConfig::new("k").with_base_url("https://example.com/api/");
assert_eq!(one.base_url, "https://example.com/api");
let many = ClientConfig::new("k").with_base_url("https://example.com/api///");
assert_eq!(many.base_url, "https://example.com/api");
let none = ClientConfig::new("k").with_base_url("https://example.com/api");
assert_eq!(none.base_url, "https://example.com/api");
}
#[test]
fn with_timeout_and_user_agent_override() {
let cfg = ClientConfig::new("k")
.with_timeout(Duration::from_millis(500))
.with_user_agent("my-app/1.0");
assert_eq!(cfg.timeout, Duration::from_millis(500));
assert_eq!(cfg.user_agent, "my-app/1.0");
}
#[test]
fn url_for_concatenates_base_and_path() {
let cfg = ClientConfig::new("k").with_base_url("https://example.com/api");
assert_eq!(cfg.url_for("/me"), "https://example.com/api/me");
assert_eq!(
cfg.url_for("/chats/c1/messages"),
"https://example.com/api/chats/c1/messages"
);
}
#[test]
fn debug_redacts_api_key() {
let cfg = ClientConfig::new("super-secret-key");
let dbg = format!("{cfg:?}");
assert!(!dbg.contains("super-secret-key"), "api key leaked in Debug");
assert!(dbg.contains("REDACTED"));
}
}