serper_sdk/http/
client.rs1use 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#[derive(Debug)]
22pub struct SerperHttpClient {
23 transport: HttpTransport,
24 api_key: ApiKey,
25 base_url: BaseUrl,
26}
27
28impl SerperHttpClient {
29 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 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 pub async fn search(&self, query: &SearchQuery) -> Result<SearchResponse> {
84 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 ResponseParser::validate_response(&search_response)?;
95
96 Ok(search_response)
97 }
98
99 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 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 pub fn api_key(&self) -> &ApiKey {
166 &self.api_key
167 }
168
169 pub fn base_url(&self) -> &BaseUrl {
171 &self.base_url
172 }
173
174 pub fn transport_config(&self) -> &TransportConfig {
176 self.transport.config()
177 }
178
179 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
192pub struct SerperHttpClientBuilder {
194 api_key: Option<ApiKey>,
195 base_url: Option<BaseUrl>,
196 transport_config: TransportConfig,
197}
198
199impl SerperHttpClientBuilder {
200 pub fn new() -> Self {
202 Self {
203 api_key: None,
204 base_url: None,
205 transport_config: TransportConfig::new(),
206 }
207 }
208
209 pub fn api_key(mut self, api_key: ApiKey) -> Self {
211 self.api_key = Some(api_key);
212 self
213 }
214
215 pub fn base_url(mut self, base_url: BaseUrl) -> Self {
217 self.base_url = Some(base_url);
218 self
219 }
220
221 pub fn transport_config(mut self, config: TransportConfig) -> Self {
223 self.transport_config = config;
224 self
225 }
226
227 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 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 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}