use std::fmt;
use std::sync::Arc;
use bytes::Bytes;
use tokio::sync::OnceCell;
use crate::Result;
use crate::backend::Client;
use crate::dataset::{Data, Dataset, Datasets};
use crate::frontier::memory::InMemFrontier;
use crate::frontier::queue::Queue;
use crate::request::{Request, Response, ResponseExt};
pub struct Context<C> {
request: Request,
client: C,
queue: Queue,
datasets: Arc<Datasets>,
response: OnceCell<Response<()>>,
}
impl<C> Context<C> {
#[must_use]
pub fn new(request: Request, client: C) -> Self {
let queue = Queue::new(InMemFrontier::fifo()).child_of(&request);
Self::from_parts(request, client, queue, Arc::default())
}
pub(crate) fn from_parts(
request: Request,
client: C,
queue: Queue,
datasets: Arc<Datasets>,
) -> Self {
Self {
request,
client,
queue,
datasets,
response: OnceCell::new(),
}
}
#[must_use]
pub fn with_queue(mut self, queue: &Queue) -> Self {
self.queue = queue.child_of(&self.request);
self
}
#[must_use]
pub fn with_dataset<T, D>(mut self, dataset: D) -> Self
where
T: Send + 'static,
D: Dataset<T>,
{
Arc::make_mut(&mut self.datasets).insert(dataset);
self
}
#[must_use]
pub const fn request(&self) -> &Request {
&self.request
}
pub const fn request_mut(&mut self) -> &mut Request {
&mut self.request
}
#[must_use]
pub const fn client(&self) -> &C {
&self.client
}
#[must_use]
pub const fn queue(&self) -> &Queue {
&self.queue
}
pub fn dataset<T: Send + 'static>(&self) -> Result<Data<T>> {
self.datasets.get()
}
pub async fn response(&self) -> Result<&Response<()>>
where
C: Client,
{
self.response
.get_or_try_init(|| async {
let response = self.client.fetch(&self.request).await?;
if let Some(uri) = response.final_uri() {
self.queue.rebase(uri.clone());
}
Ok(response)
})
.await
}
pub async fn body(&self) -> Result<Bytes>
where
C: Client,
{
self.response().await?;
self.client.body().await
}
}
impl<C> fmt::Debug for Context<C> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Context")
.field("method", self.request.method())
.field("uri", self.request.uri())
.field("queue", &self.queue)
.field("fetched", &self.response.initialized())
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use std::future::Future;
use std::sync::atomic::{AtomicUsize, Ordering};
use super::*;
use crate::backend::Noop;
use crate::dataset::InMemDataset;
use crate::frontier::seed::Seed;
use crate::request::{Depth, RequestExt};
#[tokio::test]
async fn standalone_context() {
let request = Seed::get("https://example.com/").unwrap().into_request();
let cx = Context::new(request, Noop).with_dataset(InMemDataset::<u32>::new());
cx.dataset::<u32>().unwrap().write(1).await.unwrap();
assert!(cx.dataset::<String>().is_err());
cx.queue().follow("https://example.com/next").await.unwrap();
assert_eq!(cx.queue().size_hint(), Some(1));
assert_eq!(cx.request().depth(), Depth::ROOT);
let response = cx.response().await.unwrap();
assert!(response.status().is_success());
}
struct Counting {
fetches: AtomicUsize,
fail: usize,
body: std::sync::Mutex<&'static str>,
}
impl Client for Counting {
fn fetch(&self, _request: &Request) -> impl Future<Output = Result<Response<()>>> + Send {
let fetch = self.fetches.fetch_add(1, Ordering::SeqCst);
std::future::ready(if fetch < self.fail {
Err(crate::Error::new(crate::ErrorKind::Http))
} else {
Ok(Response::new(()))
})
}
fn body(&self) -> impl Future<Output = Result<Bytes>> + Send {
let body = *self.body.lock().unwrap();
std::future::ready(Ok(Bytes::from_static(body.as_bytes())))
}
}
#[tokio::test]
async fn fetches_once_and_reads_the_current_body() {
let request = Seed::get("https://example.com/").unwrap().into_request();
let client = Counting {
fetches: AtomicUsize::new(0),
fail: 1,
body: std::sync::Mutex::new("loaded"),
};
let cx = Context::new(request, client);
assert!(cx.response().await.is_err());
assert_eq!(cx.body().await.unwrap(), "loaded");
*cx.client().body.lock().unwrap() = "clicked";
assert_eq!(cx.body().await.unwrap(), "clicked");
assert!(cx.response().await.unwrap().status().is_success());
assert_eq!(cx.client().fetches.load(Ordering::SeqCst), 2);
}
}