Skip to main content

hitbox_http/predicates/header/
predicate.rs

1//! Header predicate implementation.
2
3use async_trait::async_trait;
4use hitbox::Neutral;
5use hitbox::predicate::{Predicate, PredicateResult};
6use http::HeaderMap;
7
8use super::operation::Operation;
9
10/// A predicate that matches HTTP headers against an [`Operation`].
11///
12/// Works with any subject implementing [`HasHeaders`], including both
13/// requests and responses. Chain with other predicates using the builder pattern.
14///
15/// # Type Parameters
16///
17/// * `P` - The inner predicate to chain with. Use [`Header::new`] to start
18///   a new predicate chain (uses [`Neutral`] internally), or use the
19///   [`HeaderPredicate`] extension trait to chain onto an existing predicate.
20///
21/// # Examples
22///
23/// ```
24/// use hitbox_http::predicates::header::{Header, Operation};
25/// use http::header::CACHE_CONTROL;
26///
27/// # use bytes::Bytes;
28/// # use http_body_util::Empty;
29/// # use hitbox::Neutral;
30/// # use hitbox_http::CacheableHttpRequest;
31/// # type Subject = CacheableHttpRequest<Empty<Bytes>>;
32/// // Skip caching when Cache-Control contains "no-cache"
33/// let predicate = Header::new(Operation::Contains(
34///     CACHE_CONTROL,
35///     "no-cache".to_string(),
36/// ));
37/// # let _: &Header<Neutral<Subject>> = &predicate;
38/// ```
39#[derive(Debug)]
40pub struct Header<P> {
41    pub(crate) operation: Operation,
42    pub(crate) inner: P,
43}
44
45impl<S> Header<Neutral<S>> {
46    /// Creates a header predicate that matches headers against the operation.
47    ///
48    /// Returns [`Cacheable`](hitbox::predicate::PredicateResult::Cacheable) when
49    /// the headers satisfy the operation, [`NonCacheable`](hitbox::predicate::PredicateResult::NonCacheable) otherwise.
50    pub fn new(operation: Operation) -> Self {
51        Self {
52            operation,
53            inner: Neutral::new(),
54        }
55    }
56}
57
58/// Extension trait for adding header matching to a predicate chain.
59///
60/// # For Callers
61///
62/// Chain this to add header matching to your predicate. The request or
63/// response headers are inspected and matched against the provided [`Operation`].
64///
65/// # For Implementors
66///
67/// This trait is automatically implemented for all [`Predicate`](hitbox::predicate::Predicate)
68/// types. You don't need to implement it manually.
69pub trait HeaderPredicate: Sized {
70    /// Adds a header matching operation to this predicate chain.
71    fn header(self, operation: Operation) -> Header<Self>;
72}
73
74impl<P> HeaderPredicate for P
75where
76    P: Predicate,
77{
78    fn header(self, operation: Operation) -> Header<Self> {
79        Header {
80            operation,
81            inner: self,
82        }
83    }
84}
85
86/// Trait for types that provide access to HTTP headers.
87///
88/// Implement this trait to enable header predicates on custom types.
89/// Both [`CacheableHttpRequest`](crate::CacheableHttpRequest) and
90/// [`CacheableHttpResponse`](crate::CacheableHttpResponse) implement this trait.
91///
92/// # For Implementors
93///
94/// Return a reference to the headers associated with the HTTP message.
95/// The returned headers should reflect the current state of the message
96/// and remain valid for the lifetime of the borrow.
97///
98/// # For Callers
99///
100/// Use this trait to access headers generically from either requests or
101/// responses. Header predicates use this to inspect headers without knowing
102/// the concrete message type.
103pub trait HasHeaders {
104    /// Returns a reference to the HTTP headers.
105    fn headers(&self) -> &HeaderMap;
106}
107
108// Generic implementation for any subject that has headers
109#[async_trait]
110impl<P> Predicate for Header<P>
111where
112    P: Predicate + Send + Sync,
113    P::Subject: HasHeaders + Send,
114{
115    type Subject = P::Subject;
116
117    async fn check(&self, subject: Self::Subject) -> PredicateResult<Self::Subject> {
118        match self.inner.check(subject).await {
119            PredicateResult::Cacheable(subject) => {
120                let is_cacheable = self.operation.check(subject.headers());
121                if is_cacheable {
122                    PredicateResult::Cacheable(subject)
123                } else {
124                    PredicateResult::NonCacheable(subject)
125                }
126            }
127            PredicateResult::NonCacheable(subject) => PredicateResult::NonCacheable(subject),
128        }
129    }
130}