Add support for flash cookie. Revamp cookie support.

This commit is contained in:
Sergio Benitez 2016-09-11 18:57:04 -07:00
parent 9a9d07f044
commit e8e85f09cd
15 changed files with 174 additions and 111 deletions

View File

@ -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 = "<message>")]
fn submit(message: Message) -> Cookied<Redirect> {
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<String> {
fn index(cookies: &Cookies) -> tera::TeraResult<String> {
let message = cookies.find("message").map(|msg| msg.value);
TERA.render("index.html", ctxt(message))
}

View File

@ -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 = "<todo>")]
fn new(todo: Task) -> Result<Redirect, tera::TeraResult<String>> {
fn new(todo: Task) -> Result<Flash<Redirect>, tera::TeraResult<String>> {
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<Redirect, tera::TeraResult<String>> {
#[get("/<id>/toggle")]
fn toggle(id: i32) -> Result<Redirect, tera::TeraResult<String>> {
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("/<id>/delete")]
fn delete(id: i32) -> Result<Redirect, tera::TeraResult<String>> {
fn delete(id: i32) -> Result<Flash<Redirect>, tera::TeraResult<String>> {
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<String> {
TERA.render("index.html", ctxt(None))
fn index(msg: Option<Flash<()>>) -> tera::TeraResult<String> {
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();
}

View File

@ -1,14 +1,8 @@
use std::fs::File;
use std::io;
use std::path::{Path, PathBuf};
#[get("/<top>/<file>")]
fn all_level_one(top: &str, file: &str) -> io::Result<File> {
let file = format!("static/{}/{}", top, file);
File::open(file)
}
#[get("/<file>")]
fn all(file: &str) -> io::Result<File> {
let file = format!("static/{}", file);
File::open(file)
#[get("/<path..>", rank = 5)]
fn all(path: PathBuf) -> io::Result<File> {
File::open(Path::new("static/").join(path))
}

View File

@ -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 {

View File

@ -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;
}

View File

@ -23,9 +23,9 @@
<div class="ten columns">
<input type="text" placeholder="enter a task description..."
name="description" id="description" value="" autofocus
class="u-full-width {% if error %}field-error{% endif %}" />
{% if error %}
<small class="field-error-msg">
class="u-full-width {% if has_msg %}field-{{msg_type}}{% endif %}" />
{% if has_msg %}
<small class="field-{{msg_type}}-msg">
{{ msg }}
</small>
{% endif %}

View File

@ -9,7 +9,3 @@ log = "*"
hyper = "*"
url = "*"
mime = "*"
# [dependencies.hyper]
# git = "https://github.com/hyperium/hyper.git"
# branch = "mio"

View File

@ -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<Self, Self::Error> {
match request.headers().get::<HyperCookie>() {
// TODO: What to do about key?
Some(cookie) => Ok(cookie.to_cookie_jar(&[])),
None => Ok(Cookies::new(&[]))
}
Ok(request.cookies())
}
}

View File

@ -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>;

View File

@ -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<u8>, // FIXME: Don't read this! (bad Hyper.)
params: RefCell<Option<Vec<&'a str>>>, // This also sucks.
cookies: Cookies,
headers: HyperHeaders, // This sucks.
params: RefCell<Option<Vec<&'a str>>>, // 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<T: FromParam<'a>>(&self, n: usize) -> Result<T, Error> {
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<T, Error> {
@ -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::<HyperCookie>() {
// 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,

View File

@ -1,50 +0,0 @@
use response::*;
use std::string::ToString;
use hyper::header::{SetCookie, CookiePair};
pub struct Cookied<R: Responder> {
cookies: Option<Vec<CookiePair>>,
responder: R
}
impl<R: Responder> Cookied<R> {
pub fn new(responder: R) -> Cookied<R> {
Cookied {
cookies: None,
responder: responder
}
}
pub fn pairs(responder: R, pairs: &[(&ToString, &ToString)]) -> Cookied<R> {
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<A: ToString, B: ToString>(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<R: Responder> Responder for Cookied<R> {
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)
}
}

99
lib/src/response/flash.rs Normal file
View File

@ -0,0 +1,99 @@
use response::*;
use std::convert::AsRef;
use hyper::header::{SetCookie, CookiePair};
use request::{Request, FromRequest};
pub struct Flash<R> {
name: String,
message: String,
responder: R
}
impl<R: Responder> Flash<R> {
pub fn new<N: AsRef<str>, M: AsRef<str>>(res: R, name: N, msg: M) -> Flash<R> {
Flash {
name: name.as_ref().to_string(),
message: msg.as_ref().to_string(),
responder: res,
}
}
pub fn warning<S: AsRef<str>>(responder: R, msg: S) -> Flash<R> {
Flash::new(responder, "warning", msg)
}
pub fn success<S: AsRef<str>>(responder: R, msg: S) -> Flash<R> {
Flash::new(responder, "success", msg)
}
pub fn error<S: AsRef<str>>(responder: R, msg: S) -> Flash<R> {
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<R: Responder> Responder for Flash<R> {
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<Self, Self::Error> {
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))
})
}
}

View File

@ -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};

View File

@ -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.

View File

@ -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
})
}