Fully working todo example. Apparently didn't commit in a while. Need to be better at that.

This commit is contained in:
Sergio Benitez 2016-08-01 19:07:36 -07:00
parent 677d7c49ea
commit 578b50b1f9
25 changed files with 1169 additions and 50 deletions

View File

@ -6,3 +6,10 @@ authors = ["Sergio Benitez <sb@sergio.bz>"]
[dependencies]
rocket = { path = "../../lib" }
rocket_macros = { path = "../../macros" }
lazy_static = "*"
tera = "*"
serde = { git = "https://github.com/serde-rs/serde" }
serde_json = { git = "https://github.com/serde-rs/json" }
serde_macros = { git = "https://github.com/serde-rs/serde" }
diesel = { version = "*", features = ["sqlite"] }
diesel_codegen = { git = "https://github.com/diesel-rs/diesel/", default_features = false, features = ["nightly", "sqlite"] }

4
examples/todo/README.md Normal file
View File

@ -0,0 +1,4 @@
Rocket Todo Example
===================
Fill this in on instructions for how to create the DB, etc.

View File

@ -0,0 +1,7 @@
CREATE TABLE tasks (
id INTEGER PRIMARY KEY,
description VARCHAR NOT NULL,
completed BOOLEAN NOT NULL DEFAULT 0
);
INSERT INTO tasks (description) VALUES ("my first task");

View File

View File

@ -0,0 +1 @@
DROP TABLE tasks

View File

@ -0,0 +1,5 @@
CREATE TABLE tasks (
id INTEGER PRIMARY KEY,
description VARCHAR NOT NULL,
completed BOOLEAN DEFAULT 0
)

View File

