linkcheck2/validation/
context.rs

1use crate::{
2    validation::{Cache, Options},
3    Link,
4};
5use reqwest::{header::HeaderMap, Client, Url};
6use std::{
7    sync::{Mutex, MutexGuard},
8    time::Duration,
9};
10
11/// Contextual information that callers can provide to guide the validation
12/// process.
13pub trait Context {
14    /// The HTTP client to use.
15    fn client(&self) -> &Client;
16
17    /// Options to use when checking a link on the filesystem.
18    fn filesystem_options(&self) -> &Options;
19
20    /// Get any extra headers that should be sent when checking this [`Url`].
21    fn url_specific_headers(&self, _url: &Url) -> HeaderMap { HeaderMap::new() }
22
23    /// An optional cache that can be used to avoid unnecessary network
24    /// requests.
25    ///
26    /// We need to use internal mutability here because validation is done
27    /// concurrently. This [`MutexGuard`] is guaranteed to be short lived (just
28    /// the duration of a [`Cache::insert()`] or [`Cache::lookup()`]), so it's
29    /// okay to use a [`std::sync::Mutex`] instead of [`futures::lock::Mutex`].
30    fn cache(&self) -> Option<MutexGuard<'_, Cache>> { None }
31
32    /// How many items should we check at a time?
33    fn concurrency(&self) -> usize { 64 }
34
35    /// How long should a cached item be considered valid for before we need to
36    /// check again?
37    fn cache_timeout(&self) -> Duration {
38        // 24 hours should be a good default
39        Duration::from_secs(24 * 60 * 60)
40    }
41
42    /// Should this [`Link`] be skipped?
43    fn should_ignore(&self, _link: &Link) -> bool { false }
44}
45
46/// A basic [`Context`] implementation which uses all the defaults.
47#[derive(Debug)]
48pub struct BasicContext {
49    /// Options used when validating filesystem links.
50    pub options: Options,
51    client: Client,
52    cache: Mutex<Cache>,
53}
54
55impl BasicContext {
56    /// The User-Agent used by the [`BasicContext::client()`].
57    pub const USER_AGENT: &'static str =
58        concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
59
60    /// Create a [`BasicContext`] with an already initialized [`Client`].
61    pub fn with_client(client: Client) -> Self {
62        BasicContext {
63            client,
64            options: Options::default(),
65            cache: Mutex::new(Cache::new()),
66        }
67    }
68
69    /// Get a mutable reference to the [`Options`] used when validating
70    /// filesystem links.
71    #[deprecated = "Access the field directly instead"]
72    pub fn options_mut(&mut self) -> &mut Options { &mut self.options }
73}
74
75impl Default for BasicContext {
76    fn default() -> Self {
77        let client = Client::builder()
78            .user_agent(BasicContext::USER_AGENT)
79            .build()
80            .expect("Unable to initialize the client");
81
82        BasicContext::with_client(client)
83    }
84}
85
86impl Context for BasicContext {
87    fn client(&self) -> &Client { &self.client }
88
89    fn filesystem_options(&self) -> &Options { &self.options }
90
91    fn cache(&self) -> Option<MutexGuard<'_, Cache>> {
92        Some(self.cache.lock().expect("Mutex was poisoned"))
93    }
94}