serper_sdk/config/
mod.rs

1/// Configuration management module
2///
3/// This module provides configuration structures and utilities for
4/// managing SDK settings, environment variables, and default values.
5use crate::core::{Result, SerperError};
6use std::collections::HashMap;
7use std::time::Duration;
8
9/// Main SDK configuration
10///
11/// This struct contains all configuration options for the Serper SDK,
12/// with sensible defaults and environment variable support.
13#[derive(Debug, Clone)]
14pub struct SdkConfig {
15    /// API key for authentication
16    pub api_key: String,
17    /// Base URL for the API
18    pub base_url: String,
19    /// Request timeout duration
20    pub timeout: Duration,
21    /// Maximum number of concurrent requests
22    pub max_concurrent_requests: usize,
23    /// Default headers to include with all requests
24    pub default_headers: HashMap<String, String>,
25    /// User agent string
26    pub user_agent: String,
27    /// Enable request/response logging
28    pub enable_logging: bool,
29}
30
31impl SdkConfig {
32    /// Creates a new configuration with the specified API key
33    ///
34    /// # Arguments
35    ///
36    /// * `api_key` - The Serper API key
37    ///
38    /// # Returns
39    ///
40    /// A new SdkConfig with default values
41    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    /// Creates configuration from environment variables
57    ///
58    /// Expected environment variables:
59    /// - `SERPER_API_KEY` (required)
60    /// - `SERPER_BASE_URL` (optional)
61    /// - `SERPER_TIMEOUT_SECS` (optional)
62    /// - `SERPER_MAX_CONCURRENT` (optional)
63    /// - `SERPER_USER_AGENT` (optional)
64    /// - `SERPER_ENABLE_LOGGING` (optional)
65    ///
66    /// # Returns
67    ///
68    /// Result containing the configuration or an error if required variables are missing
69    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    /// Validates the configuration
104    ///
105    /// # Returns
106    ///
107    /// Result indicating whether the configuration is valid
108    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    /// Sets the base URL
137    pub fn with_base_url(mut self, base_url: String) -> Self {
138        self.base_url = base_url;
139        self
140    }
141
142    /// Sets the timeout
143    pub fn with_timeout(mut self, timeout: Duration) -> Self {
144        self.timeout = timeout;
145        self
146    }
147
148    /// Sets the maximum concurrent requests
149    pub fn with_max_concurrent(mut self, max_concurrent: usize) -> Self {
150        self.max_concurrent_requests = max_concurrent;
151        self
152    }
153
154    /// Adds a default header
155    pub fn with_header(mut self, key: String, value: String) -> Self {
156        self.default_headers.insert(key, value);
157        self
158    }
159
160    /// Sets the user agent
161    pub fn with_user_agent(mut self, user_agent: String) -> Self {
162        self.user_agent = user_agent;
163        self
164    }
165
166    /// Enables or disables logging
167    pub fn with_logging(mut self, enable: bool) -> Self {
168        self.enable_logging = enable;
169        self
170    }
171}
172
173/// Builder for creating SDK configurations
174pub 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    /// Creates a new configuration builder
186    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    /// Sets the API key
202    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    /// Sets the base URL
208    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    /// Sets the timeout
214    pub fn timeout(mut self, timeout: Duration) -> Self {
215        self.timeout = Some(timeout);
216        self
217    }
218
219    /// Sets the maximum concurrent requests
220    pub fn max_concurrent(mut self, max_concurrent: usize) -> Self {
221        self.max_concurrent_requests = Some(max_concurrent);
222        self
223    }
224
225    /// Adds a default header
226    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    /// Sets the user agent
232    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    /// Enables logging
238    pub fn enable_logging(mut self) -> Self {
239        self.enable_logging = true;
240        self
241    }
242
243    /// Builds the configuration
244    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        // Valid config
323        let config = SdkConfig::new("valid-key".to_string());
324        assert!(config.validate().is_ok());
325
326        // Invalid API key
327        let config = SdkConfig::new("".to_string());
328        assert!(config.validate().is_err());
329
330        // Invalid base URL
331        let config = SdkConfig::new("key".to_string()).with_base_url("invalid-url".to_string());
332        assert!(config.validate().is_err());
333
334        // Invalid timeout
335        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}