diff --git a/.gitignore b/.gitignore
index 9d051aa..ddd14cd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
/rust_web.pdf
/rust_web.tex
/simple_text_board/target
+/simple_text_board/simple_text_board.sqlite
diff --git a/rust_web.org b/rust_web.org
index 974dd15..d7755be 100644
--- a/rust_web.org
+++ b/rust_web.org
@@ -260,112 +260,280 @@
*** Steps to reproduce :noexport:
**** Create new crate
-#+BEGIN_SRC sh
-cargo init simple_text_board
-cd simple_text_board
-rustup override set nightly
-cargo r
-#+END_SRC
+ #+BEGIN_SRC sh
+ cargo init simple_text_board
+ cd simple_text_board
+ rustup override set nightly
+ cargo r
+ #+END_SRC
**** Setup Rocket
- Add Rocket dependency to ~Cargo.toml~:
-#+BEGIN_SRC toml
-[dependencies]
-rocket = "0.4"
-#+END_SRC
+***** Add Rocket to ~Cargo.toml~
+ #+BEGIN_SRC toml
+ [dependencies]
+ rocket = "0.4"
+ #+END_SRC
-**** Modify ~src/main.rs~ for Rockets "~Hello, world!~" program
-#+BEGIN_SRC rust
-#![feature(proc_macro_hygiene, decl_macro)]
+***** Add ~Rocket.toml~
+ #+BEGIN_SRC toml
+ [development]
+ address = "0.0.0.0"
+ port = 8000
+ workers = 2
+ keep_alive = 5
+ log = "normal"
+ #+END_SRC
-#[macro_use] extern crate rocket;
+***** Modify ~src/main.rs~ for Rockets "~Hello, world!~" program
+ #+BEGIN_SRC rust
+ #![feature(proc_macro_hygiene, decl_macro)]
-#[get("/")]
-fn index() -> &'static str {
- "Hello, world!"
-}
+ #[macro_use] extern crate rocket;
-fn main() {
- rocket::ignite().mount("/", routes![index]).launch();
-}
-#+END_SRC
+ #[get("/")]
+ fn index() -> &'static str {
+ "Hello, world!"
+ }
-**** Add ~Rocket.toml~
-#+BEGIN_SRC toml
-[development]
-address = "0.0.0.0"
-port = 8000
-workers = 2
-keep_alive = 5
-log = "normal"
-#+END_SRC
+ fn main() {
+ rocket::ignite().mount("/", routes![index]).launch();
+ }
+ #+END_SRC
-**** Create the base template at ~templates/base.html.tera~
-#+BEGIN_SRC html
-
-
-
-
-
- simple text board
-
-
-
- {% block content %} {% endblock %}
-
-
-#+END_SRC
+**** Create first templates
+***** Create the base template at ~templates/base.html.tera~
+ #+BEGIN_SRC html
+
+
+
+
+
+ simple text board
+
+
+
+ {% block content %} {% endblock %}
+
+
+ #+END_SRC
-**** Create the create template at ~templates/create.html.tera~
-#+BEGIN_SRC html
-{% extends "base" %}
+***** Create the create template at ~templates/create.html.tera~
+ #+BEGIN_SRC html
+ {% extends "base" %}
-{% block content %}
-simple text board
-create a post
-{% if flash %}
-{{ flash }}
-{% endif %}
-
-{% endblock content %}
-#+END_SRC
+
+ {% if flash %}{{ flash }}
{% endif %}
+
+ {% endblock content %}
+ #+END_SRC
-**** Create the ~create_form~ function
-#+BEGIN_SRC rust
-use std::collections::HashMap;
+***** Create the ~create_form~ function and add it to routing
+ #+BEGIN_SRC rust
+ #![feature(proc_macro_hygiene, decl_macro)]
-use rocket::{get, request::FlashMessage, routes};
-use rocket_contrib::templates::Template;
+ use std::collections::HashMap;
-#[get("/create")]
-fn create_form(flash: Option) -> Template {
- let mut context = HashMap::new();
+ use rocket::{get, request::FlashMessage, routes};
+ use rocket_contrib::templates::Template;
- if let Some(ref msg) = flash {
- context.insert("flash", msg.msg());
- }
+ #[get("/create")]
+ fn create_form(flash: Option) -> Template {
+ let mut context = HashMap::new();
- Template::render("create", &context)
-}
-#+END_SRC
+ if let Some(ref msg) = flash {
+ context.insert("flash", msg.msg());
+ }
+
+ Template::render("create", &context)
+ }
+
+ ...
+
+ fn main() {
+ rocket::ignite()
+ .mount("/", routes![index, create_form])
+ .attach(Template::fairing())
+ .launch();
+ }
+ #+END_SRC
+
+**** Setup Database and posts
+***** Create the ~.env~ file
+ #+BEGIN_SRC sh
+ DATABASE_URL=simple_text_board.sqlite
+ #+END_SRC
+
+***** Add diesel to the dependencies and to ~rocket_contrib~ in ~Cargo.toml~
+ #+BEGIN_SRC toml
+ ...
+ chrono = "0.4"
+ diesel = { version = "1.4", features = ["sqlite", "chrono"] }
+ log = "0.4"
+ ...
+ [dependencies.rocket_contrib]
+ ...
+ features = ["diesel_sqlite_pool", "tera_templates"]
+ #+END_SRC
+
+***** Configure database for Rocket in ~Rocket.toml~
+ #+BEGIN_SRC toml
+ [global.databases]
+ sqlite = { url = "simple_text_board.sqlite" }
+ #+END_SRC
+
+***** Create the first table schema
+ #+BEGIN_SRC sh
+ diesel setup
+ diesel migration generate create_posts
+ #+END_SRC
+
+***** How to create the table
+ #+BEGIN_SRC sql
+ CREATE TABLE posts (
+ id INTEGER NOT NULL PRIMARY KEY,
+ parent INTEGER,
+ timestamp DATETIME NOT NULL,
+ author VARCHAR NOT NULL,
+ email VARCHAR NOT NULL,
+ title VARCHAR NOT NULL,
+ content VARCHAR NOT NULL,
+ FOREIGN KEY (parent) REFERENCES posts(id)
+ );
+ #+END_SRC
+
+***** How to destroy it
+ #+BEGIN_SRC sql
+ DROP TABLE posts;
+ #+END_SRC
+
+***** Applay and test rollback
+ #+BEGIN_SRC sh
+ diesel migration run
+ diesel migration redo
+ #+END_SRC
+
+***** Create data model in ~src/models.rs~
+ #+BEGIN_SRC rust
+ use crate::schema::posts::{self, dsl::posts as table_posts};
+ use chrono::NaiveDateTime;
+ use diesel::{QueryResult, RunQueryDsl};
+
+ #[derive(Insertable)]
+ #[table_name = "posts"]
+ pub struct NewPost<'a> {
+ pub parent: Option<&'a i32>,
+ pub timestamp: NaiveDateTime,
+ pub author: &'a str,
+ pub email: &'a str,
+ pub title: &'a str,
+ pub content: &'a str,
+ }
+
+ impl NewPost<'_> {
+ pub fn insert(&self, conn: &diesel::SqliteConnection) -> QueryResult {
+ diesel::insert_into(table_posts).values(self).execute(conn)
+ }
+ }
+ #+END_SRC
+
+***** Add create post functionality to ~src/main.rs~
+ #+BEGIN_SRC rust
+ #![feature(proc_macro_hygiene, decl_macro)]
+
+ #[macro_use]
+ extern crate diesel;
+
+ use std::{collections::HashMap, net::SocketAddr};
+
+ use chrono::Local;
+ use diesel::SqliteConnection;
+ use log::{error, info};
+ use rocket::{
+ get, post,
+ request::{FlashMessage, Form},
+ response::{Flash, Redirect},
+ routes, uri, FromForm,
+ };
+ use rocket_contrib::{database, templates::Template};
+
+ mod models;
+ mod schema;
+
+ use models::NewPost;
+
+ #[database("sqlite")]
+ struct DbCon(SqliteConnection);
+
+ #[derive(FromForm)]
+ pub struct PostForm {
+ author: String,
+ email: String,
+ title: String,
+ content: String,
+ }
+
+ ...
+
+ #[post("/create", data = "")]
+ fn create_post(
+ conn: DbCon,
+ remote_address: SocketAddr,
+ post: Form,
+ ) -> Result> {
+ let post = NewPost {
+ parent: None,
+ timestamp: Local::now().naive_local(),
+ author: &post.author.trim(),
+ email: &post.email.trim(),
+ title: &post.title.trim(),
+ content: &post.content.trim(),
+ };
+
+ match post.insert(&conn) {
+ Ok(_) => {
+ info!("New post from {}", remote_address);
+ Ok(Redirect::to(uri!(index)))
+ }
+ Err(e) => {
+ error!("Could not create post from {}: {:?}", remote_address, e);
+ Err(Flash::error(
+ Redirect::to(uri!(create_form)),
+ "Could not create post.".to_string(),
+ ))
+ }
+ }
+ }
+
+ ...
+
+ fn main() {
+ rocket::ignite()
+ .mount("/", routes![index, create_form, create_post])
+ .attach(DbCon::fairing())
+ .attach(Template::fairing())
+ .launch();
+ }
+ #+END_SRC
* Conclusion
diff --git a/simple_text_board/.env b/simple_text_board/.env
new file mode 100644
index 0000000..9e95dbe
--- /dev/null
+++ b/simple_text_board/.env
@@ -0,0 +1 @@
+DATABASE_URL=simple_text_board.sqlite
diff --git a/simple_text_board/Cargo.lock b/simple_text_board/Cargo.lock
index 583813d..57dfce3 100644
--- a/simple_text_board/Cargo.lock
+++ b/simple_text_board/Cargo.lock
@@ -270,6 +270,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e2de9deab977a153492a1468d1b1c0662c1cf39e5ea87d0c060ecd59ef18d8c"
dependencies = [
"byteorder",
+ "chrono",
"diesel_derives",
"libsqlite3-sys",
"r2d2",
@@ -1163,6 +1164,9 @@ dependencies = [
name = "simple_text_board"
version = "0.1.0"
dependencies = [
+ "chrono",
+ "diesel",
+ "log 0.4.13",
"rocket",
"rocket_contrib",
]
diff --git a/simple_text_board/Cargo.toml b/simple_text_board/Cargo.toml
index 67e1079..462cfb5 100644
--- a/simple_text_board/Cargo.toml
+++ b/simple_text_board/Cargo.toml
@@ -6,8 +6,11 @@ edition = "2018"
[dependencies]
rocket = "0.4"
+chrono = "0.4"
+diesel = { version = "1.4", features = ["sqlite", "chrono"] }
+log = "0.4"
[dependencies.rocket_contrib]
version = "0.4"
default-features = false
-features = ["tera_templates"]
+features = ["diesel_sqlite_pool", "tera_templates"]
diff --git a/simple_text_board/Rocket.toml b/simple_text_board/Rocket.toml
index ae113ff..b6a1ef5 100644
--- a/simple_text_board/Rocket.toml
+++ b/simple_text_board/Rocket.toml
@@ -1,3 +1,6 @@
+[global.databases]
+sqlite = { url = "simple_text_board.sqlite" }
+
[development]
address = "0.0.0.0"
port = 8000
diff --git a/simple_text_board/diesel.toml b/simple_text_board/diesel.toml
new file mode 100644
index 0000000..92267c8
--- /dev/null
+++ b/simple_text_board/diesel.toml
@@ -0,0 +1,5 @@
+# For documentation on how to configure this file,
+# see diesel.rs/guides/configuring-diesel-cli
+
+[print_schema]
+file = "src/schema.rs"
diff --git a/simple_text_board/migrations/2021-01-12-011936_create_posts/down.sql b/simple_text_board/migrations/2021-01-12-011936_create_posts/down.sql
new file mode 100644
index 0000000..1651d89
--- /dev/null
+++ b/simple_text_board/migrations/2021-01-12-011936_create_posts/down.sql
@@ -0,0 +1 @@
+DROP TABLE posts;
diff --git a/simple_text_board/migrations/2021-01-12-011936_create_posts/up.sql b/simple_text_board/migrations/2021-01-12-011936_create_posts/up.sql
new file mode 100644
index 0000000..c419851
--- /dev/null
+++ b/simple_text_board/migrations/2021-01-12-011936_create_posts/up.sql
@@ -0,0 +1,10 @@
+CREATE TABLE posts (
+ id INTEGER NOT NULL PRIMARY KEY,
+ parent INTEGER,
+ timestamp DATETIME NOT NULL,
+ author VARCHAR NOT NULL,
+ email VARCHAR NOT NULL,
+ title VARCHAR NOT NULL,
+ content VARCHAR NOT NULL,
+ FOREIGN KEY (parent) REFERENCES posts(id)
+);
diff --git a/simple_text_board/src/main.rs b/simple_text_board/src/main.rs
index 4ae496a..75a4094 100644
--- a/simple_text_board/src/main.rs
+++ b/simple_text_board/src/main.rs
@@ -1,9 +1,36 @@
#![feature(proc_macro_hygiene, decl_macro)]
-use std::collections::HashMap;
+#[macro_use]
+extern crate diesel;
-use rocket::{get, request::FlashMessage, routes};
-use rocket_contrib::templates::Template;
+use std::{collections::HashMap, net::SocketAddr};
+
+use chrono::Local;
+use diesel::SqliteConnection;
+use log::{error, info};
+use rocket::{
+ get, post,
+ request::{FlashMessage, Form},
+ response::{Flash, Redirect},
+ routes, uri, FromForm,
+};
+use rocket_contrib::{database, templates::Template};
+
+mod models;
+mod schema;
+
+use models::NewPost;
+
+#[database("sqlite")]
+struct DbCon(SqliteConnection);
+
+#[derive(FromForm)]
+struct PostForm {
+ author: String,
+ email: String,
+ title: String,
+ content: String,
+}
#[get("/create")]
fn create_form(flash: Option) -> Template {
@@ -16,6 +43,36 @@ fn create_form(flash: Option) -> Template {
Template::render("create", &context)
}
+#[post("/create", data = "")]
+fn create_post(
+ conn: DbCon,
+ remote_address: SocketAddr,
+ post: Form,
+) -> Result> {
+ let post = NewPost {
+ parent: None,
+ timestamp: Local::now().naive_local(),
+ author: &post.author.trim(),
+ email: &post.email.trim(),
+ title: &post.title.trim(),
+ content: &post.content.trim(),
+ };
+
+ match post.insert(&conn) {
+ Ok(_) => {
+ info!("New post from {}", remote_address);
+ Ok(Redirect::to(uri!(index)))
+ }
+ Err(e) => {
+ error!("Could not create post from {}: {:?}", remote_address, e);
+ Err(Flash::error(
+ Redirect::to(uri!(create_form)),
+ "Could not create post.".to_string(),
+ ))
+ }
+ }
+}
+
#[get("/")]
fn index() -> &'static str {
"Hello, world!"
@@ -23,7 +80,8 @@ fn index() -> &'static str {
fn main() {
rocket::ignite()
- .mount("/", routes![index, create_form])
+ .mount("/", routes![index, create_form, create_post])
+ .attach(DbCon::fairing())
.attach(Template::fairing())
.launch();
}
diff --git a/simple_text_board/src/models.rs b/simple_text_board/src/models.rs
new file mode 100644
index 0000000..96edd43
--- /dev/null
+++ b/simple_text_board/src/models.rs
@@ -0,0 +1,20 @@
+use crate::schema::posts::{self, dsl::posts as table_posts};
+use chrono::NaiveDateTime;
+use diesel::{QueryResult, RunQueryDsl};
+
+#[derive(Insertable)]
+#[table_name = "posts"]
+pub struct NewPost<'a> {
+ pub parent: Option<&'a i32>,
+ pub timestamp: NaiveDateTime,
+ pub author: &'a str,
+ pub email: &'a str,
+ pub title: &'a str,
+ pub content: &'a str,
+}
+
+impl NewPost<'_> {
+ pub fn insert(&self, conn: &diesel::SqliteConnection) -> QueryResult {
+ diesel::insert_into(table_posts).values(self).execute(conn)
+ }
+}
diff --git a/simple_text_board/src/schema.rs b/simple_text_board/src/schema.rs
new file mode 100644
index 0000000..34adcb3
--- /dev/null
+++ b/simple_text_board/src/schema.rs
@@ -0,0 +1,11 @@
+table! {
+ posts (id) {
+ id -> Integer,
+ parent -> Nullable,
+ timestamp -> Timestamp,
+ author -> Text,
+ email -> Text,
+ title -> Text,
+ content -> Text,
+ }
+}
diff --git a/simple_text_board/templates/create.html.tera b/simple_text_board/templates/create.html.tera
index 16048bf..cc5897d 100644
--- a/simple_text_board/templates/create.html.tera
+++ b/simple_text_board/templates/create.html.tera
@@ -3,9 +3,6 @@
{% block content %}
simple text board
create a post
-{% if flash %}
-{{ flash }}
-{% endif %}
{% endblock content %}