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}