use bytes::Bytes;
#[derive(Debug)]
#[must_use]
pub enum FilterAction {
Continue,
Reject(Rejection),
Release,
BodyDone,
}
#[derive(Debug)]
#[must_use]
pub struct Rejection {
pub body: Option<Bytes>,
pub headers: Vec<(String, String)>,
pub status: u16,
}
impl Rejection {
pub fn status(code: u16) -> Self {
assert!(
(100..=599).contains(&code),
"HTTP status code must be 100..=599, got {code}"
);
Self {
status: code,
headers: Vec::new(),
body: None,
}
}
pub fn with_body(mut self, body: impl Into<Bytes>) -> Self {
self.body = Some(body.into());
self
}
pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.push((name.into(), value.into()));
self
}
}
#[cfg(test)]
#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::panic,
reason = "tests"
)]
mod tests {
use super::*;
#[test]
fn rejection_status_defaults() {
let r = Rejection::status(404);
assert_eq!(r.status, 404, "status should match constructor arg");
assert!(r.headers.is_empty(), "headers should default to empty");
assert!(r.body.is_none(), "body should default to None");
}
#[test]
fn rejection_status_boundary_100() {
let r = Rejection::status(100);
assert_eq!(r.status, 100, "100 is a valid HTTP status");
}
#[test]
fn rejection_status_boundary_599() {
let r = Rejection::status(599);
assert_eq!(r.status, 599, "599 is a valid HTTP status");
}
#[test]
#[should_panic(expected = "HTTP status code must be 100..=599")]
fn rejection_status_zero_panics() {
let _r = Rejection::status(0);
}
#[test]
#[should_panic(expected = "HTTP status code must be 100..=599")]
fn rejection_status_600_panics() {
let _r = Rejection::status(600);
}
#[test]
fn rejection_with_header_appends() {
let r = Rejection::status(403)
.with_header("X-Reason", "forbidden")
.with_header("X-Request-Id", "abc");
assert_eq!(r.headers.len(), 2, "should have two appended headers");
assert_eq!(
r.headers[0],
("X-Reason".into(), "forbidden".into()),
"first header should match"
);
assert_eq!(
r.headers[1],
("X-Request-Id".into(), "abc".into()),
"second header should match"
);
}
#[test]
fn rejection_with_body_sets_bytes() {
let r = Rejection::status(400).with_body(b"bad request".as_slice());
assert_eq!(
r.body.unwrap(),
Bytes::from_static(b"bad request"),
"body should contain provided bytes"
);
}
#[test]
fn filter_action_continue_variant() {
assert!(
matches!(FilterAction::Continue, FilterAction::Continue),
"Continue should match Continue"
);
}
#[test]
fn filter_action_reject_carries_rejection() {
let action = FilterAction::Reject(Rejection::status(503));
assert!(
matches!(action, FilterAction::Reject(r) if r.status == 503),
"Reject should carry rejection with status 503"
);
}
#[test]
fn filter_action_release_variant() {
assert!(
matches!(FilterAction::Release, FilterAction::Release),
"Release should match Release"
);
}
#[test]
fn filter_action_body_done_variant() {
assert!(
matches!(FilterAction::BodyDone, FilterAction::BodyDone),
"BodyDone should match BodyDone"
);
}
}