serper_sdk/search/
service.rs

1/// Search service orchestration module
2///
3/// This module provides the main search service that orchestrates
4/// query building, HTTP requests, and response processing.
5use crate::{
6    core::{Result, types::ApiKey, types::BaseUrl},
7    http::{SerperHttpClient, TransportConfig},
8    search::{SearchQuery, SearchQueryBuilder, SearchResponse},
9};
10use std::time::Duration;
11
12/// Main search service for the Serper SDK
13///
14/// This service provides the primary interface for search operations,
15/// combining query building, HTTP client management, and response processing.
16#[derive(Debug)]
17pub struct SearchService {
18    http_client: SerperHttpClient,
19}
20
21impl SearchService {
22    /// Creates a new search service with the specified API key
23    ///
24    /// # Arguments
25    ///
26    /// * `api_key` - The Serper API key
27    ///
28    /// # Returns
29    ///
30    /// Result containing the search service or an error
31    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    /// Creates a new search service with custom configuration
39    ///
40    /// # Arguments
41    ///
42    /// * `api_key` - The Serper API key
43    /// * `base_url` - Custom base URL for the API
44    /// * `config` - Transport configuration
45    ///
46    /// # Returns
47    ///
48    /// Result containing the search service or an error
49    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    /// Performs a search with the given query
58    ///
59    /// # Arguments
60    ///
61    /// * `query` - The search query to execute
62    ///
63    /// # Returns
64    ///
65    /// Result containing the search response or an error
66    pub async fn search(&self, query: &SearchQuery) -> Result<SearchResponse> {
67        self.http_client.search(query).await
68    }
69
70    /// Performs a search with a simple query string
71    ///
72    /// # Arguments
73    ///
74    /// * `query_string` - The search query string
75    ///
76    /// # Returns
77    ///
78    /// Result containing the search response or an error
79    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    /// Performs multiple searches in sequence
85    ///
86    /// # Arguments
87    ///
88    /// * `queries` - The search queries to execute
89    ///
90    /// # Returns
91    ///
92    /// Result containing a vector of search responses or an error
93    pub async fn search_multiple(&self, queries: &[SearchQuery]) -> Result<Vec<SearchResponse>> {
94        self.http_client.search_multiple(queries).await
95    }
96
97    /// Performs multiple searches concurrently
98    ///
99    /// # Arguments
100    ///
101    /// * `queries` - The search queries to execute
102    /// * `max_concurrent` - Maximum number of concurrent requests (default: 5)
103    ///
104    /// # Returns
105    ///
106    /// Result containing a vector of search responses or an error
107    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    /// Creates a new query builder
119    ///
120    /// # Returns
121    ///
122    /// A SearchQueryBuilder instance for fluent query construction
123    pub fn query_builder(&self) -> SearchQueryBuilder {
124        SearchQueryBuilder::new()
125    }
126
127    /// Searches with query builder pattern
128    ///
129    /// # Arguments
130    ///
131    /// * `builder_fn` - Function to configure the query builder
132    ///
133    /// # Returns
134    ///
135    /// Result containing the search response or an error
136    ///
137    /// # Example
138    ///
139    /// ```rust
140    /// use serper_sdk::{SearchService, SearchQueryBuilder};
141    ///
142    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
143    ///     let _service = SearchService::new("demo-key-for-docs".to_string())?;
144    ///     
145    ///     // Demonstrate the builder pattern structure
146    ///     let query = SearchQueryBuilder::new()
147    ///         .query("Hamze Ghalebi CTO at Remolab")
148    ///         .location("Paris")
149    ///         .page(1)
150    ///         .build()?;
151    ///     
152    ///     println!("Query built with search_with pattern: {}", query.q);
153    ///     // In real async usage: service.search_with(|builder| { ... }).await?
154    ///     Ok(())
155    /// }
156    /// ```
157    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    /// Gets information about the current service configuration
166    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/// Information about the search service configuration
176#[derive(Debug, Clone)]
177pub struct SearchServiceInfo {
178    /// The base URL being used for API requests
179    pub base_url: String,
180    /// Request timeout duration
181    pub timeout: Duration,
182    /// User agent string
183    pub user_agent: String,
184}
185
186/// Builder for creating search services with custom configuration
187pub struct SearchServiceBuilder {
188    api_key: Option<String>,
189    base_url: Option<String>,
190    transport_config: TransportConfig,
191}
192
193impl SearchServiceBuilder {
194    /// Creates a new search service builder
195    pub fn new() -> Self {
196        Self {
197            api_key: None,
198            base_url: None,
199            transport_config: TransportConfig::new(),
200        }
201    }
202
203    /// Sets the API key
204    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    /// Sets the base URL
210    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    /// Sets the request timeout
216    pub fn timeout(mut self, timeout: Duration) -> Self {
217        self.transport_config = self.transport_config.with_timeout(timeout);
218        self
219    }
220
221    /// Adds a default header
222    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    /// Sets the user agent
228    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    /// Builds the search service
234    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}