Skip to main content

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