serper_sdk/http/
client.rs

1/// High-level HTTP client functionality
2///
3/// This module provides a high-level HTTP client that combines transport
4/// layer functionality with Serper API-specific operations.
5use crate::{
6    core::{
7        Result,
8        types::{ApiKey, BaseUrl},
9    },
10    http::transport::{HttpTransport, TransportConfig},
11    search::{
12        query::SearchQuery,
13        response::{ResponseParser, SearchResponse},
14    },
15};
16
17/// High-level HTTP client for Serper API operations
18///
19/// This client handles authentication, request formatting, response parsing,
20/// and error handling for all Serper API interactions.
21#[derive(Debug)]
22pub struct SerperHttpClient {
23    transport: HttpTransport,
24    api_key: ApiKey,
25    base_url: BaseUrl,
26}
27
28impl SerperHttpClient {
29    /// Creates a new HTTP client with the specified API key
30    ///
31    /// # Arguments
32    ///
33    /// * `api_key` - The Serper API key
34    ///
35    /// # Returns
36    ///
37    /// Result containing the HTTP client or an error
38    pub fn new(api_key: ApiKey) -> Result<Self> {
39        let transport = HttpTransport::new()?;
40        let base_url = BaseUrl::default();
41
42        Ok(Self {
43            transport,
44            api_key,
45            base_url,
46        })
47    }
48
49    /// Creates a new HTTP client with custom configuration
50    ///
51    /// # Arguments
52    ///
53    /// * `api_key` - The Serper API key
54    /// * `base_url` - Custom base URL for the API
55    /// * `config` - Transport configuration
56    ///
57    /// # Returns
58    ///
59    /// Result containing the HTTP client or an error
60    pub fn with_config(
61        api_key: ApiKey,
62        base_url: BaseUrl,
63        config: TransportConfig,
64    ) -> Result<Self> {
65        let transport = HttpTransport::with_config(config)?;
66
67        Ok(Self {
68            transport,
69            api_key,
70            base_url,
71        })
72    }
73
74    /// Executes a search query
75    ///
76    /// # Arguments
77    ///
78    /// * `query` - The search query to execute
79    ///
80    /// # Returns
81    ///
82    /// Result containing the search response or an error
83    pub async fn search(&self, query: &SearchQuery) -> Result<SearchResponse> {
84        // Validate query before sending
85        query.validate()?;
86
87        let url = format!("{}/search", self.base_url.as_str());
88
89        let response = self.transport.post_json(&url, &self.api_key, query).await?;
90
91        let search_response = self.transport.parse_json(response).await?;
92
93        // Validate response structure
94        ResponseParser::validate_response(&search_response)?;
95
96        Ok(search_response)
97    }
98
99    /// Executes multiple search queries in sequence
100    ///
101    /// # Arguments
102    ///
103    /// * `queries` - The search queries to execute
104    ///
105    /// # Returns
106    ///
107    /// Result containing a vector of search responses or an error
108    pub async fn search_multiple(&self, queries: &[SearchQuery]) -> Result<Vec<SearchResponse>> {
109        let mut results = Vec::with_capacity(queries.len());
110
111        for query in queries {
112            let result = self.search(query).await?;
113            results.push(result);
114        }
115
116        Ok(results)
117    }
118
119    /// Executes multiple search queries concurrently
120    ///
121    /// # Arguments
122    ///
123    /// * `queries` - The search queries to execute
124    /// * `max_concurrent` - Maximum number of concurrent requests
125    ///
126    /// # Returns
127    ///
128    /// Result containing a vector of search responses or an error
129    pub async fn search_concurrent(
130        &self,
131        queries: &[SearchQuery],
132        max_concurrent: usize,
133    ) -> Result<Vec<SearchResponse>> {
134        use std::sync::Arc;
135        use tokio::sync::Semaphore;
136
137        let semaphore = Arc::new(Semaphore::new(max_concurrent));
138        let mut handles = Vec::new();
139
140        for query in queries {
141            let semaphore = Arc::clone(&semaphore);
142            let query = query.clone();
143            let client = self.clone_for_concurrent();
144
145            let handle = tokio::spawn(async move {
146                let _permit = semaphore.acquire().await.unwrap();
147                client.search(&query).await
148            });
149
150            handles.push(handle);
151        }
152
153        let mut results = Vec::with_capacity(queries.len());
154        for handle in handles {
155            let result = handle.await.map_err(|e| {
156                crate::core::SerperError::config_error(format!("Task join error: {}", e))
157            })??;
158            results.push(result);
159        }
160
161        Ok(results)
162    }
163
164    /// Gets the API key (for debugging/logging purposes)
165    pub fn api_key(&self) -> &ApiKey {
166        &self.api_key
167    }
168
169    /// Gets the base URL
170    pub fn base_url(&self) -> &BaseUrl {
171        &self.base_url
172    }
173
174    /// Gets the transport configuration
175    pub fn transport_config(&self) -> &TransportConfig {
176        self.transport.config()
177    }
178
179    /// Helper method to clone the client for concurrent operations
180    ///
181    /// This creates a new HTTP transport but reuses the API key and base URL
182    fn clone_for_concurrent(&self) -> Self {
183        Self {
184            transport: HttpTransport::with_config(self.transport.config().clone())
185                .expect("Failed to clone transport"),
186            api_key: self.api_key.clone(),
187            base_url: self.base_url.clone(),
188        }
189    }
190}
191
192/// Builder for creating HTTP clients with custom configuration
193pub struct SerperHttpClientBuilder {
194    api_key: Option<ApiKey>,
195    base_url: Option<BaseUrl>,
196    transport_config: TransportConfig,
197}
198
199impl SerperHttpClientBuilder {
200    /// Creates a new HTTP client builder
201    pub fn new() -> Self {
202        Self {
203            api_key: None,
204            base_url: None,
205            transport_config: TransportConfig::new(),
206        }
207    }
208
209    /// Sets the API key
210    pub fn api_key(mut self, api_key: ApiKey) -> Self {
211        self.api_key = Some(api_key);
212        self
213    }
214
215    /// Sets the base URL
216    pub fn base_url(mut self, base_url: BaseUrl) -> Self {
217        self.base_url = Some(base_url);
218        self
219    }
220
221    /// Sets the transport configuration
222    pub fn transport_config(mut self, config: TransportConfig) -> Self {
223        self.transport_config = config;
224        self
225    }
226
227    /// Sets the request timeout
228    pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
229        self.transport_config = self.transport_config.with_timeout(timeout);
230        self
231    }
232
233    /// Adds a default header
234    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
235        self.transport_config = self.transport_config.with_header(key.into(), value.into());
236        self
237    }
238
239    /// Builds the HTTP client
240    pub fn build(self) -> Result<SerperHttpClient> {
241        let api_key = self
242            .api_key
243            .ok_or_else(|| crate::core::SerperError::config_error("API key is required"))?;
244
245        let base_url = self.base_url.unwrap_or_default();
246
247        SerperHttpClient::with_config(api_key, base_url, self.transport_config)
248    }
249}
250
251impl Default for SerperHttpClientBuilder {
252    fn default() -> Self {
253        Self::new()
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260    use crate::core::types::ApiKey;
261
262    #[test]
263    fn test_client_builder() {
264        let api_key = ApiKey::new("test-key".to_string()).unwrap();
265        let base_url = BaseUrl::new("https://test.api.com".to_string());
266
267        let builder = SerperHttpClientBuilder::new()
268            .api_key(api_key.clone())
269            .base_url(base_url.clone())
270            .timeout(std::time::Duration::from_secs(60))
271            .header("Custom", "Value");
272
273        let client = builder.build().unwrap();
274        assert_eq!(client.api_key().as_str(), "test-key");
275        assert_eq!(client.base_url().as_str(), "https://test.api.com");
276        assert_eq!(
277            client.transport_config().timeout,
278            std::time::Duration::from_secs(60)
279        );
280    }
281
282    #[test]
283    fn test_client_creation() {
284        let api_key = ApiKey::new("test-key".to_string()).unwrap();
285        let client = SerperHttpClient::new(api_key).unwrap();
286
287        assert_eq!(client.api_key().as_str(), "test-key");
288        assert_eq!(client.base_url().as_str(), "https://google.serper.dev");
289    }
290
291    #[test]
292    fn test_builder_missing_api_key() {
293        let builder = SerperHttpClientBuilder::new();
294        let result = builder.build();
295        assert!(result.is_err());
296    }
297}