serper_sdk/http/
transport.rs

1use crate::core::{Result, SerperError, types::ApiKey};
2/// HTTP transport layer abstraction
3///
4/// This module provides a clean abstraction over HTTP operations,
5/// making it easy to swap out underlying HTTP clients or add middleware.
6use reqwest::{Client as ReqwestClient, Method, Response};
7use serde::Serialize;
8use std::collections::HashMap;
9use std::time::Duration;
10
11/// HTTP transport configuration
12#[derive(Debug, Clone)]
13pub struct TransportConfig {
14    /// Request timeout duration
15    pub timeout: Duration,
16    /// Default headers to include with all requests
17    pub default_headers: HashMap<String, String>,
18    /// User agent string
19    pub user_agent: String,
20}
21
22impl TransportConfig {
23    /// Creates a new transport configuration with default values
24    pub fn new() -> Self {
25        let mut default_headers = HashMap::new();
26        default_headers.insert("Content-Type".to_string(), "application/json".to_string());
27
28        Self {
29            timeout: Duration::from_secs(30),
30            default_headers,
31            user_agent: format!("serper-sdk/{}", env!("CARGO_PKG_VERSION")),
32        }
33    }
34
35    /// Sets the request timeout
36    pub fn with_timeout(mut self, timeout: Duration) -> Self {
37        self.timeout = timeout;
38        self
39    }
40
41    /// Adds a default header
42    pub fn with_header(mut self, key: String, value: String) -> Self {
43        self.default_headers.insert(key, value);
44        self
45    }
46
47    /// Sets the user agent
48    pub fn with_user_agent(mut self, user_agent: String) -> Self {
49        self.user_agent = user_agent;
50        self
51    }
52}
53
54impl Default for TransportConfig {
55    fn default() -> Self {
56        Self::new()
57    }
58}
59
60/// HTTP transport implementation
61///
62/// This struct handles all HTTP operations with automatic retry,
63/// error handling, and request/response logging.
64#[derive(Debug)]
65pub struct HttpTransport {
66    client: ReqwestClient,
67    config: TransportConfig,
68}
69
70impl HttpTransport {
71    /// Creates a new HTTP transport with default configuration
72    pub fn new() -> Result<Self> {
73        Self::with_config(TransportConfig::new())
74    }
75
76    /// Creates a new HTTP transport with custom configuration
77    pub fn with_config(config: TransportConfig) -> Result<Self> {
78        let client = ReqwestClient::builder()
79            .timeout(config.timeout)
80            .user_agent(&config.user_agent)
81            .build()
82            .map_err(SerperError::Request)?;
83
84        Ok(Self { client, config })
85    }
86
87    /// Makes a POST request with JSON body
88    ///
89    /// # Arguments
90    ///
91    /// * `url` - The request URL
92    /// * `api_key` - API key for authentication
93    /// * `body` - The request body that can be serialized to JSON
94    ///
95    /// # Returns
96    ///
97    /// Result containing the HTTP response or an error
98    pub async fn post_json<T: Serialize>(
99        &self,
100        url: &str,
101        api_key: &ApiKey,
102        body: &T,
103    ) -> Result<Response> {
104        let mut request = self
105            .client
106            .request(Method::POST, url)
107            .header("X-API-KEY", api_key.as_str());
108
109        // Add default headers (except Content-Type since .json() will set it)
110        for (key, value) in &self.config.default_headers {
111            if key != "Content-Type" {
112                request = request.header(key, value);
113            }
114        }
115
116        // Set JSON body (this will automatically set Content-Type: application/json)
117        request = request.json(body);
118
119        let response = request.send().await.map_err(SerperError::Request)?;
120
121        // Check for HTTP error status codes
122        if !response.status().is_success() {
123            return Err(SerperError::api_error(format!(
124                "HTTP {} - {}",
125                response.status(),
126                response
127                    .status()
128                    .canonical_reason()
129                    .unwrap_or("Unknown error")
130            )));
131        }
132
133        Ok(response)
134    }
135
136    /// Makes a GET request
137    ///
138    /// # Arguments
139    ///
140    /// * `url` - The request URL
141    /// * `api_key` - API key for authentication
142    ///
143    /// # Returns
144    ///
145    /// Result containing the HTTP response or an error
146    pub async fn get(&self, url: &str, api_key: &ApiKey) -> Result<Response> {
147        let mut request = self
148            .client
149            .request(Method::GET, url)
150            .header("X-API-KEY", api_key.as_str());
151
152        // Add default headers (except Content-Type for GET)
153        for (key, value) in &self.config.default_headers {
154            if key != "Content-Type" {
155                request = request.header(key, value);
156            }
157        }
158
159        let response = request.send().await.map_err(SerperError::Request)?;
160
161        if !response.status().is_success() {
162            return Err(SerperError::api_error(format!(
163                "HTTP {} - {}",
164                response.status(),
165                response
166                    .status()
167                    .canonical_reason()
168                    .unwrap_or("Unknown error")
169            )));
170        }
171
172        Ok(response)
173    }
174
175    /// Parses a response as JSON
176    ///
177    /// # Arguments
178    ///
179    /// * `response` - The HTTP response to parse
180    ///
181    /// # Returns
182    ///
183    /// Result containing the parsed JSON or an error
184    pub async fn parse_json<T>(&self, response: Response) -> Result<T>
185    where
186        T: serde::de::DeserializeOwned,
187    {
188        response.json().await.map_err(SerperError::Request)
189    }
190
191    /// Gets the current transport configuration
192    pub fn config(&self) -> &TransportConfig {
193        &self.config
194    }
195}
196
197impl Default for HttpTransport {
198    fn default() -> Self {
199        Self::new().expect("Failed to create default HTTP transport")
200    }
201}
202
203/// Builder for creating HTTP transports with custom configuration
204pub struct HttpTransportBuilder {
205    config: TransportConfig,
206}
207
208impl HttpTransportBuilder {
209    /// Creates a new transport builder
210    pub fn new() -> Self {
211        Self {
212            config: TransportConfig::new(),
213        }
214    }
215
216    /// Sets the request timeout
217    pub fn timeout(mut self, timeout: Duration) -> Self {
218        self.config = self.config.with_timeout(timeout);
219        self
220    }
221
222    /// Adds a default header
223    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
224        self.config = self.config.with_header(key.into(), value.into());
225        self
226    }
227
228    /// Sets the user agent
229    pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
230        self.config = self.config.with_user_agent(user_agent.into());
231        self
232    }
233
234    /// Builds the HTTP transport
235    pub fn build(self) -> Result<HttpTransport> {
236        HttpTransport::with_config(self.config)
237    }
238}
239
240impl Default for HttpTransportBuilder {
241    fn default() -> Self {
242        Self::new()
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn test_transport_config_creation() {
252        let config = TransportConfig::new();
253        assert_eq!(config.timeout, Duration::from_secs(30));
254        assert!(config.default_headers.contains_key("Content-Type"));
255    }
256
257    #[test]
258    fn test_transport_config_builder() {
259        let config = TransportConfig::new()
260            .with_timeout(Duration::from_secs(60))
261            .with_header("Custom-Header".to_string(), "value".to_string())
262            .with_user_agent("custom-agent".to_string());
263
264        assert_eq!(config.timeout, Duration::from_secs(60));
265        assert_eq!(config.user_agent, "custom-agent");
266        assert_eq!(
267            config.default_headers.get("Custom-Header"),
268            Some(&"value".to_string())
269        );
270    }
271
272    #[test]
273    fn test_transport_builder() {
274        let builder = HttpTransportBuilder::new()
275            .timeout(Duration::from_secs(45))
276            .header("Test", "Value")
277            .user_agent("test-agent");
278
279        let transport = builder.build().unwrap();
280        assert_eq!(transport.config().timeout, Duration::from_secs(45));
281        assert_eq!(transport.config().user_agent, "test-agent");
282    }
283
284    #[test]
285    fn test_api_key_validation() {
286        let result = ApiKey::new("valid-key".to_string());
287        assert!(result.is_ok());
288
289        let result = ApiKey::new("".to_string());
290        assert!(result.is_err());
291    }
292}