Skip to content

Commit c228a8b

Browse files
Merge pull request #3 from lycheeverse/plugin-prototype-docs
Add documentation to `chain` module
2 parents 7c4834d + d99ba5c commit c228a8b

File tree

4 files changed

+125
-17
lines changed

4 files changed

+125
-17
lines changed

lychee-lib/src/chain/mod.rs

+105-6
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,89 @@
1+
//! [Chain of responsibility pattern][pattern] implementation.
2+
//!
3+
//! lychee is based on a chain of responsibility, where each handler can modify
4+
//! a request and decide if it should be passed to the next element or not.
5+
//!
6+
//! The chain is implemented as a vector of [`Chainable`] handlers. It is
7+
//! traversed by calling [`Chain::traverse`], which will call
8+
//! [`Chainable::chain`] on each handler in the chain consecutively.
9+
//!
10+
//! To add external handlers, you can implement the [`Chainable`] trait and add
11+
//! the handler to the chain.
12+
//!
13+
//! [pattern]: https://github.com/lpxxn/rust-design-pattern/blob/master/behavioral/chain_of_responsibility.rs
114
use crate::Status;
215
use async_trait::async_trait;
316
use core::fmt::Debug;
417
use std::sync::Arc;
518
use tokio::sync::Mutex;
619

