use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub const KNOWN_PROVIDER_IDS: &[&str] = &[
"duckduckgo",
"brave",
"startpage",
"yahoo",
"mojeek",
"searxng",
"brave_api",
];
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ProviderKind {
HtmlScrape,
JsonApi,
ApiKey,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct ProviderCapabilities {
pub supports_safe_search: bool,
pub supports_freshness: bool,
pub supports_language: bool,
pub supports_region: bool,
pub supports_domain_filters: bool,
pub supports_news: bool,
}
impl ProviderCapabilities {
pub fn none() -> Self {
Self {
supports_safe_search: false,
supports_freshness: false,
supports_language: false,
supports_region: false,
supports_domain_filters: false,
supports_news: false,
}
}
pub fn summary(&self) -> String {
let mut caps = Vec::new();
if self.supports_safe_search {
caps.push("safe_search");
}
if self.supports_freshness {
caps.push("freshness");
}
if self.supports_language {
caps.push("language");
}
if self.supports_region {
caps.push("region");
}
if self.supports_domain_filters {
caps.push("domain_filters");
}
if self.supports_news {
caps.push("news");
}
if caps.is_empty() {
"basic".to_string()
} else {
caps.join(", ")
}
}
pub fn supports(&self, option: &CapabilityOption) -> bool {
match option {
CapabilityOption::SafeSearch => self.supports_safe_search,
CapabilityOption::Freshness => self.supports_freshness,
CapabilityOption::Language => self.supports_language,
CapabilityOption::Region => self.supports_region,
CapabilityOption::DomainFilters => self.supports_domain_filters,
CapabilityOption::News => self.supports_news,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CapabilityOption {
SafeSearch,
Freshness,
Language,
Region,
DomainFilters,
News,
}
impl CapabilityOption {
pub fn display_name(&self) -> &'static str {
match self {
Self::SafeSearch => "safe_search",
Self::Freshness => "freshness",
Self::Language => "language",
Self::Region => "region",
Self::DomainFilters => "domain_filters",
Self::News => "news",
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct ProviderDescriptor {
pub id: String,
pub display_name: String,
pub kind: ProviderKind,
pub enabled: bool,
pub default: bool,
pub requires_api_key: bool,
pub configured: bool,
pub capabilities: ProviderCapabilities,
}
pub fn built_in_provider_descriptor(
id: &str,
enabled: bool,
is_default: bool,
configured: bool,
) -> Option<ProviderDescriptor> {
match id {
"duckduckgo" => Some(ProviderDescriptor {
id: "duckduckgo".into(),
display_name: "DuckDuckGo".into(),
kind: ProviderKind::HtmlScrape,
enabled,
default: is_default,
requires_api_key: false,
configured,
capabilities: ProviderCapabilities::none(),
}),
"brave" => Some(ProviderDescriptor {
id: "brave".into(),
display_name: "Brave".into(),
kind: ProviderKind::HtmlScrape,
enabled,
default: is_default,
requires_api_key: false,
configured,
capabilities: ProviderCapabilities::none(),
}),
"startpage" => Some(ProviderDescriptor {
id: "startpage".into(),
display_name: "Startpage".into(),
kind: ProviderKind::HtmlScrape,
enabled,
default: is_default,
requires_api_key: false,
configured,
capabilities: ProviderCapabilities::none(),
}),
"yahoo" => Some(ProviderDescriptor {
id: "yahoo".into(),
display_name: "Yahoo".into(),
kind: ProviderKind::HtmlScrape,
enabled,
default: is_default,
requires_api_key: false,
configured,
capabilities: ProviderCapabilities::none(),
}),
"mojeek" => Some(ProviderDescriptor {
id: "mojeek".into(),
display_name: "Mojeek".into(),
kind: ProviderKind::HtmlScrape,
enabled,
default: is_default,
requires_api_key: false,
configured,
capabilities: ProviderCapabilities::none(),
}),
"searxng" => Some(ProviderDescriptor {
id: "searxng".into(),
display_name: "SearXNG".into(),
kind: ProviderKind::JsonApi,
enabled,
default: is_default,
requires_api_key: false,
configured: configured && enabled,
capabilities: ProviderCapabilities {
supports_safe_search: true,
supports_freshness: true,
supports_language: true,
supports_region: true,
supports_domain_filters: false,
supports_news: true,
},
}),
"brave_api" => Some(ProviderDescriptor {
id: "brave_api".into(),
display_name: "Brave Search API".into(),
kind: ProviderKind::ApiKey,
enabled,
default: is_default,
requires_api_key: true,
configured: configured && enabled,
capabilities: ProviderCapabilities {
supports_safe_search: true,
supports_freshness: true,
supports_language: true,
supports_region: true,
supports_domain_filters: false,
supports_news: false,
},
}),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_provider_ids_are_all_describable() {
for id in KNOWN_PROVIDER_IDS {
let desc = built_in_provider_descriptor(id, true, false, true)
.expect("known id should have descriptor");
assert_eq!(desc.id, *id);
}
}
#[test]
fn unknown_provider_returns_none() {
assert!(built_in_provider_descriptor("ghost", true, false, true).is_none());
}
#[test]
fn capabilities_summary_basic() {
let caps = ProviderCapabilities::none();
assert_eq!(caps.summary(), "basic");
}
#[test]
fn capabilities_summary_searxng() {
let desc = built_in_provider_descriptor("searxng", true, false, true).unwrap();
let summary = desc.capabilities.summary();
assert!(summary.contains("safe_search"));
assert!(summary.contains("language"));
assert!(summary.contains("news"));
assert!(!summary.contains("domain_filters"));
}
#[test]
fn searxng_configured_false_when_disabled() {
let desc = built_in_provider_descriptor("searxng", false, false, true).unwrap();
assert!(!desc.configured);
}
#[test]
fn searxng_configured_true_when_enabled_and_configured() {
let desc = built_in_provider_descriptor("searxng", true, false, true).unwrap();
assert!(desc.configured);
}
#[test]
fn provider_kind_serde_roundtrip() {
let kind = ProviderKind::HtmlScrape;
let json = serde_json::to_string(&kind).unwrap();
let parsed: ProviderKind = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, kind);
}
#[test]
fn provider_descriptor_serde_roundtrip() {
let desc = built_in_provider_descriptor("duckduckgo", true, true, true).unwrap();
let json = serde_json::to_string(&desc).unwrap();
let parsed: ProviderDescriptor = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.id, desc.id);
assert_eq!(parsed.kind, desc.kind);
assert_eq!(parsed.enabled, desc.enabled);
assert_eq!(parsed.default, desc.default);
assert_eq!(parsed.capabilities, desc.capabilities);
}
#[test]
fn brave_api_descriptor_is_api_key_kind() {
let desc = built_in_provider_descriptor("brave_api", true, false, true)
.expect("brave_api should have descriptor");
assert_eq!(desc.id, "brave_api");
assert_eq!(desc.display_name, "Brave Search API");
assert_eq!(desc.kind, ProviderKind::ApiKey);
assert!(desc.requires_api_key);
assert!(desc.configured);
assert!(desc.enabled);
assert!(!desc.default);
}
#[test]
fn brave_api_descriptor_configured_false_when_disabled() {
let desc = built_in_provider_descriptor("brave_api", false, false, true).unwrap();
assert!(!desc.configured);
assert!(!desc.enabled);
}
#[test]
fn brave_api_descriptor_capabilities() {
let desc = built_in_provider_descriptor("brave_api", true, false, true).unwrap();
assert!(desc.capabilities.supports_safe_search);
assert!(desc.capabilities.supports_freshness);
assert!(desc.capabilities.supports_language);
assert!(desc.capabilities.supports_region);
assert!(!desc.capabilities.supports_domain_filters);
assert!(!desc.capabilities.supports_news);
}
#[test]
fn brave_api_capabilities_summary() {
let desc = built_in_provider_descriptor("brave_api", true, false, true).unwrap();
let summary = desc.capabilities.summary();
assert!(summary.contains("safe_search"));
assert!(summary.contains("freshness"));
assert!(summary.contains("language"));
assert!(summary.contains("region"));
assert!(!summary.contains("news"));
}
#[test]
fn capability_option_supports_method() {
let caps = ProviderCapabilities {
supports_safe_search: true,
supports_freshness: false,
supports_language: true,
supports_region: false,
supports_domain_filters: true,
supports_news: false,
};
assert!(caps.supports(&CapabilityOption::SafeSearch));
assert!(!caps.supports(&CapabilityOption::Freshness));
assert!(caps.supports(&CapabilityOption::Language));
assert!(!caps.supports(&CapabilityOption::Region));
assert!(caps.supports(&CapabilityOption::DomainFilters));
assert!(!caps.supports(&CapabilityOption::News));
}
#[test]
fn capability_option_display_names() {
assert_eq!(CapabilityOption::SafeSearch.display_name(), "safe_search");
assert_eq!(CapabilityOption::Freshness.display_name(), "freshness");
assert_eq!(CapabilityOption::Language.display_name(), "language");
assert_eq!(CapabilityOption::Region.display_name(), "region");
assert_eq!(
CapabilityOption::DomainFilters.display_name(),
"domain_filters"
);
assert_eq!(CapabilityOption::News.display_name(), "news");
}
#[test]
fn capability_option_supports_none() {
let caps = ProviderCapabilities::none();
for option in [
CapabilityOption::SafeSearch,
CapabilityOption::Freshness,
CapabilityOption::Language,
CapabilityOption::Region,
CapabilityOption::DomainFilters,
CapabilityOption::News,
] {
assert!(
!caps.supports(&option),
"ProviderCapabilities::none() should not support {}",
option.display_name()
);
}
}
}