browsr_client/
config.rs

1//! Configuration for the Browsr client.
2//!
3//! The client can be configured via:
4//! - Environment variables (`BROWSR_BASE_URL`, `BROWSR_API_KEY`)
5//! - Programmatic configuration via `BrowsrClientConfig`
6//!
7//! # Environment Variables
8//!
9//! - `BROWSR_BASE_URL`: The base URL of the Browsr server (default: `https://api.browsr.dev`)
10//! - `BROWSR_API_KEY`: Optional API key for authentication
11//! - `BROWSR_API_URL`: Legacy alias for `BROWSR_BASE_URL`
12//! - `BROWSR_HOST` / `BROWSR_PORT`: Alternative way to specify local server
13//!
14//! # Example
15//!
16//! ```rust
17//! use browsr_client::{BrowsrClient, BrowsrClientConfig};
18//!
19//! // From environment variables
20//! let client = BrowsrClient::from_env();
21//!
22//! // From explicit config (cloud with API key)
23//! let config = BrowsrClientConfig::new("https://api.browsr.dev")
24//!     .with_api_key("your-api-key");
25//! let client = BrowsrClient::from_config(config);
26//! ```
27
28use serde::{Deserialize, Serialize};
29
30/// Default base URL for the Browsr cloud service
31pub const DEFAULT_BASE_URL: &str = "https://api.browsr.dev";
32
33/// Environment variable for the base URL
34pub const ENV_BASE_URL: &str = "BROWSR_BASE_URL";
35
36/// Environment variable for the API key
37pub const ENV_API_KEY: &str = "BROWSR_API_KEY";
38
39/// Configuration for the Browsr client.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct BrowsrClientConfig {
42    /// Base URL of the Browsr server
43    pub base_url: String,
44
45    /// Optional API key for authentication
46    #[serde(default, skip_serializing_if = "Option::is_none")]
47    pub api_key: Option<String>,
48
49    /// Request timeout in seconds (default: 60)
50    #[serde(default = "default_timeout")]
51    pub timeout_secs: u64,
52
53    /// Number of retry attempts for failed requests (default: 3)
54    #[serde(default = "default_retries")]
55    pub retry_attempts: u32,
56
57    /// Default headless mode for browser sessions
58    #[serde(default)]
59    pub headless: Option<bool>,
60}
61
62fn default_timeout() -> u64 {
63    60
64}
65
66fn default_retries() -> u32 {
67    3
68}
69
70impl Default for BrowsrClientConfig {
71    fn default() -> Self {
72        Self {
73            base_url: DEFAULT_BASE_URL.to_string(),
74            api_key: None,
75            timeout_secs: default_timeout(),
76            retry_attempts: default_retries(),
77            headless: None,
78        }
79    }
80}
81
82impl BrowsrClientConfig {
83    /// Create a new config with the specified base URL.
84    pub fn new(base_url: impl Into<String>) -> Self {
85        Self {
86            base_url: base_url.into().trim_end_matches('/').to_string(),
87            ..Default::default()
88        }
89    }
90
91    /// Create a config from environment variables.
92    ///
93    /// - `BROWSR_BASE_URL` or `BROWSR_API_URL`: Base URL (defaults to `https://api.browsr.dev`)
94    /// - `BROWSR_API_KEY`: Optional API key
95    /// - `BROWSR_HOST` / `BROWSR_PORT`: Alternative local server specification
96    pub fn from_env() -> Self {
97        // Check for explicit base URL
98        let base_url = std::env::var(ENV_BASE_URL)
99            .ok()
100            .unwrap_or_else(|| DEFAULT_BASE_URL.to_string())
101            .trim_end_matches('/')
102            .to_string();
103
104        let api_key = std::env::var(ENV_API_KEY).ok().filter(|s| !s.is_empty());
105
106        Self {
107            base_url,
108            api_key,
109            ..Default::default()
110        }
111    }
112
113    /// Set the API key for authentication.
114    pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
115        self.api_key = Some(api_key.into());
116        self
117    }
118
119    /// Set the request timeout in seconds.
120    pub fn with_timeout(mut self, timeout_secs: u64) -> Self {
121        self.timeout_secs = timeout_secs;
122        self
123    }
124
125    /// Set the number of retry attempts.
126    pub fn with_retries(mut self, retry_attempts: u32) -> Self {
127        self.retry_attempts = retry_attempts;
128        self
129    }
130
131    /// Set the default headless mode.
132    pub fn with_headless(mut self, headless: bool) -> Self {
133        self.headless = Some(headless);
134        self
135    }
136
137    /// Check if the client is configured for local development (localhost).
138    pub fn is_local(&self) -> bool {
139        self.base_url.contains("localhost") || self.base_url.contains("127.0.0.1")
140    }
141
142    /// Check if authentication is configured.
143    pub fn has_auth(&self) -> bool {
144        self.api_key.is_some()
145    }
146
147    /// Build a reqwest client with the configured settings.
148    pub fn build_http_client(&self) -> Result<reqwest::Client, reqwest::Error> {
149        let mut builder =
150            reqwest::Client::builder().timeout(std::time::Duration::from_secs(self.timeout_secs));
151
152        // Add default headers if API key is configured
153        if let Some(ref api_key) = self.api_key {
154            let mut headers = reqwest::header::HeaderMap::new();
155            headers.insert(
156                "x-api-key",
157                reqwest::header::HeaderValue::from_str(api_key).expect("Invalid API key format"),
158            );
159            builder = builder.default_headers(headers);
160        }
161
162        builder.build()
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_default_config() {
172        let config = BrowsrClientConfig::default();
173        assert_eq!(config.base_url, DEFAULT_BASE_URL);
174        assert!(config.api_key.is_none());
175        assert!(!config.is_local());
176    }
177
178    #[test]
179    fn test_local_config() {
180        let config = BrowsrClientConfig::new("http://localhost:8082");
181        assert!(config.is_local());
182        assert!(!config.has_auth());
183    }
184
185    #[test]
186    fn test_with_api_key() {
187        let config = BrowsrClientConfig::default().with_api_key("test-key");
188        assert!(config.has_auth());
189        assert_eq!(config.api_key, Some("test-key".to_string()));
190    }
191
192    #[test]
193    fn test_trailing_slash_removed() {
194        let config = BrowsrClientConfig::new("http://localhost:8082/");
195        assert_eq!(config.base_url, "http://localhost:8082");
196    }
197}