eggserve_core/primitives/
response.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8pub struct ResponseStatus(pub u16);
9
10impl ResponseStatus {
11 pub const OK: Self = Self(200);
12 pub const NOT_MODIFIED: Self = Self(304);
13 pub const PARTIAL_CONTENT: Self = Self(206);
14 pub const NOT_RANGE_SATISFIABLE: Self = Self(416);
15 pub const METHOD_NOT_ALLOWED: Self = Self(405);
16 pub const NOT_FOUND: Self = Self(404);
17 pub const FORBIDDEN: Self = Self(403);
18 pub const BAD_REQUEST: Self = Self(400);
19 pub const PAYLOAD_TOO_LARGE: Self = Self(413);
20 pub const INTERNAL_SERVER_ERROR: Self = Self(500);
21 pub const SERVICE_UNAVAILABLE: Self = Self(503);
22
23 pub fn as_u16(&self) -> u16 {
24 self.0
25 }
26}
27
28impl std::fmt::Display for ResponseStatus {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 write!(f, "{}", self.0)
31 }
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Hash)]
36pub struct ResponseHeader {
37 pub name: String,
38 pub value: String,
39}
40
41#[derive(Debug, Clone, Default, PartialEq, Eq)]
43pub struct HeaderMapPlan {
44 headers: Vec<ResponseHeader>,
45}
46
47impl HeaderMapPlan {
48 pub fn new() -> Self {
49 Self {
50 headers: Vec::new(),
51 }
52 }
53
54 pub fn push(&mut self, name: impl Into<String>, value: impl Into<String>) {
55 self.headers.push(ResponseHeader {
56 name: name.into(),
57 value: value.into(),
58 });
59 }
60
61 pub fn get(&self, name: &str) -> Option<&str> {
62 self.headers
63 .iter()
64 .find(|h| h.name.eq_ignore_ascii_case(name))
65 .map(|h| h.value.as_str())
66 }
67
68 pub fn contains(&self, name: &str) -> bool {
69 self.headers
70 .iter()
71 .any(|h| h.name.eq_ignore_ascii_case(name))
72 }
73
74 pub fn iter(&self) -> impl Iterator<Item = &ResponseHeader> {
75 self.headers.iter()
76 }
77
78 pub fn len(&self) -> usize {
79 self.headers.len()
80 }
81
82 pub fn is_empty(&self) -> bool {
83 self.headers.is_empty()
84 }
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
89pub struct FileRange {
90 pub start: u64,
91 pub end_inclusive: u64,
92}
93
94impl FileRange {
95 pub fn new(start: u64, end_inclusive: u64) -> Self {
96 Self {
97 start,
98 end_inclusive,
99 }
100 }
101
102 pub fn len(&self) -> u64 {
103 self.checked_len().expect("FileRange length overflow")
104 }
105
106 pub fn checked_len(&self) -> Option<u64> {
107 if self.is_empty() {
108 return Some(0);
109 }
110 self.end_inclusive
111 .checked_sub(self.start)
112 .and_then(|len| len.checked_add(1))
113 }
114
115 pub fn is_empty(&self) -> bool {
116 self.end_inclusive < self.start
117 }
118}
119
120#[derive(Debug, Clone, PartialEq, Eq)]
122pub enum BodyPlan {
123 Empty,
124 FullBytes(Vec<u8>),
125 FileFull,
126 FileRange { start: u64, end_inclusive: u64 },
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct StaticResponsePlan {
132 pub status: ResponseStatus,
133 pub headers: HeaderMapPlan,
134 pub body: BodyPlan,
135}
136
137impl StaticResponsePlan {
138 pub fn status_code(&self) -> u16 {
139 self.status.as_u16()
140 }
141}
142
143#[derive(Debug, Clone, PartialEq, Eq)]
145pub enum ConditionalRequestOutcome {
146 NotModified(HeaderMapPlan),
148 FullResponse,
150 Malformed,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
156pub enum RangeRequestOutcome {
157 Satisfiable(FileRange),
159 NotSatisfiable,
161 MalformedOrUnsupported,
163 MultipleRanges,
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn status_code_values() {
173 assert_eq!(ResponseStatus::OK.as_u16(), 200);
174 assert_eq!(ResponseStatus::NOT_MODIFIED.as_u16(), 304);
175 assert_eq!(ResponseStatus::PARTIAL_CONTENT.as_u16(), 206);
176 assert_eq!(ResponseStatus::NOT_RANGE_SATISFIABLE.as_u16(), 416);
177 }
178
179 #[test]
180 fn status_display() {
181 assert_eq!(format!("{}", ResponseStatus::OK), "200");
182 assert_eq!(format!("{}", ResponseStatus::NOT_FOUND), "404");
183 }
184
185 #[test]
186 fn header_map_push_and_get() {
187 let mut headers = HeaderMapPlan::new();
188 headers.push("content-type", "text/plain");
189 headers.push("x-custom", "value");
190
191 assert_eq!(headers.get("content-type"), Some("text/plain"));
192 assert_eq!(headers.get("Content-Type"), Some("text/plain"));
193 assert_eq!(headers.get("x-custom"), Some("value"));
194 assert_eq!(headers.get("missing"), None);
195 }
196
197 #[test]
198 fn header_map_contains() {
199 let mut headers = HeaderMapPlan::new();
200 headers.push("etag", "W/\"123\"");
201
202 assert!(headers.contains("etag"));
203 assert!(headers.contains("ETag"));
204 assert!(!headers.contains("missing"));
205 }
206
207 #[test]
208 fn header_map_len() {
209 let mut headers = HeaderMapPlan::new();
210 assert!(headers.is_empty());
211 headers.push("a", "b");
212 assert_eq!(headers.len(), 1);
213 }
214
215 #[test]
216 fn file_range_len() {
217 let range = FileRange::new(0, 4);
218 assert_eq!(range.len(), 5);
219
220 let range = FileRange::new(10, 19);
221 assert_eq!(range.len(), 10);
222 }
223
224 #[test]
225 fn invalid_file_range_is_empty() {
226 let range = FileRange::new(10, 9);
227 assert!(range.is_empty());
228 assert_eq!(range.len(), 0);
229 }
230
231 #[test]
232 fn overflowing_file_range_is_reported() {
233 let range = FileRange::new(0, u64::MAX);
234 assert_eq!(range.checked_len(), None);
235 }
236
237 #[test]
238 fn body_plan_variants() {
239 let empty = BodyPlan::Empty;
240 assert_eq!(empty, BodyPlan::Empty);
241
242 let full = BodyPlan::FullBytes(b"hello".to_vec());
243 assert!(matches!(full, BodyPlan::FullBytes(_)));
244
245 let file_full = BodyPlan::FileFull;
246 assert_eq!(file_full, BodyPlan::FileFull);
247
248 let range = BodyPlan::FileRange {
249 start: 0,
250 end_inclusive: 4,
251 };
252 assert!(matches!(range, BodyPlan::FileRange { .. }));
253 }
254
255 #[test]
256 fn static_response_plan_status() {
257 let plan = StaticResponsePlan {
258 status: ResponseStatus::OK,
259 headers: HeaderMapPlan::new(),
260 body: BodyPlan::Empty,
261 };
262 assert_eq!(plan.status_code(), 200);
263 }
264}