1use serde::{Deserialize, Serialize};
29
30pub const DEFAULT_BASE_URL: &str = "https://api.browsr.dev";
32
33pub const ENV_BASE_URL: &str = "BROWSR_BASE_URL";
35
36pub const ENV_API_KEY: &str = "BROWSR_API_KEY";
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct BrowsrClientConfig {
42 pub base_url: String,
44
45 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub api_key: Option<String>,
48
49 #[serde(default = "default_timeout")]
51 pub timeout_secs: u64,
52
53 #[serde(default = "default_retries")]
55 pub retry_attempts: u32,
56
57 #[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 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 pub fn from_env() -> Self {
97 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 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 pub fn with_timeout(mut self, timeout_secs: u64) -> Self {
121 self.timeout_secs = timeout_secs;
122 self
123 }
124
125 pub fn with_retries(mut self, retry_attempts: u32) -> Self {
127 self.retry_attempts = retry_attempts;
128 self
129 }
130
131 pub fn with_headless(mut self, headless: bool) -> Self {
133 self.headless = Some(headless);
134 self
135 }
136
137 pub fn is_local(&self) -> bool {
139 self.base_url.contains("localhost") || self.base_url.contains("127.0.0.1")
140 }
141
142 pub fn has_auth(&self) -> bool {
144 self.api_key.is_some()
145 }
146
147 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 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}