web_search/providers/
generic.rs1use 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
20pub struct GenericProvider {
22 descriptor: EngineDescriptor,
23 enabled: bool,
24 weight: f64,
25 client: reqwest::Client,
26}
27
28impl GenericProvider {
29 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 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}