Skip to main content

web_search/providers/
generic.rs

1//! Generic, descriptor-driven search provider.
2//!
3//! A single provider implementation that can speak to any engine described by a
4//! catalog [`EngineDescriptor`](super::engines::EngineDescriptor). This keeps
5//! the fetch/normalize/error plumbing in one place while each engine only
6//! declares its URL, request kind, and parser. Mirrors the JavaScript
7//! `src/providers/generic.js` (issue #3 parity requirement).
8
9use async_trait::async_trait;
10use reqwest::header::{HeaderMap, HeaderName, HeaderValue, ACCEPT, ACCEPT_LANGUAGE, CONTENT_TYPE};
11use reqwest::Method;
12
13use super::base::{SearchOptions, SearchProvider, SearchResult};
14use super::engines::{EngineDescriptor, EngineKind, HttpMethod};
15use crate::error::SearchError;
16
17const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \
18                          (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
19
20/// Descriptor-driven provider.
21pub struct GenericProvider {
22    descriptor: EngineDescriptor,
23    enabled: bool,
24    weight: f64,
25    client: reqwest::Client,
26}
27
28impl GenericProvider {
29    /// Create a new provider for the given engine descriptor.
30    pub fn new(descriptor: EngineDescriptor) -> Self {
31        Self {
32            descriptor,
33            enabled: true,
34            weight: 1.0,
35            client: reqwest::Client::builder()
36                .user_agent(USER_AGENT)
37                .build()
38                .expect("Failed to create HTTP client"),
39        }
40    }
41
42    /// The engine descriptor backing this provider.
43    pub fn descriptor(&self) -> &EngineDescriptor {
44        &self.descriptor
45    }
46
47    fn build_headers(&self, options: &SearchOptions) -> HeaderMap {
48        let mut headers = HeaderMap::new();
49        headers.insert(ACCEPT_LANGUAGE, HeaderValue::from_static("en-US,en;q=0.9"));
50        let accept = match self.descriptor.kind {
51            EngineKind::Json => "application/json",
52            EngineKind::Text | EngineKind::Html => {
53                "text/html,application/xhtml+xml,application/xml;q=0.9"
54            }
55        };
56        headers.insert(ACCEPT, HeaderValue::from_static(accept));
57
58        if let Some(extra) = self.descriptor.headers {
59            for (name, value) in extra(options) {
60                if let (Ok(name), Ok(value)) = (
61                    HeaderName::from_bytes(name.as_bytes()),
62                    HeaderValue::from_str(&value),
63                ) {
64                    headers.insert(name, value);
65                }
66            }
67        }
68        headers
69    }
70}
71
72#[async_trait]
73impl SearchProvider for GenericProvider {
74    fn name(&self) -> &str {
75        self.descriptor.id
76    }
77
78    fn is_available(&self) -> bool {
79        self.enabled
80    }
81
82    fn weight(&self) -> f64 {
83        self.weight
84    }
85
86    fn set_weight(&mut self, weight: f64) {
87        self.weight = weight.clamp(0.0, 1.0);
88    }
89
90    fn set_enabled(&mut self, enabled: bool) {
91        self.enabled = enabled;
92    }
93
94    async fn search(
95        &self,
96        query: &str,
97        options: &SearchOptions,
98    ) -> Result<Vec<SearchResult>, SearchError> {
99        if query.trim().is_empty() {
100            return Ok(Vec::new());
101        }
102
103        let d = &self.descriptor;
104        let limit = options.limit.unwrap_or(10);
105        let url = (d.build_url)(query, options);
106        let mut headers = self.build_headers(options);
107
108        let method = match d.method {
109            HttpMethod::Get => Method::GET,
110            HttpMethod::Post => Method::POST,
111        };
112
113        let mut request = self.client.request(method, &url);
114        if let (HttpMethod::Post, Some(build_body)) = (d.method, d.build_body) {
115            headers.insert(
116                CONTENT_TYPE,
117                HeaderValue::from_static("application/x-www-form-urlencoded"),
118            );
119            request = request.body((build_body)(query, options));
120        }
121        request = request.headers(headers);
122
123        let response = match request.send().await {
124            Ok(response) => response,
125            Err(error) => {
126                tracing::error!("{} search error: {}", d.id, error);
127                return Ok(Vec::new());
128            }
129        };
130
131        if !response.status().is_success() {
132            tracing::error!("{} returned status {}", d.id, response.status());
133            return Ok(Vec::new());
134        }
135
136        match response.text().await {
137            Ok(body) => Ok((d.parse)(&body, limit, options)),
138            Err(error) => {
139                tracing::error!("{} body read error: {}", d.id, error);
140                Ok(Vec::new())
141            }
142        }
143    }
144}