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