From d0e2386bdfcbb4322cc5d6e1fd2fb52f67e057f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kubrak?= Date: Sat, 6 Jul 2024 19:10:00 +0200 Subject: [PATCH] refactor: use request parts for responder, not whole request fix: minor fixes --- Cargo.lock | 8 +-- README.md | 12 +++- common/Cargo.toml | 2 +- loader/Cargo.toml | 4 +- loader/README.md | 20 ++++-- loader/src/file.rs | 6 +- loader/src/lib.rs | 20 ++++-- loader/src/responder.rs | 135 +++++++++++++++++++++------------------- packer/Cargo.toml | 4 +- tests/Cargo.toml | 8 +-- tests/src/lib.rs | 5 +- 11 files changed, 130 insertions(+), 94 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0bb251c..707ad8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1950,7 +1950,7 @@ checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "web-static-pack" -version = "0.5.0-beta.1" +version = "0.5.0-beta.2" dependencies = [ "anyhow", "http", @@ -1965,14 +1965,14 @@ dependencies = [ [[package]] name = "web-static-pack-common" -version = "0.5.0-beta.1" +version = "0.5.0-beta.2" dependencies = [ "rkyv", ] [[package]] name = "web-static-pack-packer" -version = "0.5.0-beta.1" +version = "0.5.0-beta.2" dependencies = [ "anyhow", "brotli", @@ -1989,7 +1989,7 @@ dependencies = [ [[package]] name = "web-static-pack-tests" -version = "0.5.0-beta.1" +version = "0.5.0-beta.2" dependencies = [ "anyhow", "futures", diff --git a/README.md b/README.md index 0e110d1..15f898b 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ web-static-pack is a set of tools for embedding static resources (GUI, assets, i It consists of two parts: - [web-static-pack-packer](https://crates.io/crates/web-static-pack-packer) (aka "packer") - a standalone application (can be used as a library) used to serialize your assets into single file, called `pack`. It will usually be used before you build your target application (eg. in build script / CI / build.rs). During creation of a `pack` all heavy computations are done, eg. calculating `ETag`, compressed (`gzip`, `brotli`) versions, mime guessing etc. As a result a `pack` file is created, to be used by the next part. -- [web-static-pack](https://crates.io/crates/web-static-pack) (aka "loader") - a library to include in your target application that will read the `pack` (preferably included in the application with ). Then `pack` can be used to form a `http` `service` (a function taking a request and returning response) serving files from the `pack`. +- [web-static-pack](https://crates.io/crates/web-static-pack) (aka "loader") - a library to include in your target application that will read the `pack` (preferably included in the application with ). Then `pack` can be used to form a `http` `service` (a function taking a request (parts) and returning response) serving files from the `pack`. ## Features - Precomputed (in "packer") `ETag` (using `sha3`), compressed bodies (in `gzip` and `brotli` formats), `content-type`, etc. This reduces both runtime overhead and dependencies of your target application. @@ -66,7 +66,13 @@ async fn main() -> Result<(), Error> { let service_fn = service_fn(|request: Request| async { // you can probably filter your /api requests here let (parts, _body) = request.into_parts(); - let response = responder.respond_flatten(parts); + + let response = responder.respond_flatten( + &parts.method, + parts.uri.path(), + &parts.headers, + ); + Ok::<_, Infallible>(response) }); @@ -89,7 +95,7 @@ The 0.5.0 is almost a complete rewrite, however the general idea remains the sam - Since we no longer depend on `hyper` in any way (the `http` crate is common interface), `hyper_loader` feature is no longer present in loader. - `let loader = loader::Loader::new(...)` is now `let pack_archived = loader::load(...)`. This value is still used for `Responder::new`. - `hyper_loader::Responder` is now just `responder::Responder`, and it's now built around `http` crate, compatible with `hyper` 1.0. -- `Responder` was rewritten. It now accepts request parts (without body) not whole request. `request_respond_or_error` and `parts_respond_or_error` are now `respond`. `request_respond` and `parts_respond` are now `respond_flatten`. +- `Responder` was rewritten. It now accepts (method, path, headers) not whole request. `request_respond_or_error` and `parts_respond_or_error` are squashed to `respond`. `request_respond` and `parts_respond` are squashed to `respond_flatten`. ### New features and improvements - True zero-copy deserialization with `rkyv`. diff --git a/common/Cargo.toml b/common/Cargo.toml index 88cd798..5dd7a6b 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "web-static-pack-common" -version = "0.5.0-beta.1" +version = "0.5.0-beta.2" authors = ["Paweł Kubrak "] edition = "2021" license = "MIT" diff --git a/loader/Cargo.toml b/loader/Cargo.toml index 54c6746..4bafe2d 100644 --- a/loader/Cargo.toml +++ b/loader/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "web-static-pack" -version = "0.5.0-beta.1" +version = "0.5.0-beta.2" authors = ["Paweł Kubrak "] edition = "2021" license = "MIT" @@ -12,7 +12,7 @@ keywords = ["web", "http", "static", "resources", "hyper"] categories = ["web-programming"] [dependencies] -web-static-pack-common = { version = "0.5.0-beta.1", path = "../common" } +web-static-pack-common = { version = "0.5.0-beta.2", path = "../common" } anyhow = "1.0.86" http = "1.1.0" diff --git a/loader/README.md b/loader/README.md index cf832a9..9b4c97f 100644 --- a/loader/README.md +++ b/loader/README.md @@ -15,8 +15,8 @@ http service and used with a web server like hyper. The main part of this crate is [responder::Responder]. Its [responder::Responder::respond_flatten] method makes a [http] service - a -function taking [http::Request] (actually the [http::request::Parts], as we -don't need the body) and returning [http::Response]. +function taking [http::Request] parts (method, path, headers) and returning +[http::Response]. To make a [responder::Responder], a [common::pack::Pack] is needed. It can be obtained by [loader::load] function by passing (possibly included in @@ -28,7 +28,7 @@ binary) contents of a `pack` created with the packer. ```rust use anyhow::Error; use include_bytes_aligned::include_bytes_aligned; -use http::StatusCode; +use http::{HeaderMap, Method, StatusCode}; use web_static_pack::{loader::load, responder::Responder}; // assume we have a vcard-personal-portfolio.pack available from packer examples @@ -44,7 +44,11 @@ fn main() -> Result<(), Error> { // do some checks on the responder assert_eq!( - responder.respond_flatten().status(), + responder.respond_flatten( + &Method::GET, + "/present", + &HeaderMap::default(), + ).status(), StatusCode::OK ); @@ -84,7 +88,13 @@ async fn main() -> Result<(), Error> { let service_fn = service_fn(|request: Request| async { // you can probably filter your /api requests here let (parts, _body) = request.into_parts(); - let response = responder.respond_flatten(parts); + + let response = responder.respond_flatten( + &parts.method, + parts.uri.path(), + &parts.headers + ); + Ok::<_, Infallible>(response) }); diff --git a/loader/src/file.rs b/loader/src/file.rs index 3a249c2..e1d97f8 100644 --- a/loader/src/file.rs +++ b/loader/src/file.rs @@ -6,9 +6,9 @@ use crate::{ }; use http::HeaderValue; -/// Trait for single file inside a `pack`. Consists of body in different encodings -/// (`identity` aka `normal`, `gzip`, `brotli`), some precomputed header values -/// etc. +/// Trait for single file inside a `pack`. Consists of body in different +/// encodings (`identity` aka `normal`, `gzip`, `brotli`), some precomputed +/// header values etc. /// /// Most users will indirectly use [FileArchived] implementation, obtained from /// [crate::pack::Pack::get_file_by_path] (implemented by diff --git a/loader/src/lib.rs b/loader/src/lib.rs index 20dd9c3..719bb79 100644 --- a/loader/src/lib.rs +++ b/loader/src/lib.rs @@ -13,8 +13,8 @@ //! //! The main part of this crate is [responder::Responder]. Its //! [responder::Responder::respond_flatten] method makes a [http] service - a -//! function taking [http::Request] (actually the [http::request::Parts], as we -//! don't need the body) and returning [http::Response]. +//! function taking [http::Request] parts (method, path, headers) and returning +//! [http::Response]. //! //! To make a [responder::Responder], a [common::pack::Pack] is needed. It can //! be obtained by [loader::load] function by passing (possibly included in @@ -26,7 +26,7 @@ //! ```ignore //! use anyhow::Error; //! use include_bytes_aligned::include_bytes_aligned; -//! use http::StatusCode; +//! use http::{HeaderMap, Method, StatusCode}; //! use web_static_pack::{loader::load, responder::Responder}; //! //! // assume we have a vcard-personal-portfolio.pack available from packer examples @@ -42,7 +42,11 @@ //! //! // do some checks on the responder //! assert_eq!( -//! responder.respond_flatten().status(), +//! responder.respond_flatten( +//! &Method::GET, +//! "/present", +//! &HeaderMap::default(), +//! ).status(), //! StatusCode::OK //! ); //! @@ -82,7 +86,13 @@ //! let service_fn = service_fn(|request: Request| async { //! // you can probably filter your /api requests here //! let (parts, _body) = request.into_parts(); -//! let response = responder.respond_flatten(parts); +//! +//! let response = responder.respond_flatten( +//! &parts.method, +//! parts.uri.path(), +//! &parts.headers +//! ); +//! //! Ok::<_, Infallible>(response) //! }); //! diff --git a/loader/src/responder.rs b/loader/src/responder.rs index 4014c89..896f38b 100644 --- a/loader/src/responder.rs +++ b/loader/src/responder.rs @@ -1,5 +1,5 @@ -//! Module containing [Responder] - service taking http requests and returning -//! http responses. +//! Module containing [Responder] - service taking http request (parts) and +//! returning http responses. use crate::{ body::Body, @@ -9,9 +9,8 @@ use crate::{ }; use http::{ header, - request::Parts as RequestParts, response::{Builder as ResponseBuilder, Response as HttpResponse}, - Method, StatusCode, + HeaderMap, Method, StatusCode, }; /// Http response type specialization. @@ -35,16 +34,28 @@ pub type Response<'a> = HttpResponse>; /// let responder = web_static_pack::responder::Responder::new(pack_archived); /// /// assert_eq!( -/// responder.respond_flatten().status(), +/// responder.respond_flatten( +/// &Method::GET, +/// "/present", +/// &HeaderMap::default(), +/// ).status(), /// StatusCode::OK /// ); /// assert_eq!( -/// responder.respond_flatten().status(), +/// responder.respond_flatten( +/// &Method::GET, +/// "/missing", +/// &HeaderMap::default(), +/// ).status(), /// StatusCode::NOT_FOUND /// ); /// /// assert_eq!( -/// responder.respond(), +/// responder.respond( +/// &Method::GET, +/// "/missing", +/// &HeaderMap::default(), +/// ), /// Err(ResponderRespondError::PackPathNotFound) /// ); /// ``` @@ -67,8 +78,8 @@ where Self { pack } } - /// Returns http response for given request or rust error to be handled by - /// user. + /// Returns http response for given request parts or rust error to be + /// handled by user. /// /// Inside this method: /// - Checks http method (accepts GET or HEAD). @@ -82,10 +93,12 @@ where /// [Self::respond_flatten]. pub fn respond( &self, - request: RequestParts, + method: &Method, + path: &str, + headers: &HeaderMap, ) -> Result, ResponderRespondError> { // only GET and HEAD are supported - let body_in_response = match request.method { + let body_in_response = match *method { Method::GET => true, Method::HEAD => false, _ => { @@ -93,8 +106,8 @@ where } }; - // find file for given request - let file = match self.pack.get_file_by_path(request.uri.path()) { + // find file for given path + let file = match self.pack.get_file_by_path(path) { Some(file_descriptor) => file_descriptor, None => { return Err(ResponderRespondError::PackPathNotFound); @@ -103,7 +116,7 @@ where // check for possible `ETag` // if `ETag` exists and matches current file, return 304 - if let Some(etag_request) = request.headers.get(header::IF_NONE_MATCH) + if let Some(etag_request) = headers.get(header::IF_NONE_MATCH) && etag_request.as_bytes() == file.etag().as_bytes() { let response = ResponseBuilder::new() @@ -116,7 +129,7 @@ where // resolve content and content-encoding header let content_content_encoding = ContentContentEncoding::resolve( - &match EncodingAccepted::from_headers(&request.headers) { + &match EncodingAccepted::from_headers(headers) { Ok(content_encoding_encoding_accepted) => content_encoding_encoding_accepted, Err(_) => return Err(ResponderRespondError::UnparsableAcceptEncoding), }, @@ -153,9 +166,11 @@ where /// For manual error handling, see [Self::respond]. pub fn respond_flatten( &self, - request: RequestParts, + method: &Method, + path: &str, + headers: &HeaderMap, ) -> Response<'p> { - match self.respond(request) { + match self.respond(method, path, headers) { Ok(response) => response, Err(responder_error) => responder_error.into_response(), } @@ -200,10 +215,7 @@ mod test_responder { use super::{Responder, ResponderRespondError}; use crate::{cache_control::CacheControl, file::File, pack::Pack}; use anyhow::anyhow; - use http::{ - header, method::Method, request::Builder as RequestBuilder, status::StatusCode, HeaderMap, - HeaderName, HeaderValue, - }; + use http::{header, method::Method, status::StatusCode, HeaderMap, HeaderName, HeaderValue}; struct FileMock; impl File for FileMock { @@ -259,16 +271,25 @@ mod test_responder { #[test] fn resolves_typical_request() { - let (request, _) = RequestBuilder::new() - .method(Method::GET) - .uri("/present") - .header(header::ACCEPT_ENCODING, "br, gzip") - .header(header::IF_NONE_MATCH, "\"invalidetag\"") - .body(()) - .unwrap() - .into_parts(); + let response = RESPONDER + .respond( + &Method::GET, + "/present", + &[ + ( + header::ACCEPT_ENCODING, + HeaderValue::from_static("br, gzip"), + ), + ( + header::IF_NONE_MATCH, + HeaderValue::from_static("\"invalidetag\""), + ), + ] + .into_iter() + .collect::(), + ) + .unwrap(); - let response = RESPONDER.respond(request).unwrap(); let headers = response.headers(); assert_eq!(response.status(), StatusCode::OK); @@ -299,14 +320,9 @@ mod test_responder { #[test] fn resolves_no_body_for_head_request() { - let (request, _) = RequestBuilder::new() - .method(Method::HEAD) - .uri("/present") - .body(()) - .unwrap() - .into_parts(); - - let response = RESPONDER.respond(request).unwrap(); + let response = RESPONDER + .respond(&Method::HEAD, "/present", &HeaderMap::default()) + .unwrap(); let headers = response.headers(); assert_eq!(response.status(), StatusCode::OK); @@ -333,15 +349,18 @@ mod test_responder { #[test] fn resolves_not_modified_for_matching_etag() { - let (request, _) = RequestBuilder::new() - .method(Method::GET) - .uri("/present") - .header(header::IF_NONE_MATCH, "\"etagvalue\"") - .body(()) - .unwrap() - .into_parts(); - - let response = RESPONDER.respond(request).unwrap(); + let response = RESPONDER + .respond( + &Method::GET, + "/present", + &[( + header::IF_NONE_MATCH, + HeaderValue::from_static("\"etagvalue\""), + )] + .into_iter() + .collect::(), + ) + .unwrap(); let headers = response.headers(); assert_eq!(response.status(), StatusCode::NOT_MODIFIED); @@ -359,14 +378,9 @@ mod test_responder { #[test] fn resolves_error_for_invalid_method() { - let (request, _) = RequestBuilder::new() - .method(Method::POST) - .uri("/present") - .body(()) - .unwrap() - .into_parts(); - - let response_error = RESPONDER.respond(request).unwrap_err(); + let response_error = RESPONDER + .respond(&Method::POST, "/present", &HeaderMap::default()) + .unwrap_err(); assert_eq!( response_error, ResponderRespondError::HttpMethodNotSupported @@ -378,14 +392,9 @@ mod test_responder { #[test] fn resolves_error_for_file_not_found() { - let (request, _) = RequestBuilder::new() - .method(Method::GET) - .uri("/missing") - .body(()) - .unwrap() - .into_parts(); - - let response_error = RESPONDER.respond(request).unwrap_err(); + let response_error = RESPONDER + .respond(&Method::GET, "/missing", &HeaderMap::default()) + .unwrap_err(); assert_eq!(response_error, ResponderRespondError::PackPathNotFound); let response_flatten = response_error.into_response(); diff --git a/packer/Cargo.toml b/packer/Cargo.toml index 7d2fd2d..d045199 100644 --- a/packer/Cargo.toml +++ b/packer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "web-static-pack-packer" -version = "0.5.0-beta.1" +version = "0.5.0-beta.2" authors = ["Paweł Kubrak "] edition = "2021" license = "MIT" @@ -12,7 +12,7 @@ keywords = ["web", "http", "static", "resources", "hyper"] categories = ["web-programming"] [dependencies] -web-static-pack-common = { version = "0.5.0-beta.1", path = "../common" } +web-static-pack-common = { version = "0.5.0-beta.2", path = "../common" } anyhow = "1.0.86" brotli = "6.0.0" diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 7b8c5b3..3a9895c 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "web-static-pack-tests" -version = "0.5.0-beta.1" +version = "0.5.0-beta.2" authors = ["Paweł Kubrak "] edition = "2021" license = "MIT" @@ -10,9 +10,9 @@ repository = "https://github.com/peku33/web-static-pack" publish = false [dependencies] -web-static-pack-common = { version = "0.5.0-beta.1", path = "../common" } -web-static-pack = { version = "0.5.0-beta.1", path = "../loader" } -web-static-pack-packer = { version = "0.5.0-beta.1", path = "../packer" } +web-static-pack-common = { version = "0.5.0-beta.2", path = "../common" } +web-static-pack = { version = "0.5.0-beta.2", path = "../loader" } +web-static-pack-packer = { version = "0.5.0-beta.2", path = "../packer" } anyhow = "1.0.86" futures = "0.3.30" diff --git a/tests/src/lib.rs b/tests/src/lib.rs index 5178b9a..4fbd5c6 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -6,7 +6,8 @@ use futures::{ future::{select, Either}, pin_mut, }; -use hyper::{body::Incoming, header, server::conn::http1, service::service_fn, Request}; +use http::{header, Request}; +use hyper::{body::Incoming, server::conn::http1, service::service_fn}; use hyper_util::{rt::TokioIo, server::graceful::GracefulShutdown}; use memmap2::Mmap; use ouroboros::self_referencing; @@ -108,7 +109,7 @@ where let (parts, _body) = request.into_parts(); log::info!("serving {}", parts.uri); - let response = responder.respond_flatten(parts); + let response = responder.respond_flatten(&parts.method, parts.uri.path(), &parts.headers); Ok::<_, Infallible>(response) });