use crate::core::{Result, SerperError, types::ApiKey};
use reqwest::{Client as ReqwestClient, Method, Response};
use serde::Serialize;
use std::collections::HashMap;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct TransportConfig {
pub timeout: Duration,
pub default_headers: HashMap<String, String>,
pub user_agent: String,
}
impl TransportConfig {
pub fn new() -> Self {
let mut default_headers = HashMap::new();
default_headers.insert("Content-Type".to_string(), "application/json".to_string());
Self {
timeout: Duration::from_secs(30),
default_headers,
user_agent: format!("serper-sdk/{}", env!("CARGO_PKG_VERSION")),
}
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
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
}
}
impl Default for TransportConfig {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct HttpTransport {
client: ReqwestClient,
config: TransportConfig,
}
impl HttpTransport {
pub fn new() -> Result<Self> {
Self::with_config(TransportConfig::new())
}
pub fn with_config(config: TransportConfig) -> Result<Self> {
let client = ReqwestClient::builder()
.timeout(config.timeout)
.user_agent(&config.user_agent)
.build()
.map_err(SerperError::Request)?;
Ok(Self { client, config })
}
pub async fn post_json<T: Serialize>(
&self,
url: &str,
api_key: &ApiKey,
body: &T,
) -> Result<Response> {
let mut request = self
.client
.request(Method::POST, url)
.header("X-API-KEY", api_key.as_str());
for (key, value) in &self.config.default_headers {
if key != "Content-Type" {
request = request.header(key, value);
}
}
request = request.json(body);
let response = request.send().await.map_err(SerperError::Request)?;
if !response.status().is_success() {
return Err(SerperError::api_error(format!(
"HTTP {} - {}",
response.status(),
response
.status()
.canonical_reason()
.unwrap_or("Unknown error")
)));
}
Ok(response)
}
pub async fn get(&self, url: &str, api_key: &ApiKey) -> Result<Response> {
let mut request = self
.client
.request(Method::GET, url)
.header("X-API-KEY", api_key.as_str());
for (key, value) in &self.config.default_headers {
if key != "Content-Type" {
request = request.header(key, value);
}
}
let response = request.send().await.map_err(SerperError::Request)?;
if !response.status().is_success() {
return Err(SerperError::api_error(format!(
"HTTP {} - {}",
response.status(),
response
.status()
.canonical_reason()
.unwrap_or("Unknown error")
)));
}
Ok(response)
}
pub async fn parse_json<T>(&self, response: Response) -> Result<T>
where
T: serde::de::DeserializeOwned,
{
response.json().await.map_err(SerperError::Request)
}
pub fn config(&self) -> &TransportConfig {
&self.config
}
}
impl Default for HttpTransport {
fn default() -> Self {
Self::new().expect("Failed to create default HTTP transport")
}
}
pub struct HttpTransportBuilder {
config: TransportConfig,
}
impl HttpTransportBuilder {
pub fn new() -> Self {
Self {
config: TransportConfig::new(),
}
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.config = self.config.with_timeout(timeout);
self
}
pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.config = self.config.with_header(key.into(), value.into());
self
}
pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
self.config = self.config.with_user_agent(user_agent.into());
self
}
pub fn build(self) -> Result<HttpTransport> {
HttpTransport::with_config(self.config)
}
}
impl Default for HttpTransportBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transport_config_creation() {
let config = TransportConfig::new();
assert_eq!(config.timeout, Duration::from_secs(30));
assert!(config.default_headers.contains_key("Content-Type"));
}
#[test]
fn test_transport_config_builder() {
let config = TransportConfig::new()
.with_timeout(Duration::from_secs(60))
.with_header("Custom-Header".to_string(), "value".to_string())
.with_user_agent("custom-agent".to_string());
assert_eq!(config.timeout, Duration::from_secs(60));
assert_eq!(config.user_agent, "custom-agent");
assert_eq!(
config.default_headers.get("Custom-Header"),
Some(&"value".to_string())
);
}
#[test]
fn test_transport_builder() {
let builder = HttpTransportBuilder::new()
.timeout(Duration::from_secs(45))
.header("Test", "Value")
.user_agent("test-agent");
let transport = builder.build().unwrap();
assert_eq!(transport.config().timeout, Duration::from_secs(45));
assert_eq!(transport.config().user_agent, "test-agent");
}
#[test]
fn test_api_key_validation() {
let result = ApiKey::new("valid-key".to_string());
assert!(result.is_ok());
let result = ApiKey::new("".to_string());
assert!(result.is_err());
}
}