1use crate::{
5 body::Body,
6 content_encoding::{ContentContentEncoding, EncodingAccepted},
7 file::File,
8 pack::Pack,
9};
10use http::{
11 HeaderMap, Method, StatusCode, header,
12 response::{Builder as ResponseBuilder, Response as HttpResponse},
13};
14
15pub type Response<'a> = HttpResponse<Body<'a>>;
17
18#[derive(Debug)]
65pub struct Responder<'p, P>
66where
67 P: Pack,
68{
69 pack: &'p P,
70}
71impl<'p, P> Responder<'p, P>
72where
73 P: Pack,
74{
75 pub const fn new(pack: &'p P) -> Self {
77 Self { pack }
78 }
79
80 pub fn respond(
94 &self,
95 method: &Method,
96 path: &str,
97 headers: &HeaderMap,
98 ) -> Result<Response<'p>, ResponderRespondError> {
99 let body_in_response = match *method {
101 Method::GET => true,
102 Method::HEAD => false,
103 _ => {
104 return Err(ResponderRespondError::HttpMethodNotSupported);
105 }
106 };
107
108 let file = match self.pack.get_file_by_path(path) {
110 Some(file_descriptor) => file_descriptor,
111 None => {
112 return Err(ResponderRespondError::PackPathNotFound);
113 }
114 };
115
116 if let Some(etag_request) = headers.get(header::IF_NONE_MATCH)
119 && etag_request.as_bytes() == file.etag().as_bytes()
120 {
121 let response = ResponseBuilder::new()
122 .status(StatusCode::NOT_MODIFIED)
123 .header(header::ETAG, file.etag()) .body(Body::empty())
125 .unwrap();
126 return Ok(response);
127 };
128
129 let content_content_encoding = ContentContentEncoding::resolve(
131 &match EncodingAccepted::from_headers(headers) {
132 Ok(content_encoding_encoding_accepted) => content_encoding_encoding_accepted,
133 Err(_) => return Err(ResponderRespondError::UnparsableAcceptEncoding),
134 },
135 file,
136 );
137
138 let response = ResponseBuilder::new()
140 .header(header::CONTENT_TYPE, file.content_type())
141 .header(header::ETAG, file.etag())
142 .header(header::CACHE_CONTROL, file.cache_control().cache_control())
143 .header(
144 header::CONTENT_LENGTH,
145 content_content_encoding.content.len(),
146 )
147 .header(
148 header::CONTENT_ENCODING,
149 content_content_encoding.content_encoding,
150 )
151 .body(if body_in_response {
152 Body::new(content_content_encoding.content)
153 } else {
154 Body::empty()
155 })
156 .unwrap();
157
158 Ok(response)
159 }
160
161 pub fn respond_flatten(
167 &self,
168 method: &Method,
169 path: &str,
170 headers: &HeaderMap,
171 ) -> Response<'p> {
172 match self.respond(method, path, headers) {
173 Ok(response) => response,
174 Err(responder_error) => responder_error.into_response(),
175 }
176 }
177}
178
179#[derive(PartialEq, Eq, Debug)]
181pub enum ResponderRespondError {
182 HttpMethodNotSupported,
184
185 PackPathNotFound,
187
188 UnparsableAcceptEncoding,
191}
192impl ResponderRespondError {
193 pub fn status_code(&self) -> StatusCode {
195 match self {
196 ResponderRespondError::HttpMethodNotSupported => StatusCode::METHOD_NOT_ALLOWED,
197 ResponderRespondError::PackPathNotFound => StatusCode::NOT_FOUND,
198 ResponderRespondError::UnparsableAcceptEncoding => StatusCode::BAD_REQUEST,
199 }
200 }
201
202 pub fn into_response(&self) -> Response<'static> {
204 let response = ResponseBuilder::new()
205 .status(self.status_code())
206 .body(Body::empty())
207 .unwrap();
208 response
209 }
210}
211
212#[cfg(test)]
213mod test_responder {
214 use super::{Responder, ResponderRespondError};
215 use crate::{cache_control::CacheControl, file::File, pack::Pack};
216 use anyhow::anyhow;
217 use http::{HeaderMap, HeaderName, HeaderValue, header, method::Method, status::StatusCode};
218
219 struct FileMock;
220 impl File for FileMock {
221 fn content(&self) -> &[u8] {
222 b"content-identity"
223 }
224 fn content_gzip(&self) -> Option<&[u8]> {
225 None
226 }
227 fn content_brotli(&self) -> Option<&[u8]> {
228 Some(b"content-br")
229 }
230
231 fn content_type(&self) -> HeaderValue {
232 HeaderValue::from_static("text/plain; charset=utf-8")
233 }
234 fn etag(&self) -> HeaderValue {
235 HeaderValue::from_static("\"etagvalue\"")
236 }
237 fn cache_control(&self) -> CacheControl {
238 CacheControl::MaxCache
239 }
240 }
241
242 struct PackMock;
243 impl Pack for PackMock {
244 type File = FileMock;
245
246 fn get_file_by_path(
247 &self,
248 path: &str,
249 ) -> Option<&Self::File> {
250 match path {
251 "/present" => Some(&FileMock),
252 _ => None,
253 }
254 }
255 }
256
257 static RESPONDER: Responder<'static, PackMock> = Responder::new(&PackMock);
258
259 fn header_as_string(
260 headers: &HeaderMap,
261 name: HeaderName,
262 ) -> &str {
263 headers
264 .get(&name)
265 .ok_or_else(|| anyhow!("missing header {name}"))
266 .unwrap()
267 .to_str()
268 .unwrap()
269 }
270
271 #[test]
272 fn resolves_typical_request() {
273 let response = RESPONDER
274 .respond(
275 &Method::GET,
276 "/present",
277 &[
278 (
279 header::ACCEPT_ENCODING,
280 HeaderValue::from_static("br, gzip"),
281 ),
282 (
283 header::IF_NONE_MATCH,
284 HeaderValue::from_static("\"invalidetag\""),
285 ),
286 ]
287 .into_iter()
288 .collect::<HeaderMap>(),
289 )
290 .unwrap();
291
292 let headers = response.headers();
293
294 assert_eq!(response.status(), StatusCode::OK);
295
296 assert_eq!(
297 header_as_string(headers, header::CONTENT_TYPE),
298 "text/plain; charset=utf-8"
299 );
300 assert_eq!(
301 header_as_string(headers, header::ETAG), "\"etagvalue\""
303 );
304 assert_eq!(
305 header_as_string(headers, header::CACHE_CONTROL), "max-age=31536000, immutable"
307 );
308 assert_eq!(
309 header_as_string(headers, header::CONTENT_LENGTH), "10"
311 );
312 assert_eq!(
313 header_as_string(headers, header::CONTENT_ENCODING), "br"
315 );
316
317 assert_eq!(response.body().data(), b"content-br");
318 }
319
320 #[test]
321 fn resolves_no_body_for_head_request() {
322 let response = RESPONDER
323 .respond(&Method::HEAD, "/present", &HeaderMap::default())
324 .unwrap();
325 let headers = response.headers();
326
327 assert_eq!(response.status(), StatusCode::OK);
328
329 assert_eq!(
330 header_as_string(headers, header::CONTENT_TYPE),
331 "text/plain; charset=utf-8"
332 );
333 assert_eq!(
334 header_as_string(headers, header::ETAG), "\"etagvalue\""
336 );
337 assert_eq!(
338 header_as_string(headers, header::CONTENT_LENGTH), "16"
340 );
341 assert_eq!(
342 header_as_string(headers, header::CONTENT_ENCODING),
343 "identity"
344 );
345
346 assert_eq!(response.body().data(), b"");
347 }
348
349 #[test]
350 fn resolves_not_modified_for_matching_etag() {
351 let response = RESPONDER
352 .respond(
353 &Method::GET,
354 "/present",
355 &[(
356 header::IF_NONE_MATCH,
357 HeaderValue::from_static("\"etagvalue\""),
358 )]
359 .into_iter()
360 .collect::<HeaderMap>(),
361 )
362 .unwrap();
363 let headers = response.headers();
364
365 assert_eq!(response.status(), StatusCode::NOT_MODIFIED);
366
367 assert_eq!(
369 header_as_string(headers, header::ETAG), "\"etagvalue\""
371 );
372 assert!(headers.get(header::CONTENT_TYPE).is_none());
373
374 assert_eq!(response.body().data(), b"");
376 }
377
378 #[test]
379 fn resolves_error_for_invalid_method() {
380 let response_error = RESPONDER
381 .respond(&Method::POST, "/present", &HeaderMap::default())
382 .unwrap_err();
383 assert_eq!(
384 response_error,
385 ResponderRespondError::HttpMethodNotSupported
386 );
387
388 let response_flatten = response_error.into_response();
389 assert_eq!(response_flatten.status(), StatusCode::METHOD_NOT_ALLOWED);
390 }
391
392 #[test]
393 fn resolves_error_for_file_not_found() {
394 let response_error = RESPONDER
395 .respond(&Method::GET, "/missing", &HeaderMap::default())
396 .unwrap_err();
397 assert_eq!(response_error, ResponderRespondError::PackPathNotFound);
398
399 let response_flatten = response_error.into_response();
400 assert_eq!(response_flatten.status(), StatusCode::NOT_FOUND);
401 }
402}