serper_sdk/search/
query.rs

1use crate::core::{
2    error::{Result, SerperError},
3    types::{Location, Pagination},
4};
5/// Search query construction and validation module
6///
7/// This module provides functionality for building and validating search queries
8/// with type-safe parameter handling and fluent builder patterns.
9use serde::{Deserialize, Serialize};
10
11/// Represents a search query with all possible parameters
12///
13/// This struct encapsulates all the parameters that can be sent to the Serper API
14/// for search requests, with optional fields for flexible query construction.
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
16pub struct SearchQuery {
17    /// The search query string (required)
18    pub q: String,
19
20    /// Optional location specification
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub location: Option<String>,
23
24    /// Optional country code (gl parameter)
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub gl: Option<String>,
27
28    /// Optional language code (hl parameter)
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub hl: Option<String>,
31
32    /// Optional page number for pagination
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub page: Option<u32>,
35
36    /// Optional number of results per page
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub num: Option<u32>,
39}
40
41impl SearchQuery {
42    /// Creates a new search query with the specified query string
43    ///
44    /// # Arguments
45    ///
46    /// * `query` - The search query string
47    ///
48    /// # Returns
49    ///
50    /// A Result containing the SearchQuery or an error if validation fails
51    pub fn new(query: String) -> Result<Self> {
52        if query.trim().is_empty() {
53            return Err(SerperError::validation_error(
54                "Query string cannot be empty",
55            ));
56        }
57
58        Ok(Self {
59            q: query,
60            location: None,
61            gl: None,
62            hl: None,
63            page: None,
64            num: None,
65        })
66    }
67
68    /// Sets the location for the search query
69    ///
70    /// # Arguments
71    ///
72    /// * `location` - The location string (e.g., "Paris, France")
73    pub fn with_location(mut self, location: String) -> Self {
74        self.location = Some(location);
75        self
76    }
77
78    /// Sets the country code for the search query
79    ///
80    /// # Arguments
81    ///
82    /// * `country` - The country code (e.g., "fr", "us")
83    pub fn with_country(mut self, country: String) -> Self {
84        self.gl = Some(country);
85        self
86    }
87
88    /// Sets the language code for the search query
89    ///
90    /// # Arguments
91    ///
92    /// * `language` - The language code (e.g., "en", "fr")
93    pub fn with_language(mut self, language: String) -> Self {
94        self.hl = Some(language);
95        self
96    }
97
98    /// Sets the page number for pagination
99    ///
100    /// # Arguments
101    ///
102    /// * `page` - The page number (1-based)
103    pub fn with_page(mut self, page: u32) -> Self {
104        self.page = Some(page);
105        self
106    }
107
108    /// Sets the number of results per page
109    ///
110    /// # Arguments
111    ///
112    /// * `num` - The number of results (typically 1-100)
113    pub fn with_num_results(mut self, num: u32) -> Self {
114        self.num = Some(num);
115        self
116    }
117
118    /// Applies location settings from a Location struct
119    ///
120    /// # Arguments
121    ///
122    /// * `location` - The location configuration
123    pub fn with_location_config(mut self, location: Location) -> Self {
124        if let Some(loc) = location.location {
125            self.location = Some(loc);
126        }
127        if let Some(country) = location.country_code {
128            self.gl = Some(country);
129        }
130        if let Some(language) = location.language_code {
131            self.hl = Some(language);
132        }
133        self
134    }
135
136    /// Applies pagination settings from a Pagination struct
137    ///
138    /// # Arguments
139    ///
140    /// * `pagination` - The pagination configuration
141    pub fn with_pagination(mut self, pagination: Pagination) -> Self {
142        if let Some(page) = pagination.page {
143            self.page = Some(page);
144        }
145        if let Some(num) = pagination.num_results {
146            self.num = Some(num);
147        }
148        self
149    }
150
151    /// Validates the search query parameters
152    ///
153    /// # Returns
154    ///
155    /// Result indicating whether the query is valid
156    pub fn validate(&self) -> Result<()> {
157        if self.q.trim().is_empty() {
158            return Err(SerperError::validation_error(
159                "Query string cannot be empty",
160            ));
161        }
162
163        if let Some(page) = self.page
164            && page == 0
165        {
166            return Err(SerperError::validation_error(
167                "Page number must be greater than 0",
168            ));
169        }
170
171        if let Some(num) = self.num
172            && (num == 0 || num > 100)
173        {
174            return Err(SerperError::validation_error(
175                "Number of results must be between 1 and 100",
176            ));
177        }
178
179        Ok(())
180    }
181
182    /// Gets the query string
183    pub fn query(&self) -> &str {
184        &self.q
185    }
186
187    /// Checks if the query has location parameters
188    pub fn has_location_params(&self) -> bool {
189        self.location.is_some() || self.gl.is_some() || self.hl.is_some()
190    }
191
192    /// Checks if the query has pagination parameters
193    pub fn has_pagination_params(&self) -> bool {
194        self.page.is_some() || self.num.is_some()
195    }
196}
197
198/// Builder for creating search queries with validation
199pub struct SearchQueryBuilder {
200    query: Option<String>,
201    location: Option<String>,
202    country: Option<String>,
203    language: Option<String>,
204    page: Option<u32>,
205    num_results: Option<u32>,
206}
207
208impl SearchQueryBuilder {
209    /// Creates a new search query builder
210    pub fn new() -> Self {
211        Self {
212            query: None,
213            location: None,
214            country: None,
215            language: None,
216            page: None,
217            num_results: None,
218        }
219    }
220
221    /// Sets the search query string
222    pub fn query(mut self, query: impl Into<String>) -> Self {
223        self.query = Some(query.into());
224        self
225    }
226
227    /// Sets the location
228    pub fn location(mut self, location: impl Into<String>) -> Self {
229        self.location = Some(location.into());
230        self
231    }
232
233    /// Sets the country code
234    pub fn country(mut self, country: impl Into<String>) -> Self {
235        self.country = Some(country.into());
236        self
237    }
238
239    /// Sets the language code
240    pub fn language(mut self, language: impl Into<String>) -> Self {
241        self.language = Some(language.into());
242        self
243    }
244
245    /// Sets the page number
246    pub fn page(mut self, page: u32) -> Self {
247        self.page = Some(page);
248        self
249    }
250
251    /// Sets the number of results
252    pub fn num_results(mut self, num: u32) -> Self {
253        self.num_results = Some(num);
254        self
255    }
256
257    /// Builds the search query with validation
258    pub fn build(self) -> Result<SearchQuery> {
259        let query = self
260            .query
261            .ok_or_else(|| SerperError::validation_error("Query string is required"))?;
262
263        let mut search_query = SearchQuery::new(query)?;
264
265        if let Some(location) = self.location {
266            search_query = search_query.with_location(location);
267        }
268        if let Some(country) = self.country {
269            search_query = search_query.with_country(country);
270        }
271        if let Some(language) = self.language {
272            search_query = search_query.with_language(language);
273        }
274        if let Some(page) = self.page {
275            search_query = search_query.with_page(page);
276        }
277        if let Some(num) = self.num_results {
278            search_query = search_query.with_num_results(num);
279        }
280
281        search_query.validate()?;
282        Ok(search_query)
283    }
284}
285
286impl Default for SearchQueryBuilder {
287    fn default() -> Self {
288        Self::new()
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295
296    #[test]
297    fn test_search_query_new() {
298        let query = SearchQuery::new("test query".to_string()).unwrap();
299        assert_eq!(query.q, "test query");
300        assert_eq!(query.location, None);
301        assert_eq!(query.gl, None);
302        assert_eq!(query.hl, None);
303        assert_eq!(query.page, None);
304        assert_eq!(query.num, None);
305    }
306
307    #[test]
308    fn test_search_query_empty_fails() {
309        let result = SearchQuery::new("".to_string());
310        assert!(result.is_err());
311    }
312
313    #[test]
314    fn test_search_query_with_location() {
315        let query = SearchQuery::new("test".to_string())
316            .unwrap()
317            .with_location("Paris".to_string());
318        assert_eq!(query.location, Some("Paris".to_string()));
319    }
320
321    #[test]
322    fn test_search_query_builder() {
323        let query = SearchQueryBuilder::new()
324            .query("test query")
325            .location("Paris")
326            .country("fr")
327            .language("en")
328            .page(1)
329            .num_results(10)
330            .build()
331            .unwrap();
332
333        assert_eq!(query.q, "test query");
334        assert_eq!(query.location, Some("Paris".to_string()));
335        assert_eq!(query.gl, Some("fr".to_string()));
336        assert_eq!(query.hl, Some("en".to_string()));
337        assert_eq!(query.page, Some(1));
338        assert_eq!(query.num, Some(10));
339    }
340
341    #[test]
342    fn test_search_query_validation() {
343        let query = SearchQuery::new("test".to_string()).unwrap().with_page(0);
344
345        assert!(query.validate().is_err());
346
347        let query = SearchQuery::new("test".to_string())
348            .unwrap()
349            .with_num_results(101);
350
351        assert!(query.validate().is_err());
352    }
353
354    #[test]
355    fn test_search_query_helper_methods() {
356        let query = SearchQuery::new("test".to_string())
357            .unwrap()
358            .with_location("Paris".to_string())
359            .with_page(1);
360
361        assert!(query.has_location_params());
362        assert!(query.has_pagination_params());
363        assert_eq!(query.query(), "test");
364    }
365}