1pub mod error;
36pub mod multi_provider;
37pub mod providers;
38pub mod types;
39pub mod utils;
40
41pub use error::{SearchError, SearchResult as Result};
43pub use types::{DebugOptions, SearchOptions, SearchProvider, SearchResult};
44
45pub async fn web_search(options: SearchOptions) -> Result<Vec<SearchResult>> {
72 use error::SearchError;
73 use utils::debug;
74
75 if options.query.is_empty() && options.id_list.is_none() {
77 return Err(SearchError::InvalidInput(
78 "A search query or ID list (for Arxiv) is required".to_string(),
79 ));
80 }
81
82 debug::log(
84 &options.debug,
85 "Performing search",
86 &format!(
87 "provider: {}, query: {}",
88 options.provider.name(),
89 options.query
90 ),
91 );
92
93 match options.provider.search(&options).await {
95 Ok(results) => {
96 debug::log_response(
97 &options.debug,
98 &format!("Received {} results", results.len()),
99 );
100 Ok(results)
101 }
102 Err(error) => {
103 let troubleshooting = get_troubleshooting_info(options.provider.name(), &error);
104 let detailed_error = format!(
105 "Search with provider '{}' failed: {}\n\nTroubleshooting: {}",
106 options.provider.name(),
107 error,
108 troubleshooting
109 );
110
111 debug::log(&options.debug, "Search error", &detailed_error);
112 Err(SearchError::ProviderError(detailed_error))
113 }
114 }
115}
116
117fn get_troubleshooting_info(provider_name: &str, error: &SearchError) -> String {
119 let mut suggestions = String::new();
120
121 match error {
123 SearchError::HttpError {
124 status_code: Some(401 | 403),
125 ..
126 } => {
127 suggestions = "This is likely an authentication issue. Check your API key and make sure it's valid and has the correct permissions.".to_string();
128 }
129 SearchError::HttpError {
130 status_code: Some(400),
131 ..
132 } => {
133 suggestions = "This is likely due to invalid request parameters. Check your query and other search options.".to_string();
134 }
135 SearchError::HttpError {
136 status_code: Some(429),
137 ..
138 } => {
139 suggestions = "You've exceeded the rate limit for this API. Try again later or reduce your request frequency.".to_string();
140 }
141 SearchError::HttpError {
142 status_code: Some(500..=599),
143 ..
144 } => {
145 suggestions =
146 "The search provider is experiencing server issues. Try again later.".to_string();
147 }
148 _ => {}
149 }
150
151 match provider_name {
153 "google" => {
154 if suggestions.is_empty() {
155 suggestions = "Make sure your Google API key is valid and has the Custom Search API enabled. Also check if your Search Engine ID (cx) is correct.".to_string();
156 }
157 }
158 "serpapi" => {
159 if suggestions.is_empty() {
160 suggestions = "Check that your SerpAPI key is valid. Verify that you have enough credits remaining in your SerpAPI account.".to_string();
161 }
162 }
163 "brave" => {
164 if suggestions.is_empty() {
165 suggestions = "Ensure your Brave Search API token is valid. Check your subscription status in the Brave Developer Hub.".to_string();
166 }
167 }
168 "searxng" => {
169 if suggestions.is_empty() {
170 suggestions = "Check if your SearXNG instance URL is correct and that the server is running. Verify the format of your search URL.".to_string();
171 }
172 }
173 "duckduckgo" => {
174 if suggestions.is_empty() {
175 suggestions = "You may be making too many requests to DuckDuckGo. Try adding a delay between requests or reduce your request frequency.".to_string();
176 }
177 }
178 _ => {
179 if suggestions.is_empty() {
180 suggestions = format!(
181 "Check your {provider_name} API credentials and make sure your search request is valid."
182 );
183 }
184 }
185 }
186
187 suggestions
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193 use crate::types::*;
194 use async_trait::async_trait;
195
196 #[derive(Debug)]
198 struct MockProvider {
199 name: String,
200 should_error: bool,
201 error_type: Option<SearchError>,
202 results: Vec<SearchResult>,
203 }
204
205 impl MockProvider {
206 fn new(name: &str) -> Self {
207 Self {
208 name: name.to_string(),
209 should_error: false,
210 error_type: None,
211 results: vec![
212 SearchResult {
213 title: "Test Result 1".to_string(),
214 url: "https://example.com/1".to_string(),
215 snippet: Some("Test content 1".to_string()),
216 domain: None,
217 published_date: None,
218 provider: Some(name.to_string()),
219 raw: None,
220 },
221 SearchResult {
222 title: "Test Result 2".to_string(),
223 url: "https://example.com/2".to_string(),
224 snippet: Some("Test content 2".to_string()),
225 domain: None,
226 published_date: None,
227 provider: Some(name.to_string()),
228 raw: None,
229 },
230 ],
231 }
232 }
233
234 fn with_error(mut self, error: SearchError) -> Self {
235 self.should_error = true;
236 self.error_type = Some(error);
237 self
238 }
239
240 fn with_results(mut self, results: Vec<SearchResult>) -> Self {
241 self.results = results;
242 self
243 }
244 }
245
246 #[async_trait]
247 impl SearchProvider for MockProvider {
248 fn name(&self) -> &str {
249 &self.name
250 }
251
252 async fn search(&self, _options: &SearchOptions) -> Result<Vec<SearchResult>> {
253 if self.should_error {
254 Err(self
255 .error_type
256 .clone()
257 .unwrap_or(SearchError::Other("Mock error".to_string())))
258 } else {
259 Ok(self.results.clone())
260 }
261 }
262 }
263
264 #[tokio::test]
265 async fn test_web_search_success() {
266 let provider = MockProvider::new("test");
267 let options = SearchOptions {
268 query: "test query".to_string(),
269 provider: Box::new(provider),
270 ..Default::default()
271 };
272
273 let results = web_search(options).await.unwrap();
274 assert_eq!(results.len(), 2);
275 assert_eq!(results[0].title, "Test Result 1");
276 assert_eq!(results[0].url, "https://example.com/1");
277 assert_eq!(results[0].provider, Some("test".to_string()));
278 }
279
280 #[tokio::test]
281 async fn test_web_search_empty_query() {
282 let provider = MockProvider::new("test");
283 let options = SearchOptions {
284 query: "".to_string(),
285 provider: Box::new(provider),
286 ..Default::default()
287 };
288
289 let result = web_search(options).await;
290 assert!(result.is_err());
291 match result.unwrap_err() {
292 SearchError::InvalidInput(msg) => {
293 assert!(msg.contains("search query or ID list"));
294 }
295 _ => panic!("Expected InvalidInput error"),
296 }
297 }
298
299 #[tokio::test]
300 async fn test_web_search_provider_error() {
301 let provider = MockProvider::new("test").with_error(SearchError::HttpError {
302 status_code: Some(401),
303 message: "Unauthorized".to_string(),
304 response_body: None,
305 });
306 let options = SearchOptions {
307 query: "test query".to_string(),
308 provider: Box::new(provider),
309 ..Default::default()
310 };
311
312 let result = web_search(options).await;
313 assert!(result.is_err());
314 match result.unwrap_err() {
315 SearchError::ProviderError(msg) => {
316 assert!(msg.contains("failed"));
317 assert!(msg.contains("authentication issue"));
318 }
319 _ => panic!("Expected ProviderError"),
320 }
321 }
322
323 #[tokio::test]
324 async fn test_troubleshooting_info_http_errors() {
325 let test_cases = vec![
326 (
327 SearchError::HttpError {
328 status_code: Some(401),
329 message: "Unauthorized".to_string(),
330 response_body: None,
331 },
332 "authentication issue",
333 ),
334 (
335 SearchError::HttpError {
336 status_code: Some(403),
337 message: "Forbidden".to_string(),
338 response_body: None,
339 },
340 "authentication issue",
341 ),
342 (
343 SearchError::HttpError {
344 status_code: Some(400),
345 message: "Bad Request".to_string(),
346 response_body: None,
347 },
348 "invalid request parameters",
349 ),
350 (
351 SearchError::HttpError {
352 status_code: Some(429),
353 message: "Too Many Requests".to_string(),
354 response_body: None,
355 },
356 "rate limit",
357 ),
358 (
359 SearchError::HttpError {
360 status_code: Some(500),
361 message: "Internal Server Error".to_string(),
362 response_body: None,
363 },
364 "server issues",
365 ),
366 ];
367
368 for (error, expected_text) in test_cases {
369 let info = get_troubleshooting_info("test", &error);
370 assert!(
371 info.to_lowercase().contains(expected_text),
372 "Expected '{info}' to contain '{expected_text}'"
373 );
374 }
375 }
376
377 #[tokio::test]
378 async fn test_troubleshooting_info_providers() {
379 let providers = vec![
380 ("google", "Google API key"),
381 ("serpapi", "SerpAPI key"),
382 ("brave", "Brave Search API token"),
383 ("searxng", "SearXNG instance URL"),
384 ("duckduckgo", "too many requests"),
385 ];
386
387 let generic_error = SearchError::Other("test error".to_string());
388
389 for (provider, expected_text) in providers {
390 let info = get_troubleshooting_info(provider, &generic_error);
391 assert!(
392 info.contains(expected_text),
393 "Expected troubleshooting for '{provider}' to contain '{expected_text}'"
394 );
395 }
396 }
397
398 #[tokio::test]
399 async fn test_web_search_with_arxiv_id_list() {
400 let provider = MockProvider::new("arxiv");
401 let options = SearchOptions {
402 query: "".to_string(), id_list: Some("1234.5678,2345.6789".to_string()),
404 provider: Box::new(provider),
405 ..Default::default()
406 };
407
408 let results = web_search(options).await.unwrap();
409 assert_eq!(results.len(), 2);
410 }
411
412 #[tokio::test]
413 async fn test_web_search_max_results() {
414 let results = vec![
415 SearchResult {
416 title: "Result 1".to_string(),
417 url: "https://example.com/1".to_string(),
418 snippet: Some("Content 1".to_string()),
419 domain: None,
420 published_date: None,
421 provider: Some("test".to_string()),
422 raw: None,
423 },
424 SearchResult {
425 title: "Result 2".to_string(),
426 url: "https://example.com/2".to_string(),
427 snippet: Some("Content 2".to_string()),
428 domain: None,
429 published_date: None,
430 provider: Some("test".to_string()),
431 raw: None,
432 },
433 SearchResult {
434 title: "Result 3".to_string(),
435 url: "https://example.com/3".to_string(),
436 snippet: Some("Content 3".to_string()),
437 domain: None,
438 published_date: None,
439 provider: Some("test".to_string()),
440 raw: None,
441 },
442 ];
443
444 let provider = MockProvider::new("test").with_results(results);
445 let options = SearchOptions {
446 query: "test".to_string(),
447 max_results: Some(2),
448 provider: Box::new(provider),
449 ..Default::default()
450 };
451
452 let search_results = web_search(options).await.unwrap();
453 assert!(search_results.len() >= 2);
455 }
456}