1use serde::{Deserialize, Serialize};
32
33pub const DEFAULT_BASE_URL: &str = "https://api.browsr.dev";
35
36pub const ENV_BASE_URL: &str = "BROWSR_BASE_URL";
38
39pub const ENV_API_URL: &str = "BROWSR_API_URL";
41
42pub const ENV_API_KEY: &str = "BROWSR_API_KEY";
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct BrowsrClientConfig {
48 pub base_url: String,
50
51 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub api_key: Option<String>,
54
55 #[serde(default = "default_timeout")]
57 pub timeout_secs: u64,
58
59 #[serde(default = "default_retries")]
61 pub retry_attempts: u32,
62
63 #[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 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 pub fn from_env() -> Self {
103 let base_url = std::env::var(ENV_BASE_URL)
105 .or_else(|_| std::env::var(ENV_API_URL))
106 .ok()
107 .or_else(|| {
108 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 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 pub fn with_timeout(mut self, timeout_secs: u64) -> Self {
136 self.timeout_secs = timeout_secs;
137 self
138 }
139
140 pub fn with_retries(mut self, retry_attempts: u32) -> Self {
142 self.retry_attempts = retry_attempts;
143 self
144 }
145
146 pub fn with_headless(mut self, headless: bool) -> Self {
148 self.headless = Some(headless);
149 self
150 }
151
152 pub fn is_local(&self) -> bool {
154 self.base_url.contains("localhost") || self.base_url.contains("127.0.0.1")
155 }
156
157 pub fn has_auth(&self) -> bool {
159 self.api_key.is_some()
160 }
161
162 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 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