1use crate::core::{Result, SerperError};
6use std::collections::HashMap;
7use std::time::Duration;
8
9#[derive(Debug, Clone)]
14pub struct SdkConfig {
15 pub api_key: String,
17 pub base_url: String,
19 pub timeout: Duration,
21 pub max_concurrent_requests: usize,
23 pub default_headers: HashMap<String, String>,
25 pub user_agent: String,
27 pub enable_logging: bool,
29}
30
31impl SdkConfig {
32 pub fn new(api_key: String) -> Self {
42 let mut default_headers = HashMap::new();
43 default_headers.insert("Content-Type".to_string(), "application/json".to_string());
44
45 Self {
46 api_key,
47 base_url: "https://google.serper.dev".to_string(),
48 timeout: Duration::from_secs(30),
49 max_concurrent_requests: 5,
50 default_headers,
51 user_agent: format!("serper-sdk/{}", env!("CARGO_PKG_VERSION")),
52 enable_logging: false,
53 }
54 }
55
56 pub fn from_env() -> Result<Self> {
70 let api_key = std::env::var("SERPER_API_KEY").map_err(|_| {
71 SerperError::config_error("SERPER_API_KEY environment variable is required")
72 })?;
73
74 let mut config = Self::new(api_key);
75
76 if let Ok(base_url) = std::env::var("SERPER_BASE_URL") {
77 config.base_url = base_url;
78 }
79
80 if let Ok(timeout_str) = std::env::var("SERPER_TIMEOUT_SECS")
81 && let Ok(timeout_secs) = timeout_str.parse::<u64>()
82 {
83 config.timeout = Duration::from_secs(timeout_secs);
84 }
85
86 if let Ok(max_concurrent_str) = std::env::var("SERPER_MAX_CONCURRENT")
87 && let Ok(max_concurrent) = max_concurrent_str.parse::<usize>()
88 {
89 config.max_concurrent_requests = max_concurrent;
90 }
91
92 if let Ok(user_agent) = std::env::var("SERPER_USER_AGENT") {
93 config.user_agent = user_agent;
94 }
95
96 if let Ok(enable_logging_str) = std::env::var("SERPER_ENABLE_LOGGING") {
97 config.enable_logging = enable_logging_str.to_lowercase() == "true";
98 }
99
100 Ok(config)
101 }
102
103 pub fn validate(&self) -> Result<()> {
109 if self.api_key.trim().is_empty() {
110 return Err(SerperError::config_error("API key cannot be empty"));
111 }
112
113 if self.base_url.trim().is_empty() {
114 return Err(SerperError::config_error("Base URL cannot be empty"));
115 }
116
117 if !self.base_url.starts_with("http://") && !self.base_url.starts_with("https://") {
118 return Err(SerperError::config_error(
119 "Base URL must start with http:// or https://",
120 ));
121 }
122
123 if self.timeout.as_secs() == 0 {
124 return Err(SerperError::config_error("Timeout must be greater than 0"));
125 }
126
127 if self.max_concurrent_requests == 0 {
128 return Err(SerperError::config_error(
129 "Max concurrent requests must be greater than 0",
130 ));
131 }
132
133 Ok(())
134 }
135
136 pub fn with_base_url(mut self, base_url: String) -> Self {
138 self.base_url = base_url;
139 self
140 }
141
142 pub fn with_timeout(mut self, timeout: Duration) -> Self {
144 self.timeout = timeout;
145 self
146 }
147
148 pub fn with_max_concurrent(mut self, max_concurrent: usize) -> Self {
150 self.max_concurrent_requests = max_concurrent;
151 self
152 }
153
154 pub fn with_header(mut self, key: String, value: String) -> Self {
156 self.default_headers.insert(key, value);
157 self
158 }
159
160 pub fn with_user_agent(mut self, user_agent: String) -> Self {
162 self.user_agent = user_agent;
163 self
164 }
165
166 pub fn with_logging(mut self, enable: bool) -> Self {
168 self.enable_logging = enable;
169 self
170 }
171}
172
173pub struct SdkConfigBuilder {
175 api_key: Option<String>,
176 base_url: Option<String>,
177 timeout: Option<Duration>,
178 max_concurrent_requests: Option<usize>,
179 default_headers: HashMap<String, String>,
180 user_agent: Option<String>,
181 enable_logging: bool,
182}
183
184impl SdkConfigBuilder {
185 pub fn new() -> Self {
187 let mut default_headers = HashMap::new();
188 default_headers.insert("Content-Type".to_string(), "application/json".to_string());
189
190 Self {
191 api_key: None,
192 base_url: None,
193 timeout: None,
194 max_concurrent_requests: None,
195 default_headers,
196 user_agent: None,
197 enable_logging: false,
198 }
199 }
200
201 pub fn api_key(mut self, api_key: impl Into<String>) -> Self {
203 self.api_key = Some(api_key.into());
204 self
205 }
206
207 pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
209 self.base_url = Some(base_url.into());
210 self
211 }
212
213 pub fn timeout(mut self, timeout: Duration) -> Self {
215 self.timeout = Some(timeout);
216 self
217 }
218
219 pub fn max_concurrent(mut self, max_concurrent: usize) -> Self {
221 self.max_concurrent_requests = Some(max_concurrent);
222 self
223 }
224
225 pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
227 self.default_headers.insert(key.into(), value.into());
228 self
229 }
230
231 pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
233 self.user_agent = Some(user_agent.into());
234 self
235 }
236
237 pub fn enable_logging(mut self) -> Self {
239 self.enable_logging = true;
240 self
241 }
242
243 pub fn build(self) -> Result<SdkConfig> {
245 let api_key = self
246 .api_key
247 .ok_or_else(|| SerperError::config_error("API key is required"))?;
248
249 let mut config = SdkConfig::new(api_key);
250
251 if let Some(base_url) = self.base_url {
252 config.base_url = base_url;
253 }
254
255 if let Some(timeout) = self.timeout {
256 config.timeout = timeout;
257 }
258
259 if let Some(max_concurrent) = self.max_concurrent_requests {
260 config.max_concurrent_requests = max_concurrent;
261 }
262
263 config.default_headers = self.default_headers;
264
265 if let Some(user_agent) = self.user_agent {
266 config.user_agent = user_agent;
267 }
268
269 config.enable_logging = self.enable_logging;
270
271 config.validate()?;
272 Ok(config)
273 }
274}
275
276impl Default for SdkConfigBuilder {
277 fn default() -> Self {
278 Self::new()
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[test]
287 fn test_config_creation() {
288 let config = SdkConfig::new("test-key".to_string());
289 assert_eq!(config.api_key, "test-key");
290 assert_eq!(config.base_url, "https://google.serper.dev");
291 assert_eq!(config.timeout, Duration::from_secs(30));
292 assert!(config.validate().is_ok());
293 }
294
295 #[test]
296 fn test_config_builder() {
297 let config = SdkConfigBuilder::new()
298 .api_key("test-key")
299 .base_url("https://custom.api.com")
300 .timeout(Duration::from_secs(60))
301 .max_concurrent(10)
302 .header("Custom", "Value")
303 .user_agent("custom-agent")
304 .enable_logging()
305 .build()
306 .unwrap();
307
308 assert_eq!(config.api_key, "test-key");
309 assert_eq!(config.base_url, "https://custom.api.com");
310 assert_eq!(config.timeout, Duration::from_secs(60));
311 assert_eq!(config.max_concurrent_requests, 10);
312 assert_eq!(config.user_agent, "custom-agent");
313 assert!(config.enable_logging);
314 assert_eq!(
315 config.default_headers.get("Custom"),
316 Some(&"Value".to_string())
317 );
318 }
319
320 #[test]
321 fn test_config_validation() {
322 let config = SdkConfig::new("valid-key".to_string());
324 assert!(config.validate().is_ok());
325
326 let config = SdkConfig::new("".to_string());
328 assert!(config.validate().is_err());
329
330 let config = SdkConfig::new("key".to_string()).with_base_url("invalid-url".to_string());
332 assert!(config.validate().is_err());
333
334 let config = SdkConfig::new("key".to_string()).with_timeout(Duration::from_secs(0));
336 assert!(config.validate().is_err());
337 }
338
339 #[test]
340 fn test_builder_missing_api_key() {
341 let builder = SdkConfigBuilder::new();
342 let result = builder.build();
343 assert!(result.is_err());
344 }
345
346 #[test]
347 fn test_fluent_configuration() {
348 let config = SdkConfig::new("key".to_string())
349 .with_base_url("https://test.com".to_string())
350 .with_timeout(Duration::from_secs(45))
351 .with_max_concurrent(8)
352 .with_header("X-Test".to_string(), "value".to_string())
353 .with_user_agent("test-agent".to_string())
354 .with_logging(true);
355
356 assert_eq!(config.base_url, "https://test.com");
357 assert_eq!(config.timeout, Duration::from_secs(45));
358 assert_eq!(config.max_concurrent_requests, 8);
359 assert_eq!(config.user_agent, "test-agent");
360 assert!(config.enable_logging);
361 }
362}