serper_sdk/search/
service.rs1use crate::{
6 core::{Result, types::ApiKey, types::BaseUrl},
7 http::{SerperHttpClient, TransportConfig},
8 search::{SearchQuery, SearchQueryBuilder, SearchResponse},
9};
10use std::time::Duration;
11
12#[derive(Debug)]
17pub struct SearchService {
18 http_client: SerperHttpClient,
19}
20
21impl SearchService {
22 pub fn new(api_key: String) -> Result<Self> {
32 let api_key = ApiKey::new(api_key)?;
33 let http_client = SerperHttpClient::new(api_key)?;
34
35 Ok(Self { http_client })
36 }
37
38 pub fn with_config(api_key: String, base_url: String, config: TransportConfig) -> Result<Self> {
50 let api_key = ApiKey::new(api_key)?;
51 let base_url = BaseUrl::new(base_url);
52 let http_client = SerperHttpClient::with_config(api_key, base_url, config)?;
53
54 Ok(Self { http_client })
55 }
56
57 pub async fn search(&self, query: &SearchQuery) -> Result<SearchResponse> {
67 self.http_client.search(query).await
68 }
69
70 pub async fn search_simple(&self, query_string: &str) -> Result<SearchResponse> {
80 let query = SearchQuery::new(query_string.to_string())?;
81 self.search(&query).await
82 }
83
84 pub async fn search_multiple(&self, queries: &[SearchQuery]) -> Result<Vec<SearchResponse>> {
94 self.http_client.search_multiple(queries).await
95 }
96
97 pub async fn search_concurrent(
108 &self,
109 queries: &[SearchQuery],
110 max_concurrent: Option<usize>,
111 ) -> Result<Vec<SearchResponse>> {
112 let max_concurrent = max_concurrent.unwrap_or(5);
113 self.http_client
114 .search_concurrent(queries, max_concurrent)
115 .await
116 }
117
118 pub fn query_builder(&self) -> SearchQueryBuilder {
124 SearchQueryBuilder::new()
125 }
126
127 pub async fn search_with<F>(&self, builder_fn: F) -> Result<SearchResponse>
158 where
159 F: FnOnce(SearchQueryBuilder) -> SearchQueryBuilder,
160 {
161 let query = builder_fn(self.query_builder()).build()?;
162 self.search(&query).await
163 }
164
165 pub fn info(&self) -> SearchServiceInfo {
167 SearchServiceInfo {
168 base_url: self.http_client.base_url().as_str().to_string(),
169 timeout: self.http_client.transport_config().timeout,
170 user_agent: self.http_client.transport_config().user_agent.clone(),
171 }
172 }
173}
174
175#[derive(Debug, Clone)]
177pub struct SearchServiceInfo {
178 pub base_url: String,
180 pub timeout: Duration,
182 pub user_agent: String,
184}
185
186pub struct SearchServiceBuilder {
188 api_key: Option<String>,
189 base_url: Option<String>,
190 transport_config: TransportConfig,
191}
192
193impl SearchServiceBuilder {
194 pub fn new() -> Self {
196 Self {
197 api_key: None,
198 base_url: None,
199 transport_config: TransportConfig::new(),
200 }
201 }
202
203 pub fn api_key(mut self, api_key: impl Into<String>) -> Self {
205 self.api_key = Some(api_key.into());
206 self
207 }
208
209 pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
211 self.base_url = Some(base_url.into());
212 self
213 }
214
215 pub fn timeout(mut self, timeout: Duration) -> Self {
217 self.transport_config = self.transport_config.with_timeout(timeout);
218 self
219 }
220
221 pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
223 self.transport_config = self.transport_config.with_header(key.into(), value.into());
224 self
225 }
226
227 pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
229 self.transport_config = self.transport_config.with_user_agent(user_agent.into());
230 self
231 }
232
233 pub fn build(self) -> Result<SearchService> {
235 let api_key = self
236 .api_key
237 .ok_or_else(|| crate::core::SerperError::config_error("API key is required"))?;
238
239 match self.base_url {
240 Some(base_url) => SearchService::with_config(api_key, base_url, self.transport_config),
241 None => {
242 let api_key_obj = ApiKey::new(api_key)?;
243 let http_client = SerperHttpClient::with_config(
244 api_key_obj,
245 BaseUrl::default(),
246 self.transport_config,
247 )?;
248 Ok(SearchService { http_client })
249 }
250 }
251 }
252}
253
254impl Default for SearchServiceBuilder {
255 fn default() -> Self {
256 Self::new()
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 #[test]
265 fn test_service_builder() {
266 let builder = SearchServiceBuilder::new()
267 .api_key("test-key")
268 .timeout(Duration::from_secs(60))
269 .user_agent("test-agent");
270
271 let service = builder.build().unwrap();
272 let info = service.info();
273
274 assert_eq!(info.timeout, Duration::from_secs(60));
275 assert_eq!(info.user_agent, "test-agent");
276 assert_eq!(info.base_url, "https://google.serper.dev");
277 }
278
279 #[test]
280 fn test_service_creation() {
281 let service = SearchService::new("test-key".to_string()).unwrap();
282 let info = service.info();
283
284 assert_eq!(info.base_url, "https://google.serper.dev");
285 assert_eq!(info.timeout, Duration::from_secs(30));
286 }
287
288 #[test]
289 fn test_builder_missing_api_key() {
290 let builder = SearchServiceBuilder::new();
291 let result = builder.build();
292 assert!(result.is_err());
293 }
294
295 #[test]
296 fn test_query_builder() {
297 let service = SearchService::new("test-key".to_string()).unwrap();
298 let builder = service.query_builder();
299
300 let query = builder
301 .query("test")
302 .location("Paris")
303 .page(1)
304 .build()
305 .unwrap();
306
307 assert_eq!(query.query(), "test");
308 assert_eq!(query.location, Some("Paris".to_string()));
309 assert_eq!(query.page, Some(1));
310 }
311}