1use std::collections::BTreeMap;
4
5use serde::Serialize;
6
7use crate::enums::{CrawlerContentFormat, CrawlerWebhookEvent};
8use crate::error::ScrapflyError;
9
10#[derive(Debug, Clone, Default, Serialize)]
15pub struct CrawlerConfig {
16 pub url: String,
18
19 #[serde(skip_serializing_if = "Option::is_none")]
21 pub page_limit: Option<u32>,
22 #[serde(skip_serializing_if = "Option::is_none")]
24 pub max_depth: Option<u32>,
25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub max_duration: Option<u32>,
28 #[serde(skip_serializing_if = "Option::is_none")]
30 pub max_api_credit: Option<u32>,
31
32 #[serde(skip_serializing_if = "Vec::is_empty")]
34 pub exclude_paths: Vec<String>,
35 #[serde(skip_serializing_if = "Vec::is_empty")]
37 pub include_only_paths: Vec<String>,
38
39 #[serde(skip_serializing_if = "is_false")]
41 pub ignore_base_path_restriction: bool,
42 #[serde(skip_serializing_if = "is_false")]
44 pub follow_external_links: bool,
45 #[serde(skip_serializing_if = "Vec::is_empty")]
47 pub allowed_external_domains: Vec<String>,
48
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub follow_internal_subdomains: Option<bool>,
52 #[serde(skip_serializing_if = "Vec::is_empty")]
54 pub allowed_internal_subdomains: Vec<String>,
55
56 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
58 pub headers: BTreeMap<String, String>,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub delay: Option<u32>,
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub user_agent: Option<String>,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub max_concurrency: Option<u32>,
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub rendering_delay: Option<u32>,
71
72 #[serde(skip_serializing_if = "is_false")]
74 pub use_sitemaps: bool,
75 #[serde(skip_serializing_if = "is_false")]
77 pub ignore_no_follow: bool,
78
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub respect_robots_txt: Option<bool>,
82
83 #[serde(skip_serializing_if = "is_false")]
85 pub cache: bool,
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub cache_ttl: Option<u32>,
89 #[serde(skip_serializing_if = "is_false")]
91 pub cache_clear: bool,
92
93 #[serde(skip_serializing_if = "Vec::is_empty")]
95 pub content_formats: Vec<CrawlerContentFormat>,
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub extraction_rules: Option<serde_json::Value>,
99
100 #[serde(skip_serializing_if = "is_false")]
102 pub asp: bool,
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub proxy_pool: Option<String>,
106 #[serde(skip_serializing_if = "Option::is_none")]
108 pub country: Option<String>,
109
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub webhook_name: Option<String>,
113 #[serde(skip_serializing_if = "Vec::is_empty")]
115 pub webhook_events: Vec<CrawlerWebhookEvent>,
116}
117
118fn is_false(v: &bool) -> bool {
119 !*v
120}
121
122impl CrawlerConfig {
123 pub fn builder(url: impl Into<String>) -> CrawlerConfigBuilder {
125 CrawlerConfigBuilder {
126 cfg: CrawlerConfig {
127 url: url.into(),
128 ..Default::default()
129 },
130 }
131 }
132
133 pub fn validate(&self) -> Result<(), ScrapflyError> {
136 if self.url.is_empty() {
137 return Err(ScrapflyError::Config("url is required".into()));
138 }
139 if let Some(d) = self.max_duration {
140 if !(15..=10800).contains(&d) {
141 return Err(ScrapflyError::Config(format!(
142 "max_duration must be between 15 and 10800 seconds, got {}",
143 d
144 )));
145 }
146 }
147 if let Some(rd) = self.rendering_delay {
148 if rd > 25000 {
149 return Err(ScrapflyError::Config(format!(
150 "rendering_delay must be between 0 and 25000 ms, got {}",
151 rd
152 )));
153 }
154 }
155 if let Some(delay) = self.delay {
156 if delay > 15000 {
157 return Err(ScrapflyError::Config(format!(
158 "delay must be between 0 and 15000 ms, got {}",
159 delay
160 )));
161 }
162 }
163 if let Some(ttl) = self.cache_ttl {
164 if ttl > 604800 {
165 return Err(ScrapflyError::Config(format!(
166 "cache_ttl must be between 0 and 604800 seconds, got {}",
167 ttl
168 )));
169 }
170 }
171 if self.exclude_paths.len() > 100 {
172 return Err(ScrapflyError::Config(format!(
173 "exclude_paths is limited to 100 entries, got {}",
174 self.exclude_paths.len()
175 )));
176 }
177 if self.include_only_paths.len() > 100 {
178 return Err(ScrapflyError::Config(format!(
179 "include_only_paths is limited to 100 entries, got {}",
180 self.include_only_paths.len()
181 )));
182 }
183 if !self.exclude_paths.is_empty() && !self.include_only_paths.is_empty() {
184 return Err(ScrapflyError::Config(
185 "exclude_paths and include_only_paths are mutually exclusive".into(),
186 ));
187 }
188 if self.allowed_external_domains.len() > 250 {
189 return Err(ScrapflyError::Config(format!(
190 "allowed_external_domains is limited to 250 entries, got {}",
191 self.allowed_external_domains.len()
192 )));
193 }
194 if self.allowed_internal_subdomains.len() > 250 {
195 return Err(ScrapflyError::Config(format!(
196 "allowed_internal_subdomains is limited to 250 entries, got {}",
197 self.allowed_internal_subdomains.len()
198 )));
199 }
200 Ok(())
201 }
202
203 pub fn to_json_body(&self) -> Result<Vec<u8>, ScrapflyError> {
205 self.validate()?;
206 Ok(serde_json::to_vec(self)?)
207 }
208}
209
210#[derive(Debug, Clone)]
212pub struct CrawlerConfigBuilder {
213 cfg: CrawlerConfig,
214}
215
216impl CrawlerConfigBuilder {
217 pub fn page_limit(mut self, v: u32) -> Self {
219 self.cfg.page_limit = Some(v);
220 self
221 }
222 pub fn max_depth(mut self, v: u32) -> Self {
224 self.cfg.max_depth = Some(v);
225 self
226 }
227 pub fn max_duration(mut self, v: u32) -> Self {
229 self.cfg.max_duration = Some(v);
230 self
231 }
232 pub fn max_api_credit(mut self, v: u32) -> Self {
234 self.cfg.max_api_credit = Some(v);
235 self
236 }
237 pub fn exclude_paths(mut self, v: Vec<String>) -> Self {
239 self.cfg.exclude_paths = v;
240 self
241 }
242 pub fn include_only_paths(mut self, v: Vec<String>) -> Self {
244 self.cfg.include_only_paths = v;
245 self
246 }
247 pub fn ignore_base_path_restriction(mut self, v: bool) -> Self {
249 self.cfg.ignore_base_path_restriction = v;
250 self
251 }
252 pub fn follow_external_links(mut self, v: bool) -> Self {
254 self.cfg.follow_external_links = v;
255 self
256 }
257 pub fn allowed_external_domains(mut self, v: Vec<String>) -> Self {
259 self.cfg.allowed_external_domains = v;
260 self
261 }
262 pub fn follow_internal_subdomains(mut self, v: bool) -> Self {
264 self.cfg.follow_internal_subdomains = Some(v);
265 self
266 }
267 pub fn allowed_internal_subdomains(mut self, v: Vec<String>) -> Self {
269 self.cfg.allowed_internal_subdomains = v;
270 self
271 }
272 pub fn header(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
274 self.cfg.headers.insert(k.into(), v.into());
275 self
276 }
277 pub fn delay(mut self, v: u32) -> Self {
279 self.cfg.delay = Some(v);
280 self
281 }
282 pub fn user_agent(mut self, v: impl Into<String>) -> Self {
284 self.cfg.user_agent = Some(v.into());
285 self
286 }
287 pub fn max_concurrency(mut self, v: u32) -> Self {
289 self.cfg.max_concurrency = Some(v);
290 self
291 }
292 pub fn rendering_delay(mut self, v: u32) -> Self {
294 self.cfg.rendering_delay = Some(v);
295 self
296 }
297 pub fn use_sitemaps(mut self, v: bool) -> Self {
299 self.cfg.use_sitemaps = v;
300 self
301 }
302 pub fn ignore_no_follow(mut self, v: bool) -> Self {
304 self.cfg.ignore_no_follow = v;
305 self
306 }
307 pub fn respect_robots_txt(mut self, v: bool) -> Self {
309 self.cfg.respect_robots_txt = Some(v);
310 self
311 }
312 pub fn cache(mut self, v: bool) -> Self {
314 self.cfg.cache = v;
315 self
316 }
317 pub fn cache_ttl(mut self, v: u32) -> Self {
319 self.cfg.cache_ttl = Some(v);
320 self
321 }
322 pub fn cache_clear(mut self, v: bool) -> Self {
324 self.cfg.cache_clear = v;
325 self
326 }
327 pub fn content_format(mut self, v: CrawlerContentFormat) -> Self {
329 self.cfg.content_formats.push(v);
330 self
331 }
332 pub fn extraction_rules(mut self, v: serde_json::Value) -> Self {
334 self.cfg.extraction_rules = Some(v);
335 self
336 }
337 pub fn asp(mut self, v: bool) -> Self {
339 self.cfg.asp = v;
340 self
341 }
342 pub fn proxy_pool(mut self, v: impl Into<String>) -> Self {
344 self.cfg.proxy_pool = Some(v.into());
345 self
346 }
347 pub fn country(mut self, v: impl Into<String>) -> Self {
349 self.cfg.country = Some(v.into());
350 self
351 }
352 pub fn webhook_name(mut self, v: impl Into<String>) -> Self {
354 self.cfg.webhook_name = Some(v.into());
355 self
356 }
357 pub fn webhook_event(mut self, v: CrawlerWebhookEvent) -> Self {
359 self.cfg.webhook_events.push(v);
360 self
361 }
362 pub fn build(self) -> Result<CrawlerConfig, ScrapflyError> {
364 self.cfg.validate()?;
365 Ok(self.cfg)
366 }
367}