use super::types::{AccessMethod, SearchEngineType};
use crate::constants::{
ENV_BRAVE_API_KEY, ENV_GEMINI_API_KEY, ENV_SEARX_HOST, ENV_SERPER_API_KEY, ENV_TAVILY_API_KEY,
};
use crate::error::TarziError;
use crate::search::api::searxng::normalize_searx_endpoint;
pub fn resolve_api_key(
engine: SearchEngineType,
config_api_key: &Option<String>,
) -> Option<String> {
let env_name = match engine {
SearchEngineType::BraveSearch => Some(ENV_BRAVE_API_KEY),
SearchEngineType::GoogleSerper => Some(ENV_SERPER_API_KEY),
SearchEngineType::Tavily => Some(ENV_TAVILY_API_KEY),
SearchEngineType::GoogleAi => Some(ENV_GEMINI_API_KEY),
_ => None,
};
if let Some(name) = env_name
&& let Ok(value) = std::env::var(name)
&& !value.is_empty()
{
return Some(value);
}
if engine.requires_api_key() {
return config_api_key.as_ref().filter(|k| !k.is_empty()).cloned();
}
config_api_key.as_ref().filter(|k| !k.is_empty()).cloned()
}
pub fn resolve_base_url(
engine: SearchEngineType,
config_base_url: &Option<String>,
) -> Option<String> {
if !engine.requires_base_url() {
return None;
}
if let Ok(value) = std::env::var(ENV_SEARX_HOST)
&& !value.is_empty()
{
return Some(normalize_searx_endpoint(&value));
}
config_base_url
.as_ref()
.filter(|u| !u.is_empty())
.map(|u| normalize_searx_endpoint(u))
}
pub fn has_api_credentials(
engine: SearchEngineType,
api_key: &Option<String>,
base_url: &Option<String>,
) -> bool {
if engine.requires_base_url() {
base_url.as_ref().is_some_and(|u| !u.is_empty())
} else {
api_key.as_ref().is_some_and(|k| !k.is_empty())
}
}
pub fn resolve_access(
engine: SearchEngineType,
has_credentials: bool,
browser_enabled: bool,
) -> Result<Vec<AccessMethod>, TarziError> {
let mut methods = Vec::new();
if engine.supports_api() && has_credentials {
methods.push(AccessMethod::Api);
}
if engine.supports_web() {
methods.push(AccessMethod::PlainHttp);
if browser_enabled {
methods.push(AccessMethod::Browser);
}
}
if methods.is_empty() {
if engine.is_api_only() && !has_credentials {
return Err(TarziError::Search(engine.missing_credentials_message()));
}
return Err(TarziError::Search(format!(
"No access methods available for engine {engine:?}"
)));
}
Ok(methods)
}
#[cfg(test)]
mod tests {
use super::*;
fn all_engines() -> Vec<SearchEngineType> {
vec![
SearchEngineType::Bing,
SearchEngineType::DuckDuckGo,
SearchEngineType::Google,
SearchEngineType::GoogleSerper,
SearchEngineType::BraveSearch,
SearchEngineType::Baidu,
SearchEngineType::SougouWeixin,
SearchEngineType::Tavily,
SearchEngineType::GoogleAi,
SearchEngineType::SearxNG,
]
}
fn web_only_engines() -> Vec<SearchEngineType> {
all_engines()
.into_iter()
.filter(|e| e.supports_web() && !e.supports_api())
.collect()
}
fn api_only_engines() -> Vec<SearchEngineType> {
all_engines()
.into_iter()
.filter(|e| e.is_api_only())
.collect()
}
#[test]
fn test_resolve_access_matrix() {
let web_cascade = vec![AccessMethod::PlainHttp, AccessMethod::Browser];
let web_no_browser = vec![AccessMethod::PlainHttp];
let full_cascade = vec![
AccessMethod::Api,
AccessMethod::PlainHttp,
AccessMethod::Browser,
];
for engine in all_engines() {
for has_key in [true, false] {
for browser in [true, false] {
let result = resolve_access(engine, has_key, browser);
let label = format!("{engine:?} has_key={has_key} browser={browser}");
match (engine, has_key, browser) {
(e, true, true) if e.is_api_only() => {
assert_eq!(result.unwrap(), vec![AccessMethod::Api], "{label}");
}
(e, true, false) if e.is_api_only() => {
assert_eq!(result.unwrap(), vec![AccessMethod::Api], "{label}");
}
(e, false, _) if e.is_api_only() => {
assert!(result.is_err(), "{label} should require credentials");
}
(SearchEngineType::BraveSearch, true, true) => {
assert_eq!(result.unwrap(), full_cascade, "{label}");
}
(SearchEngineType::BraveSearch, true, false) => {
assert_eq!(
result.unwrap(),
vec![AccessMethod::Api, AccessMethod::PlainHttp],
"{label}"
);
}
(SearchEngineType::BraveSearch, false, true) => {
assert_eq!(result.unwrap(), web_cascade, "{label}");
}
(SearchEngineType::BraveSearch, false, false) => {
assert_eq!(result.unwrap(), web_no_browser, "{label}");
}
(e, _, true) if e.supports_web() && !e.supports_api() => {
assert_eq!(result.unwrap(), web_cascade, "{label}");
}
(e, _, false) if e.supports_web() && !e.supports_api() => {
assert_eq!(result.unwrap(), web_no_browser, "{label}");
}
_ => panic!("Unhandled matrix cell: {label}"),
}
}
}
}
}
#[test]
fn test_web_only_engines_never_get_api() {
for engine in web_only_engines() {
let methods = resolve_access(engine, true, true).unwrap();
assert!(
!methods.contains(&AccessMethod::Api),
"{engine:?} must not use API even with a key"
);
}
}
#[test]
fn test_api_only_engines_list() {
let engines = api_only_engines();
assert!(engines.contains(&SearchEngineType::Tavily));
assert!(engines.contains(&SearchEngineType::GoogleAi));
assert!(engines.contains(&SearchEngineType::SearxNG));
assert!(engines.contains(&SearchEngineType::GoogleSerper));
}
#[test]
fn test_brave_with_and_without_key() {
let with_key = resolve_access(SearchEngineType::BraveSearch, true, true).unwrap();
assert_eq!(
with_key,
vec![
AccessMethod::Api,
AccessMethod::PlainHttp,
AccessMethod::Browser
]
);
let without = resolve_access(SearchEngineType::BraveSearch, false, true).unwrap();
assert_eq!(
without,
vec![AccessMethod::PlainHttp, AccessMethod::Browser]
);
}
#[test]
fn test_bing_browser_toggle() {
let on = resolve_access(SearchEngineType::Bing, false, true).unwrap();
assert_eq!(on, vec![AccessMethod::PlainHttp, AccessMethod::Browser]);
let off = resolve_access(SearchEngineType::Bing, false, false).unwrap();
assert_eq!(off, vec![AccessMethod::PlainHttp]);
}
#[test]
fn test_google_serper_requires_key() {
let err = resolve_access(SearchEngineType::GoogleSerper, false, true);
assert!(err.is_err());
let methods = resolve_access(SearchEngineType::GoogleSerper, true, true).unwrap();
assert_eq!(methods, vec![AccessMethod::Api]);
}
#[test]
fn test_tavily_requires_key() {
let err = resolve_access(SearchEngineType::Tavily, false, true);
assert!(err.is_err());
assert!(err.unwrap_err().to_string().contains("TAVILY_API_KEY"));
let methods = resolve_access(SearchEngineType::Tavily, true, false).unwrap();
assert_eq!(methods, vec![AccessMethod::Api]);
}
#[test]
fn test_searxng_requires_host() {
let err = resolve_access(SearchEngineType::SearxNG, false, true);
assert!(err.is_err());
assert!(err.unwrap_err().to_string().contains("SEARX_HOST"));
}
#[test]
fn test_resolve_api_key_from_config() {
let key = resolve_api_key(SearchEngineType::BraveSearch, &Some("cfg-key".to_string()));
if std::env::var(ENV_BRAVE_API_KEY)
.ok()
.filter(|v| !v.is_empty())
.is_none()
{
assert_eq!(key.as_deref(), Some("cfg-key"));
}
}
#[test]
fn test_resolve_api_key_tavily() {
let key = resolve_api_key(SearchEngineType::Tavily, &Some("tvly-test".to_string()));
if std::env::var(ENV_TAVILY_API_KEY)
.ok()
.filter(|v| !v.is_empty())
.is_none()
{
assert_eq!(key.as_deref(), Some("tvly-test"));
}
}
#[test]
fn test_resolve_base_url_searxng() {
if std::env::var(ENV_SEARX_HOST)
.ok()
.filter(|v| !v.is_empty())
.is_none()
{
let url = resolve_base_url(
SearchEngineType::SearxNG,
&Some("http://localhost:8080".to_string()),
);
assert_eq!(url.as_deref(), Some("http://localhost:8080/search"));
}
assert!(resolve_base_url(SearchEngineType::Tavily, &Some("x".to_string())).is_none());
}
#[test]
fn test_has_api_credentials() {
assert!(has_api_credentials(
SearchEngineType::Tavily,
&Some("k".to_string()),
&None
));
assert!(!has_api_credentials(SearchEngineType::Tavily, &None, &None));
assert!(has_api_credentials(
SearchEngineType::SearxNG,
&None,
&Some("http://localhost:8080/search".to_string())
));
assert!(!has_api_credentials(
SearchEngineType::SearxNG,
&Some("k".to_string()),
&None
));
}
#[test]
fn test_resolve_api_key_ignored_for_web_only_engines() {
for engine in web_only_engines() {
let key = resolve_api_key(engine, &Some("cfg-key".to_string()));
assert_eq!(
key.as_deref(),
Some("cfg-key"),
"{engine:?} should still surface config api_key when present"
);
}
}
}