1use serde::{Deserialize, Serialize};
30
31pub const DEFAULT_BASE_URL: &str = "https://api.browsr.dev";
33
34pub const ENV_BASE_URL: &str = "BROWSR_BASE_URL";
36
37pub const ENV_API_KEY: &str = "BROWSR_API_KEY";
39
40pub const ENV_BEARER_TOKEN: &str = "BROWSR_BEARER_TOKEN";
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct BrowsrClientConfig {
46 pub base_url: String,
48
49 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub api_key: Option<String>,
52
53 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub bearer_token: Option<String>,
56
57 #[serde(default = "default_timeout")]
59 pub timeout_secs: u64,
60
61 #[serde(default = "default_retries")]
63 pub retry_attempts: u32,
64
65 #[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 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 pub fn from_env() -> Self {
107 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 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 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 pub fn with_timeout(mut self, timeout_secs: u64) -> Self {
141 self.timeout_secs = timeout_secs;
142 self
143 }
144
145 pub fn with_retries(mut self, retry_attempts: u32) -> Self {
147 self.retry_attempts = retry_attempts;
148 self
149 }
150
151 pub fn with_headless(mut self, headless: bool) -> Self {
153 self.headless = Some(headless);
154 self
155 }
156
157 pub fn is_local(&self) -> bool {
159 self.base_url.contains("localhost") || self.base_url.contains("127.0.0.1")
160 }
161
162 pub fn has_auth(&self) -> bool {
164 self.api_key.is_some() || self.bearer_token.is_some()
165 }
166
167 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 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}