20+
/// Result of a handler.
21+
///
22+
/// This is used to decide if the chain should continue to the next handler or
23+
/// stop and return the result:
24+
///
25+
/// - If the chain should continue, the handler should return
26+
/// [`ChainResult::Next`]. This will traverse the next handler in the chain.
27+
/// - If the chain should stop, the handler should return [`ChainResult::Done`].
28+
/// All subsequent chain elements are skipped and the result is returned.
729
#[derive(Debug, PartialEq)]
8-
pub(crate) enum ChainResult<T, R> {
30+
pub enum ChainResult<T, R> {
31+
/// Continue to the next handler in the chain.
932
Next(T),
33+
/// Stop the chain and return the result.
1034
Done(R),
1135
}
1236

37+
/// Request chain type
38+
///
39+
/// This takes a request and returns a status.
1340
pub(crate) type RequestChain = Chain<reqwest::Request, Status>;
1441

42+
/// Inner chain type.
43+
///
44+
/// This holds all handlers, which were chained together.
45+
/// Handlers are traversed in order.
46+
///
47+
/// Each handler needs to implement the `Chainable` trait and be `Send`, because
48+
/// the chain is traversed concurrently and the handlers can be sent between
49+
/// threads.
1550
pub(crate) type InnerChain<T, R> = Vec<Box<dyn Chainable<T, R> + Send>>;
1651

52+
/// The outer chain type.
53+
///
54+
/// This is a wrapper around the inner chain type and allows for
55+
/// concurrent access to the chain.
1756
#[derive(Debug)]
1857
pub struct Chain<T, R>(Arc<Mutex<InnerChain<T, R>>>);
1958

2059
impl<T, R> Default for Chain<T, R> {
2160
fn default() -> Self {
22-
Self(Arc::new(Mutex::new(vec![])))
61+
Self(Arc::new(Mutex::new(InnerChain::default())))
2362
}
2463
}
2564

2665
impl<T, R> Clone for Chain<T, R> {
2766
fn clone(&self) -> Self {
67+
// Cloning the chain is a cheap operation, because the inner chain is
68+
// wrapped in an `Arc` and `Mutex`.
2869
Self(self.0.clone())
2970
}
3071
}
3172

3273
impl<T, R> Chain<T, R> {
74+
/// Create a new chain from a vector of chainable handlers
3375
pub(crate) fn new(values: InnerChain<T, R>) -> Self {
3476
Self(Arc::new(Mutex::new(values)))
3577
}
3678

79+
/// Traverse the chain with the given input.
80+
///
81+
/// This will call `chain` on each handler in the chain and return
82+
/// the result. If a handler returns `ChainResult::Done`, the chain
83+
/// will stop and return.
84+
///
85+
/// If no handler returns `ChainResult::Done`, the chain will return
86+
/// `ChainResult::Next` with the input.
3787
pub(crate) async fn traverse(&self, mut input: T) -> ChainResult<T, R> {
3888
use ChainResult::{Done, Next};
3989
for e in self.0.lock().await.iter_mut() {
@@ -49,23 +99,71 @@ impl<T, R> Chain<T, R> {
4999
}
50100
}
51101

102+
/// Chainable trait for implementing request handlers
103+
///
104+
/// This trait needs to be implemented by all chainable handlers.
105+
/// It is the only requirement to handle requests in lychee.
106+
///
107+
/// It takes an input request and returns a [`ChainResult`], which can be either
108+
/// [`ChainResult::Next`] to continue to the next handler or
109+
/// [`ChainResult::Done`] to stop the chain.
110+
///
111+
/// The request can be modified by the handler before it is passed to the next
112+
/// handler. This allows for modifying the request, such as adding headers or
113+
/// changing the URL (e.g. for remapping or filtering).
52114
#[async_trait]
53-
pub(crate) trait Chainable<T, R>: Debug {
115+
pub trait Chainable<T, R>: Debug {
116+
/// Given an input request, return a [`ChainResult`] to continue or stop the
117+
/// chain.
118+
///
119+
/// The input request can be modified by the handler before it is passed to
120+
/// the next handler.
121+
///
122+
/// # Example
123+
///
124+
/// ```
125+
/// use lychee_lib::{Chainable, ChainResult, Status};
126+
/// use reqwest::Request;
127+
/// use async_trait::async_trait;
128+
///
129+
/// #[derive(Debug)]
130+
/// struct AddHeader;
131+
///
132+
/// #[async_trait]
133+
/// impl Chainable<Request, Status> for AddHeader {
134+
/// async fn chain(&mut self, mut request: Request) -> ChainResult<Request, Status> {
135+
/// // You can modify the request however you like here
136+
/// request.headers_mut().append("X-Header", "value".parse().unwrap());
137+
///
138+
/// // Pass the request to the next handler
139+
/// ChainResult::Next(request)
140+
/// }
141+
/// }
142+
/// ```
54143
async fn chain(&mut self, input: T) -> ChainResult<T, R>;
55144
}
56145

146+
/// Client request chains
147+
///
148+
/// This struct holds all request chains.
149+
///
150+
/// Usually, this is used to hold the default request chain and the external
151+
/// plugin request chain.
57152
#[derive(Debug)]
58-
pub(crate) struct ClientRequestChain<'a> {
153+
pub(crate) struct ClientRequestChains<'a> {
59154
chains: Vec<&'a RequestChain>,
60155
}
61156

62-
impl<'a> ClientRequestChain<'a> {
157+
impl<'a> ClientRequestChains<'a> {
158+
/// Create a new chain of request chains.
63159
pub(crate) fn new(chains: Vec<&'a RequestChain>) -> Self {
64160
Self { chains }
65161
}
66162

163+
/// Traverse all request chains and resolve to a status.
67164
pub(crate) async fn traverse(&self, mut input: reqwest::Request) -> Status {
68165
use ChainResult::{Done, Next};
166+
69167
for e in &self.chains {
70168
match e.traverse(input).await {
71169
Next(r) => input = r,
@@ -75,7 +173,8 @@ impl<'a> ClientRequestChain<'a> {
75173
}
76174
}
77175

78-
// consider as excluded if no chain element has converted it to a done
176+
// Consider the request to be excluded if no chain element has converted
177+
// it to a `ChainResult::Done`
79178
Status::Excluded
80179
}
81180
}

lychee-lib/src/checker.rs

+10-3
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,17 @@ impl Checker {
5959
}
6060
}
6161

62-
/// SAFETY: unwrapping the `try_clone` of `reqwest::Request` is safe because a request only fails to be cloned when `body` of `Request` is a stream
63-
/// and `body` cannot be a stream as long as the `stream` feature is disabled.
62+
/// Clones a `reqwest::Request`.
63+
///
64+
/// # Safety
65+
///
66+
/// This panics if the request cannot be cloned. This should only happen if the
67+
/// request body is a `reqwest` stream. We disable the `stream` feature, so the
68+
/// body should never be a stream.
69+
///
70+
/// See <https://github.com/seanmonstar/reqwest/blob/de5dbb1ab849cc301dcefebaeabdf4ce2e0f1e53/src/async_impl/body.rs#L168>
6471
fn clone_unwrap(request: &Request) -> Request {
65-
request.try_clone().unwrap()
72+
request.try_clone().expect("Failed to clone request: body was a stream, which should be impossible with `stream` feature disabled")
6673
}
6774

6875
#[async_trait]

lychee-lib/src/client.rs

+8-8
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ use secrecy::{ExposeSecret, SecretString};
3030
use typed_builder::TypedBuilder;
3131

3232
use crate::{
33-
chain::{Chain, ClientRequestChain, RequestChain},
33+
chain::{Chain, ClientRequestChains, RequestChain},
3434
checker::Checker,
3535
filter::{Excludes, Filter, Includes},
3636
quirks::Quirks,
@@ -486,7 +486,7 @@ impl Client {
486486
return Ok(Response::new(uri.clone(), Status::Excluded, source));
487487
}
488488

489-
let chain: RequestChain = Chain::new(vec![
489+
let default_chain: RequestChain = Chain::new(vec![
490490
Box::<Quirks>::default(),
491491
Box::new(credentials),
492492
Box::new(Checker::new(
@@ -500,7 +500,7 @@ impl Client {
500500
let status = match uri.scheme() {
501501
_ if uri.is_file() => self.check_file(uri).await,
502502
_ if uri.is_mail() => self.check_mail(uri).await,
503-
_ => self.check_website(uri, chain).await?,
503+
_ => self.check_website(uri, default_chain).await?,
504504
};
505505

506506
Ok(Response::new(uri.clone(), status, source))
@@ -533,11 +533,11 @@ impl Client {
533533
/// - The request failed.
534534
/// - The response status code is not accepted.
535535
/// - The URI cannot be converted to HTTPS.
536-
pub async fn check_website(&self, uri: &Uri, chain: RequestChain) -> Result<Status> {
537-
match self.check_website_inner(uri, &chain).await {
536+
pub async fn check_website(&self, uri: &Uri, default_chain: RequestChain) -> Result<Status> {
537+
match self.check_website_inner(uri, &default_chain).await {
538538
Status::Ok(code) if self.require_https && uri.scheme() == "http" => {
539539
if self
540-
.check_website_inner(&uri.to_https()?, &chain)
540+
.check_website_inner(&uri.to_https()?, &default_chain)
541541
.await
542542
.is_success()
543543
{
@@ -562,7 +562,7 @@ impl Client {
562562
/// - The URI is invalid.
563563
/// - The request failed.
564564
/// - The response status code is not accepted.
565-
pub async fn check_website_inner(&self, uri: &Uri, chain: &RequestChain) -> Status {
565+
pub async fn check_website_inner(&self, uri: &Uri, default_chain: &RequestChain) -> Status {
566566
// Workaround for upstream reqwest panic
567567
if validate_url(&uri.url) {
568568
if matches!(uri.scheme(), "http" | "https") {
@@ -587,7 +587,7 @@ impl Client {
587587
Err(e) => return e.into(),
588588
};
589589

590-
let status = ClientRequestChain::new(vec![&self.plugin_request_chain, chain])
590+
let status = ClientRequestChains::new(vec![&self.plugin_request_chain, default_chain])
591591
.traverse(request)
592592
.await;
593593

lychee-lib/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ use openssl_sys as _; // required for vendored-openssl feature
8585
#[doc(inline)]
8686
pub use crate::{
8787
basic_auth::BasicAuthExtractor,
88+
// Expose the `Chainable` trait to allow defining external handlers (plugins)
89+
chain::{ChainResult, Chainable},
8890
// Constants get exposed so that the CLI can use the same defaults as the library
8991
client::{
9092
check, Client, ClientBuilder, DEFAULT_MAX_REDIRECTS, DEFAULT_MAX_RETRIES,

0 commit comments

Comments
 (0)