diff options
Diffstat (limited to 'README.md')
-rw-r--r-- | README.md | 89 |
1 files changed, 37 insertions, 52 deletions
@@ -1,54 +1,38 @@ # Sputnik -A microframework based on [Hyper](https://hyper.rs/) that forces you to: +A microframework based on [Hyper](https://hyper.rs/) providing traits to: -* make error handling explicit (no possible failures hidden behind macros) -* implement your own error type ([because you need to anyway](#error-handling)) +* extend `http::request::Parts` with query parameter deserialization & cookie parsing +* extend `hyper::Body` with form deserialization (and optional [CSRF](https://en.wikipedia.org/wiki/Cross-site_request_forgery) protection) +* extend `http::response::Builder` with methods to set & delete cookies and set the Content-Type -Sputnik provides: +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. -* convenience wrappers around hyper's `Request` & `Response` - * parse, set and delete cookies - (powered by the [cookie](https://crates.io/crates/cookie) crate) - * parse query strings and HTML form data (powered by the - [serde_urlencoded](https://crates.io/crates/serde_urlencoded) crate) -* cookie-based [CSRF](https://en.wikipedia.org/wiki/Cross-site_request_forgery) tokens -* `Key`: a convenience wrapper around HMAC (stolen from the cookie crate, so - that you don't have to use `CookieJar`s if you don't need them) -* `decode_expiring_claim` & `encode_expiring_claim`, which can be combined with - `Key` to implement [signed & expiring cookies](#signed--expiring-cookies) - (with the expiry date encoded into the signed cookie value) +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: for most web apps `match`ing on (method, path) suffices -* handle configuration: we recommend [toml](https://crates.io/crates/toml) -* handle persistence: we recommend [diesel](https://diesel.rs/) -* handle templating: we recommend [maud](https://maud.lambda.xyz/) - -## Error handling - -Rust provides convenient short-circuiting with the `?` operator, which -converts errors with `From::from()`. Since you probably want to short-circuit -errors from other crates (e.g. database errors), a web framework cannot -provide you an error type since Rust disallows you from defining a `From` -conversion between two foreign types. - -This does imply that you need to define your own error type, allowing you to -implement a `From` conversion for every error type you want to short-circuit -with `?`. Fortunately the [thiserror](https://crates.io/crates/thiserror) -crate makes defining custom errors and `From` implementations trivial. +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 `?` +operator. This can be easily done with [thiserror](https://crates.io/crates/thiserror) +because Sputnik restricts its error types to the `'static` lifetime. ## Example ```rust use std::convert::Infallible; use hyper::service::{service_fn, make_service_fn}; -use hyper::{Method, Server, StatusCode}; +use hyper::{Method, Server, StatusCode, Body}; +use hyper::http::request::Parts; +use hyper::http::response::Builder; use serde::Deserialize; -use sputnik::security::CsrfToken; -use sputnik::{request::{Parts, Body}, response::Response}; -use sputnik::request::error::*; +use sputnik::{mime, request::{SputnikParts, SputnikBody}, response::SputnikBuilder}; +use sputnik::request::CsrfProtectedFormError; + +type Response = hyper::Response<Body>; #[derive(thiserror::Error, Debug)] enum Error { @@ -65,8 +49,8 @@ fn render_error(err: Error) -> (StatusCode, String) { } } -async fn route(req: &mut Parts, body: Body) -> Result<Response,Error> { - match (req.method(), req.uri().path()) { +async fn route(req: &mut Parts, body: Body) -> Result<Response, Error> { + match (&req.method, req.uri.path()) { (&Method::GET, "/form") => get_form(req).await, (&Method::POST, "/form") => post_form(req, body).await, _ => return Err(Error::NotFound("page not found".to_owned())) @@ -74,29 +58,30 @@ async fn route(req: &mut Parts, body: Body) -> Result<Response,Error> { } async fn get_form(req: &mut Parts) -> Result<Response, Error> { - let mut response = Response::new(); - let csrf_token = CsrfToken::from_parts(req, &mut response); - *response.body() = format!("<form method=post> - <input name=text>{}<button>Submit</button></form>", csrf_token.html_input()).into(); - Ok(response) + let mut response = Builder::new(); + let csrf_token = req.csrf_token(&mut response); + Ok(response.content_type(mime::TEXT_HTML).body( + format!("<form method=post> + <input name=text>{}<button>Submit</button></form>", csrf_token.html_input()).into() + ).unwrap()) } #[derive(Deserialize)] struct FormData {text: String} async fn post_form(req: &mut Parts, body: Body) -> Result<Response, Error> { - let mut response = Response::new(); - let csrf_token = CsrfToken::from_parts(req, &mut response); + let mut response = Builder::new(); + let csrf_token = req.csrf_token(&mut response); let msg: FormData = body.into_form_csrf(&csrf_token).await?; - *response.body() = format!("hello {}", msg.text).into(); - Ok(response) + Ok(response.body( + format!("hello {}", msg.text).into() + ).unwrap()) } -/// adapt between Hyper's types and Sputnik's convenience types async fn service(req: hyper::Request<hyper::Body>) -> Result<hyper::Response<hyper::Body>, Infallible> { - let (mut parts, body) = sputnik::request::adapt(req); + let (mut parts, body) = req.into_parts(); match route(&mut parts, body).await { - Ok(res) => Ok(res.into()), + Ok(res) => Ok(res), Err(err) => { let (code, message) = render_error(err); // you can easily wrap or log errors here |