use std::fmt;
pub trait SearchProvider: Send + Sync {
fn search(
&self,
query: &str,
options: &SearchOptions,
client: Option<&reqwest::Client>,
) -> impl std::future::Future<Output = Result<SearchResults, SearchError>> + Send;
fn provider_name(&self) -> &'static str;
}
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SearchOptions {
pub limit: Option<usize>,
pub country: Option<String>,
pub language: Option<String>,
pub site_filter: Option<Vec<String>>,
pub exclude_domains: Option<Vec<String>>,
pub time_range: Option<TimeRange>,
pub include_keywords: Option<Vec<String>>,
}
impl SearchOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
pub fn with_country(mut self, country: impl Into<String>) -> Self {
self.country = Some(country.into());
self
}
pub fn with_language(mut self, language: impl Into<String>) -> Self {
self.language = Some(language.into());
self
}
pub fn with_site_filter(mut self, domains: Vec<String>) -> Self {
self.site_filter = Some(domains);
self
}
pub fn with_exclude_domains(mut self, domains: Vec<String>) -> Self {
self.exclude_domains = Some(domains);
self
}
pub fn with_time_range(mut self, range: TimeRange) -> Self {
self.time_range = Some(range);
self
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TimeRange {
Day,
Week,
Month,
Year,
Custom {
start: String,
end: String,
},
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SearchResults {
pub query: String,
pub results: Vec<SearchResult>,
pub total_results: Option<u64>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub metadata: Option<serde_json::Value>,
}
impl SearchResults {
pub fn new(query: impl Into<String>) -> Self {
Self {
query: query.into(),
results: Vec::new(),
total_results: None,
metadata: None,
}
}
pub fn push(&mut self, result: SearchResult) {
self.results.push(result);
}
pub fn urls(&self) -> Vec<&str> {
self.results.iter().map(|r| r.url.as_str()).collect()
}
pub fn is_empty(&self) -> bool {
self.results.is_empty()
}
pub fn len(&self) -> usize {
self.results.len()
}
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SearchResult {
pub title: String,
pub url: String,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub snippet: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub date: Option<String>,
pub position: usize,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub score: Option<f32>,
}
impl SearchResult {
pub fn new(title: impl Into<String>, url: impl Into<String>, position: usize) -> Self {
Self {
title: title.into(),
url: url.into(),
snippet: None,
date: None,
position,
score: None,
}
}
pub fn with_snippet(mut self, snippet: impl Into<String>) -> Self {
self.snippet = Some(snippet.into());
self
}
pub fn with_date(mut self, date: impl Into<String>) -> Self {
self.date = Some(date.into());
self
}
pub fn with_score(mut self, score: f32) -> Self {
self.score = Some(score);
self
}
}
#[derive(Debug)]
pub enum SearchError {
RequestFailed(String),
AuthenticationFailed,
RateLimited,
InvalidQuery(String),
ProviderError(String),
NoProvider,
}
impl fmt::Display for SearchError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::RequestFailed(msg) => write!(f, "Search request failed: {}", msg),
Self::AuthenticationFailed => write!(f, "Search authentication failed"),
Self::RateLimited => write!(f, "Search rate limit exceeded"),
Self::InvalidQuery(msg) => write!(f, "Invalid search query: {}", msg),
Self::ProviderError(msg) => write!(f, "Search provider error: {}", msg),
Self::NoProvider => write!(f, "No search provider configured"),
}
}
}
impl std::error::Error for SearchError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_search_options_builder() {
let opts = SearchOptions::new()
.with_limit(10)
.with_country("us")
.with_language("en");
assert_eq!(opts.limit, Some(10));
assert_eq!(opts.country.as_deref(), Some("us"));
assert_eq!(opts.language.as_deref(), Some("en"));
}
#[test]
fn test_search_result_builder() {
let result = SearchResult::new("Test Title", "https://example.com", 1)
.with_snippet("Test snippet")
.with_score(0.95);
assert_eq!(result.title, "Test Title");
assert_eq!(result.url, "https://example.com");
assert_eq!(result.position, 1);
assert_eq!(result.snippet.as_deref(), Some("Test snippet"));
assert_eq!(result.score, Some(0.95));
}
#[test]
fn test_search_results() {
let mut results = SearchResults::new("test query");
results.push(SearchResult::new("Title 1", "https://example1.com", 1));
results.push(SearchResult::new("Title 2", "https://example2.com", 2));
assert_eq!(results.len(), 2);
assert!(!results.is_empty());
assert_eq!(
results.urls(),
vec!["https://example1.com", "https://example2.com"]
);
}
}