Skip to main content

hitbox_http/predicates/body/
predicate.rs

1//! Body predicate implementation.
2//!
3//! Provides [`Body`] predicate for matching request or response bodies.
4
5use async_trait::async_trait;
6use hitbox::Neutral;
7use hitbox::predicate::{Predicate, PredicateResult};
8use hyper::body::Body as HttpBody;
9
10use super::operation::Operation;
11use crate::CacheableSubject;
12
13/// A predicate that matches HTTP bodies against an [`Operation`].
14///
15/// Works with both requests and responses through the [`CacheableSubject`] trait.
16/// Chain with other predicates using the builder pattern.
17///
18/// # Type Parameters
19///
20/// * `P` - The inner predicate to chain with. Use [`Body::new`] to start
21///   a new predicate chain (uses [`Neutral`] internally), or use the
22///   [`BodyPredicate`] extension trait to chain onto an existing predicate.
23///
24/// # Examples
25///
26/// ```
27/// use hitbox_http::predicates::body::{Body, Operation};
28///
29/// # use bytes::Bytes;
30/// # use http_body_util::Empty;
31/// # use hitbox::Neutral;
32/// # use hitbox_http::CacheableHttpRequest;
33/// # type Subject = CacheableHttpRequest<Empty<Bytes>>;
34/// // Only cache responses smaller than 1MB
35/// let predicate = Body::new(Operation::Limit { bytes: 1024 * 1024 });
36/// # let _: &Body<Neutral<Subject>> = &predicate;
37/// ```
38///
39/// # Caveats
40///
41/// Body predicates consume bytes from the stream. The body is buffered during
42/// evaluation and returned in a [`BufferedBody`](crate::BufferedBody) state.
43/// Order body predicates last in a chain when possible.
44#[derive(Debug)]
45pub struct Body<P> {
46    pub(crate) operation: Operation,
47    pub(crate) inner: P,
48}
49
50impl<S> Body<Neutral<S>> {
51    /// Creates a predicate that matches body content against the operation.
52    ///
53    /// Returns [`Cacheable`](hitbox::predicate::PredicateResult::Cacheable) when
54    /// the body satisfies the operation, [`NonCacheable`](hitbox::predicate::PredicateResult::NonCacheable) otherwise.
55    ///
56    /// Chain onto existing predicates using [`BodyPredicate::body`] instead
57    /// if you already have a predicate chain.
58    pub fn new(operation: Operation) -> Self {
59        Self {
60            operation,
61            inner: Neutral::new(),
62        }
63    }
64}
65
66/// Extension trait for adding body matching to a predicate chain.
67///
68/// # For Callers
69///
70/// Chain this to add body content matching to your predicate. The body is
71/// inspected and matched against the provided [`Operation`].
72///
73/// **Important**: Body predicates consume bytes from the stream. Place them
74/// last in your predicate chain when possible.
75///
76/// # For Implementors
77///
78/// This trait is automatically implemented for all [`Predicate`](hitbox::predicate::Predicate)
79/// types. You don't need to implement it manually.
80pub trait BodyPredicate: Sized {
81    /// Adds a body matching operation to this predicate chain.
82    fn body(self, operation: Operation) -> Body<Self>;
83}
84
85impl<P> BodyPredicate for P
86where
87    P: Predicate,
88{
89    fn body(self, operation: Operation) -> Body<Self> {
90        Body {
91            operation,
92            inner: self,
93        }
94    }
95}
96
97// Generic implementation for any CacheableSubject
98#[async_trait]
99impl<P> Predicate for Body<P>
100where
101    P: Predicate + Send + Sync,
102    P::Subject: CacheableSubject + Send,
103    <P::Subject as CacheableSubject>::Body: Send + Unpin + 'static,
104    <P::Subject as CacheableSubject>::Parts: Send,
105    <<P::Subject as CacheableSubject>::Body as HttpBody>::Error: Send,
106    <<P::Subject as CacheableSubject>::Body as HttpBody>::Data: Send,
107{
108    type Subject = P::Subject;
109
110    async fn check(&self, subject: Self::Subject) -> PredicateResult<Self::Subject> {
111        let inner_result = self.inner.check(subject).await;
112
113        let (was_cacheable, subject) = match inner_result {
114            PredicateResult::Cacheable(s) => (true, s),
115            PredicateResult::NonCacheable(s) => (false, s),
116        };
117
118        let (parts, body) = subject.into_parts();
119        let body_result = self.operation.check(body).await;
120
121        // Combine: final is Cacheable only if both inner AND body are Cacheable
122        match body_result {
123            PredicateResult::Cacheable(buffered_body) => {
124                let subject = P::Subject::from_parts(parts, buffered_body);
125                if was_cacheable {
126                    PredicateResult::Cacheable(subject)
127                } else {
128                    PredicateResult::NonCacheable(subject)
129                }
130            }
131            PredicateResult::NonCacheable(buffered_body) => {
132                PredicateResult::NonCacheable(P::Subject::from_parts(parts, buffered_body))
133            }
134        }
135    }
136}