1use serde::Deserialize;
6use std::collections::HashMap;
7
8#[derive(Debug, Deserialize, PartialEq)]
13pub struct SearchResponse {
14 pub search_metadata: Option<SearchMetadata>,
16
17 pub organic: Option<Vec<OrganicResult>>,
19
20 pub answer_box: Option<AnswerBox>,
22
23 pub knowledge_graph: Option<KnowledgeGraph>,
25
26 pub related_questions: Option<Vec<RelatedQuestion>>,
28
29 pub shopping: Option<Vec<ShoppingResult>>,
31
32 pub news: Option<Vec<NewsResult>>,
34}
35
36impl SearchResponse {
37 pub fn new() -> Self {
39 Self {
40 search_metadata: None,
41 organic: None,
42 answer_box: None,
43 knowledge_graph: None,
44 related_questions: None,
45 shopping: None,
46 news: None,
47 }
48 }
49
50 pub fn has_results(&self) -> bool {
52 self.organic.as_ref().is_some_and(|o| !o.is_empty())
53 || self.answer_box.is_some()
54 || self.knowledge_graph.is_some()
55 || self.shopping.as_ref().is_some_and(|s| !s.is_empty())
56 || self.news.as_ref().is_some_and(|n| !n.is_empty())
57 }
58
59 pub fn organic_count(&self) -> usize {
61 self.organic.as_ref().map_or(0, |o| o.len())
62 }
63
64 pub fn organic_results(&self) -> &[OrganicResult] {
66 self.organic.as_deref().unwrap_or(&[])
67 }
68
69 pub fn first_result(&self) -> Option<&OrganicResult> {
71 self.organic.as_ref()?.first()
72 }
73
74 pub fn extract_urls(&self) -> Vec<&str> {
76 self.organic_results()
77 .iter()
78 .map(|result| result.link.as_str())
79 .collect()
80 }
81}
82
83impl Default for SearchResponse {
84 fn default() -> Self {
85 Self::new()
86 }
87}
88
89#[derive(Debug, Deserialize, PartialEq)]
91pub struct SearchMetadata {
92 pub id: String,
94
95 pub status: String,
97
98 pub created_at: String,
100
101 pub request_time_taken: f64,
103
104 pub total_time_taken: f64,
106}
107
108#[derive(Debug, Deserialize, PartialEq, Clone)]
110pub struct OrganicResult {
111 pub title: String,
113
114 pub link: String,
116
117 pub snippet: Option<String>,
119
120 pub position: u32,
122
123 #[serde(flatten)]
125 pub extra: HashMap<String, serde_json::Value>,
126}
127
128impl OrganicResult {
129 pub fn new(title: String, link: String, position: u32) -> Self {
131 Self {
132 title,
133 link,
134 snippet: None,
135 position,
136 extra: HashMap::new(),
137 }
138 }
139
140 pub fn has_snippet(&self) -> bool {
142 self.snippet.is_some()
143 }
144
145 pub fn snippet_or_default(&self) -> &str {
147 self.snippet
148 .as_deref()
149 .unwrap_or("No description available")
150 }
151
152 pub fn domain(&self) -> Option<String> {
154 url::Url::parse(&self.link)
155 .ok()?
156 .host_str()
157 .map(|host| host.to_string())
158 }
159}
160
161#[derive(Debug, Deserialize, PartialEq)]
163pub struct AnswerBox {
164 pub answer: Option<String>,
166
167 pub snippet: Option<String>,
169
170 pub title: Option<String>,
172
173 pub link: Option<String>,
175}
176
177impl AnswerBox {
178 pub fn has_answer(&self) -> bool {
180 self.answer.is_some()
181 }
182
183 pub fn best_text(&self) -> Option<&str> {
185 self.answer.as_deref().or(self.snippet.as_deref())
186 }
187}
188
189#[derive(Debug, Deserialize, PartialEq)]
191pub struct KnowledgeGraph {
192 pub title: Option<String>,
194
195 pub description: Option<String>,
197
198 #[serde(rename = "type")]
200 pub entity_type: Option<String>,
201
202 pub website: Option<String>,
204
205 #[serde(flatten)]
207 pub attributes: HashMap<String, serde_json::Value>,
208}
209
210#[derive(Debug, Deserialize, PartialEq)]
212pub struct RelatedQuestion {
213 pub question: String,
215
216 pub snippet: Option<String>,
218
219 pub title: Option<String>,
221
222 pub link: Option<String>,
224}
225
226#[derive(Debug, Deserialize, PartialEq)]
228pub struct ShoppingResult {
229 pub title: String,
231
232 pub link: String,
234
235 pub price: Option<String>,
237
238 pub source: Option<String>,
240
241 pub image: Option<String>,
243
244 pub position: u32,
246}
247
248#[derive(Debug, Deserialize, PartialEq)]
250pub struct NewsResult {
251 pub title: String,
253
254 pub link: String,
256
257 pub snippet: Option<String>,
259
260 pub source: Option<String>,
262
263 pub date: Option<String>,
265
266 pub position: u32,
268}
269
270pub struct ResponseParser;
272
273impl ResponseParser {
274 pub fn parse_response(json_str: &str) -> crate::core::Result<SearchResponse> {
284 serde_json::from_str(json_str).map_err(crate::core::error::SerperError::Json)
285 }
286
287 pub fn validate_response(response: &SearchResponse) -> crate::core::Result<()> {
289 if let Some(metadata) = &response.search_metadata
291 && metadata.id.is_empty()
292 {
293 return Err(crate::core::error::SerperError::validation_error(
294 "Response metadata has empty ID",
295 ));
296 }
297
298 if let Some(organic) = &response.organic {
300 for (idx, result) in organic.iter().enumerate() {
301 if result.title.is_empty() {
302 return Err(crate::core::error::SerperError::validation_error(format!(
303 "Organic result {} has empty title",
304 idx
305 )));
306 }
307 if result.link.is_empty() {
308 return Err(crate::core::error::SerperError::validation_error(format!(
309 "Organic result {} has empty link",
310 idx
311 )));
312 }
313 }
314 }
315
316 Ok(())
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323 use serde_json::json;
324
325 #[test]
326 fn test_search_response_creation() {
327 let response = SearchResponse::new();
328 assert!(!response.has_results());
329 assert_eq!(response.organic_count(), 0);
330 }
331
332 #[test]
333 fn test_organic_result() {
334 let result = OrganicResult::new(
335 "Test Title".to_string(),
336 "https://example.com".to_string(),
337 1,
338 );
339
340 assert_eq!(result.title, "Test Title");
341 assert_eq!(result.position, 1);
342 assert!(!result.has_snippet());
343 assert_eq!(result.snippet_or_default(), "No description available");
344 }
345
346 #[test]
347 fn test_response_parsing() {
348 let json_data = json!({
349 "search_metadata": {
350 "id": "test-123",
351 "status": "Success",
352 "created_at": "2023-01-01T00:00:00Z",
353 "request_time_taken": 0.5,
354 "total_time_taken": 1.0
355 },
356 "organic": [
357 {
358 "title": "Test Result",
359 "link": "https://example.com",
360 "snippet": "Test snippet",
361 "position": 1
362 }
363 ]
364 });
365
366 let response: SearchResponse = serde_json::from_value(json_data).unwrap();
367 assert!(response.has_results());
368 assert_eq!(response.organic_count(), 1);
369
370 let first = response.first_result().unwrap();
371 assert_eq!(first.title, "Test Result");
372 }
373
374 #[test]
375 fn test_answer_box() {
376 let answer_box = AnswerBox {
377 answer: Some("42".to_string()),
378 snippet: Some("The answer to everything".to_string()),
379 title: None,
380 link: None,
381 };
382
383 assert!(answer_box.has_answer());
384 assert_eq!(answer_box.best_text(), Some("42"));
385 }
386
387 #[test]
388 fn test_response_validation() {
389 let mut response = SearchResponse::new();
390
391 assert!(ResponseParser::validate_response(&response).is_ok());
393
394 response.organic = Some(vec![OrganicResult {
396 title: "".to_string(),
397 link: "https://example.com".to_string(),
398 snippet: None,
399 position: 1,
400 extra: HashMap::new(),
401 }]);
402
403 assert!(ResponseParser::validate_response(&response).is_err());
404 }
405}