#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ResponseStatus(pub u16);
impl ResponseStatus {
pub const OK: Self = Self(200);
pub const NOT_MODIFIED: Self = Self(304);
pub const PARTIAL_CONTENT: Self = Self(206);
pub const NOT_RANGE_SATISFIABLE: Self = Self(416);
pub const METHOD_NOT_ALLOWED: Self = Self(405);
pub const NOT_FOUND: Self = Self(404);
pub const FORBIDDEN: Self = Self(403);
pub const BAD_REQUEST: Self = Self(400);
pub const PAYLOAD_TOO_LARGE: Self = Self(413);
pub const INTERNAL_SERVER_ERROR: Self = Self(500);
pub const SERVICE_UNAVAILABLE: Self = Self(503);
pub fn as_u16(&self) -> u16 {
self.0
}
}
impl std::fmt::Display for ResponseStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ResponseHeader {
pub name: String,
pub value: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct HeaderMapPlan {
headers: Vec<ResponseHeader>,
}
impl HeaderMapPlan {
pub fn new() -> Self {
Self {
headers: Vec::new(),
}
}
pub fn push(&mut self, name: impl Into<String>, value: impl Into<String>) {
self.headers.push(ResponseHeader {
name: name.into(),
value: value.into(),
});
}
pub fn get(&self, name: &str) -> Option<&str> {
self.headers
.iter()
.find(|h| h.name.eq_ignore_ascii_case(name))
.map(|h| h.value.as_str())
}
pub fn contains(&self, name: &str) -> bool {
self.headers
.iter()
.any(|h| h.name.eq_ignore_ascii_case(name))
}
pub fn iter(&self) -> impl Iterator<Item = &ResponseHeader> {
self.headers.iter()
}
pub fn len(&self) -> usize {
self.headers.len()
}
pub fn is_empty(&self) -> bool {
self.headers.is_empty()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FileRange {
pub start: u64,
pub end_inclusive: u64,
}
impl FileRange {
pub fn new(start: u64, end_inclusive: u64) -> Self {
Self {
start,
end_inclusive,
}
}
pub fn len(&self) -> u64 {
self.checked_len().expect("FileRange length overflow")
}
pub fn checked_len(&self) -> Option<u64> {
if self.is_empty() {
return Some(0);
}
self.end_inclusive
.checked_sub(self.start)
.and_then(|len| len.checked_add(1))
}
pub fn is_empty(&self) -> bool {
self.end_inclusive < self.start
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BodyPlan {
Empty,
FullBytes(Vec<u8>),
FileFull,
FileRange { start: u64, end_inclusive: u64 },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StaticResponsePlan {
pub status: ResponseStatus,
pub headers: HeaderMapPlan,
pub body: BodyPlan,
}
impl StaticResponsePlan {
pub fn status_code(&self) -> u16 {
self.status.as_u16()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConditionalRequestOutcome {
NotModified(HeaderMapPlan),
FullResponse,
Malformed,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RangeRequestOutcome {
Satisfiable(FileRange),
NotSatisfiable,
MalformedOrUnsupported,
MultipleRanges,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn status_code_values() {
assert_eq!(ResponseStatus::OK.as_u16(), 200);
assert_eq!(ResponseStatus::NOT_MODIFIED.as_u16(), 304);
assert_eq!(ResponseStatus::PARTIAL_CONTENT.as_u16(), 206);
assert_eq!(ResponseStatus::NOT_RANGE_SATISFIABLE.as_u16(), 416);
}
#[test]
fn status_display() {
assert_eq!(format!("{}", ResponseStatus::OK), "200");
assert_eq!(format!("{}", ResponseStatus::NOT_FOUND), "404");
}
#[test]
fn header_map_push_and_get() {
let mut headers = HeaderMapPlan::new();
headers.push("content-type", "text/plain");
headers.push("x-custom", "value");
assert_eq!(headers.get("content-type"), Some("text/plain"));
assert_eq!(headers.get("Content-Type"), Some("text/plain"));
assert_eq!(headers.get("x-custom"), Some("value"));
assert_eq!(headers.get("missing"), None);
}
#[test]
fn header_map_contains() {
let mut headers = HeaderMapPlan::new();
headers.push("etag", "W/\"123\"");
assert!(headers.contains("etag"));
assert!(headers.contains("ETag"));
assert!(!headers.contains("missing"));
}
#[test]
fn header_map_len() {
let mut headers = HeaderMapPlan::new();
assert!(headers.is_empty());
headers.push("a", "b");
assert_eq!(headers.len(), 1);
}
#[test]
fn file_range_len() {
let range = FileRange::new(0, 4);
assert_eq!(range.len(), 5);
let range = FileRange::new(10, 19);
assert_eq!(range.len(), 10);
}
#[test]
fn invalid_file_range_is_empty() {
let range = FileRange::new(10, 9);
assert!(range.is_empty());
assert_eq!(range.len(), 0);
}
#[test]
fn overflowing_file_range_is_reported() {
let range = FileRange::new(0, u64::MAX);
assert_eq!(range.checked_len(), None);
}
#[test]
fn body_plan_variants() {
let empty = BodyPlan::Empty;
assert_eq!(empty, BodyPlan::Empty);
let full = BodyPlan::FullBytes(b"hello".to_vec());
assert!(matches!(full, BodyPlan::FullBytes(_)));
let file_full = BodyPlan::FileFull;
assert_eq!(file_full, BodyPlan::FileFull);
let range = BodyPlan::FileRange {
start: 0,
end_inclusive: 4,
};
assert!(matches!(range, BodyPlan::FileRange { .. }));
}
#[test]
fn static_response_plan_status() {
let plan = StaticResponsePlan {
status: ResponseStatus::OK,
headers: HeaderMapPlan::new(),
body: BodyPlan::Empty,
};
assert_eq!(plan.status_code(), 200);
}
}