diff options
-rw-r--r-- | Cargo.toml | 13 | ||||
-rw-r--r-- | README.md | 24 | ||||
-rw-r--r-- | examples/form/Cargo.toml | 2 | ||||
-rw-r--r-- | examples/form/src/main.rs | 4 | ||||
-rw-r--r-- | src/hyper_body.rs | 64 | ||||
-rw-r--r-- | src/lib.rs | 10 | ||||
-rw-r--r-- | src/request.rs | 66 | ||||
-rw-r--r-- | src/response.rs | 10 |
8 files changed, 106 insertions, 87 deletions
@@ -3,19 +3,20 @@ name = "sputnik" version = "0.3.6" authors = ["Martin Fischer <martin@push-f.com>"] license = "MIT" -description = "A lightweight layer on top of hyper to facilitate building web applications." +description = "Extends the types from the http crate with methods to deal with cookies/content-types (and optionally adds deserialization methods to hyper::Body)." repository = "https://git.push-f.com/sputnik" edition = "2018" categories = ["web-programming::http-server"] -keywords = ["hyper", "web", "cookie", "hmac"] +keywords = ["web", "cookie", "hyper", "hmac"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -json = ["serde_json"] +hyper_body = ["hyper", "async-trait"] +hyper_body_json = ["serde_json", "hyper_body"] [dependencies] -hyper = "0.14" +http = "0.2" cookie = { version = "0.15", features = ["percent-encode"] } serde = { version = "1.0", features = ["derive"] } serde_urlencoded = "0.7.0" @@ -27,7 +28,9 @@ rand = "0.8" sha2 = "0.9" time = "0.2" thiserror = "1.0" -async-trait = "0.1" + +hyper = { version = "0.14", optional = true } +async-trait = { version = "0.1", optional = true } serde_json = { version = "1.0", optional = true } [package.metadata.docs.rs] @@ -1,19 +1,25 @@ # Sputnik -A microframework based on [Hyper](https://hyper.rs/) providing traits to: +This library extends the types from the [http](https://crates.io/crates/http) crate: -* extend `http::request::Parts` with query parameter deserialization & cookie parsing -* extend `hyper::Body` with form deserialization (and JSON deserialization with the `json` feature) -* extend `http::response::Builder` with methods to set & delete cookies and set the Content-Type +* extends `http::request::Parts` with query parameter deserialization & cookie parsing +* extends `http::response::Builder` with methods to set cookies and content-types + +If you use [Hyper](https://hyper.rs/) and want to deserialize request bodies +with [Serde](https://serde.rs/) you can enable the following feature flags: + +- `hyper_body` provides a trait to extend `hyper::Body` with an + `into_form` method for parsing data submitted from HTML forms. +- `hyper_body_json` additionaly provides an `into_json` method Furthermore Sputnik provides what's necessary to implement [signed & expiring cookies](#signed--expiring-cookies) with the expiry date encoded into the signed cookie value, providing a more lightweight alternative to JWT if you don't need interoperability. -Sputnik does **not** handle routing. For most web applications `match`ing on -(method, path) suffices. If you need path variables, you can use one of the -many [router crates](https://crates.io/keywords/router). +Sputnik does **not** handle routing because even complex routing can be quite +easily implemented with nested `match` blocks. If you want a more high-level +router, you can check out the [router crates](https://crates.io/keywords/router). Sputnik encourages you to create your own error enum and implement `From` conversions for every error type, which you want to short-circuit with the `?` @@ -35,8 +41,8 @@ use hyper::{Method, Server, StatusCode, Body}; use hyper::http::request::Parts; use hyper::http::response::Builder; use serde::Deserialize; -use sputnik::{mime, request::{SputnikParts, SputnikBody}, response::SputnikBuilder}; -use sputnik::request::FormError; +use sputnik::{mime, request::SputnikParts, response::SputnikBuilder}; +use sputnik::hyper_body::{SputnikBody, FormError}; type Response = hyper::Response<Body>; diff --git a/examples/form/Cargo.toml b/examples/form/Cargo.toml index c6eb1c3..69901c5 100644 --- a/examples/form/Cargo.toml +++ b/examples/form/Cargo.toml @@ -9,7 +9,7 @@ publish = false [dependencies] hyper = { version = "0.14", features = ["full"] } -sputnik = {path = "../../"} +sputnik = { path = "../../", features = ["hyper_body"] } serde = { version = "1.0", features = ["derive"] } tokio = { version = "1", features = ["full"] } thiserror = "1.0"
\ No newline at end of file diff --git a/examples/form/src/main.rs b/examples/form/src/main.rs index e354f9f..9e6748e 100644 --- a/examples/form/src/main.rs +++ b/examples/form/src/main.rs @@ -4,8 +4,8 @@ use hyper::{Method, Server, StatusCode, Body}; use hyper::http::request::Parts; use hyper::http::response::Builder; use serde::Deserialize; -use sputnik::{mime, request::{SputnikParts, SputnikBody}, response::SputnikBuilder}; -use sputnik::request::FormError; +use sputnik::{mime, request::SputnikParts, response::SputnikBuilder}; +use sputnik::hyper_body::{SputnikBody, FormError}; type Response = hyper::Response<Body>; diff --git a/src/hyper_body.rs b/src/hyper_body.rs new file mode 100644 index 0000000..c6824fb --- /dev/null +++ b/src/hyper_body.rs @@ -0,0 +1,64 @@ +//! Extends `hyper::Body` with [`SputnikBody`]. +use hyper::http::{self, response::Builder}; +use async_trait::async_trait; +use serde::de::DeserializeOwned; + +use crate::response::EmptyBuilder; + +impl EmptyBuilder<hyper::Body> for Builder { + fn empty(self) -> http::Result<http::response::Response<hyper::Body>> { + self.body(hyper::Body::empty()) + } +} + +/// Adds deserialization methods to [`hyper::Body`]. +#[async_trait] +pub trait SputnikBody { + /// Parses a `application/x-www-form-urlencoded` request body into a given struct. + async fn into_form<T: DeserializeOwned>(self) -> Result<T, FormError>; + + /// Attempts to deserialize the request body as JSON. + #[cfg(feature = "hyper_body_json")] + #[cfg_attr(docsrs, doc(cfg(feature = "hyper_body_json")))] + async fn into_json<T: DeserializeOwned>(self) -> Result<T, JsonError>; +} + +#[async_trait] +impl SputnikBody for hyper::Body { + + async fn into_form<T: DeserializeOwned>(self) -> Result<T, FormError> { + let full_body = hyper::body::to_bytes(self).await.map_err(BodyError)?; + Ok(serde_urlencoded::from_bytes::<T>(&full_body)?) + } + + #[cfg(feature = "hyper_body_json")] + #[cfg_attr(docsrs, doc(cfg(feature = "hyper_body_json")))] + async fn into_json<T: DeserializeOwned>(self) -> Result<T, JsonError> { + let full_body = hyper::body::to_bytes(self).await.map_err(BodyError)?; + Ok(serde_json::from_slice::<T>(&full_body)?) + } +} + +#[derive(thiserror::Error, Debug)] +#[error("failed to read body")] +pub struct BodyError(pub hyper::Error); + +#[derive(thiserror::Error, Debug)] +pub enum FormError { + #[error("{0}")] + Body(#[from] BodyError), + + #[error("form deserialize error: {0}")] + Deserialize(#[from] serde_urlencoded::de::Error), +} + +#[cfg(feature = "hyper_body_json")] +#[cfg_attr(docsrs, doc(cfg(feature = "hyper_body_json")))] +#[derive(thiserror::Error, Debug)] +pub enum JsonError { + #[error("{0}")] + Body(#[from] BodyError), + + #[error("json deserialize error: {0}")] + Deserialize(#[from] serde_json::Error), +}
\ No newline at end of file @@ -9,4 +9,12 @@ pub use httpdate; pub mod security; pub mod request; pub mod response; -mod signed;
\ No newline at end of file +mod signed; +#[cfg(feature="hyper_body")] +#[cfg_attr(docsrs, doc(cfg(feature = "hyper_body")))] +pub mod hyper_body; + +#[cfg(not(feature="hyper_body"))] +use http; +#[cfg(feature="hyper_body")] +use hyper::http;
\ No newline at end of file diff --git a/src/request.rs b/src/request.rs index 95f5a32..6efacc9 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,14 +1,13 @@ -//! Provides the [`SputnikParts`] and [`SputnikBody`] traits. +//! Provides the [`SputnikParts`] trait. use cookie::Cookie; use mime::Mime; use serde::de::DeserializeOwned; -use hyper::{HeaderMap, body::Bytes, header, http::request::Parts}; use time::Duration; use std::{collections::HashMap, sync::Arc}; -use async_trait::async_trait; use crate::response::{SputnikHeaders, delete_cookie}; +use crate::http::{HeaderMap, header, request::Parts}; /// Adds convenience methods to [`http::request::Parts`](Parts). pub trait SputnikParts { @@ -22,7 +21,7 @@ pub trait SputnikParts { fn enforce_content_type(&self, mime: Mime) -> Result<(), WrongContentTypeError>; /// A map of response headers to allow methods of this trait to set response - /// headers without needing to take a [`Response`](hyper::http::response::Response) as an argument. + /// headers without needing to take a [`Response`](crate::http::response::Response) as an argument. /// /// You need to take care to append these headers to the response yourself. /// This is intended to be done after your routing logic so that your @@ -137,85 +136,30 @@ impl Flash { } } -/// Adds deserialization methods to [`hyper::Body`]. -#[async_trait] -pub trait SputnikBody { - async fn into_bytes(self) -> Result<Bytes, BodyError>; - - /// Parses a `application/x-www-form-urlencoded` request body into a given struct. - async fn into_form<T: DeserializeOwned>(self) -> Result<T, FormError>; - - /// Attempts to deserialize the request body as JSON. - #[cfg(feature = "json")] - #[cfg_attr(docsrs, doc(cfg(feature = "json")))] - async fn into_json<T: DeserializeOwned>(self) -> Result<T, JsonError>; -} - -#[async_trait] -impl SputnikBody for hyper::Body { - async fn into_bytes(self) -> Result<Bytes, BodyError> { - hyper::body::to_bytes(self).await.map_err(BodyError) - } - - async fn into_form<T: DeserializeOwned>(self) -> Result<T, FormError> { - let full_body = self.into_bytes().await?; - Ok(serde_urlencoded::from_bytes::<T>(&full_body)?) - } - - #[cfg(feature = "json")] - #[cfg_attr(docsrs, doc(cfg(feature = "json")))] - async fn into_json<T: DeserializeOwned>(self) -> Result<T, JsonError> { - let full_body = self.into_bytes().await?; - Ok(serde_json::from_slice::<T>(&full_body)?) - } -} #[derive(thiserror::Error, Debug)] #[error("query deserialize error: {0}")] pub struct QueryError(pub serde_urlencoded::de::Error); #[derive(thiserror::Error, Debug)] -#[error("failed to read body")] -pub struct BodyError(pub hyper::Error); - -#[derive(thiserror::Error, Debug)] #[error("expected Content-Type {expected} but received {}", received.as_ref().unwrap_or(&"nothing".to_owned()))] pub struct WrongContentTypeError { pub expected: Mime, pub received: Option<String>, } -#[derive(thiserror::Error, Debug)] -pub enum FormError { - #[error("{0}")] - Body(#[from] BodyError), - - #[error("form deserialize error: {0}")] - Deserialize(#[from] serde_urlencoded::de::Error), -} - -#[cfg(feature = "json")] -#[cfg_attr(docsrs, doc(cfg(feature = "json")))] -#[derive(thiserror::Error, Debug)] -pub enum JsonError { - #[error("{0}")] - Body(#[from] BodyError), - - #[error("json deserialize error: {0}")] - Deserialize(#[from] serde_json::Error), -} #[cfg(test)] mod tests { use std::convert::TryInto; - use hyper::{Request, header}; + use crate::http::{Request, header}; use super::SputnikParts; #[test] fn test_enforce_content_type() { - let (mut parts, _body) = Request::new(hyper::Body::empty()).into_parts(); + let (mut parts, _body) = Request::new("").into_parts(); assert!(parts.enforce_content_type(mime::APPLICATION_JSON).is_err()); parts.headers.append(header::CONTENT_TYPE, "application/json".try_into().unwrap()); diff --git a/src/response.rs b/src/response.rs index 2d5d469..cb87a80 100644 --- a/src/response.rs +++ b/src/response.rs @@ -3,9 +3,9 @@ use std::convert::TryInto; use cookie::Cookie; -use hyper::{HeaderMap, StatusCode, header, http}; use time::{Duration, OffsetDateTime}; -use hyper::http::response::Builder; + +use crate::http::{self, HeaderMap, StatusCode, header, response::Builder}; /// Adds convenience methods to [`Builder`]. pub trait SputnikBuilder { @@ -66,12 +66,6 @@ pub trait EmptyBuilder<B> { fn empty(self) -> http::Result<http::response::Response<B>>; } -impl EmptyBuilder<hyper::Body> for Builder { - fn empty(self) -> http::Result<http::response::Response<hyper::Body>> { - self.body(hyper::Body::empty()) - } -} - #[cfg(test)] mod tests { use super::*; |