1use std::path::PathBuf;
2use std::time::Duration;
3
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Default, Serialize, Deserialize)]
8pub enum SearchProviderKind {
9 #[default]
10 DuckDuckGo,
11 Brave,
12 Tavily,
13}
14
15#[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#[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_mins(1))
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_mins(1));
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}