From 099bcaadc66607d77915dd6b7209519ec35581c9 Mon Sep 17 00:00:00 2001 From: finga Date: Wed, 14 Apr 2021 17:07:10 +0200 Subject: [PATCH 1/2] List posts --- rust_web.org | 77 +++++++++++++++++++++ simple_text_board/Cargo.lock | 16 +++++ simple_text_board/Cargo.toml | 3 +- simple_text_board/src/main.rs | 23 ++++-- simple_text_board/src/models.rs | 20 +++++- simple_text_board/templates/index.html.tera | 11 +++ 6 files changed, 142 insertions(+), 8 deletions(-) create mode 100644 simple_text_board/templates/index.html.tera diff --git a/rust_web.org b/rust_web.org index d7755be..15c7f6d 100644 --- a/rust_web.org +++ b/rust_web.org @@ -535,6 +535,83 @@ } #+END_SRC +**** Create a template to list all posts + +***** Add a template for the index page ~templates/index.html.tera~ + #+BEGIN_SRC html + {% extends "base" %} + + {% block content %} +

simple text board

+

index

+ + {% endblock content %} + #+END_SRC + +***** Add serde dependencies to ~Cargo.toml~ + #+BEGIN_SRC toml + chrono = { version = "0.4", features = ["serde"] } + ... + serde = { version = "1.0", features = ["derive"] } + #+END_SRC + +***** Add `Post` to ~src/models.rs~ + #+BEGIN_SRC rust + ... + use diesel::{QueryResult, RunQueryDsl, SqliteConnection}; + use serde::Serialize; + + ... + + #[derive(Debug, Queryable, Serialize)] + pub struct Post { + pub id: i32, + pub parent: Option, + pub timestamp: NaiveDateTime, + pub author: String, + pub email: String, + pub title: String, + pub content: String, + } + + impl Post { + pub fn get_all(connection: &SqliteConnection) -> QueryResult> { + posts::table.load::(connection) + } + } + #+END_SRC + +***** Add index template in ~src/main.rs~ + #+BEGIN_SRC rust + ... + use serde::Serialize; + + ... + + use models::{NewPost, Post}; + + ... + + #[derive(Serialize)] + struct Posts { + posts: Vec, + } + + #[get("/")] + fn index(conn: DbCon) -> Template { + Template::render( + "index", + Posts { + posts: Post::get_all(&conn).unwrap(), + }, + ) + } + #+END_SRC + * Conclusion ** What did we do? diff --git a/simple_text_board/Cargo.lock b/simple_text_board/Cargo.lock index 57dfce3..77d9e9e 100644 --- a/simple_text_board/Cargo.lock +++ b/simple_text_board/Cargo.lock @@ -195,6 +195,7 @@ dependencies = [ "libc", "num-integer", "num-traits", + "serde", "time", "winapi 0.3.9", ] @@ -1124,6 +1125,20 @@ name = "serde" version = "1.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bdd36f49e35b61d49efd8aa7fc068fd295961fd2286d0b2ee9a4c7a14e99cc3" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552954ce79a059ddd5fd68c271592374bd15cab2274970380c000118aeffe1cd" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.8", + "syn 1.0.58", +] [[package]] name = "serde_json" @@ -1169,6 +1184,7 @@ dependencies = [ "log 0.4.13", "rocket", "rocket_contrib", + "serde", ] [[package]] diff --git a/simple_text_board/Cargo.toml b/simple_text_board/Cargo.toml index 462cfb5..05a9df5 100644 --- a/simple_text_board/Cargo.toml +++ b/simple_text_board/Cargo.toml @@ -6,9 +6,10 @@ edition = "2018" [dependencies] rocket = "0.4" -chrono = "0.4" +chrono = { version = "0.4", features = ["serde"] } diesel = { version = "1.4", features = ["sqlite", "chrono"] } log = "0.4" +serde = { version = "1.0", features = ["derive"] } [dependencies.rocket_contrib] version = "0.4" diff --git a/simple_text_board/src/main.rs b/simple_text_board/src/main.rs index 75a4094..dcbf5d4 100644 --- a/simple_text_board/src/main.rs +++ b/simple_text_board/src/main.rs @@ -15,11 +15,12 @@ use rocket::{ routes, uri, FromForm, }; use rocket_contrib::{database, templates::Template}; +use serde::Serialize; mod models; mod schema; -use models::NewPost; +use models::{NewPost, Post}; #[database("sqlite")] struct DbCon(SqliteConnection); @@ -32,6 +33,21 @@ struct PostForm { content: String, } +#[derive(Serialize)] +struct Posts { + posts: Vec, +} + +#[get("/")] +fn index(conn: DbCon) -> Template { + Template::render( + "index", + Posts { + posts: Post::get_all(&conn).unwrap(), + }, + ) +} + #[get("/create")] fn create_form(flash: Option) -> Template { let mut context = HashMap::new(); @@ -73,11 +89,6 @@ fn create_post( } } -#[get("/")] -fn index() -> &'static str { - "Hello, world!" -} - fn main() { rocket::ignite() .mount("/", routes![index, create_form, create_post]) diff --git a/simple_text_board/src/models.rs b/simple_text_board/src/models.rs index 96edd43..dc8078b 100644 --- a/simple_text_board/src/models.rs +++ b/simple_text_board/src/models.rs @@ -1,6 +1,7 @@ use crate::schema::posts::{self, dsl::posts as table_posts}; use chrono::NaiveDateTime; -use diesel::{QueryResult, RunQueryDsl}; +use diesel::{QueryResult, RunQueryDsl, SqliteConnection}; +use serde::Serialize; #[derive(Insertable)] #[table_name = "posts"] @@ -18,3 +19,20 @@ impl NewPost<'_> { diesel::insert_into(table_posts).values(self).execute(conn) } } + +#[derive(Debug, Queryable, Serialize)] +pub struct Post { + pub id: i32, + pub parent: Option, + pub timestamp: NaiveDateTime, + pub author: String, + pub email: String, + pub title: String, + pub content: String, +} + +impl Post { + pub fn get_all(connection: &SqliteConnection) -> QueryResult> { + posts::table.load::(connection) + } +} diff --git a/simple_text_board/templates/index.html.tera b/simple_text_board/templates/index.html.tera new file mode 100644 index 0000000..5ab3970 --- /dev/null +++ b/simple_text_board/templates/index.html.tera @@ -0,0 +1,11 @@ +{% extends "base" %} + +{% block content %} +

