1mod error;
2mod serde;
3mod toml;
4
5pub use self::{
6 error::ConfigError,
7 serde::{SerializableConfig, compile_config},
8 toml::read_config,
9};
10use alloc::sync::Arc;
11use core::{cmp::Reverse, ops::Deref, time::Duration};
12use http::{HeaderMap, StatusCode};
13use regex::Regex;
14use rlimit::{Resource, getrlimit};
15use std::collections::{HashMap, HashSet};
16use url::Url;
17
18pub const DEFAULT_ACCEPTED_SCHEMES: &[&str] = &["http", "https"];
20pub const DEFAULT_ACCEPTED_STATUS_CODES: &[StatusCode] = &[StatusCode::OK];
22pub const DEFAULT_MAX_REDIRECTS: usize = 16;
24pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
26
27const DEFAULT_MINIMUM_CONCURRENCY: usize = 256;
28
29pub fn default_concurrency() -> usize {
31 getrlimit(Resource::NOFILE)
32 .map(|(count, _)| (count / 2) as _)
33 .unwrap_or(DEFAULT_MINIMUM_CONCURRENCY)
34}
35
36#[derive(Clone, Debug)]
38pub struct Config {
39 roots: Vec<String>,
40 ignored_links: Vec<Regex>,
41 default: Arc<SiteConfig>,
42 sites: HashMap<String, Vec<(String, Arc<SiteConfig>)>>,
43 concurrency: ConcurrencyConfig,
44 persistent_cache: bool,
45 rate_limit: RateLimitConfig,
46}
47
48impl Config {
49 pub fn new(
51 roots: Vec<String>,
52 default: Arc<SiteConfig>,
53 sites: HashMap<String, HashMap<String, Arc<SiteConfig>>>,
54 ) -> Self {
55 Self {
56 roots,
57 ignored_links: Default::default(),
58 default,
59 sites: sites
60 .into_iter()
61 .map(|(host, value)| {
62 let mut paths = value.into_iter().collect::<Vec<_>>();
63 paths.sort_by_key(|(path, _)| Reverse(path.clone()));
64 (host, paths)
65 })
66 .collect(),
67 concurrency: Default::default(),
68 persistent_cache: false,
69 rate_limit: Default::default(),
70 }
71 }
72
73 pub fn roots(&self) -> impl Iterator<Item = &str> {
75 self.roots.iter().map(Deref::deref)
76 }
77
78 pub fn ignored_links(&self) -> impl Iterator<Item = &Regex> {
80 self.ignored_links.iter()
81 }
82
83 pub const fn sites(&self) -> &HashMap<String, Vec<(String, Arc<SiteConfig>)>> {
85 &self.sites
86 }
87
88 pub fn site(&self, url: &Url) -> &SiteConfig {
90 self.get_site(url).unwrap_or(&self.default)
91 }
92
93 pub const fn concurrency(&self) -> &ConcurrencyConfig {
95 &self.concurrency
96 }
97
98 pub const fn persistent_cache(&self) -> bool {
100 self.persistent_cache
101 }
102
103 pub const fn rate_limit(&self) -> &RateLimitConfig {
105 &self.rate_limit
106 }
107
108 pub fn set_concurrency(mut self, concurrency: ConcurrencyConfig) -> Self {
110 self.concurrency = concurrency;
111 self
112 }
113
114 pub fn set_ignored_links(mut self, links: Vec<Regex>) -> Self {
116 self.ignored_links = links;
117 self
118 }
119
120 pub const fn set_persistent_cache(mut self, persistent_cache: bool) -> Self {
122 self.persistent_cache = persistent_cache;
123 self
124 }
125
126 pub fn set_rate_limit(mut self, rate_limit: RateLimitConfig) -> Self {
128 self.rate_limit = rate_limit;
129 self
130 }
131
132 fn get_site(&self, url: &Url) -> Option<&SiteConfig> {
133 self.sites()
134 .get(url.host_str()?)?
135 .iter()
136 .find_map(|(path, config)| url.path().starts_with(path).then_some(config.as_ref()))
137 }
138}
139
140#[derive(Clone, Debug, Default, PartialEq)]
142pub struct SiteConfig {
143 id: Option<Arc<str>>,
144 cache: CacheConfig,
145 fragments_ignored: bool,
146 headers: HeaderMap,
147 max_redirects: usize,
148 recursive: bool,
149 retry: Arc<RetryConfig>,
150 scheme: SchemeConfig,
151 status: StatusConfig,
152 timeout: Option<Duration>,
153 validation: ValidationConfig,
154}
155
156impl SiteConfig {
157 pub fn new() -> Self {
159 Self::default()
160 }
161
162 pub const fn id(&self) -> Option<&Arc<str>> {
164 self.id.as_ref()
165 }
166
167 pub const fn cache(&self) -> &CacheConfig {
169 &self.cache
170 }
171
172 pub const fn fragments_ignored(&self) -> bool {
174 self.fragments_ignored
175 }
176
177 pub const fn headers(&self) -> &HeaderMap {
179 &self.headers
180 }
181
182 pub const fn retry(&self) -> &Arc<RetryConfig> {
184 &self.retry
185 }
186
187 pub const fn status(&self) -> &StatusConfig {
189 &self.status
190 }
191
192 pub const fn scheme(&self) -> &SchemeConfig {
194 &self.scheme
195 }
196
197 pub const fn max_redirects(&self) -> usize {
199 self.max_redirects
200 }
201
202 pub const fn timeout(&self) -> Option<Duration> {
204 self.timeout
205 }
206
207 pub const fn recursive(&self) -> bool {
209 self.recursive
210 }
211
212 pub const fn validation(&self) -> &ValidationConfig {
214 &self.validation
215 }
216
217 pub fn set_id(mut self, id: Option<Arc<str>>) -> Self {
219 self.id = id;
220 self
221 }
222
223 pub const fn set_cache(mut self, cache: CacheConfig) -> Self {
225 self.cache = cache;
226 self
227 }
228
229 pub const fn set_fragments_ignored(mut self, ignored: bool) -> Self {
231 self.fragments_ignored = ignored;
232 self
233 }
234
235 pub fn set_headers(mut self, headers: HeaderMap) -> Self {
237 self.headers = headers;
238 self
239 }
240
241 pub fn set_retry(mut self, retry: Arc<RetryConfig>) -> Self {
243 self.retry = retry;
244 self
245 }
246
247 pub fn set_status(mut self, status: StatusConfig) -> Self {
249 self.status = status;
250 self
251 }
252
253 pub fn set_scheme(mut self, scheme: SchemeConfig) -> Self {
255 self.scheme = scheme;
256 self
257 }
258
259 pub const fn set_max_redirects(mut self, count: usize) -> Self {
261 self.max_redirects = count;
262 self
263 }
264
265 pub const fn set_timeout(mut self, duration: Option<Duration>) -> Self {
267 self.timeout = duration;
268 self
269 }
270
271 pub const fn set_recursive(mut self, recursive: bool) -> Self {
273 self.recursive = recursive;
274 self
275 }
276
277 pub fn set_validation(mut self, validation: ValidationConfig) -> Self {
279 self.validation = validation;
280 self
281 }
282}
283
284#[derive(Clone, Debug, Eq, PartialEq)]
286pub struct StatusConfig {
287 accepted: HashSet<StatusCode>,
288}
289
290impl StatusConfig {
291 pub const fn new(accepted: HashSet<StatusCode>) -> Self {
293 Self { accepted }
294 }
295
296 pub fn accepted(&self, status: StatusCode) -> bool {
298 self.accepted.contains(&status)
299 }
300}
301
302impl Default for StatusConfig {
303 fn default() -> Self {
304 Self {
305 accepted: DEFAULT_ACCEPTED_STATUS_CODES.iter().copied().collect(),
306 }
307 }
308}
309
310#[derive(Clone, Debug, Eq, PartialEq)]
312pub struct SchemeConfig {
313 accepted: HashSet<String>,
314}
315
316impl SchemeConfig {
317 pub const fn new(accepted: HashSet<String>) -> Self {
319 Self { accepted }
320 }
321
322 pub fn accepted(&self, scheme: &str) -> bool {
324 self.accepted.contains(scheme)
325 }
326}
327
328impl Default for SchemeConfig {
329 fn default() -> Self {
330 Self {
331 accepted: DEFAULT_ACCEPTED_SCHEMES
332 .iter()
333 .copied()
334 .map(ToOwned::to_owned)
335 .collect(),
336 }
337 }
338}
339
340#[derive(Clone, Debug, Eq, PartialEq, Default)]
342pub struct ValidationConfig {
343 html: Option<MarkupConfig>,
344 svg: Option<MarkupConfig>,
345 css: bool,
346}
347
348impl ValidationConfig {
349 pub const fn html(&self) -> Option<&MarkupConfig> {
351 self.html.as_ref()
352 }
353
354 pub const fn svg(&self) -> Option<&MarkupConfig> {
356 self.svg.as_ref()
357 }
358
359 pub const fn css(&self) -> bool {
361 self.css
362 }
363
364 pub fn set_html(mut self, config: Option<MarkupConfig>) -> Self {
366 self.html = config;
367 self
368 }
369
370 pub fn set_svg(mut self, config: Option<MarkupConfig>) -> Self {
372 self.svg = config;
373 self
374 }
375
376 pub const fn set_css(mut self, enabled: bool) -> Self {
378 self.css = enabled;
379 self
380 }
381}
382
383#[derive(Clone, Debug, Default)]
385pub struct MarkupConfig {
386 ignored_attributes: Vec<Regex>,
387 ignored_elements: Vec<Regex>,
388}
389
390impl MarkupConfig {
391 pub const fn new(ignored_attributes: Vec<Regex>, ignored_elements: Vec<Regex>) -> Self {
393 Self {
394 ignored_attributes,
395 ignored_elements,
396 }
397 }
398
399 pub fn ignored_attributes(&self) -> &[Regex] {
401 &self.ignored_attributes
402 }
403
404 pub fn ignored_elements(&self) -> &[Regex] {
406 &self.ignored_elements
407 }
408}
409
410impl PartialEq for MarkupConfig {
411 fn eq(&self, other: &Self) -> bool {
412 self.ignored_attributes.len() == other.ignored_attributes.len()
413 && self.ignored_elements.len() == other.ignored_elements.len()
414 && self
415 .ignored_attributes
416 .iter()
417 .zip(&other.ignored_attributes)
418 .chain(self.ignored_elements.iter().zip(&other.ignored_elements))
419 .all(|(one, other)| one.as_str() == other.as_str())
420 }
421}
422
423impl Eq for MarkupConfig {}
424
425#[derive(Clone, Debug, Default, Eq, PartialEq)]
427pub struct CacheConfig {
428 max_age: Duration,
429 stale_while_revalidate: Duration,
430}
431
432impl CacheConfig {
433 pub fn new() -> Self {
435 Self::default()
436 }
437
438 pub const fn max_age(&self) -> Duration {
440 self.max_age
441 }
442
443 pub const fn stale_while_revalidate(&self) -> Duration {
445 self.stale_while_revalidate
446 }
447
448 pub const fn set_max_age(mut self, age: Duration) -> Self {
450 self.max_age = age;
451 self
452 }
453
454 pub const fn set_stale_while_revalidate(mut self, period: Duration) -> Self {
456 self.stale_while_revalidate = period;
457 self
458 }
459}
460
461#[derive(Clone, Debug, Default, PartialEq)]
463pub struct RetryConfig {
464 count: usize,
465 factor: f64,
466 interval: RetryDurationConfig,
467 statuses: HashSet<StatusCode>,
468}
469
470impl RetryConfig {
471 pub fn new() -> Self {
473 Self {
474 count: 0,
475 factor: 1.0,
476 interval: Default::default(),
477 statuses: Default::default(),
478 }
479 }
480
481 pub const fn count(&self) -> usize {
483 self.count
484 }
485
486 pub const fn factor(&self) -> f64 {
488 self.factor
489 }
490
491 pub const fn interval(&self) -> &RetryDurationConfig {
493 &self.interval
494 }
495
496 pub const fn statuses(&self) -> &HashSet<StatusCode> {
498 &self.statuses
499 }
500
501 pub const fn set_count(mut self, count: usize) -> Self {
503 self.count = count;
504 self
505 }
506
507 pub const fn set_factor(mut self, factor: f64) -> Self {
509 self.factor = factor;
510 self
511 }
512
513 pub const fn set_interval(mut self, duration: RetryDurationConfig) -> Self {
515 self.interval = duration;
516 self
517 }
518
519 pub fn set_statuses(mut self, statuses: HashSet<StatusCode>) -> Self {
521 self.statuses = statuses;
522 self
523 }
524}
525
526#[derive(Clone, Debug, Default, Eq, PartialEq)]
528pub struct RetryDurationConfig {
529 initial: Duration,
530 cap: Option<Duration>,
531}
532
533impl RetryDurationConfig {
534 pub fn new() -> Self {
536 Self::default()
537 }
538
539 pub const fn initial(&self) -> Duration {
541 self.initial
542 }
543
544 pub const fn cap(&self) -> Option<Duration> {
546 self.cap
547 }
548
549 pub const fn set_initial(mut self, duration: Duration) -> Self {
551 self.initial = duration;
552 self
553 }
554
555 pub const fn set_cap(mut self, duration: Option<Duration>) -> Self {
557 self.cap = duration;
558 self
559 }
560}
561
562#[derive(Clone, Debug, Default, Eq, PartialEq)]
564pub struct ConcurrencyConfig {
565 global: Option<usize>,
566 sites: HashMap<String, usize>,
567}
568
569impl ConcurrencyConfig {
570 pub fn new() -> Self {
572 Self::default()
573 }
574
575 pub const fn global(&self) -> Option<usize> {
577 self.global
578 }
579
580 pub const fn sites(&self) -> &HashMap<String, usize> {
582 &self.sites
583 }
584
585 pub const fn set_global(mut self, concurrency: Option<usize>) -> Self {
587 self.global = concurrency;
588 self
589 }
590
591 pub fn set_sites(mut self, sites: HashMap<String, usize>) -> Self {
593 self.sites = sites;
594 self
595 }
596}
597
598#[derive(Clone, Debug, Default, Eq, PartialEq)]
600pub struct RateLimitConfig {
601 global: Option<SiteRateLimitConfig>,
602 sites: HashMap<String, SiteRateLimitConfig>,
603}
604
605impl RateLimitConfig {
606 pub fn new() -> Self {
608 Self::default()
609 }
610
611 pub const fn global(&self) -> Option<&SiteRateLimitConfig> {
613 self.global.as_ref()
614 }
615
616 pub const fn sites(&self) -> &HashMap<String, SiteRateLimitConfig> {
618 &self.sites
619 }
620
621 pub const fn set_global(mut self, rate_limit: Option<SiteRateLimitConfig>) -> Self {
623 self.global = rate_limit;
624 self
625 }
626
627 pub fn set_sites(mut self, sites: HashMap<String, SiteRateLimitConfig>) -> Self {
629 self.sites = sites;
630 self
631 }
632}
633
634#[derive(Clone, Debug, Default, Eq, PartialEq)]
636pub struct SiteRateLimitConfig {
637 supply: u64,
638 window: Duration,
639}
640
641impl SiteRateLimitConfig {
642 pub const fn new(supply: u64, window: Duration) -> Self {
644 Self { supply, window }
645 }
646
647 pub const fn supply(&self) -> u64 {
649 self.supply
650 }
651
652 pub const fn window(&self) -> Duration {
654 self.window
655 }
656
657 pub const fn set_supply(mut self, supply: u64) -> Self {
659 self.supply = supply;
660 self
661 }
662
663 pub const fn set_window(mut self, window: Duration) -> Self {
665 self.window = window;
666 self
667 }
668}
669
670#[cfg(test)]
671mod tests {
672 use super::*;
673
674 #[test]
675 fn site_config_path_order() {
676 let config = Config::new(
677 vec![],
678 Default::default(),
679 [(
680 "example.com".to_string(),
681 [
682 (
683 "/foo".to_string(),
684 SiteConfig::default()
685 .set_id(Some("foo".into()))
686 .set_recursive(true)
687 .into(),
688 ),
689 (
690 "/bar".to_string(),
691 SiteConfig::default()
692 .set_id(Some("bar".into()))
693 .set_recursive(true)
694 .into(),
695 ),
696 (
697 "/".to_string(),
698 SiteConfig::default()
699 .set_id(Some("top".into()))
700 .set_recursive(false)
701 .into(),
702 ),
703 (
704 "/baz".to_string(),
705 SiteConfig::default()
706 .set_id(Some("baz".into()))
707 .set_recursive(true)
708 .into(),
709 ),
710 (
711 "/qux".to_string(),
712 SiteConfig::default()
713 .set_id(Some("qux".into()))
714 .set_recursive(true)
715 .into(),
716 ),
717 ]
718 .into_iter()
719 .collect(),
720 )]
721 .into(),
722 );
723
724 assert!(
725 config
726 .site(&Url::parse("http://example.com/foo").unwrap())
727 .recursive()
728 );
729 assert!(
730 config
731 .site(&Url::parse("http://example.com/bar").unwrap())
732 .recursive()
733 );
734 assert!(
735 config
736 .site(&Url::parse("http://example.com/baz").unwrap())
737 .recursive()
738 );
739 assert!(
740 config
741 .site(&Url::parse("http://example.com/qux").unwrap())
742 .recursive()
743 );
744 assert!(
745 !config
746 .site(&Url::parse("http://example.com/other").unwrap())
747 .recursive()
748 );
749 }
750
751 #[test]
752 fn default_validation_config() {
753 let config = ValidationConfig::default();
754
755 assert!(config.html().is_none());
756 assert!(config.svg().is_none());
757 assert!(!config.css());
758 }
759
760 #[test]
761 fn set_validation_config_enabled() {
762 let config = ValidationConfig::default()
763 .set_html(Some(MarkupConfig::default()))
764 .set_svg(Some(MarkupConfig::default()))
765 .set_css(true);
766
767 assert!(config.html().is_some());
768 assert!(config.svg().is_some());
769 assert!(config.css());
770 }
771
772 #[test]
773 fn validate_site_config() {
774 let config = SiteConfig::default();
775
776 assert!(config.validation().html().is_none());
777 assert!(
778 config
779 .set_validation(ValidationConfig::default().set_html(Some(MarkupConfig::default())))
780 .validation()
781 .html()
782 .is_some()
783 );
784 }
785
786 #[test]
787 fn retry_config_statuses() {
788 let config = RetryConfig::new().set_statuses(HashSet::from([StatusCode::REQUEST_TIMEOUT]));
789
790 assert_eq!(
791 config.statuses(),
792 &HashSet::from([StatusCode::REQUEST_TIMEOUT])
793 );
794 }
795}