use zeph_common::secret::Secret;
use zeph_config::tools::SearchConfig;
use super::brave::BraveSearchProvider;
#[derive(Debug, Clone, serde::Serialize)]
pub struct SearchResult {
pub title: String,
pub url: String,
pub snippet: String,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SearchError {
#[error("no API key configured for backend {backend}")]
MissingApiKey {
backend: &'static str,
},
#[error("HTTP error {status}: {message}")]
Http {
status: u16,
message: String,
},
#[error("request timed out")]
Timeout,
#[error("blocked: {reason}")]
Blocked {
reason: String,
status: Option<u16>,
},
#[error("failed to parse response: {0}")]
Parse(String),
#[error("provider error: {0}")]
Provider(String),
}
pub trait SearchProvider: Send + Sync {
fn search(
&self,
client: &reqwest::Client,
query: &str,
limit: usize,
) -> impl Future<Output = Result<Vec<SearchResult>, SearchError>> + Send;
fn endpoint(&self) -> &url::Url;
fn name(&self) -> &'static str;
}
#[derive(Debug)]
#[non_exhaustive]
pub enum SearchBackend {
Brave(BraveSearchProvider),
}
impl SearchBackend {
pub fn from_config(
cfg: &SearchConfig,
max_body_bytes: usize,
api_key: Option<Secret>,
) -> Result<Self, SearchError> {
match cfg.backend.as_str() {
"brave" => Ok(Self::Brave(BraveSearchProvider::new(
cfg,
max_body_bytes,
api_key,
)?)),
other => Err(SearchError::Provider(format!(
"unknown search backend: {other}"
))),
}
}
pub async fn search(
&self,
client: &reqwest::Client,
query: &str,
limit: usize,
) -> Result<Vec<SearchResult>, SearchError> {
match self {
Self::Brave(provider) => provider.search(client, query, limit).await,
}
}
#[must_use]
pub fn endpoint(&self) -> &url::Url {
match self {
Self::Brave(provider) => provider.endpoint(),
}
}
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::Brave(provider) => provider.name(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_config_unknown_backend_errors() {
let cfg = SearchConfig {
backend: "unknown".to_owned(),
..SearchConfig::default()
};
let err = SearchBackend::from_config(&cfg, 1_048_576, None).unwrap_err();
assert!(matches!(err, SearchError::Provider(_)));
}
#[test]
fn from_config_brave_without_key_errors() {
let cfg = SearchConfig::default();
let err = SearchBackend::from_config(&cfg, 1_048_576, None).unwrap_err();
assert!(matches!(
err,
SearchError::MissingApiKey { backend: "brave" }
));
}
#[test]
fn from_config_brave_with_key_succeeds() {
let cfg = SearchConfig::default();
let backend =
SearchBackend::from_config(&cfg, 1_048_576, Some(Secret::new("test-key"))).unwrap();
assert_eq!(backend.name(), "brave");
}
}