use serde::{Deserialize, Serialize};
pub const DEFAULT_BASE_URL: &str = "https://api.browsr.dev";
pub const ENV_BASE_URL: &str = "BROWSR_BASE_URL";
pub const ENV_API_KEY: &str = "BROWSR_API_KEY";
pub const ENV_BEARER_TOKEN: &str = "BROWSR_BEARER_TOKEN";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrowsrClientConfig {
pub base_url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub api_key: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bearer_token: Option<String>,
#[serde(default = "default_timeout")]
pub timeout_secs: u64,
#[serde(default = "default_retries")]
pub retry_attempts: u32,
#[serde(default)]
pub headless: Option<bool>,
}
fn default_timeout() -> u64 {
60
}
fn default_retries() -> u32 {
3
}
impl Default for BrowsrClientConfig {
fn default() -> Self {
Self {
base_url: DEFAULT_BASE_URL.to_string(),
api_key: None,
bearer_token: None,
timeout_secs: default_timeout(),
retry_attempts: default_retries(),
headless: None,
}
}
}
impl BrowsrClientConfig {
pub fn new(base_url: impl Into<String>) -> Self {
Self {
base_url: base_url.into().trim_end_matches('/').to_string(),
..Default::default()
}
}
pub fn from_env() -> Self {
let base_url = std::env::var(ENV_BASE_URL)
.ok()
.unwrap_or_else(|| DEFAULT_BASE_URL.to_string())
.trim_end_matches('/')
.to_string();
let api_key = std::env::var(ENV_API_KEY).ok().filter(|s| !s.is_empty());
let bearer_token = std::env::var(ENV_BEARER_TOKEN)
.ok()
.filter(|s| !s.is_empty());
Self {
base_url,
api_key,
bearer_token,
..Default::default()
}
}
pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
self.api_key = Some(api_key.into());
self
}
pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
self.bearer_token = Some(bearer_token.into());
self
}
pub fn with_timeout(mut self, timeout_secs: u64) -> Self {
self.timeout_secs = timeout_secs;
self
}
pub fn with_retries(mut self, retry_attempts: u32) -> Self {
self.retry_attempts = retry_attempts;
self
}
pub fn with_headless(mut self, headless: bool) -> Self {
self.headless = Some(headless);
self
}
pub fn is_local(&self) -> bool {
self.base_url.contains("localhost") || self.base_url.contains("127.0.0.1")
}
pub fn has_auth(&self) -> bool {
self.api_key.is_some() || self.bearer_token.is_some()
}
pub fn build_http_client(&self) -> Result<reqwest::Client, reqwest::Error> {
let mut builder =
reqwest::Client::builder().timeout(std::time::Duration::from_secs(self.timeout_secs));
let mut headers = reqwest::header::HeaderMap::new();
if let Some(ref api_key) = self.api_key {
headers.insert(
"x-api-key",
reqwest::header::HeaderValue::from_str(api_key).expect("Invalid API key format"),
);
}
if let Some(ref bearer_token) = self.bearer_token {
let value = format!("Bearer {}", bearer_token);
headers.insert(
reqwest::header::AUTHORIZATION,
reqwest::header::HeaderValue::from_str(&value)
.expect("Invalid bearer token format"),
);
}
if !headers.is_empty() {
builder = builder.default_headers(headers);
}
builder.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = BrowsrClientConfig::default();
assert_eq!(config.base_url, DEFAULT_BASE_URL);
assert!(config.api_key.is_none());
assert!(config.bearer_token.is_none());
assert!(!config.is_local());
}
#[test]
fn test_local_config() {
let config = BrowsrClientConfig::new("http://localhost:8082");
assert!(config.is_local());
assert!(!config.has_auth());
}
#[test]
fn test_with_api_key() {
let config = BrowsrClientConfig::default().with_api_key("test-key");
assert!(config.has_auth());
assert_eq!(config.api_key, Some("test-key".to_string()));
}
#[test]
fn test_with_bearer_token() {
let config = BrowsrClientConfig::default().with_bearer_token("test-token");
assert!(config.has_auth());
assert_eq!(config.bearer_token, Some("test-token".to_string()));
}
#[test]
fn test_trailing_slash_removed() {
let config = BrowsrClientConfig::new("http://localhost:8082/");
assert_eq!(config.base_url, "http://localhost:8082");
}
}