Skip to main content

spire_core/
context.rs

1//! The [`Context`] a worker handles.
2
3use std::fmt;
4use std::sync::Arc;
5
6use bytes::Bytes;
7use tokio::sync::OnceCell;
8
9use crate::Result;
10use crate::backend::Client;
11use crate::dataset::{Data, Dataset, Datasets};
12use crate::frontier::memory::InMemFrontier;
13use crate::frontier::queue::Queue;
14use crate::request::{Request, Response, ResponseExt};
15
16/// A request being crawled, with everything needed to handle it.
17///
18/// It holds the [`Request`], a client to fetch it with, a [`Queue`] to add
19/// the links it leads to, and the crawl's datasets. Handlers borrow it, so
20/// several of them can handle one request: the request is fetched once, by
21/// the first [`response`](Self::response) or [`body`](Self::body), and the
22/// response's head is kept. The body is always read from the client, so a
23/// browser page reads as currently rendered.
24pub struct Context<C> {
25    request: Request,
26    client: C,
27    queue: Queue,
28    datasets: Arc<Datasets>,
29    response: OnceCell<Response<()>>,
30}
31
32impl<C> Context<C> {
33    /// Creates a standalone context, e.g. to test a handler.
34    ///
35    /// It gets an empty queue of its own and no datasets; see
36    /// [`with_queue`](Self::with_queue) and
37    /// [`with_dataset`](Self::with_dataset).
38    #[must_use]
39    pub fn new(request: Request, client: C) -> Self {
40        let queue = Queue::new(InMemFrontier::fifo()).child_of(&request);
41        Self::from_parts(request, client, queue, Arc::default())
42    }
43
44    pub(crate) fn from_parts(
45        request: Request,
46        client: C,
47        queue: Queue,
48        datasets: Arc<Datasets>,
49    ) -> Self {
50        Self {
51            request,
52            client,
53            queue,
54            datasets,
55            response: OnceCell::new(),
56        }
57    }
58
59    /// Replaces the queue, binding it to the request.
60    #[must_use]
61    pub fn with_queue(mut self, queue: &Queue) -> Self {
62        self.queue = queue.child_of(&self.request);
63        self
64    }
65
66    /// Registers `dataset` for items of type `T`.
67    #[must_use]
68    pub fn with_dataset<T, D>(mut self, dataset: D) -> Self
69    where
70        T: Send + 'static,
71        D: Dataset<T>,
72    {
73        Arc::make_mut(&mut self.datasets).insert(dataset);
74        self
75    }
76
77    /// Returns the request.
78    #[must_use]
79    pub const fn request(&self) -> &Request {
80        &self.request
81    }
82
83    /// Returns the request mutably, e.g. to add headers before fetching.
84    pub const fn request_mut(&mut self) -> &mut Request {
85        &mut self.request
86    }
87
88    /// Returns the client.
89    #[must_use]
90    pub const fn client(&self) -> &C {
91        &self.client
92    }
93
94    /// Returns the queue for the links this request leads to.
95    #[must_use]
96    pub const fn queue(&self) -> &Queue {
97        &self.queue
98    }
99
100    /// Returns the dataset for items of type `T`.
101    ///
102    /// # Errors
103    ///
104    /// Returns a [`Dataset`](crate::ErrorKind::Dataset) error if no dataset
105    /// is registered for `T`.
106    pub fn dataset<T: Send + 'static>(&self) -> Result<Data<T>> {
107        self.datasets.get()
108    }
109
110    /// Returns the response's head, fetching the request on the first call.
111    ///
112    /// Later calls return the same head without fetching again. A failed
113    /// fetch is not kept: the next call tries again. Once fetched, links
114    /// followed through the queue, including clones taken earlier, resolve
115    /// against the URI the response was served from.
116    ///
117    /// # Errors
118    ///
119    /// Returns an error if the client fails to fetch the request.
120    pub async fn response(&self) -> Result<&Response<()>>
121    where
122        C: Client,
123    {
124        self.response
125            .get_or_try_init(|| async {
126                let response = self.client.fetch(&self.request).await?;
127                if let Some(uri) = response.final_uri() {
128                    self.queue.rebase(uri.clone());
129                }
130                Ok(response)
131            })
132            .await
133    }
134
135    /// Returns the response's body, fetching the request first if needed:
136    /// the bytes received, or a browser page as currently rendered.
137    ///
138    /// # Errors
139    ///
140    /// Returns an error if the request cannot be fetched or its body read.
141    pub async fn body(&self) -> Result<Bytes>
142    where
143        C: Client,
144    {
145        self.response().await?;
146        self.client.body().await
147    }
148}
149
150impl<C> fmt::Debug for Context<C> {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        f.debug_struct("Context")
153            .field("method", self.request.method())
154            .field("uri", self.request.uri())
155            .field("queue", &self.queue)
156            .field("fetched", &self.response.initialized())
157            .finish_non_exhaustive()
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use std::future::Future;
164    use std::sync::atomic::{AtomicUsize, Ordering};
165
166    use super::*;
167    use crate::backend::Noop;
168    use crate::dataset::InMemDataset;
169    use crate::frontier::seed::Seed;
170    use crate::request::{Depth, RequestExt};
171
172    #[tokio::test]
173    async fn standalone_context() {
174        let request = Seed::get("https://example.com/").unwrap().into_request();
175        let cx = Context::new(request, Noop).with_dataset(InMemDataset::<u32>::new());
176
177        cx.dataset::<u32>().unwrap().write(1).await.unwrap();
178        assert!(cx.dataset::<String>().is_err());
179
180        cx.queue().follow("https://example.com/next").await.unwrap();
181        assert_eq!(cx.queue().size_hint(), Some(1));
182        assert_eq!(cx.request().depth(), Depth::ROOT);
183
184        let response = cx.response().await.unwrap();
185        assert!(response.status().is_success());
186    }
187
188    /// Counts its fetches, failing the first `fail` of them, and serves a
189    /// body that changes after the fetch, as a browser page can.
190    struct Counting {
191        fetches: AtomicUsize,
192        fail: usize,
193        body: std::sync::Mutex<&'static str>,
194    }
195
196    impl Client for Counting {
197        fn fetch(&self, _request: &Request) -> impl Future<Output = Result<Response<()>>> + Send {
198            let fetch = self.fetches.fetch_add(1, Ordering::SeqCst);
199            std::future::ready(if fetch < self.fail {
200                Err(crate::Error::new(crate::ErrorKind::Http))
201            } else {
202                Ok(Response::new(()))
203            })
204        }
205
206        fn body(&self) -> impl Future<Output = Result<Bytes>> + Send {
207            let body = *self.body.lock().unwrap();
208            std::future::ready(Ok(Bytes::from_static(body.as_bytes())))
209        }
210    }
211
212    #[tokio::test]
213    async fn fetches_once_and_reads_the_current_body() {
214        let request = Seed::get("https://example.com/").unwrap().into_request();
215        let client = Counting {
216            fetches: AtomicUsize::new(0),
217            fail: 1,
218            body: std::sync::Mutex::new("loaded"),
219        };
220        let cx = Context::new(request, client);
221
222        // A failed fetch is tried again; a successful one is kept.
223        assert!(cx.response().await.is_err());
224        assert_eq!(cx.body().await.unwrap(), "loaded");
225        *cx.client().body.lock().unwrap() = "clicked";
226        assert_eq!(cx.body().await.unwrap(), "clicked");
227        assert!(cx.response().await.unwrap().status().is_success());
228        assert_eq!(cx.client().fetches.load(Ordering::SeqCst), 2);
229    }
230}