@ -1,39 +1,73 @@
#![feature(plugin, custom_derive)]
#![plugin(rocket_macros)]
#![feature(plugin, custom_derive, custom_attribute)]
#![plugin(rocket_macros, diesel_codegen, serde_macros)]
extern crate rocket;
extern crate tera;
#[macro_use] extern crate diesel;
#[macro_use] extern crate lazy_static;
extern crate serde_json;
extern crate serde;
mod static_files;
mod task;
use rocket::Rocket;
use rocket::response::Redirect;
use task::Task;
#[derive(FromForm)]
struct Todo<'r> {
description: &'r str,
lazy_static!(static ref TERA: tera::Tera = tera::Tera::new("templates/**/*"););
fn ctxt(error: Option<&str>) -> tera::Context {
let mut context = tera::Context::new();
context.add("error", &error.is_some());
context.add("msg", &error.unwrap_or("").to_string());
context.add("tasks", &Task::all());
context
}
#[route(POST, path = "/todo", form = "<todo>")]
fn new_todo(todo: Todo) -> Result<Redirect, &'static str> {
// if todos.add(todo).is_ok() {
// Ok(Redirect::to("/"))
// } else {
// Err("Could not add todo to list.")
// }
Ok(Redirect::to("/"))
#[route(POST, path = "", form = "<todo>")]
fn new(todo: Task) -> Result<Redirect, tera::TeraResult<String>> {
if todo.description.is_empty() {
let context = ctxt(Some("Description cannot be empty."));
Err(TERA.render("index.html", context))
} else if todo.insert() {
Ok(Redirect::to("/")) // Say that it was added...somehow.
} else {
let context = ctxt(Some("Whoops! The server failed."));
Err(TERA.render("index.html", context))
}
}
#[route(GET, path = "/todos")]
fn list_todos() -> &'static str {
"List all of the todos here!"
// Should likely do something to simulate PUT.
#[route(GET, path = "/<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.
} else {
let context = ctxt(Some("Could not toggle that task."));
Err(TERA.render("index.html", context))
}
}
// Should likely do something to simulate DELETE.
#[route(GET, path = "/<id>/delete")]
fn delete(id: i32) -> Result<Redirect, tera::TeraResult<String>> {
if Task::delete_with_id(id) {
Ok(Redirect::to("/")) // Say that it was added...somehow.
} else {
let context = ctxt(Some("Could not delete that task."));
Err(TERA.render("index.html", context))
}
}
#[route(GET, path = "/")]
fn index() -> Redirect {
Redirect::to("/todos")
fn index() -> tera::TeraResult<String> {
TERA.render("index.html", ctxt(None))
}
fn main() {
let mut rocket = Rocket::new("localhost", 8000);
rocket.mount("/", routes![index, list_todos, new_todo]);
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.launch();
}

View File

@ -0,0 +1,14 @@
use std::fs::File;
use std::io;
#[route(GET, path = "/<top>/<file>")]
fn all_level_one(top: &str, file: &str) -> io::Result<File> {
let file = format!("static/{}/{}", top, file);
File::open(file)
}
#[route(GET, path = "/<file>")]
fn all(file: &str) -> io::Result<File> {
let file = format!("static/{}", file);
File::open(file)
}

51
examples/todo/src/task.rs Normal file
View File

@ -0,0 +1,51 @@
use diesel;
use serde::Serialize;
use diesel::prelude::*;
use diesel::sqlite::SqliteConnection;
use self::schema::tasks;
use self::schema::tasks::dsl::{tasks as all_tasks, completed as task_completed};
mod schema {
infer_schema!("db/db.sql");
}
fn db() -> SqliteConnection {
SqliteConnection::establish("db/db.sql").expect("Failed to connect to db.")
}
#[insertable_into(tasks)]
#[derive(Serialize, Queryable, FromForm, Debug, Clone)]
pub struct Task {
id: Option<i32>,
pub description: String,
pub completed: Option<bool>
}
impl Task {
pub fn all() -> Vec<Task> {
all_tasks.order(tasks::id.desc()).load::<Task>(&db()).unwrap()
}
pub fn insert(&self) -> bool {
diesel::insert(self).into(tasks::table).execute(&db()).is_ok()
}
pub fn toggle_with_id(id: i32) -> bool {
let task = all_tasks.find(id).get_result::<Task>(&db());
if task.is_err() {
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()
}
pub fn delete_with_id(id: i32) -> bool {
diesel::delete(all_tasks.find(id)).execute(&db()).is_ok()
}
}

427
examples/todo/static/css/normalize.css vendored Normal file
View File

@ -0,0 +1,427 @@
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
/**
* 1. Set default font family to sans-serif.
* 2. Prevent iOS text size adjust after orientation change, without disabling
* user zoom.
*/
html {
font-family: sans-serif; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/**
* Remove default margin.
*/
body {
margin: 0;
}
/* HTML5 display definitions
========================================================================== */
/**
* Correct `block` display not defined for any HTML5 element in IE 8/9.
* Correct `block` display not defined for `details` or `summary` in IE 10/11
* and Firefox.
* Correct `block` display not defined for `main` in IE 11.
*/
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
nav,
section,
summary {
display: block;
}
/**
* 1. Correct `inline-block` display not defined in IE 8/9.
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
*/
audio,
canvas,
progress,
video {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Prevent modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Address `[hidden]` styling not present in IE 8/9/10.
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
*/
[hidden],
template {
display: none;
}
/* Links
========================================================================== */
/**
* Remove the gray background color from active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* Improve readability when focused and also mouse hovered in all browsers.
*/
a:active,
a:hover {
outline: 0;
}
/* Text-level semantics
========================================================================== */
/**
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
*/
abbr[title] {
border-bottom: 1px dotted;
}
/**
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
*/
b,
strong {
font-weight: bold;
}
/**
* Address styling not present in Safari and Chrome.
*/
dfn {
font-style: italic;
}
/**
* Address variable `h1` font-size and margin within `section` and `article`
* contexts in Firefox 4+, Safari, and Chrome.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/**
* Address styling not present in IE 8/9.
*/
mark {
background: #ff0;
color: #000;
}
/**
* Address inconsistent and variable font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
/* Embedded content
========================================================================== */
/**
* Remove border when inside `a` element in IE 8/9/10.
*/
img {
border: 0;
}
/**
* Correct overflow not hidden in IE 9/10/11.
*/
svg:not(:root) {
overflow: hidden;
}
/* Grouping content
========================================================================== */
/**
* Address margin not present in IE 8/9 and Safari.
*/
figure {
margin: 1em 40px;
}
/**
* Address differences between Firefox and other browsers.
*/
hr {
-moz-box-sizing: content-box;
box-sizing: content-box;
height: 0;
}
/**
* Contain overflow in all browsers.
*/
pre {
overflow: auto;
}
/**
* Address odd `em`-unit font size rendering in all browsers.
*/
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
/* Forms
========================================================================== */
/**
* Known limitation: by default, Chrome and Safari on OS X allow very limited
* styling of `select`, unless a `border` property is set.
*/
/**
* 1. Correct color not being inherited.
* Known issue: affects color of disabled elements.
* 2. Correct font properties not being inherited.
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
*/
button,
input,
optgroup,
select,
textarea {
color: inherit; /* 1 */
font: inherit; /* 2 */
margin: 0; /* 3 */
}
/**
* Address `overflow` set to `hidden` in IE 8/9/10/11.
*/
button {
overflow: visible;
}
/**
* Address inconsistent `text-transform` inheritance for `button` and `select`.
* All other form control elements do not inherit `text-transform` values.
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
* Correct `select` style inheritance in Firefox.
*/
button,
select {
text-transform: none;
}
/**
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
* and `video` controls.
* 2. Correct inability to style clickable `input` types in iOS.
* 3. Improve usability and consistency of cursor style between image-type
* `input` and others.
*/
button,
html input[type="button"], /* 1 */
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button; /* 2 */
cursor: pointer; /* 3 */
}
/**
* Re-set default cursor for disabled elements.
*/
button[disabled],
html input[disabled] {
cursor: default;
}
/**
* Remove inner padding and border in Firefox 4+.
*/
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
/**
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
* the UA stylesheet.
*/
input {
line-height: normal;
}
/**
* It's recommended that you don't attempt to style these elements.
* Firefox's implementation doesn't respect box-sizing, padding, or width.
*
* 1. Address box sizing set to `content-box` in IE 8/9/10.
* 2. Remove excess padding in IE 8/9/10.
*/
input[type="checkbox"],
input[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
* `font-size` values of the `input`, it causes the cursor style of the
* decrement button to change from `default` to `text`.
*/
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
* (include `-moz` to future-proof).
*/
input[type="search"] {
-webkit-appearance: textfield; /* 1 */
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box; /* 2 */
box-sizing: content-box;
}
/**
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
* Safari (but not Chrome) clips the cancel button when the search input has
* padding (and `textfield` appearance).
*/
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* Define consistent border, margin, and padding.
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/**
* 1. Correct `color` not being inherited in IE 8/9/10/11.
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
*/
legend {
border: 0; /* 1 */
padding: 0; /* 2 */
}
/**
* Remove default vertical scrollbar in IE 8/9/10/11.
*/
textarea {
overflow: auto;
}
/**
* Don't inherit the `font-weight` (applied by a rule above).
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
*/
optgroup {
font-weight: bold;
}
/* Tables
========================================================================== */
/**
* Remove most spacing between table cells.
*/
table {
border-collapse: collapse;
border-spacing: 0;
}
td,
th {
padding: 0;
}

418
examples/todo/static/css/skeleton.css vendored Normal file
View File

@ -0,0 +1,418 @@
/*
* Skeleton V2.0.4
* Copyright 2014, Dave Gamache
* www.getskeleton.com
* Free to use under the MIT license.
* http://www.opensource.org/licenses/mit-license.php
* 12/29/2014
*/
/* Table of contents
- Grid
- Base Styles
- Typography
- Links
- Buttons
- Forms
- Lists
- Code
- Tables
- Spacing
- Utilities
- Clearing
- Media Queries
*/
/* Grid
*/
.container {
position: relative;
width: 100%;
max-width: 960px;
margin: 0 auto;
padding: 0 20px;
box-sizing: border-box; }
.column,
.columns {
width: 100%;
float: left;
box-sizing: border-box; }
/* For devices larger than 400px */
@media (min-width: 400px) {
.container {
width: 85%;
padding: 0; }
}
/* For devices larger than 550px */
@media (min-width: 550px) {
.container {
width: 80%; }
.column,
.columns {
margin-left: 4%; }
.column:first-child,
.columns:first-child {
margin-left: 0; }
.one.column,
.one.columns { width: 4.66666666667%; }
.two.columns { width: 13.3333333333%; }
.three.columns { width: 22%; }
.four.columns { width: 30.6666666667%; }
.five.columns { width: 39.3333333333%; }
.six.columns { width: 48%; }
.seven.columns { width: 56.6666666667%; }
.eight.columns { width: 65.3333333333%; }
.nine.columns { width: 74.0%; }
.ten.columns { width: 82.6666666667%; }
.eleven.columns { width: 91.3333333333%; }
.twelve.columns { width: 100%; margin-left: 0; }
.one-third.column { width: 30.6666666667%; }
.two-thirds.column { width: 65.3333333333%; }
.one-half.column { width: 48%; }
/* Offsets */
.offset-by-one.column,
.offset-by-one.columns { margin-left: 8.66666666667%; }
.offset-by-two.column,
.offset-by-two.columns { margin-left: 17.3333333333%; }
.offset-by-three.column,
.offset-by-three.columns { margin-left: 26%; }
.offset-by-four.column,
.offset-by-four.columns { margin-left: 34.6666666667%; }
.offset-by-five.column,
.offset-by-five.columns { margin-left: 43.3333333333%; }
.offset-by-six.column,
.offset-by-six.columns { margin-left: 52%; }
.offset-by-seven.column,
.offset-by-seven.columns { margin-left: 60.6666666667%; }
.offset-by-eight.column,
.offset-by-eight.columns { margin-left: 69.3333333333%; }
.offset-by-nine.column,
.offset-by-nine.columns { margin-left: 78.0%; }
.offset-by-ten.column,
.offset-by-ten.columns { margin-left: 86.6666666667%; }
.offset-by-eleven.column,
.offset-by-eleven.columns { margin-left: 95.3333333333%; }
.offset-by-one-third.column,
.offset-by-one-third.columns { margin-left: 34.6666666667%; }
.offset-by-two-thirds.column,
.offset-by-two-thirds.columns { margin-left: 69.3333333333%; }
.offset-by-one-half.column,
.offset-by-one-half.columns { margin-left: 52%; }
}
/* Base Styles
*/
/* NOTE
html is set to 62.5% so that all the REM measurements throughout Skeleton
are based on 10px sizing. So basically 1.5rem = 15px :) */
html {
font-size: 62.5%; }
body {
font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */
line-height: 1.6;
font-weight: 400;
font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #222; }
/* Typography
*/
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 2rem;
font-weight: 300; }
h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;}
h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; }
h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; }
h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; }
h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; }
h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; }
/* Larger than phablet */
@media (min-width: 550px) {
h1 { font-size: 5.0rem; }
h2 { font-size: 4.2rem; }
h3 { font-size: 3.6rem; }
h4 { font-size: 3.0rem; }
h5 { font-size: 2.4rem; }
h6 { font-size: 1.5rem; }
}
p {
margin-top: 0; }
/* Links
*/
a {
color: #1EAEDB; }
a:hover {
color: #0FA0CE; }
/* Buttons
*/
.button,
button,
input[type="submit"],
input[type="reset"],
input[type="button"] {
display: inline-block;
height: 38px;
padding: 0 30px;
color: #555;
text-align: center;
font-size: 11px;
font-weight: 600;
line-height: 38px;
letter-spacing: .1rem;
text-transform: uppercase;
text-decoration: none;
white-space: nowrap;
background-color: transparent;
border-radius: 4px;
border: 1px solid #bbb;
cursor: pointer;
box-sizing: border-box; }
.button:hover,
button:hover,
input[type="submit"]:hover,
input[type="reset"]:hover,
input[type="button"]:hover,
.button:focus,
button:focus,
input[type="submit"]:focus,
input[type="reset"]:focus,
input[type="button"]:focus {
color: #333;
border-color: #888;
outline: 0; }
.button.button-primary,
button.button-primary,
input[type="submit"].button-primary,
input[type="reset"].button-primary,
input[type="button"].button-primary {
color: #FFF;
background-color: #33C3F0;
border-color: #33C3F0; }
.button.button-primary:hover,
button.button-primary:hover,
input[type="submit"].button-primary:hover,
input[type="reset"].button-primary:hover,
input[type="button"].button-primary:hover,
.button.button-primary:focus,
button.button-primary:focus,
input[type="submit"].button-primary:focus,
input[type="reset"].button-primary:focus,
input[type="button"].button-primary:focus {
color: #FFF;
background-color: #1EAEDB;
border-color: #1EAEDB; }
/* Forms
*/
input[type="email"],
input[type="number"],
input[type="search"],
input[type="text"],
input[type="tel"],
input[type="url"],
input[type="password"],
textarea,
select {
height: 38px;
padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */
background-color: #fff;
border: 1px solid #D1D1D1;
border-radius: 4px;
box-shadow: none;
box-sizing: border-box; }
/* Removes awkward default styles on some inputs for iOS */
input[type="email"],
input[type="number"],
input[type="search"],
input[type="text"],
input[type="tel"],
input[type="url"],
input[type="password"],
textarea {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none; }
textarea {
min-height: 65px;
padding-top: 6px;
padding-bottom: 6px; }
input[type="email"]:focus,
input[type="number"]:focus,
input[type="search"]:focus,
input[type="text"]:focus,
input[type="tel"]:focus,
input[type="url"]:focus,
input[type="password"]:focus,
textarea:focus,
select:focus {
border: 1px solid #33C3F0;
outline: 0; }
label,
legend {
display: block;
margin-bottom: .5rem;
font-weight: 600; }
fieldset {
padding: 0;
border-width: 0; }
input[type="checkbox"],
input[type="radio"] {
display: inline; }
label > .label-body {
display: inline-block;
margin-left: .5rem;
font-weight: normal; }
/* Lists
*/
ul {
list-style: circle inside; }
ol {
list-style: decimal inside; }
ol, ul {
padding-left: 0;
margin-top: 0; }
ul ul,
ul ol,
ol ol,
ol ul {
margin: 1.5rem 0 1.5rem 3rem;
font-size: 90%; }
li {
margin-bottom: 1rem; }
/* Code
*/
code {
padding: .2rem .5rem;
margin: 0 .2rem;
font-size: 90%;
white-space: nowrap;
background: #F1F1F1;
border: 1px solid #E1E1E1;
border-radius: 4px; }
pre > code {
display: block;
padding: 1rem 1.5rem;
white-space: pre; }
/* Tables
*/
th,
td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #E1E1E1; }
th:first-child,
td:first-child {
padding-left: 0; }
th:last-child,
td:last-child {
padding-right: 0; }
/* Spacing
*/
button,
.button {
margin-bottom: 1rem; }
input,
textarea,
select,
fieldset {
margin-bottom: 1.5rem; }
pre,
blockquote,
dl,
figure,
table,
p,
ul,
ol,
form {
margin-bottom: 2.5rem; }
/* Utilities
*/
.u-full-width {
width: 100%;
box-sizing: border-box; }
.u-max-full-width {
max-width: 100%;
box-sizing: border-box; }
.u-pull-right {
float: right; }
.u-pull-left {
float: left; }
/* Misc
*/
hr {
margin-top: 3rem;
margin-bottom: 3.5rem;
border-width: 0;
border-top: 1px solid #E1E1E1; }
/* Clearing
*/
/* Self Clearing Goodness */
.container:after,
.row:after,
.u-cf {
content: "";
display: table;
clear: both; }
/* Media Queries
*/
/*
Note: The best way to structure the use of media queries is to create the queries
near the relevant code. For example, if you wanted to change the styles for buttons
on small devices, paste the mobile query code up in the buttons section and style it
there.
*/
/* Larger than mobile */
@media (min-width: 400px) {}
/* Larger than phablet (also point when grid becomes active) */
@media (min-width: 550px) {}
/* Larger than tablet */
@media (min-width: 750px) {}
/* Larger than desktop */
@media (min-width: 1000px) {}
/* Larger than Desktop HD */
@media (min-width: 1200px) {}

View File

@ -0,0 +1,13 @@
.field-error {
border: 1px solid #ff0000 !important;
}
.field-error-msg {
color: #ff0000;
display: block;
margin: -10px 0 10px 0;
}
span.completed {
text-decoration: line-through;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Rocket Todo Example</title>
<meta name="description" content="A todo application written in Rocket.">
<meta name="author" content="Sergio Benitez">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="//fonts.googleapis.com/css?family=Raleway:400,300,600" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="/css/normalize.css">
<link rel="stylesheet" href="/css/skeleton.css">
<link rel="stylesheet" href="/css/style.css">
<link rel="icon" type="image/png" href="/images/favicon.png">
</head>
<body>
<div class="container">
<p><!--Nothing to see here --></p>
<div class="row">
<h4>Rocket Todo</h4>
<form action="/todo" method="post" accept-charset="utf-8">
<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">
{{ msg }}
</small>
{% endif %}
</div>
<div class="two columns">
<input type="submit" value="add task">
</div>
</form>
</div>
<div class="row">
<div class="twelve columns">
<ul>
{% for task in tasks %}
{% if task.completed %}
<li>
<span class="completed">{{ task.description }}</span>
<a href="/todo/{{ task.id }}/toggle">undo</a>
<a href="/todo/{{ task.id }}/delete">delete</a>
</li>
{% else %}
<li>
<a href="/todo/{{ task.id }}/toggle">{{ task.description }}</a>
</li>
{% endif %}
{% endfor %}
</ul>
</div>
</div>
</div>
</body>
</html>

View File

@ -6,6 +6,7 @@ authors = ["Sergio Benitez <sb@sergio.bz>"]
[dependencies]
term-painter = "*"
hyper = "*"
url = "*"
# [dependencies.hyper]
# git = "https://github.com/hyperium/hyper.git"

View File

@ -1,7 +1,5 @@
use handler::Handler;
use error::Error;
use response::Response;
use request::Request;
use error::RoutingError;
use codegen::StaticCatchInfo;

View File

@ -1,5 +1,6 @@
use std::str::FromStr;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6, SocketAddr};
use url::{self};
use error::Error;
@ -11,6 +12,13 @@ pub trait FromFormValue<'v>: Sized {
type Error;
fn parse(v: &'v str) -> Result<Self, Self::Error>;
// Returns a default value to be used when the form field does not exist. If
// this returns None, then the field is required. Otherwise, this should
// return Some(default_value).
fn default() -> Option<Self> {
None
}
}
impl<'v> FromFormValue<'v> for &'v str {
@ -21,6 +29,30 @@ impl<'v> FromFormValue<'v> for &'v str {
}
}
impl<'v> FromFormValue<'v> for String {
type Error = &'v str;
// This actually parses the value according to the standard.
fn parse(v: &'v str) -> Result<Self, Self::Error> {
let decoder = url::percent_encoding::percent_decode(v.as_bytes());
let res = decoder.decode_utf8().map_err(|_| v).map(|s| s.into_owned());
match res {
e@Err(_) => e,
Ok(mut string) => Ok({
unsafe {
for c in string.as_mut_vec() {
if *c == b'+' {
*c = b' ';
}
}
}
string
})
}
}
}
macro_rules! impl_with_fromstr {
($($T:ident),+) => ($(
impl<'v> FromFormValue<'v> for $T {
@ -33,7 +65,7 @@ macro_rules! impl_with_fromstr {
}
impl_with_fromstr!(f32, f64, isize, i8, i16, i32, i64, usize, u8, u16, u32, u64,
bool, String, IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6,
bool, IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6,
SocketAddr);
impl<'v, T: FromFormValue<'v>> FromFormValue<'v> for Option<T> {
@ -45,6 +77,10 @@ impl<'v, T: FromFormValue<'v>> FromFormValue<'v> for Option<T> {
Err(_) => Ok(None)
}
}
fn default() -> Option<Option<T>> {
Some(None)
}
}
// TODO: Add more useful implementations (range, regex, etc.).

View File

@ -3,6 +3,7 @@
extern crate term_painter;
extern crate hyper;
extern crate url;
mod method;
mod error;

View File

@ -8,6 +8,7 @@ pub use hyper::server::Response as HyperResponse;
pub use hyper::net::Fresh as HyperFresh;
pub use hyper::status::StatusCode;
pub use hyper::header;
pub use hyper::mime;
pub use self::responder::Responder;
pub use self::empty::{Empty, Forward};

View File

@ -5,7 +5,7 @@ pub struct Redirect(StatusCode, String);
impl Redirect {
pub fn to(uri: &str) -> Redirect {
Redirect(StatusCode::TemporaryRedirect, String::from(uri))
Redirect(StatusCode::Found, String::from(uri))
}
pub fn created(uri: &str) -> Redirect {
@ -19,6 +19,10 @@ impl Redirect {
pub fn permanent(uri: &str) -> Redirect {
Redirect(StatusCode::PermanentRedirect, String::from(uri))
}
pub fn temporary(uri: &str) -> Redirect {
Redirect(StatusCode::TemporaryRedirect, String::from(uri))
}
}
impl<'a> Responder for Redirect {

View File

@ -1,4 +1,5 @@
use response::*;
use response::mime::{Mime, TopLevel, SubLevel};
use std::io::{Read, Write};
use std::fs::File;
use std::fmt;
@ -12,20 +13,27 @@ pub trait Responder {
}
impl<'a> Responder for &'a str {
fn respond<'b>(&mut self, res: FreshHyperResponse<'b>) -> Outcome<'b> {
fn respond<'b>(&mut self, mut res: FreshHyperResponse<'b>) -> Outcome<'b> {
let mime = Mime(TopLevel::Text, SubLevel::Html, vec![]);
res.headers_mut().set(header::ContentType(mime));
res.send(self.as_bytes()).unwrap();
Outcome::Complete
}
}
impl Responder for String {
fn respond<'a>(&mut self, res: FreshHyperResponse<'a>) -> Outcome<'a> {
fn respond<'a>(&mut self, mut res: FreshHyperResponse<'a>) -> Outcome<'a> {
let mime = Mime(TopLevel::Text, SubLevel::Html, vec![]);
res.headers_mut().set(header::ContentType(mime));
res.send(self.as_bytes()).unwrap();
Outcome::Complete
}
}
// FIXME: Should we set a content-type here? Safari needs text/html to render.
// Unfortunately, the file name is gone at this point. Should fix this. There's
// a way to retrieve a file based on its fd, strangely enough. See...
// https://stackoverflow.com/questions/1188757/getting-filename-from-file-descriptor-in-c
impl Responder for File {
fn respond<'a>(&mut self, mut res: FreshHyperResponse<'a>) -> Outcome<'a> {
let size = self.metadata().unwrap().len();
@ -47,7 +55,7 @@ impl<T: Responder> Responder for Option<T> {
if self.is_none() {
println!("Option is none.");
// TODO: Should this be a 404 or 500?
return Empty::new(StatusCode::NotFound).respond(res)
return Outcome::FailForward(res);
}
self.as_mut().unwrap().respond(res)
@ -61,7 +69,7 @@ impl<T: Responder, E: fmt::Debug> Responder for Result<T, E> {
if self.is_err() {
println!("Error: {:?}", self.as_ref().err().unwrap());
// TODO: Should this be a 404 or 500?
return Empty::new(StatusCode::NotFound).respond(res)
return Outcome::FailForward(res);
}
self.as_mut().unwrap().respond(res)

View File

@ -40,7 +40,7 @@ fn method_is_valid(method: &HyperMethod) -> bool {
impl HyperHandler for Rocket {
fn handle<'a, 'k>(&'a self, req: HyperRequest<'a, 'k>,
res: FreshHyperResponse<'a>) {
mut res: FreshHyperResponse<'a>) {
println!("{:?} '{}'", Green.paint(&req.method), Blue.paint(&req.uri));
let finalize = |mut req: HyperRequest, _res: FreshHyperResponse| {
@ -61,6 +61,8 @@ impl HyperHandler for Rocket {
return finalize(req, res);
}
res.headers_mut().set(response::header::Server("rocket".to_string()));
self.dispatch(req, res)
}
}

View File

@ -13,6 +13,7 @@ use method::Method;
type Selector = (Method, usize);
#[derive(Default)]
pub struct Router {
routes: HashMap<Selector, Vec<Route>> // using 'selector' for now
}

View File

@ -1,6 +1,7 @@
#![allow(unused_imports)] // FIXME: Why is this coming from quote_tokens?
use syntax::ext::base::{Annotatable, ExtCtxt};
use syntax::print::pprust::{stmt_to_string};
use syntax::ast::{ItemKind, Expr, MetaItem, Mutability, VariantData};
use syntax::codemap::Span;
use syntax::ext::build::AstBuilder;
@ -11,10 +12,11 @@ use syntax_ext::deriving::generic::MethodDef;
use syntax_ext::deriving::generic::{StaticStruct, Substructure, TraitDef, ty};
use syntax_ext::deriving::generic::combine_substructure as c_s;
const DEBUG: bool = false;
const DEBUG: bool = true;
static ONLY_STRUCTS_ERR: &'static str = "`FromForm` can only be derived for \
structures with named fields.";
static PRIVATE_LIFETIME: &'static str = "'rocket";
fn get_struct_lifetime(ecx: &mut ExtCtxt, item: &Annotatable, span: Span)
-> Option<&'static str> {
@ -48,7 +50,14 @@ fn get_struct_lifetime(ecx: &mut ExtCtxt, item: &Annotatable, span: Span)
pub fn from_form_derive(ecx: &mut ExtCtxt, span: Span, meta_item: &MetaItem,
annotated: &Annotatable, push: &mut FnMut(Annotatable)) {
let lifetime_var = get_struct_lifetime(ecx, annotated, span);
let struct_lifetime = get_struct_lifetime(ecx, annotated, span);
let (lifetime_var, trait_generics) = match struct_lifetime {
lifetime@Some(_) => (lifetime, ty::LifetimeBounds::empty()),
None => (Some(PRIVATE_LIFETIME), ty::LifetimeBounds {
lifetimes: vec![(PRIVATE_LIFETIME, vec![])],
bounds: vec![]
})
};
let trait_def = TraitDef {
is_unsafe: false,
@ -61,7 +70,7 @@ pub fn from_form_derive(ecx: &mut ExtCtxt, span: Span, meta_item: &MetaItem,
global: true,
},
additional_bounds: Vec::new(),
generics: ty::LifetimeBounds::empty(),
generics: trait_generics,
methods: vec![
MethodDef {
name: "from_form_string",
@ -143,15 +152,18 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct
let initial_block = quote_block!(cx, {
let mut items = [("", ""); $num_fields];
let form_count = ::rocket::form::form_items($arg, &mut items);
if form_count != items.len() {
$return_err_stmt;
};
// if form_count != items.len() {
// println!("\t Form parse: Wrong number of items!");
// $return_err_stmt;
// };
});
stmts.extend(initial_block.unwrap().stmts);
// Generate the let bindings for parameters that will be unwrapped and
// placed into the final struct
// placed into the final struct. They start out as `None` and are changed
// to Some when a parse completes, or some default value if the parse was
// unsuccessful and default() returns Some.
for &(ref ident, ref ty) in &fields_and_types {
stmts.push(quote_stmt!(cx,
let mut $ident: ::std::option::Option<$ty> = None;
@ -174,30 +186,38 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct
// The actual match statement. Uses the $arms generated above.
stmts.push(quote_stmt!(cx,
for &(k, v) in &items {
match k {
$arms
_ => $return_err_stmt
for &(k, v) in &items[..form_count] {
match k {
$arms
// Return error when a field is in the form but not in struct.
_ => {
println!("\t{}={} has no matching field in struct.", k, v);
$return_err_stmt
}
};
}
).unwrap());
// This looks complicated but just generates the boolean condition checking
// that each parameter actually is Some(), IE, had a key/value and parsed.
// that each parameter actually is Some() or has a default value.
let mut failure_conditions = vec![];
for (i, &(ref ident, _)) in (&fields_and_types).iter().enumerate() {
for (i, &(ref ident, ref ty)) in (&fields_and_types).iter().enumerate() {
if i > 0 {
failure_conditions.push(quote_tokens!(cx, || $ident.is_none()));
} else {
failure_conditions.push(quote_tokens!(cx, $ident.is_none()));
failure_conditions.push(quote_tokens!(cx, ||));
}
failure_conditions.push(quote_tokens!(cx, $ident.is_none() &&
<$ty as ::rocket::form::FromFormValue>::default().is_none()));
}
// The fields of the struct, which are just the let bindings declared above.
// The fields of the struct, which are just the let bindings declared above
// or the default value.
let mut result_fields = vec![];
for &(ref ident, _) in &fields_and_types {
for &(ref ident, ref ty) in &fields_and_types {
result_fields.push(quote_tokens!(cx,
$ident: $ident.unwrap(),
$ident: $ident.unwrap_or_else(||
<$ty as ::rocket::form::FromFormValue>::default().unwrap()
),
));
}
@ -206,6 +226,7 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct
let self_ident = substr.type_ident;
let final_block = quote_block!(cx, {
if $failure_conditions {
println!("\tOne of the fields didn't parse.");
$return_err_stmt;
}
@ -215,6 +236,12 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct
});
stmts.extend(final_block.unwrap().stmts);
debug!("Form statements:");
for stmt in &stmts {
debug!("{:?}", stmt_to_string(stmt));
}
cx.expr_block(cx.block(trait_span, stmts))
}

View File

@ -116,7 +116,6 @@ fn parse_route(ecx: &mut ExtCtxt, meta_item: &MetaItem) -> Params {
}
}
// TODO: Put something like this in the library. Maybe as an iterator?
pub fn extract_params<'a>(ecx: &ExtCtxt, params: &Spanned<&'a str>)
-> Vec<Spanned<&'a str>> {