simple text board

+

index

+
    + {% for post in posts %} +
  • {{ loop.index }} - {{ post.title }} - {{ post.author }}
  • + {% endfor %} +
+{% endblock content %} From 13aacc3b6babc35452459fdff4e941044949bd7e Mon Sep 17 00:00:00 2001 From: finga Date: Wed, 14 Apr 2021 17:12:12 +0200 Subject: [PATCH 2/2] Add content from last presentation --- rust_web.org | 421 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 358 insertions(+), 63 deletions(-) diff --git a/rust_web.org b/rust_web.org index 15c7f6d..2291ef7 100644 --- a/rust_web.org +++ b/rust_web.org @@ -1,14 +1,16 @@ +#+STARTUP: beamer #+OPTIONS: ':nil *:t -:t ::t <:t H:3 \n:nil ^:t arch:headline #+OPTIONS: author:t broken-links:nil c:nil creator:nil #+OPTIONS: d:(not "LOGBOOK") date:t e:t email:nil f:t inline:t num:t #+OPTIONS: p:nil pri:nil prop:nil stat:t tags:t tasks:t tex:t #+OPTIONS: timestamp:t title:t toc:t todo:t |:t +#+OPTIONS: ^:nil #+TITLE: Rust, Rocket and Diesel -#+SUBTITLE: Web apps with rust +#+SUBTITLE: Web apps with Rust #+DESCRIPTION: An introduction to Rust, Rocket and Diesel. #+KEYWORDS: rust rocket diesel -#+AUTHOR: \href{mailto:rocket-presentation@onders.org}{finga} -#+EMAIL: rocket-presentation@onders.org +#+AUTHOR: \href{mailto:finga@onders.org}{finga} +#+EMAIL: finga@onders.org #+LANGUAGE: en #+SELECT_TAGS: export #+EXCLUDE_TAGS: noexport @@ -16,9 +18,10 @@ #+OPTIONS: H:2 #+LATEX_CLASS: beamer -#+LATEX_CLASS_OPTIONS: [aspectration=1610] +#+LATEX_CLASS_OPTIONS: [aspectratio=1610] # #+LATEX_HEADER: \hypersetup{colorlinks=true,linkcolor=gray} #+LATEX_HEADER: \hypersetup{colorlinks=true,linkcolor=black,urlcolor=gray} +# #+LATEX_HEADER: \usepackage{listings, listings-rust} #+COLUMNS: %45ITEM %10BEAMER_env(Env) %10BEAMER_act(Act) %4BEAMER_col(Col) %8BEAMER_opt(Opt) #+BEAMER_THEME: Frankfurt #+BEAMER_COLOR_THEME: seagull @@ -37,48 +40,11 @@ *** Rust, the language - About 10 years young (2010) - Memory safe without gcing, optional ref counting - - Ownership, lifetimes, traits + - Ownership, lifetimes, traits, functional paradigms - Variables are immutable by default and can be shadowed - Performance of idiomatic Rust is comparable to the performance of idiomatic cpp -** Where to get further information? - -*** Docs... - - [[https://doc.rust-lang.org/book/][The rust book]] (~$ rustup docs --book~) - - [[https://doc.rust-lang.org/rust-by-example/][Rust by example]] (~$ rustup docs --rust-by-example~) - - [[https://doc.rust-lang.org/std/][The Rust Standard Library]] (~$ rustup docs --std~) - - Use '~$ rustup help docs~' to get an overview - - [[https://cheats.rs][cheats.rs]] - - [[https://programming-idioms.org/cheatsheet/Rust][programming-idioms.org]] - - [[https://cargo.io][cargo.io]]/[[https://lib.rs][lib.rs]] - -*** Chat (Discord) - - [[https://discord.gg/rust-lang][The Rust Programming Language]] - - [[https://discord.com/invite/tcbkpyQ][The Rust Programming Language Community Server]] - -** What are we going to use? - -*** Definitely - :PROPERTIES: - :BEAMER_col: 0.45 - :BEAMER_env: block - :END: - - [[https://rocket.rs][rocket]] - - [[https://diesel.rs][diesel]] - - log/env_logger - -*** Maybe - :PROPERTIES: - :BEAMER_col: 0.45 - :BEAMER_env: block - :END: - - anyhow - - lettre - - sha3 - - serde - - chrono - ** How to install *** Install Rust @@ -92,16 +58,209 @@ $ rustup toolchain install nightly #+END_SRC -*** Install rustfmt (code formatter) +*** Install rustfmt (linter and code formatter) #+BEGIN_SRC sh $ rustup component add rustfmt #+END_SRC -*** Install clippy (linter and static code analysis) +*** Install clippy (static code analysis) #+BEGIN_SRC sh $ rustup component add clippy #+END_SRC +** Where to find more information? + +*** Docs... + - [[https://doc.rust-lang.org/book/][The Rust Book]] (~$ rustup docs --book~) + - [[https://doc.rust-lang.org/rust-by-example/][Rust by Example]] (~$ rustup docs --rust-by-example~) + - [[https://doc.rust-lang.org/std/][The Rust Standard Library]] (~$ rustup docs --std~) + - Use '~$ rustup help docs~' to get an overview + - [[https://cheats.rs][cheats.rs]] + - [[https://programming-idioms.org/cheatsheet/Rust][programming-idioms.org]] + - [[https://crates.io][crates.io]]/[[https://lib.rs][lib.rs]] + +*** Chat (Discord) + - [[https://discord.gg/rust-lang][The Rust Programming Language]] + - [[https://discord.com/invite/tcbkpyQ][The Rust Programming Language Community Server]] + +** Hello, World! + Executing ~`cargo new foobar`~ creates a new project... + +*** foobar/Cargo.toml + #+BEGIN_SRC toml + [package] + name = "foo" + version = "0.1.0" + authors = ["finga "] + edition = "2018" + #+END_SRC + \tiny + #+BEGIN_SRC toml + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + #+END_SRC + \normalsize + #+BEGIN_SRC toml + [dependencies] + #+END_SRC + +*** foobar/src/main.rs + #+BEGIN_SRC rust + fn main() { + println!("Hello, world!"); + } + #+END_SRC + +** Mutability and shadowing + +*** Immutable variable + #+BEGIN_SRC rust + let foo = 5; + #+END_SRC + +*** Mutable variable + #+BEGIN_SRC rust + let mut bar = 5; + bar = 6; + #+END_SRC + +*** Shadowing + #+BEGIN_SRC rust + let baz = 5; + let baz = baz + 3; + #+END_SRC + +** Structs + +*** Definition of a struct + #+BEGIN_SRC rust + struct Something { + id: usize, + item: String, + } + #+END_SRC + +*** Instantiation of a struct + #+BEGIN_SRC rust + let something = Something { + id: 0, + item: String::from("an item"), + }; + #+END_SRC + +*** Access of a struct's field + #+BEGIN_SRC rust + something.item + #+END_SRC + +** Tuple structs + +*** Definition of a tuple struct + #+BEGIN_SRC rust + struct Box(usize, usize, usize); + #+END_SRC + +*** Instantiation of a tuple struct + #+BEGIN_SRC rust + let first_box = Box(3, 5, 7); + #+END_SRC + +*** Access of a tuple struct's field + #+BEGIN_SRC rust + first_box.0; + first_box.1; + first_box.2; + #+END_SRC + +** Error related types + - Heavy use of ~Option~ and ~Result~ + +*** Option + #+BEGIN_SRC rust + pub enum Option { + None, + Some(T), + } + #+END_SRC + +*** Result + #+BEGIN_SRC rust + pub enum Result { + Ok(T), + Err(E), + } + #+END_SRC + +** Error handling + +*** Unrecoverable errors \rightarrow panics + #+BEGIN_SRC rust + fn get_data() -> Data { + ... + panic!("Cannot get data"); + } + #+END_SRC + +*** Recoverable error with anyhow + #+BEGIN_SRC rust + fn get_data() -> Result { + ... + Ok(data) // return is only used for "early" returns + } + ... + let data = get_data()?; + ... + #+END_SRC + +** ~match~ statements + - Must be exhaustive + - All match arms must return the same type! + - Can be used everywhere e.g. ~let foo = match ...~ + - @@latex:{\color{red}@@All match arms must return the same type!!1!@@latex:}@@ + +*** Recoverable error with anyhow + #+BEGIN_SRC rust + match age { + 0 => unreachable!("Too young to be a person"), + n @ 1..=12 => println!("Child of age {}", n), + n @ 13..=19 => println!("Teen of age {}", n), + n @ 20..=150 => println!("Person of age {}", n), + _ => unimplemented!("Nobody I know got that old"), + } + #+END_SRC + +** ~if let~ statements + +*** Sometimes an ~if let~ is nicer than a ~match~ or an ~if/else~ statement + #+BEGIN_SRC rust + if let Some(actual_age) = age { + println!("The age is {}", actual_age); + } + #+END_SRC + +** Use functional language features + - ~vec![1, 2, 3].iter()~ + - ~vec![1, 2, 3].iter().sum()~ + - ~vec![1, 2, 3].iter().map(|x| x + 1).collect()~ + - ~foo.into_iter().filter(|i| i.bar == 0).collect()~ + - ~Iterator~ trait + +** Tests + - Use ~assert!()~ family + - Unit tests + - Put them into a test module + - Annotate the test module with ~#[cfg(test)]~ + - Annotate the tests with ~#[test]~ + - Documentation testing + - Integration testing + - Integration tests are put into the ~src/tests/~ directory + +** Docs + - Documentation comments: ~///~ + - Markdown notation + - ~cargo doc~ runns ~rustdoc~ which generates html docs + - ~cargo doc --open~ runs ~rustdoc~ and opens the result in a browser + - ~rustdoc~ documentation: https://doc.rust-lang.org/beta/rustdoc/index.html + * Rocket ** What is Rocket? @@ -120,7 +279,7 @@ - Type Safe - Extensible - Templating - - Cookies + - (Secure) Cookies **** Col right :PROPERTIES: @@ -129,6 +288,112 @@ - Streams - Testing (Unit tests) - Typed URIs + - Single binary + +** Create a new Rocket project +*** Create the crate + #+BEGIN_SRC sh + cargo init new_project + cd new_project + rustup override set nightly + cargo r + #+END_SRC + +*** Add Rocket dependencies to ~Cargo.toml~ + #+BEGIN_SRC toml + [dependencies] + rocket = "0.4" + #+END_SRC + +** Hello Rocket +*** ~src/main.rs~ + #+BEGIN_SRC rust + #![feature(decl_macro)] + + use rocket::{get, routes}; + + #[get("/hello//")] + fn hello(name: String, age: u8) -> String { + format!("Hello, {} year old named {}!", age, name) + } + + fn main() { + rocket::ignite().mount("/", routes![hello]).launch(); + } + #+END_SRC + +** Receive data +*** Struct ~EmailForm~ + #+BEGIN_SRC rust + #[derive(Debug, FromForm)] + struct EmailForm { + email: String, + } + #+END_SRC + +*** Procedure using that form data + #+BEGIN_SRC rust + #[post("/reset", data = "")] + fn reset_email(email: Form) -> Flash { + ... + Flash::success( + Redirect::to(uri!(reset)), + format!("Email was sent to {}", email.email), + ) + } + #+END_SRC + +** Flash messages + #+BEGIN_SRC rust + #[get("/reset")] + fn reset(flash: Option) -> String { + if let Some(ref msg) = flash { // `let ref x = y` is + msg.msg() // equivalent to `let x = &y` + } else { + format!("Hello, reset!"); + } + } + #+END_SRC + +** Templates + - Use the ~rocket_contrib~ crate! +*** ~templates/name.html.tera~ + :PROPERTIES: + :BEAMER_col: 0.35 + :BEAMER_env: block + :END: + #+BEGIN_SRC html + + + + ... + + +

