Skip to main content

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}