hitbox_http/predicates/response/status.rs
1use crate::CacheableHttpResponse;
2use async_trait::async_trait;
3use hitbox::Neutral;
4use hitbox::predicate::{Predicate, PredicateResult};
5
6/// HTTP status code classes for broad matching.
7///
8/// Use this to match entire categories of responses instead of specific codes.
9///
10/// # Examples
11///
12/// ```
13/// use hitbox_http::predicates::response::StatusClass;
14///
15/// // Match any 2xx response
16/// let class = StatusClass::Success;
17/// ```
18#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
19pub enum StatusClass {
20 /// 1xx (100-199): Informational responses.
21 Informational,
22 /// 2xx (200-299): Successful responses.
23 Success,
24 /// 3xx (300-399): Redirection responses.
25 Redirect,
26 /// 4xx (400-499): Client error responses.
27 ClientError,
28 /// 5xx (500-599): Server error responses.
29 ServerError,
30}
31
32impl StatusClass {
33 fn matches(&self, code: http::StatusCode) -> bool {
34 match self {
35 StatusClass::Informational => code.is_informational(),
36 StatusClass::Success => code.is_success(),
37 StatusClass::Redirect => code.is_redirection(),
38 StatusClass::ClientError => code.is_client_error(),
39 StatusClass::ServerError => code.is_server_error(),
40 }
41 }
42}
43
44/// Matching operations for HTTP status codes.
45///
46/// # Variants
47///
48/// - [`Eq`](Self::Eq): Matches exactly one status code
49/// - [`In`](Self::In): Matches any code in the provided list
50/// - [`Range`](Self::Range): Matches codes within an inclusive range
51/// - [`Class`](Self::Class): Matches all codes in a status class (1xx, 2xx, etc.)
52#[derive(Debug)]
53pub enum Operation {
54 /// Match a specific status code.
55 Eq(http::StatusCode),
56 /// Match any of the specified status codes.
57 In(Vec<http::StatusCode>),
58 /// Match status codes within a range (inclusive).
59 Range(http::StatusCode, http::StatusCode),
60 /// Match all status codes in a class (e.g., all 2xx).
61 Class(StatusClass),
62}
63
64impl Operation {
65 fn matches(&self, status: http::StatusCode) -> bool {
66 match self {
67 Operation::Eq(expected) => status == *expected,
68 Operation::In(codes) => codes.contains(&status),
69 Operation::Range(start, end) => {
70 status.as_u16() >= start.as_u16() && status.as_u16() <= end.as_u16()
71 }
72 Operation::Class(class) => class.matches(status),
73 }
74 }
75}
76
77/// A predicate that matches responses by HTTP status code.
78///
79/// # Type Parameters
80///
81/// * `P` - The inner predicate to chain with. Use [`StatusCode::new`] to start
82/// a new predicate chain (uses [`Neutral`] internally), or use the
83/// [`StatusCodePredicate`] extension trait to chain onto an existing predicate.
84///
85/// # Examples
86///
87/// Match only 200 OK responses:
88///
89/// ```
90/// use hitbox_http::predicates::response::StatusCode;
91///
92/// # use bytes::Bytes;
93/// # use http_body_util::Empty;
94/// # use hitbox::Neutral;
95/// # use hitbox_http::CacheableHttpResponse;
96/// # type Subject = CacheableHttpResponse<Empty<Bytes>>;
97/// let predicate = StatusCode::new(http::StatusCode::OK);
98/// # let _: &StatusCode<Neutral<Subject>> = &predicate;
99/// ```
100///
101/// Chain with body predicate:
102///
103/// ```
104/// use hitbox_http::predicates::response::StatusCode;
105/// use hitbox_http::predicates::body::{BodyPredicate, Operation as BodyOperation, PlainOperation};
106///
107/// # use bytes::Bytes;
108/// # use http_body_util::Empty;
109/// # use hitbox::Neutral;
110/// # use hitbox_http::CacheableHttpResponse;
111/// # use hitbox_http::predicates::body::Body;
112/// # type Subject = CacheableHttpResponse<Empty<Bytes>>;
113/// let predicate = StatusCode::new(http::StatusCode::OK)
114/// .body(BodyOperation::Plain(PlainOperation::Contains("success".into())));
115/// # let _: &Body<StatusCode<Neutral<Subject>>> = &predicate;
116/// ```
117#[derive(Debug)]
118pub struct StatusCode<P> {
119 operation: Operation,
120 inner: P,
121}
122
123impl<S> StatusCode<Neutral<S>> {
124 /// Creates a predicate matching a specific status code.
125 pub fn new(status_code: http::StatusCode) -> Self {
126 Self {
127 operation: Operation::Eq(status_code),
128 inner: Neutral::new(),
129 }
130 }
131}
132
133impl<P> StatusCode<P> {
134 /// Creates a predicate matching any of the specified status codes.
135 ///
136 /// Returns [`Cacheable`](hitbox::predicate::PredicateResult::Cacheable) when
137 /// the response status code is in the provided list.
138 ///
139 /// Use this for caching multiple specific status codes (e.g., 200 and 304).
140 ///
141 /// # Examples
142 ///
143 /// ```
144 /// use hitbox::Neutral;
145 /// use hitbox_http::predicates::response::StatusCode;
146 ///
147 /// # use bytes::Bytes;
148 /// # use http_body_util::Empty;
149 /// # use hitbox_http::CacheableHttpResponse;
150 /// # type Subject = CacheableHttpResponse<Empty<Bytes>>;
151 /// // Cache 200 OK and 304 Not Modified responses
152 /// let predicate = StatusCode::new_in(
153 /// Neutral::new(),
154 /// vec![http::StatusCode::OK, http::StatusCode::NOT_MODIFIED],
155 /// );
156 /// # let _: &StatusCode<Neutral<Subject>> = &predicate;
157 /// ```
158 pub fn new_in(inner: P, codes: Vec<http::StatusCode>) -> Self {
159 Self {
160 operation: Operation::In(codes),
161 inner,
162 }
163 }
164
165 /// Creates a predicate matching status codes within a range (inclusive).
166 ///
167 /// Returns [`Cacheable`](hitbox::predicate::PredicateResult::Cacheable) when
168 /// the response status code is between `start` and `end` (inclusive).
169 ///
170 /// Use this for custom status code ranges not covered by [`StatusClass`].
171 ///
172 /// # Examples
173 ///
174 /// ```
175 /// use hitbox::Neutral;
176 /// use hitbox_http::predicates::response::StatusCode;
177 ///
178 /// # use bytes::Bytes;
179 /// # use http_body_util::Empty;
180 /// # use hitbox_http::CacheableHttpResponse;
181 /// # type Subject = CacheableHttpResponse<Empty<Bytes>>;
182 /// // Cache responses with status codes 200-299 and 304
183 /// let predicate = StatusCode::new_range(
184 /// Neutral::new(),
185 /// http::StatusCode::OK,
186 /// http::StatusCode::from_u16(299).unwrap(),
187 /// );
188 /// # let _: &StatusCode<Neutral<Subject>> = &predicate;
189 /// ```
190 pub fn new_range(inner: P, start: http::StatusCode, end: http::StatusCode) -> Self {
191 Self {
192 operation: Operation::Range(start, end),
193 inner,
194 }
195 }
196
197 /// Creates a predicate matching all status codes in a class.
198 ///
199 /// Returns [`Cacheable`](hitbox::predicate::PredicateResult::Cacheable) when
200 /// the response status code belongs to the specified class (e.g., all 2xx).
201 ///
202 /// Use this for broad caching rules like "cache all successful responses".
203 ///
204 /// # Examples
205 ///
206 /// ```
207 /// use hitbox::Neutral;
208 /// use hitbox_http::predicates::response::{StatusCode, StatusClass};
209 ///
210 /// # use bytes::Bytes;
211 /// # use http_body_util::Empty;
212 /// # use hitbox_http::CacheableHttpResponse;
213 /// # type Subject = CacheableHttpResponse<Empty<Bytes>>;
214 /// // Cache all successful (2xx) responses
215 /// let predicate = StatusCode::new_class(Neutral::new(), StatusClass::Success);
216 /// # let _: &StatusCode<Neutral<Subject>> = &predicate;
217 /// ```
218 pub fn new_class(inner: P, class: StatusClass) -> Self {
219 Self {
220 operation: Operation::Class(class),
221 inner,
222 }
223 }
224}
225
226/// Extension trait for adding status code matching to a predicate chain.
227///
228/// # For Callers
229///
230/// Chain these methods to match responses by their HTTP status code.
231/// Use `status_code` for exact matches, `status_code_class` for broad
232/// categories (like "all 2xx"), or `status_code_in`/`status_code_range`
233/// for flexible matching.
234///
235/// # For Implementors
236///
237/// This trait is automatically implemented for all [`Predicate`]
238/// types. You don't need to implement it manually.
239pub trait StatusCodePredicate: Sized {
240 /// Matches an exact status code.
241 fn status_code(self, status_code: http::StatusCode) -> StatusCode<Self>;
242 /// Matches any of the specified status codes.
243 fn status_code_in(self, codes: Vec<http::StatusCode>) -> StatusCode<Self>;
244 /// Matches status codes within a range (inclusive).
245 fn status_code_range(self, start: http::StatusCode, end: http::StatusCode) -> StatusCode<Self>;
246 /// Matches all status codes in a class (e.g., all 2xx).
247 fn status_code_class(self, class: StatusClass) -> StatusCode<Self>;
248}
249
250impl<P> StatusCodePredicate for P
251where
252 P: Predicate,
253{
254 fn status_code(self, status_code: http::StatusCode) -> StatusCode<Self> {
255 StatusCode {
256 operation: Operation::Eq(status_code),
257 inner: self,
258 }
259 }
260
261 fn status_code_in(self, codes: Vec<http::StatusCode>) -> StatusCode<Self> {
262 StatusCode::new_in(self, codes)
263 }
264
265 fn status_code_range(self, start: http::StatusCode, end: http::StatusCode) -> StatusCode<Self> {
266 StatusCode::new_range(self, start, end)
267 }
268
269 fn status_code_class(self, class: StatusClass) -> StatusCode<Self> {
270 StatusCode::new_class(self, class)
271 }
272}
273
274#[async_trait]
275impl<P, ReqBody> Predicate for StatusCode<P>
276where
277 P: Predicate<Subject = CacheableHttpResponse<ReqBody>> + Send + Sync,
278 ReqBody: hyper::body::Body + Send + 'static,
279 ReqBody::Error: Send,
280{
281 type Subject = P::Subject;
282
283 async fn check(&self, response: Self::Subject) -> PredicateResult<Self::Subject> {
284 match self.inner.check(response).await {
285 PredicateResult::Cacheable(response) => {
286 if self.operation.matches(response.parts.status) {
287 PredicateResult::Cacheable(response)
288 } else {
289 PredicateResult::NonCacheable(response)
290 }
291 }
292 PredicateResult::NonCacheable(response) => PredicateResult::NonCacheable(response),
293 }
294 }
295}