Skip to main content

hitbox_http/predicates/body/
operation.rs

1use hitbox::predicate::PredicateResult;
2use hyper::body::Body as HttpBody;
3
4use super::{
5    jq::{JqExpression, JqOperation},
6    plain::PlainOperation,
7};
8use crate::BufferedBody;
9
10/// Matching operations for HTTP body content.
11///
12/// # Caveats
13///
14/// Body predicates may consume bytes from the stream. The body is transitioned
15/// to [`BufferedBody::Partial`] or [`BufferedBody::Complete`] after evaluation.
16///
17/// # Examples
18///
19/// ## Size Limit
20///
21/// Only cache responses smaller than 1MB:
22///
23/// ```
24/// use hitbox_http::predicates::body::Operation;
25///
26/// let op = Operation::Limit { bytes: 1024 * 1024 };
27/// ```
28///
29/// ## Plain Text Matching
30///
31/// Cache only if body contains a success marker:
32///
33/// ```
34/// use bytes::Bytes;
35/// use hitbox_http::predicates::body::{Operation, PlainOperation};
36///
37/// let op = Operation::Plain(PlainOperation::Contains(Bytes::from("\"success\":true")));
38/// ```
39///
40/// Cache only if body starts with JSON array:
41///
42/// ```
43/// use bytes::Bytes;
44/// use hitbox_http::predicates::body::{Operation, PlainOperation};
45///
46/// let op = Operation::Plain(PlainOperation::Starts(Bytes::from("[")));
47/// ```
48///
49/// Cache only if body matches a regex pattern:
50///
51/// ```
52/// use hitbox_http::predicates::body::{Operation, PlainOperation};
53///
54/// let regex = regex::bytes::Regex::new(r#""status":\s*"(ok|success)""#).unwrap();
55/// let op = Operation::Plain(PlainOperation::RegExp(regex));
56/// ```
57///
58/// ## JQ (JSON) Matching
59///
60/// Cache only if response has non-empty items array:
61///
62/// ```
63/// use hitbox_http::predicates::body::{Operation, JqExpression, JqOperation};
64///
65/// let op = Operation::Jq {
66///     filter: JqExpression::compile(".items | length > 0").unwrap(),
67///     operation: JqOperation::Eq(serde_json::Value::Bool(true)),
68/// };
69/// ```
70///
71/// Cache only if user role exists:
72///
73/// ```
74/// use hitbox_http::predicates::body::{Operation, JqExpression, JqOperation};
75///
76/// let op = Operation::Jq {
77///     filter: JqExpression::compile(".user.role").unwrap(),
78///     operation: JqOperation::Exist,
79/// };
80/// ```
81///
82/// Cache only if status is one of allowed values:
83///
84/// ```
85/// use hitbox_http::predicates::body::{Operation, JqExpression, JqOperation};
86///
87/// let op = Operation::Jq {
88///     filter: JqExpression::compile(".status").unwrap(),
89///     operation: JqOperation::In(vec![
90///         serde_json::json!("published"),
91///         serde_json::json!("approved"),
92///     ]),
93/// };
94/// ```
95#[derive(Debug)]
96pub enum Operation {
97    /// Use when you need to limit cached response sizes.
98    ///
99    /// Best for preventing cache bloat from large responses like file downloads.
100    /// Reads up to `bytes + 1` to determine if the limit is exceeded.
101    Limit {
102        /// Maximum body size in bytes.
103        bytes: usize,
104    },
105    /// Use when matching raw body bytes without parsing.
106    ///
107    /// Best for text responses, checking signatures, or simple content matching.
108    Plain(PlainOperation),
109    /// Use when caching depends on JSON content structure or values.
110    ///
111    /// Best for APIs where cacheability depends on response data (e.g., user roles, feature flags).
112    Jq {
113        /// The compiled JQ filter expression.
114        filter: JqExpression,
115        /// The operation to apply to the filter result.
116        operation: JqOperation,
117    },
118}
119
120impl Operation {
121    /// Check if the operation matches the body.
122    /// Returns `PredicateResult::Cacheable` if the operation is satisfied,
123    /// `PredicateResult::NonCacheable` otherwise.
124    pub async fn check<B>(&self, body: BufferedBody<B>) -> PredicateResult<BufferedBody<B>>
125    where
126        B: HttpBody + Unpin,
127        B::Data: Send,
128    {
129        match self {
130            Operation::Limit { bytes } => {
131                use crate::CollectExactResult;
132
133                // Check size hint first for optimization
134                if let Some(upper) = body.size_hint().upper()
135                    && upper > *bytes as u64
136                {
137                    // Size hint indicates body exceeds limit - non-cacheable
138                    return PredicateResult::NonCacheable(body);
139                }
140
141                // Try to read limit+1 bytes to check if body exceeds limit
142                let result = body.collect_exact(*bytes + 1).await;
143
144                match result {
145                    CollectExactResult::AtLeast { .. } => {
146                        // Got limit+1 bytes, so body exceeds limit
147                        PredicateResult::NonCacheable(result.into_buffered_body())
148                    }
149                    CollectExactResult::Incomplete { ref error, .. } => {
150                        let is_error = error.is_some();
151                        let body = result.into_buffered_body();
152                        if is_error {
153                            // Error occurred - non-cacheable
154                            PredicateResult::NonCacheable(body)
155                        } else {
156                            // Within limit, no error - cacheable
157                            PredicateResult::Cacheable(body)
158                        }
159                    }
160                }
161            }
162            Operation::Plain(plain_op) => plain_op.check(body).await,
163            Operation::Jq { filter, operation } => operation.check(filter, body).await,
164        }
165    }
166}