Skip to main content

a3s_search/
engine.rs

1//! Search engine trait and configuration.
2
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5
6use crate::{Result, SearchQuery, SearchResult};
7
8/// Categories for search engines.
9#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "lowercase")]
11pub enum EngineCategory {
12    #[default]
13    General,
14    Images,
15    Videos,
16    News,
17    Maps,
18    Music,
19    Files,
20    Science,
21    Social,
22}
23
24/// Configuration for a search engine.
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct EngineConfig {
27    /// Display name of the engine.
28    pub name: String,
29    /// Short identifier (e.g., "ddg" for DuckDuckGo).
30    pub shortcut: String,
31    /// Categories this engine belongs to.
32    pub categories: Vec<EngineCategory>,
33    /// Weight for ranking (higher = more influence).
34    #[serde(default = "default_weight")]
35    pub weight: f64,
36    /// Request timeout in seconds.
37    #[serde(default = "default_timeout")]
38    pub timeout: u64,
39    /// Whether the engine is enabled.
40    #[serde(default = "default_enabled")]
41    pub enabled: bool,
42    /// Whether pagination is supported.
43    #[serde(default)]
44    pub paging: bool,
45    /// Whether safe search is supported.
46    #[serde(default)]
47    pub safesearch: bool,
48}
49
50fn default_weight() -> f64 {
51    1.0
52}
53
54fn default_timeout() -> u64 {
55    5
56}
57
58fn default_enabled() -> bool {
59    true
60}
61
62impl Default for EngineConfig {
63    fn default() -> Self {
64        Self {
65            name: String::new(),
66            shortcut: String::new(),
67            categories: vec![EngineCategory::General],
68            weight: 1.0,
69            timeout: 5,
70            enabled: true,
71            paging: false,
72            safesearch: false,
73        }
74    }
75}
76
77/// Trait for implementing search engines.
78///
79/// Each search engine must implement this trait to be used with the meta search.
80#[async_trait]
81pub trait Engine: Send + Sync {
82    /// Returns the engine configuration.
83    fn config(&self) -> &EngineConfig;
84
85    /// Performs a search and returns results.
86    async fn search(&self, query: &SearchQuery) -> Result<Vec<SearchResult>>;
87
88    /// Returns the engine name.
89    fn name(&self) -> &str {
90        &self.config().name
91    }
92
93    /// Returns the engine shortcut.
94    fn shortcut(&self) -> &str {
95        &self.config().shortcut
96    }
97
98    /// Returns the engine weight.
99    fn weight(&self) -> f64 {
100        self.config().weight
101    }
102
103    /// Returns whether the engine is enabled.
104    fn is_enabled(&self) -> bool {
105        self.config().enabled
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn test_engine_category_default() {
115        let default: EngineCategory = Default::default();
116        assert_eq!(default, EngineCategory::General);
117    }
118
119    #[test]
120    fn test_engine_category_variants() {
121        let categories = vec![
122            EngineCategory::General,
123            EngineCategory::Images,
124            EngineCategory::Videos,
125            EngineCategory::News,
126            EngineCategory::Maps,
127            EngineCategory::Music,
128            EngineCategory::Files,
129            EngineCategory::Science,
130            EngineCategory::Social,
131        ];
132        assert_eq!(categories.len(), 9);
133    }
134
135    #[test]
136    fn test_engine_config_default() {
137        let config = EngineConfig::default();
138        assert_eq!(config.name, "");
139        assert_eq!(config.shortcut, "");
140        assert_eq!(config.categories, vec![EngineCategory::General]);
141        assert_eq!(config.weight, 1.0);
142        assert_eq!(config.timeout, 5);
143        assert!(config.enabled);
144        assert!(!config.paging);
145        assert!(!config.safesearch);
146    }
147
148    #[test]
149    fn test_engine_config_custom() {
150        let config = EngineConfig {
151            name: "Test Engine".to_string(),
152            shortcut: "test".to_string(),
153            categories: vec![EngineCategory::Images, EngineCategory::Videos],
154            weight: 2.0,
155            timeout: 10,
156            enabled: false,
157            paging: true,
158            safesearch: true,
159        };
160        assert_eq!(config.name, "Test Engine");
161        assert_eq!(config.shortcut, "test");
162        assert_eq!(config.weight, 2.0);
163        assert_eq!(config.timeout, 10);
164        assert!(!config.enabled);
165        assert!(config.paging);
166        assert!(config.safesearch);
167    }
168
169    #[test]
170    fn test_engine_config_serialization() {
171        let config = EngineConfig {
172            name: "Test".to_string(),
173            shortcut: "t".to_string(),
174            ..Default::default()
175        };
176        let json = serde_json::to_string(&config).unwrap();
177        assert!(json.contains("\"name\":\"Test\""));
178        assert!(json.contains("\"shortcut\":\"t\""));
179    }
180
181    #[test]
182    fn test_engine_config_deserialization() {
183        let json = r#"{"name":"Test","shortcut":"t","categories":["general"]}"#;
184        let config: EngineConfig = serde_json::from_str(json).unwrap();
185        assert_eq!(config.name, "Test");
186        assert_eq!(config.shortcut, "t");
187        assert_eq!(config.weight, 1.0); // default
188        assert_eq!(config.timeout, 5); // default
189        assert!(config.enabled); // default
190    }
191
192    #[test]
193    fn test_engine_category_serialization() {
194        let category = EngineCategory::Images;
195        let json = serde_json::to_string(&category).unwrap();
196        assert_eq!(json, "\"images\"");
197    }
198
199    #[test]
200    fn test_engine_category_deserialization() {
201        let json = "\"videos\"";
202        let category: EngineCategory = serde_json::from_str(json).unwrap();
203        assert_eq!(category, EngineCategory::Videos);
204    }
205
206    #[test]
207    fn test_engine_category_hash() {
208        use std::collections::HashSet;
209        let mut set = HashSet::new();
210        set.insert(EngineCategory::General);
211        set.insert(EngineCategory::Images);
212        set.insert(EngineCategory::General); // duplicate
213        assert_eq!(set.len(), 2);
214    }
215}