Hello, + {{ name }}!

+ + + #+END_SRC + +*** ~src/main.rs~ + :PROPERTIES: + :BEAMER_col: 0.60 + :BEAMER_env: block + :END: + #+BEGIN_SRC rust + #[get("/hello/")] + fn hello(name: String) -> Template { + let mut context = HashMap::new(); + context.insert("name", name); + + Template::render("name", &context) + } + ... + rocket::ignite() + .mount(...) + .attach(Template::fairing()) + .launch(); + #+END_SRC ** Where to get further information? @@ -152,7 +417,7 @@ * Diesel -** What is an ORM? +** ORM Object-relational-mapping is a programming technique for converting data between incompatible type systems using the paradigms of your programming language. @@ -219,11 +484,21 @@ comment: "A comment", }; + ... + insert_into(comments) .values(&user) .execute(conn); #+END_SRC +** About Diesel + - Enable connection pooling via the ~r2d2~ crate + - Nested joins are not yet nicely implemented + - Database specific SQL regarding each DBMS + - Take a look at the [[https://github.com/diesel-rs/diesel/tree/master/diesel_cli][diesel_cli]] crate for multi DBMS support + - Use ~diesel_cli~ macros and functions for compiling migration + functionality into the application + ** Where to get further information? *** Docs... @@ -247,7 +522,7 @@ * Lets code something ** Lets create a small web app - A mini text board where everybody can post and reply. + A chat box where everybody can post. *** Caution HTML is not disabled! @@ -286,18 +561,20 @@ ***** Modify ~src/main.rs~ for Rockets "~Hello, world!~" program #+BEGIN_SRC rust - #![feature(proc_macro_hygiene, decl_macro)] + // #![feature(proc_macro_hygiene, decl_macro)] + #![feature(decl_macro)] - #[macro_use] extern crate rocket; + // #[macro_use] extern crate rocket; + // #[macro_use] extern crate diesel; - #[get("/")] - fn index() -> &'static str { - "Hello, world!" - } + #[get("/")] + fn index() -> &'static str { + "Hello, world!" + } - fn main() { - rocket::ignite().mount("/", routes![index]).launch(); - } + fn main() { + rocket::ignite().mount("/", routes![index]).launch(); + } #+END_SRC **** Create first templates @@ -612,15 +889,32 @@ } #+END_SRC +**** Create page for posts + +***** Create template ~templates/post.html.tera~ + +**** Create possibility to reply + * Conclusion +** In general + - Rust's community and ecosystem is growing steadily + - Rust covers a broad field from low level code to high level + - Rocket and libraries alike improve creating security and + performance focused web application development + - Diesel is on a good way to become a serious ORM + - Alltogether Rust is ready to be used for web apps + ** What did we do? - Setup Rust and Diesel - Create a project with Rocket and Diesel prerequisites - - Handle a SQLite DB with Diesel - - Create Tera templates + - Store a small model with Diesel in a SQLite DB + - Create and use Tera templates -** What did you hopefully learn! +** Where to continue + - Move DB setup into the project + - Add features (List posts by Author, paging, etc..) + - (Private) Cookies ** Two small projects @@ -630,17 +924,18 @@ :BEAMER_env: block :END: - LDAP password reset tool - - ($\sim$ 300 SloC) + - ($\sim$ 300 SloC without tests) - First release - Tests -*** [[https://git.onders.org/finga/filerly][filerly]] +*** [[https://git.onders.org/finga/webhookey][webhookey]] :PROPERTIES: :BEAMER_col: 0.45 :BEAMER_env: block :END: - - File sharing a la NextCloud - - Not yet ready, work in progress... + - Execute scripts triggered by http(s) requests + - Is becoming more and more robust + - ($\sim$ 400 SloC without tests) ** The End #+BEGIN_CENTER