aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.toml13
-rw-r--r--README.md24
-rw-r--r--examples/form/Cargo.toml2
-rw-r--r--examples/form/src/main.rs4
-rw-r--r--src/hyper_body.rs64
-rw-r--r--src/lib.rs10
-rw-r--r--src/request.rs66
-rw-r--r--src/response.rs10
8 files changed, 106 insertions, 87 deletions
diff --git a/Cargo.toml b/Cargo.toml
index f28c39b..cc9e454 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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]
diff --git a/README.md b/README.md
index 9d61453..f2a81fd 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/src/lib.rs b/src/lib.rs
index 2f30028..760acf1 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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::*;