From e8e85f09cd8ffc1d1944258f9056080d6f913675 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sun, 11 Sep 2016 18:57:04 -0700 Subject: [PATCH] Add support for flash cookie. Revamp cookie support. --- examples/cookies/src/main.rs | 11 ++- examples/todo/src/main.rs | 38 +++---- examples/todo/src/static_files.rs | 14 +-- examples/todo/src/task.rs | 9 +- examples/todo/static/css/style.css | 10 ++ .../todo/{templates => static}/index.html | 6 +- lib/Cargo.toml | 4 - lib/src/request/from_request.rs | 9 +- lib/src/request/mod.rs | 1 + lib/src/request/request.rs | 19 +++- lib/src/response/cookied.rs | 50 ---------- lib/src/response/flash.rs | 99 +++++++++++++++++++ lib/src/response/mod.rs | 4 +- lib/src/rocket.rs | 9 +- lib/src/router/mod.rs | 2 +- 15 files changed, 174 insertions(+), 111 deletions(-) rename examples/todo/{templates => static}/index.html (91%) delete mode 100644 lib/src/response/cookied.rs create mode 100644 lib/src/response/flash.rs diff --git a/examples/cookies/src/main.rs b/examples/cookies/src/main.rs index 04d305a1..50ac237c 100644 --- a/examples/cookies/src/main.rs +++ b/examples/cookies/src/main.rs @@ -7,8 +7,8 @@ extern crate rocket; extern crate tera; use rocket::Rocket; -use rocket::response::{Cookied, Redirect}; -use rocket::request::Cookies; +use rocket::response::Redirect; +use rocket::request::{Cookie, Cookies}; lazy_static!(static ref TERA: tera::Tera = tera::Tera::new("templates/**/*");); @@ -25,12 +25,13 @@ struct Message { } #[post("/submit", form = "")] -fn submit(message: Message) -> Cookied { - Cookied::new(Redirect::to("/")).add("message", &message.message) +fn submit(cookies: &Cookies, message: Message) -> Redirect { + cookies.add(Cookie::new("message".into(), message.message)); + Redirect::to("/") } #[get("/")] -fn index(cookies: Cookies) -> tera::TeraResult { +fn index(cookies: &Cookies) -> tera::TeraResult { let message = cookies.find("message").map(|msg| msg.value); TERA.render("index.html", ctxt(message)) } diff --git a/examples/todo/src/main.rs b/examples/todo/src/main.rs index 21d734d3..68f801ea 100644 --- a/examples/todo/src/main.rs +++ b/examples/todo/src/main.rs @@ -11,28 +11,30 @@ mod static_files; mod task; use rocket::Rocket; -use rocket::response::Redirect; +use rocket::response::{Flash, Redirect}; use task::Task; -lazy_static!(static ref TERA: tera::Tera = tera::Tera::new("templates/**/*");); +lazy_static!(static ref TERA: tera::Tera = tera::Tera::new("static/*.html");); -fn ctxt(error: Option<&str>) -> tera::Context { +fn ctxt(msg: Option<(&str, &str)>) -> tera::Context { + let unwrapped_msg = msg.unwrap_or(("", "")); let mut context = tera::Context::new(); - context.add("error", &error.is_some()); - context.add("msg", &error.unwrap_or("").to_string()); + context.add("has_msg", &msg.is_some()); + context.add("msg_type", &unwrapped_msg.0.to_string()); + context.add("msg", &unwrapped_msg.1.to_string()); context.add("tasks", &Task::all()); context } #[post("/", form = "")] -fn new(todo: Task) -> Result> { +fn new(todo: Task) -> Result, tera::TeraResult> { if todo.description.is_empty() { - let context = ctxt(Some("Description cannot be empty.")); + let context = ctxt(Some(("error", "Description cannot be empty."))); Err(TERA.render("index.html", context)) } else if todo.insert() { - Ok(Redirect::to("/")) // Say that it was added...somehow. + Ok(Flash::success(Redirect::to("/"), "Todo successfully added.")) } else { - let context = ctxt(Some("Whoops! The server failed.")); + let context = ctxt(Some(("error", "Whoops! The server failed."))); Err(TERA.render("index.html", context)) } } @@ -41,32 +43,32 @@ fn new(todo: Task) -> Result> { #[get("//toggle")] fn toggle(id: i32) -> Result> { if Task::toggle_with_id(id) { - Ok(Redirect::to("/")) // Say that it was added...somehow. + Ok(Redirect::to("/")) } else { - let context = ctxt(Some("Could not toggle that task.")); + let context = ctxt(Some(("error", "Could not toggle that task."))); Err(TERA.render("index.html", context)) } } // Should likely do something to simulate DELETE. #[get("//delete")] -fn delete(id: i32) -> Result> { +fn delete(id: i32) -> Result, tera::TeraResult> { if Task::delete_with_id(id) { - Ok(Redirect::to("/")) // Say that it was added...somehow. + Ok(Flash::success(Redirect::to("/"), "Todo was deleted.")) } else { - let context = ctxt(Some("Could not delete that task.")); + let context = ctxt(Some(("error", "Could not delete that task."))); Err(TERA.render("index.html", context)) } } #[get("/")] -fn index() -> tera::TeraResult { - TERA.render("index.html", ctxt(None)) +fn index(msg: Option>) -> tera::TeraResult { + TERA.render("index.html", ctxt(msg.as_ref().map(|m| (m.name(), m.msg())))) } fn main() { let mut rocket = Rocket::new("127.0.0.1", 8000); - rocket.mount("/", routes![index, static_files::all, static_files::all_level_one]); - rocket.mount("/todo/", routes![new, delete, toggle]); + rocket.mount("/", routes![index, static_files::all]) + .mount("/todo/", routes![new, delete, toggle]); rocket.launch(); } diff --git a/examples/todo/src/static_files.rs b/examples/todo/src/static_files.rs index 6abe79fe..aa32f53a 100644 --- a/examples/todo/src/static_files.rs +++ b/examples/todo/src/static_files.rs @@ -1,14 +1,8 @@ use std::fs::File; use std::io; +use std::path::{Path, PathBuf}; -#[get("//")] -fn all_level_one(top: &str, file: &str) -> io::Result { - let file = format!("static/{}/{}", top, file); - File::open(file) -} - -#[get("/")] -fn all(file: &str) -> io::Result { - let file = format!("static/{}", file); - File::open(file) +#[get("/", rank = 5)] +fn all(path: PathBuf) -> io::Result { + File::open(Path::new("static/").join(path)) } diff --git a/examples/todo/src/task.rs b/examples/todo/src/task.rs index 17a74128..097fc163 100644 --- a/examples/todo/src/task.rs +++ b/examples/todo/src/task.rs @@ -35,12 +35,9 @@ impl Task { return false; } - Task::update_with_id(id, !task.unwrap().completed.unwrap()) - } - - pub fn update_with_id(id: i32, completed: bool) -> bool { - let task = diesel::update(all_tasks.find(id)); - task.set(task_completed.eq(completed)).execute(&db()).is_ok() + let new_status = !task.unwrap().completed.unwrap(); + let updated_task = diesel::update(all_tasks.find(id)); + updated_task.set(task_completed.eq(new_status)).execute(&db()).is_ok() } pub fn delete_with_id(id: i32) -> bool { diff --git a/examples/todo/static/css/style.css b/examples/todo/static/css/style.css index cc4b188e..6bb7ddb4 100644 --- a/examples/todo/static/css/style.css +++ b/examples/todo/static/css/style.css @@ -8,6 +8,16 @@ margin: -10px 0 10px 0; } +.field-success { + border: 1px solid #5AB953 !important; +} + +.field-success-msg { + color: #5AB953; + display: block; + margin: -10px 0 10px 0; +} + span.completed { text-decoration: line-through; } diff --git a/examples/todo/templates/index.html b/examples/todo/static/index.html similarity index 91% rename from examples/todo/templates/index.html rename to examples/todo/static/index.html index 16c3cf85..e549792d 100644 --- a/examples/todo/templates/index.html +++ b/examples/todo/static/index.html @@ -23,9 +23,9 @@
- {% if error %} - + class="u-full-width {% if has_msg %}field-{{msg_type}}{% endif %}" /> + {% if has_msg %} + {{ msg }} {% endif %} diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 569f07cb..2bd8e1d8 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -9,7 +9,3 @@ log = "*" hyper = "*" url = "*" mime = "*" - -# [dependencies.hyper] -# git = "https://github.com/hyperium/hyper.git" -# branch = "mio" diff --git a/lib/src/request/from_request.rs b/lib/src/request/from_request.rs index 98a675f3..033b86ce 100644 --- a/lib/src/request/from_request.rs +++ b/lib/src/request/from_request.rs @@ -25,15 +25,10 @@ impl<'r, 'c> FromRequest<'r, 'c> for Method { } } -impl<'r, 'c> FromRequest<'r, 'c> for Cookies { +impl<'r, 'c> FromRequest<'r, 'c> for &'r Cookies { type Error = (); - fn from_request(request: &'r Request<'c>) -> Result { - match request.headers().get::() { - // TODO: What to do about key? - Some(cookie) => Ok(cookie.to_cookie_jar(&[])), - None => Ok(Cookies::new(&[])) - } + Ok(request.cookies()) } } diff --git a/lib/src/request/mod.rs b/lib/src/request/mod.rs index 93c2231a..4fce89c5 100644 --- a/lib/src/request/mod.rs +++ b/lib/src/request/mod.rs @@ -9,4 +9,5 @@ pub use hyper::header::Headers as HyperHeaders; pub use hyper::header::Cookie as HyperCookie; use hyper::header::CookieJar; +pub use hyper::header::CookiePair as Cookie; pub type Cookies = CookieJar<'static>; diff --git a/lib/src/request/request.rs b/lib/src/request/request.rs index 950280a6..f19a8c17 100644 --- a/lib/src/request/request.rs +++ b/lib/src/request/request.rs @@ -17,18 +17,19 @@ use router::URI; use router::Route; // Hyper stuff. -use request::{HyperHeaders, HyperRequest}; +use request::{Cookies, HyperCookie, HyperHeaders, HyperRequest}; pub struct Request<'a> { pub method: Method, pub uri: URIBuf, // FIXME: Should be URI (without Hyper). pub data: Vec, // FIXME: Don't read this! (bad Hyper.) - params: RefCell>>, // This also sucks. + cookies: Cookies, headers: HyperHeaders, // This sucks. + params: RefCell>>, // This also sucks. } impl<'a> Request<'a> { - // FIXME: Don't do the parsing here. I think. Not sure. Decide. + // FIXME: Don't do the from_param parsing here. I think. Not sure. Decide. pub fn get_param>(&self, n: usize) -> Result { let params = self.params.borrow(); if params.is_none() || n >= params.as_ref().unwrap().len() { @@ -39,6 +40,10 @@ impl<'a> Request<'a> { } } + pub fn cookies<'r>(&'r self) -> &'r Cookies { + &self.cookies + } + /// i is the index of the first segment to consider pub fn get_segments<'r: 'a, T: FromSegments<'a>>(&'r self, i: usize) -> Result { @@ -58,6 +63,7 @@ impl<'a> Request<'a> { Request { params: RefCell::new(None), method: method, + cookies: Cookies::new(&[]), uri: URIBuf::from(uri), data: vec![], headers: HyperHeaders::new() @@ -118,6 +124,12 @@ impl<'a> Request<'a> { _ => return Err(format!("Bad method: {}", h_method)) }; + let cookies = match h_headers.get::() { + // TODO: What to do about key? + Some(cookie) => cookie.to_cookie_jar(&[]), + None => Cookies::new(&[]) + }; + // FIXME: GRRR. let mut data = vec![]; h_body.read_to_end(&mut data).unwrap(); @@ -125,6 +137,7 @@ impl<'a> Request<'a> { let request = Request { params: RefCell::new(None), method: method, + cookies: cookies, uri: uri, data: data, headers: h_headers, diff --git a/lib/src/response/cookied.rs b/lib/src/response/cookied.rs deleted file mode 100644 index e4ddb083..00000000 --- a/lib/src/response/cookied.rs +++ /dev/null @@ -1,50 +0,0 @@ -use response::*; -use std::string::ToString; -use hyper::header::{SetCookie, CookiePair}; - -pub struct Cookied { - cookies: Option>, - responder: R -} - -impl Cookied { - pub fn new(responder: R) -> Cookied { - Cookied { - cookies: None, - responder: responder - } - } - - pub fn pairs(responder: R, pairs: &[(&ToString, &ToString)]) -> Cookied { - Cookied { - cookies: Some( - pairs.iter() - .map(|p| CookiePair::new(p.0.to_string(), p.1.to_string())) - .collect() - ), - responder: responder - } - } - - #[inline(always)] - pub fn add(mut self, a: A, b: B) -> Self { - let new_pair = CookiePair::new(a.to_string(), b.to_string()); - match self.cookies { - Some(ref mut pairs) => pairs.push(new_pair), - None => self.cookies = Some(vec![new_pair]) - }; - - self - } -} - -impl Responder for Cookied { - fn respond<'b>(&mut self, mut res: FreshHyperResponse<'b>) -> Outcome<'b> { - if let Some(pairs) = self.cookies.take() { - res.headers_mut().set(SetCookie(pairs)); - } - - self.responder.respond(res) - } -} - diff --git a/lib/src/response/flash.rs b/lib/src/response/flash.rs new file mode 100644 index 00000000..35ab4208 --- /dev/null +++ b/lib/src/response/flash.rs @@ -0,0 +1,99 @@ +use response::*; +use std::convert::AsRef; +use hyper::header::{SetCookie, CookiePair}; +use request::{Request, FromRequest}; + +pub struct Flash { + name: String, + message: String, + responder: R +} + +impl Flash { + pub fn new, M: AsRef>(res: R, name: N, msg: M) -> Flash { + Flash { + name: name.as_ref().to_string(), + message: msg.as_ref().to_string(), + responder: res, + } + } + + pub fn warning>(responder: R, msg: S) -> Flash { + Flash::new(responder, "warning", msg) + } + + pub fn success>(responder: R, msg: S) -> Flash { + Flash::new(responder, "success", msg) + } + + pub fn error>(responder: R, msg: S) -> Flash { + Flash::new(responder, "error", msg) + } + + pub fn cookie_pair(&self) -> CookiePair { + let content = format!("{}{}{}", self.name.len(), self.name, self.message); + let mut pair = CookiePair::new("flash".to_string(), content); + pair.path = Some("/".to_string()); + pair.max_age = Some(300); + pair + } +} + +impl Responder for Flash { + fn respond<'b>(&mut self, mut res: FreshHyperResponse<'b>) -> Outcome<'b> { + trace_!("Flash: setting message: {}:{}", self.name, self.message); + res.headers_mut().set(SetCookie(vec![self.cookie_pair()])); + self.responder.respond(res) + } +} + +impl Flash<()> { + fn named(name: &str, msg: &str) -> Flash<()> { + Flash { + name: name.to_string(), + message: msg.to_string(), + responder: (), + } + } + + pub fn name(&self) -> &str { + self.name.as_str() + } + + pub fn msg(&self) -> &str { + self.message.as_str() + } +} + +// TODO: Using Flash<()> is ugly. Either create a type FlashMessage = Flash<()> +// or create a Flash under request that does this. +// TODO: Consider not removing the 'flash' cookie until after this thing is +// dropped. This is because, at the moment, if Flash is including as a +// from_request param, and some other param fails, then the flash message will +// be dropped needlessly. This may or may not be the intended behavior. +// Alternatively, provide a guarantee about the order that from_request params +// will be evaluated and recommend that Flash is last. +impl<'r, 'c> FromRequest<'r, 'c> for Flash<()> { + type Error = (); + + fn from_request(request: &'r Request<'c>) -> Result { + trace_!("Flash: attemping to retrieve message."); + request.cookies().find("flash").ok_or(()).and_then(|cookie| { + // Clear the flash message. + trace_!("Flash: retrieving message: {:?}", cookie); + request.cookies().remove("flash"); + + // Parse the flash. + let content = cookie.pair().1; + let (len_str, rest) = match content.find(|c: char| !c.is_digit(10)) { + Some(i) => (&content[..i], &content[i..]), + None => (content, "") + }; + + let name_len: usize = len_str.parse().map_err(|_| ())?; + let (name, msg) = (&rest[..name_len], &rest[name_len..]); + Ok(Flash::named(name, msg)) + }) + } +} + diff --git a/lib/src/response/mod.rs b/lib/src/response/mod.rs index 268266c5..5e06ee38 100644 --- a/lib/src/response/mod.rs +++ b/lib/src/response/mod.rs @@ -3,7 +3,7 @@ mod responder; mod redirect; mod with_status; mod outcome; -mod cookied; +mod flash; mod data_type; pub use hyper::server::Response as HyperResponse; @@ -18,7 +18,7 @@ pub use self::empty::{Empty, Forward}; pub use self::redirect::Redirect; pub use self::with_status::StatusResponse; pub use self::outcome::Outcome; -pub use self::cookied::Cookied; +pub use self::flash::Flash; use std::ops::{Deref, DerefMut}; diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index bc81c3b0..5ebfbefe 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -10,6 +10,7 @@ use term_painter::ToStyle; use hyper::server::Server as HyperServer; use hyper::server::Handler as HyperHandler; +use hyper::header::SetCookie; pub struct Rocket { address: String, @@ -49,8 +50,12 @@ impl Rocket { info_!("Matched: {}", route); request.set_params(route); - // Here's the magic: dispatch the request to the handler. - let outcome = (route.handler)(&request).respond(res); + // Dispatch the request to the handler and update the cookies. + let mut responder = (route.handler)(&request); + res.headers_mut().set(SetCookie(request.cookies().delta())); + + // Get the response. + let outcome = responder.respond(res); info_!("{} {}", White.paint("Outcome:"), outcome); // Get the result if we failed so we can try again. diff --git a/lib/src/router/mod.rs b/lib/src/router/mod.rs index 7d8cbaae..3a03e16c 100644 --- a/lib/src/router/mod.rs +++ b/lib/src/router/mod.rs @@ -39,12 +39,12 @@ impl Router { // let num_segments = req.uri.segment_count(); // self.routes.get(&(req.method, num_segments)).map_or(vec![], |routes| { self.routes.get(&req.method).map_or(vec![], |routes| { - trace_!("All possible matches: {:?}", routes); let mut matches: Vec<_> = routes.iter().filter(|r| { r.collides_with(req) }).collect(); matches.sort_by(|a, b| a.rank.cmp(&b.rank)); + trace_!("All matches: {:?}", matches); matches }) }