web_search/providers/
generic.rs1use async_trait::async_trait;
10use std::collections::BTreeMap;
11
12use super::base::{SearchOptions, SearchProvider, SearchResult};
13use super::engines::{EngineDescriptor, EngineKind, HttpMethod};
14use crate::error::SearchError;
15use crate::transport::{ReqwestTransport, SearchTransport, TransportRequest};
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}
26
27impl GenericProvider {
28 pub fn new(descriptor: EngineDescriptor) -> Self {
30 Self {
31 descriptor,
32 enabled: true,
33 weight: 1.0,
34 }
35 }
36
37 pub fn descriptor(&self) -> &EngineDescriptor {
39 &self.descriptor
40 }
41
42 fn build_headers(&self, options: &SearchOptions) -> BTreeMap<String, String> {
43 let mut headers = BTreeMap::new();
44 headers.insert("User-Agent".to_string(), USER_AGENT.to_string());
45 headers.insert("Accept-Language".to_string(), "en-US,en;q=0.9".to_string());
46 let accept = match self.descriptor.kind {
47 EngineKind::Json => "application/json",
48 EngineKind::Text | EngineKind::Html => {
49 "text/html,application/xhtml+xml,application/xml;q=0.9"
50 }
51 };
52 headers.insert("Accept".to_string(), accept.to_string());
53
54 if let Some(extra) = self.descriptor.headers {
55 for (name, value) in extra(options) {
56 headers.insert(name, value);
57 }
58 }
59 headers
60 }
61}
62
63#[async_trait]
64impl SearchProvider for GenericProvider {
65 fn name(&self) -> &str {
66 self.descriptor.id
67 }
68
69 fn is_available(&self) -> bool {
70 self.enabled
71 }
72
73 fn weight(&self) -> f64 {
74 self.weight
75 }
76
77 fn set_weight(&mut self, weight: f64) {
78 self.weight = weight.clamp(0.0, 1.0);
79 }
80
81 fn set_enabled(&mut self, enabled: bool) {
82 self.enabled = enabled;
83 }
84
85 async fn search(
86 &self,
87 query: &str,
88 options: &SearchOptions,
89 ) -> Result<Vec<SearchResult>, SearchError> {
90 match self
91 .search_with_transport(query, options, &ReqwestTransport::default())
92 .await
93 {
94 Ok(results) => Ok(results),
95 Err(error) => {
96 tracing::error!("{} search error: {}", self.name(), error);
97 Ok(Vec::new())
98 }
99 }
100 }
101
102 async fn search_with_transport(
103 &self,
104 query: &str,
105 options: &SearchOptions,
106 transport: &dyn SearchTransport,
107 ) -> Result<Vec<SearchResult>, SearchError> {
108 if query.trim().is_empty() {
109 return Ok(Vec::new());
110 }
111
112 let d = &self.descriptor;
113 let limit = options.limit.unwrap_or(10);
114 let url = (d.build_url)(query, options);
115 let mut headers = self.build_headers(options);
116
117 let method = match d.method {
118 HttpMethod::Get => "GET",
119 HttpMethod::Post => "POST",
120 };
121 let mut body = None;
122 if let (HttpMethod::Post, Some(build_body)) = (d.method, d.build_body) {
123 headers.insert(
124 "Content-Type".to_string(),
125 "application/x-www-form-urlencoded".to_string(),
126 );
127 body = Some((build_body)(query, options).into_bytes());
128 }
129 let response = transport
130 .execute(TransportRequest {
131 method: method.to_string(),
132 url,
133 headers,
134 body,
135 })
136 .await?;
137
138 if !(200..300).contains(&response.status) {
139 return Err(SearchError::ApiError {
140 provider: d.id.to_string(),
141 message: format!("HTTP {}", response.status),
142 });
143 }
144 let body = String::from_utf8_lossy(&response.body);
145 Ok((d.parse)(&body, limit, options))
146 }
147}