Skip to main content

swink_agent_plugin_web/
config.rs

1use std::path::PathBuf;
2use std::time::Duration;
3
4use serde::{Deserialize, Serialize};
5
6/// Which search backend to use.
7#[derive(Debug, Clone, Default, Serialize, Deserialize)]
8pub enum SearchProviderKind {
9    #[default]
10    DuckDuckGo,
11    Brave,
12    Tavily,
13}
14
15/// Configuration for the web plugin.
16#[derive(Debug, Clone)]
17pub struct WebPluginConfig {
18    pub search_provider_kind: SearchProviderKind,
19    pub brave_api_key: Option<String>,
20    pub tavily_api_key: Option<String>,
21    pub domain_allowlist: Vec<String>,
22    pub domain_denylist: Vec<String>,
23    pub block_private_ips: bool,
24    pub rate_limit_rpm: u32,
25    pub max_content_length: usize,
26    pub max_redirects: u32,
27    pub max_search_results: usize,
28    pub playwright_path: Option<PathBuf>,
29    pub screenshot_timeout: Duration,
30    pub request_timeout: Duration,
31    pub viewport_width: u32,
32    pub viewport_height: u32,
33    pub sanitizer_enabled: bool,
34    pub user_agent: String,
35}
36
37impl Default for WebPluginConfig {
38    fn default() -> Self {
39        Self {
40            search_provider_kind: SearchProviderKind::default(),
41            brave_api_key: None,
42            tavily_api_key: None,
43            domain_allowlist: Vec::new(),
44            domain_denylist: Vec::new(),
45            block_private_ips: true,
46            rate_limit_rpm: 30,
47            max_content_length: 50_000,
48            max_redirects: 10,
49            max_search_results: 10,
50            playwright_path: None,
51            screenshot_timeout: Duration::from_secs(15),
52            request_timeout: Duration::from_secs(30),
53            viewport_width: 1280,
54            viewport_height: 720,
55            sanitizer_enabled: true,
56            user_agent: String::from("SwinkAgent/0.5"),
57        }
58    }
59}
60
61/// Builder for `WebPluginConfig`.
62#[derive(Debug, Default)]
63pub struct WebPluginConfigBuilder {
64    config: WebPluginConfig,
65}
66
67impl WebPluginConfigBuilder {
68    #[must_use]
69    pub fn new() -> Self {
70        Self::default()
71    }
72
73    #[must_use]
74    pub fn with_search_provider(mut self, kind: SearchProviderKind) -> Self {
75        self.config.search_provider_kind = kind;
76        self
77    }
78
79    #[must_use]
80    pub fn with_brave_api_key(mut self, key: impl Into<String>) -> Self {
81        self.config.brave_api_key = Some(key.into());
82        self
83    }
84
85    #[must_use]
86    pub fn with_tavily_api_key(mut self, key: impl Into<String>) -> Self {
87        self.config.tavily_api_key = Some(key.into());
88        self
89    }
90
91    #[must_use]
92    pub fn with_domain_allowlist(mut self, domains: Vec<String>) -> Self {
93        self.config.domain_allowlist = domains;
94        self
95    }
96
97    #[must_use]
98    pub fn with_domain_denylist(mut self, domains: Vec<String>) -> Self {
99        self.config.domain_denylist = domains;
100        self
101    }
102
103    #[must_use]
104    pub fn with_block_private_ips(mut self, block: bool) -> Self {
105        self.config.block_private_ips = block;
106        self
107    }
108
109    #[must_use]
110    pub fn with_rate_limit_rpm(mut self, rpm: u32) -> Self {
111        self.config.rate_limit_rpm = rpm;
112        self
113    }
114
115    #[must_use]
116    pub fn with_max_content_length(mut self, length: usize) -> Self {
117        self.config.max_content_length = length;
118        self
119    }
120
121    #[must_use]
122    pub fn with_max_redirects(mut self, max: u32) -> Self {
123        self.config.max_redirects = max;
124        self
125    }
126
127    #[must_use]
128    pub fn with_max_search_results(mut self, max: usize) -> Self {
129        self.config.max_search_results = max;
130        self
131    }
132
133    #[must_use]
134    pub fn with_playwright_path(mut self, path: PathBuf) -> Self {
135        self.config.playwright_path = Some(path);
136        self
137    }
138
139    #[must_use]
140    pub fn with_screenshot_timeout(mut self, timeout: Duration) -> Self {
141        self.config.screenshot_timeout = timeout;
142        self
143    }
144
145    #[must_use]
146    pub fn with_request_timeout(mut self, timeout: Duration) -> Self {
147        self.config.request_timeout = timeout;
148        self
149    }
150
151    #[must_use]
152    pub fn with_viewport(mut self, width: u32, height: u32) -> Self {
153        self.config.viewport_width = width;
154        self.config.viewport_height = height;
155        self
156    }
157
158    #[must_use]
159    pub fn with_sanitizer_enabled(mut self, enabled: bool) -> Self {
160        self.config.sanitizer_enabled = enabled;
161        self
162    }
163
164    #[must_use]
165    pub fn with_user_agent(mut self, ua: impl Into<String>) -> Self {
166        self.config.user_agent = ua.into();
167        self
168    }
169
170    #[must_use]
171    pub fn build(self) -> WebPluginConfig {
172        self.config
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn default_config_has_expected_values() {
182        let config = WebPluginConfigBuilder::new().build();
183        assert!(matches!(
184            config.search_provider_kind,
185            SearchProviderKind::DuckDuckGo
186        ));
187        assert!(config.block_private_ips);
188        assert_eq!(config.rate_limit_rpm, 30);
189        assert_eq!(config.max_content_length, 50_000);
190        assert_eq!(config.max_redirects, 10);
191        assert_eq!(config.max_search_results, 10);
192        assert!(config.sanitizer_enabled);
193        assert_eq!(config.user_agent, "SwinkAgent/0.5");
194        assert_eq!(config.viewport_width, 1280);
195        assert_eq!(config.viewport_height, 720);
196        assert!(config.domain_allowlist.is_empty());
197        assert!(config.domain_denylist.is_empty());
198        assert!(config.brave_api_key.is_none());
199        assert!(config.tavily_api_key.is_none());
200        assert!(config.playwright_path.is_none());
201        assert_eq!(config.screenshot_timeout, Duration::from_secs(15));
202        assert_eq!(config.request_timeout, Duration::from_secs(30));
203    }
204
205    #[test]
206    fn builder_chaining_applies_overrides() {
207        let config = WebPluginConfigBuilder::new()
208            .with_search_provider(SearchProviderKind::Brave)
209            .with_brave_api_key("test-key")
210            .with_rate_limit_rpm(60)
211            .with_max_content_length(100_000)
212            .with_block_private_ips(false)
213            .with_user_agent("TestAgent/1.0")
214            .with_viewport(1920, 1080)
215            .with_max_redirects(3)
216            .with_max_search_results(25)
217            .with_sanitizer_enabled(false)
218            .with_request_timeout(Duration::from_secs(60))
219            .with_screenshot_timeout(Duration::from_secs(30))
220            .with_domain_allowlist(vec!["example.com".into()])
221            .with_domain_denylist(vec!["evil.com".into()])
222            .build();
223
224        assert!(matches!(
225            config.search_provider_kind,
226            SearchProviderKind::Brave
227        ));
228        assert_eq!(config.brave_api_key.as_deref(), Some("test-key"));
229        assert_eq!(config.rate_limit_rpm, 60);
230        assert_eq!(config.max_content_length, 100_000);
231        assert!(!config.block_private_ips);
232        assert_eq!(config.user_agent, "TestAgent/1.0");
233        assert_eq!(config.viewport_width, 1920);
234        assert_eq!(config.viewport_height, 1080);
235        assert_eq!(config.max_redirects, 3);
236        assert_eq!(config.max_search_results, 25);
237        assert!(!config.sanitizer_enabled);
238        assert_eq!(config.request_timeout, Duration::from_secs(60));
239        assert_eq!(config.screenshot_timeout, Duration::from_secs(30));
240        assert_eq!(config.domain_allowlist, vec!["example.com"]);
241        assert_eq!(config.domain_denylist, vec!["evil.com"]);
242    }
243}