Skip to main content

web_search/providers/
registry.rs

1//! Typed provider registry.
2//!
3//! A single source of truth describing every search provider this library can
4//! use, grouped into the four categories that `formal-ai` consumes (`search`,
5//! `knowledge`, `papers`, `code`). The registry powers provider discovery
6//! (CLI/server/`/providers`) and is the factory that instantiates the correct
7//! provider implementation for each id. Mirrors the JavaScript
8//! `src/providers/registry.js` (issue #3 parity requirement).
9
10use serde::Serialize;
11
12use super::base::SearchProvider;
13use super::bing::{BingConfig, BingProvider};
14use super::duckduckgo::DuckDuckGoProvider;
15use super::engines::{access_for, all_descriptor_engines, EngineDescriptor};
16use super::generic::GenericProvider;
17use super::google::{GoogleConfig, GoogleProvider};
18use super::web_capture::{WebCaptureProvider, SUPPORTED_PROVIDERS};
19
20/// Provider categories, mirroring `formal-ai`'s `web_search_core` registry.
21pub const CATEGORIES: [&str; 4] = ["search", "knowledge", "papers", "code"];
22
23/// Public metadata describing a single registered provider.
24#[derive(Debug, Clone, Serialize)]
25#[serde(rename_all = "camelCase")]
26pub struct RegistryEntry {
27    /// Stable provider id.
28    pub id: String,
29    /// Human-readable label.
30    pub label: String,
31    /// Provider category (one of [`CATEGORIES`]).
32    pub category: String,
33    /// Whether the endpoint is browser-CORS readable.
34    pub cors_readable: bool,
35    /// Whether this is its category's default provider.
36    pub default_for_category: bool,
37    /// How results are obtained (`api`, `html`, `hybrid`, `component`, ...).
38    pub access: String,
39}
40
41/// Engine configuration used to instantiate providers.
42#[derive(Debug, Clone, Default)]
43pub struct BuildConfig {
44    /// Google Custom Search API key.
45    pub google_api_key: Option<String>,
46    /// Google Custom Search Engine ID.
47    pub google_cx: Option<String>,
48    /// Bing Search API key.
49    pub bing_api_key: Option<String>,
50}
51
52/// Metadata for a class-based provider (google/bing/duckduckgo) that predates
53/// the descriptor catalog and keeps its dedicated API + scraping logic.
54struct ClassEngine {
55    id: &'static str,
56    label: &'static str,
57    category: &'static str,
58    cors_readable: bool,
59    default_for_category: bool,
60    access: &'static str,
61}
62
63const CLASS_ENGINES: [ClassEngine; 3] = [
64    ClassEngine {
65        id: "google",
66        label: "Google",
67        category: "search",
68        cors_readable: false,
69        default_for_category: true,
70        access: "hybrid",
71    },
72    ClassEngine {
73        id: "bing",
74        label: "Bing",
75        category: "search",
76        cors_readable: false,
77        default_for_category: false,
78        access: "hybrid",
79    },
80    ClassEngine {
81        id: "duckduckgo",
82        label: "DuckDuckGo",
83        category: "search",
84        cors_readable: false,
85        default_for_category: false,
86        access: "html",
87    },
88];
89
90fn descriptor_entry(d: &EngineDescriptor) -> RegistryEntry {
91    RegistryEntry {
92        id: d.id.to_string(),
93        label: d.label.to_string(),
94        category: d.category.to_string(),
95        cors_readable: d.cors_readable,
96        default_for_category: d.default_for_category,
97        access: access_for(d.kind).to_string(),
98    }
99}
100
101/// Build the full registry of provider entries, in catalog order
102/// (class engines, descriptor engines, then web-capture engines).
103pub fn get_registry() -> Vec<RegistryEntry> {
104    let mut entries = Vec::new();
105
106    for e in &CLASS_ENGINES {
107        entries.push(RegistryEntry {
108            id: e.id.to_string(),
109            label: e.label.to_string(),
110            category: e.category.to_string(),
111            cors_readable: e.cors_readable,
112            default_for_category: e.default_for_category,
113            access: e.access.to_string(),
114        });
115    }
116    for d in all_descriptor_engines() {
117        entries.push(descriptor_entry(&d));
118    }
119    for engine in SUPPORTED_PROVIDERS {
120        entries.push(RegistryEntry {
121            id: format!("wc:{engine}"),
122            label: format!("web-capture ({engine})"),
123            category: "search".to_string(),
124            cors_readable: engine == "wikipedia",
125            default_for_category: false,
126            access: "component".to_string(),
127        });
128    }
129
130    entries
131}
132
133/// Get all provider ids, optionally filtered by category.
134pub fn get_provider_ids(category: Option<&str>) -> Vec<String> {
135    get_registry()
136        .into_iter()
137        .filter(|e| category.is_none_or(|c| e.category == c))
138        .map(|e| e.id)
139        .collect()
140}
141
142/// Get the default provider ids used when the caller does not specify providers.
143pub fn get_default_provider_ids() -> Vec<String> {
144    ["duckduckgo", "google", "bing", "wikipedia"]
145        .iter()
146        .map(|s| s.to_string())
147        .collect()
148}
149
150/// Whether `category` is a known category.
151pub fn is_known_category(category: &str) -> bool {
152    CATEGORIES.contains(&category)
153}
154
155/// Instantiate every registered provider, keyed by id, in catalog order.
156pub fn build_providers(config: &BuildConfig) -> Vec<(String, Box<dyn SearchProvider>)> {
157    let mut providers: Vec<(String, Box<dyn SearchProvider>)> = Vec::new();
158
159    providers.push((
160        "google".to_string(),
161        Box::new(GoogleProvider::new(GoogleConfig {
162            api_key: config.google_api_key.clone(),
163            search_engine_id: config.google_cx.clone(),
164        })),
165    ));
166    providers.push((
167        "bing".to_string(),
168        Box::new(BingProvider::new(BingConfig {
169            api_key: config.bing_api_key.clone(),
170        })),
171    ));
172    providers.push((
173        "duckduckgo".to_string(),
174        Box::new(DuckDuckGoProvider::new()),
175    ));
176
177    for d in all_descriptor_engines() {
178        providers.push((d.id.to_string(), Box::new(GenericProvider::new(d))));
179    }
180
181    for engine in SUPPORTED_PROVIDERS {
182        providers.push((
183            format!("wc:{engine}"),
184            Box::new(WebCaptureProvider::new(engine)),
185        ));
186    }
187
188    providers
189}