use crate::core::{Result, SerperError};
use std::collections::HashMap;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct SdkConfig {
pub api_key: String,
pub base_url: String,
pub timeout: Duration,
pub max_concurrent_requests: usize,
pub default_headers: HashMap<String, String>,
pub user_agent: String,
pub enable_logging: bool,
}
impl SdkConfig {
pub fn new(api_key: String) -> Self {
let mut default_headers = HashMap::new();
default_headers.insert("Content-Type".to_string(), "application/json".to_string());
Self {
api_key,
base_url: "https://google.serper.dev".to_string(),
timeout: Duration::from_secs(30),
max_concurrent_requests: 5,
default_headers,
user_agent: format!("serper-sdk/{}", env!("CARGO_PKG_VERSION")),
enable_logging: false,
}
}
pub fn from_env() -> Result<Self> {
let api_key = std::env::var("SERPER_API_KEY").map_err(|_| {
SerperError::config_error("SERPER_API_KEY environment variable is required")
})?;
let mut config = Self::new(api_key);
if let Ok(base_url) = std::env::var("SERPER_BASE_URL") {
config.base_url = base_url;
}
if let Ok(timeout_str) = std::env::var("SERPER_TIMEOUT_SECS")
&& let Ok(timeout_secs) = timeout_str.parse::<u64>()
{
config.timeout = Duration::from_secs(timeout_secs);
}
if let Ok(max_concurrent_str) = std::env::var("SERPER_MAX_CONCURRENT")
&& let Ok(max_concurrent) = max_concurrent_str.parse::<usize>()
{
config.max_concurrent_requests = max_concurrent;
}
if let Ok(user_agent) = std::env::var("SERPER_USER_AGENT") {
config.user_agent = user_agent;
}
if let Ok(enable_logging_str) = std::env::var("SERPER_ENABLE_LOGGING") {
config.enable_logging = enable_logging_str.to_lowercase() == "true";
}
Ok(config)
}
pub fn validate(&self) -> Result<()> {
if self.api_key.trim().is_empty() {
return Err(SerperError::config_error("API key cannot be empty"));
}
if self.base_url.trim().is_empty() {
return Err(SerperError::config_error("Base URL cannot be empty"));
}
if !self.base_url.starts_with("http://") && !self.base_url.starts_with("https://") {
return Err(SerperError::config_error(
"Base URL must start with http:// or https://",
));
}
if self.timeout.as_secs() == 0 {
return Err(SerperError::config_error("Timeout must be greater than 0"));
}
if self.max_concurrent_requests == 0 {
return Err(SerperError::config_error(
"Max concurrent requests must be greater than 0",
));
}
Ok(())
}
pub fn with_base_url(mut self, base_url: String) -> Self {
self.base_url = base_url;
self
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn with_max_concurrent(mut self, max_concurrent: usize) -> Self {
self.max_concurrent_requests = max_concurrent;
self
}
pub fn with_header(mut self, key: String, value: String) -> Self {
self.default_headers.insert(key, value);
self
}
pub fn with_user_agent(mut self, user_agent: String) -> Self {
self.user_agent = user_agent;
self
}
pub fn with_logging(mut self, enable: bool) -> Self {
self.enable_logging = enable;
self
}
}
pub struct SdkConfigBuilder {
api_key: Option<String>,
base_url: Option<String>,
timeout: Option<Duration>,
max_concurrent_requests: Option<usize>,
default_headers: HashMap<String, String>,
user_agent: Option<String>,
enable_logging: bool,
}
impl SdkConfigBuilder {
pub fn new() -> Self {
let mut default_headers = HashMap::new();
default_headers.insert("Content-Type".to_string(), "application/json".to_string());
Self {
api_key: None,
base_url: None,
timeout: None,
max_concurrent_requests: None,
default_headers,
user_agent: None,
enable_logging: false,
}
}
pub fn api_key(mut self, api_key: impl Into<String>) -> Self {
self.api_key = Some(api_key.into());
self
}
pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = Some(base_url.into());
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn max_concurrent(mut self, max_concurrent: usize) -> Self {
self.max_concurrent_requests = Some(max_concurrent);
self
}
pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.default_headers.insert(key.into(), value.into());
self
}
pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
self.user_agent = Some(user_agent.into());
self
}
pub fn enable_logging(mut self) -> Self {
self.enable_logging = true;
self
}
pub fn build(self) -> Result<SdkConfig> {
let api_key = self
.api_key
.ok_or_else(|| SerperError::config_error("API key is required"))?;
let mut config = SdkConfig::new(api_key);
if let Some(base_url) = self.base_url {
config.base_url = base_url;
}
if let Some(timeout) = self.timeout {
config.timeout = timeout;
}
if let Some(max_concurrent) = self.max_concurrent_requests {
config.max_concurrent_requests = max_concurrent;
}
config.default_headers = self.default_headers;
if let Some(user_agent) = self.user_agent {
config.user_agent = user_agent;
}
config.enable_logging = self.enable_logging;
config.validate()?;
Ok(config)
}
}
impl Default for SdkConfigBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_creation() {
let config = SdkConfig::new("test-key".to_string());
assert_eq!(config.api_key, "test-key");
assert_eq!(config.base_url, "https://google.serper.dev");
assert_eq!(config.timeout, Duration::from_secs(30));
assert!(config.validate().is_ok());
}
#[test]
fn test_config_builder() {
let config = SdkConfigBuilder::new()
.api_key("test-key")
.base_url("https://custom.api.com")
.timeout(Duration::from_secs(60))
.max_concurrent(10)
.header("Custom", "Value")
.user_agent("custom-agent")
.enable_logging()
.build()
.unwrap();
assert_eq!(config.api_key, "test-key");
assert_eq!(config.base_url, "https://custom.api.com");
assert_eq!(config.timeout, Duration::from_secs(60));
assert_eq!(config.max_concurrent_requests, 10);
assert_eq!(config.user_agent, "custom-agent");
assert!(config.enable_logging);
assert_eq!(
config.default_headers.get("Custom"),
Some(&"Value".to_string())
);
}
#[test]
fn test_config_validation() {
let config = SdkConfig::new("valid-key".to_string());
assert!(config.validate().is_ok());
let config = SdkConfig::new("".to_string());
assert!(config.validate().is_err());
let config = SdkConfig::new("key".to_string()).with_base_url("invalid-url".to_string());
assert!(config.validate().is_err());
let config = SdkConfig::new("key".to_string()).with_timeout(Duration::from_secs(0));
assert!(config.validate().is_err());
}
#[test]
fn test_builder_missing_api_key() {
let builder = SdkConfigBuilder::new();
let result = builder.build();
assert!(result.is_err());
}
#[test]
fn test_fluent_configuration() {
let config = SdkConfig::new("key".to_string())
.with_base_url("https://test.com".to_string())
.with_timeout(Duration::from_secs(45))
.with_max_concurrent(8)
.with_header("X-Test".to_string(), "value".to_string())
.with_user_agent("test-agent".to_string())
.with_logging(true);
assert_eq!(config.base_url, "https://test.com");
assert_eq!(config.timeout, Duration::from_secs(45));
assert_eq!(config.max_concurrent_requests, 8);
assert_eq!(config.user_agent, "test-agent");
assert!(config.enable_logging);
}
}