#+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 #+DESCRIPTION: An introduction to Rust, Rocket and Diesel. #+KEYWORDS: rust rocket diesel #+AUTHOR: \href{mailto:finga@onders.org}{finga} #+EMAIL: finga@onders.org #+LANGUAGE: en #+SELECT_TAGS: export #+EXCLUDE_TAGS: noexport #+CREATOR: Emacs 26.1 (Org mode 9.1.9) #+OPTIONS: H:2 #+LATEX_CLASS: beamer #+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 #+BEAMER_FONT_THEME: #+BEAMER_INNER_THEME: #+BEAMER_OUTER_THEME: #+BEAMER_HEADER: \institute[INST]{\href{https://cccsbg.at}{cccsbg}} * Rust ** What is Rust? #+BEGIN_CENTER #+LaTeX:\includegraphics[width = 0.65\textwidth]{img/Bruine_roest_op_tarwe_(Puccinia_recondita_f.sp._tritici_on_Triticum_aestivum).jpg} #+END_CENTER *** Rust, the language - About 10 years young (2010) - Memory safe without gcing, optional ref counting - 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 ** How to install *** Install Rust #+BEGIN_SRC sh $ curl --proto '=https' --tlsv1.2 -sSf \ https://sh.rustup.rs | sh #+END_SRC *** Install nightly Rust (needed for Rocket) #+BEGIN_SRC sh $ rustup toolchain install nightly #+END_SRC *** Install rustfmt (linter and code formatter) #+BEGIN_SRC sh $ rustup component add rustfmt #+END_SRC *** 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? #+BEGIN_QUOTE Rocket is a web framework for Rust that makes it simple to write fast, secure web applications without sacrificing flexibility, usability, or type safety. #+END_QUOTE *** Features **** Col left :PROPERTIES: :BEAMER_col: 0.45 :END: - Type Safe - Extensible - Templating - (Secure) Cookies **** Col right :PROPERTIES: :BEAMER_col: 0.45 :END: - 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? *** Docs... :PROPERTIES: :BEAMER_col: 0.45 :BEAMER_env: block :END: - [[https://rocket.rs][rocket.rs]] - [[https://rocket.rs/v0.4/guide/][Rocket Guide]] - [[https://api.rocket.rs/v0.4/rocket/][Rocket API docs]] - [[https://github.com/SergioBenitez/Rocket/tree/v0.4/examples][Examples]] *** Chat :PROPERTIES: :BEAMER_col: 0.45 :BEAMER_env: block :END: - Matrix: [[https://chat.mozilla.org/#/room/%23rocket:mozilla.org][#rocket:mozilla.org]] - IRC on Freenode: [[https://kiwiirc.com/client/chat.freenode.net/#rocket][#rocket]] * Diesel ** ORM Object-relational-mapping is a programming technique for converting data between incompatible type systems using the paradigms of your programming language. *** Pros :PROPERTIES: :BEAMER_col: 0.45 :BEAMER_env: block :END: - Models are DRY (don't repeat yourself) - Cleaner separation - Prepared and sanitised queries - Changes are versioned *** Cons :PROPERTIES: :BEAMER_col: 0.45 :BEAMER_env: block :END: - Debugging can get more complicated (performance) - No need to learn SQL ** Prerequisites ~diesel_cli~ *** Setup Diesel for default DBMS #+BEGIN_SRC sh $ sudo apt install libpq-dev libsqlite3-dev \ default-libmysqlclient-dev $ cargo install diesel_cli #+END_SRC *** Setup Diesel for SQLite only #+BEGIN_SRC sh $ sudo apt install libsqlite3-dev $ cargo install diesel_cli -no-default-features \ --features sqlite #+END_SRC ** How does that look in Rust *** Define a struct :PROPERTIES: :BEAMER_col: 0.50 :BEAMER_env: block :END: #+BEGIN_SRC rust #[derive(Queryable)] pub struct NewComment<'a> { name: &'a str, email: &'a str, comment: &'a str, } #+END_SRC *** Insert something :PROPERTIES: :BEAMER_col: 0.45 :BEAMER_env: block :END: #+BEGIN_SRC rust let user = NewUser { name: "foo", email: "foo@bar.baz", 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... :PROPERTIES: :BEAMER_col: 0.45 :BEAMER_env: block :END: - [[https://diesel.rs][diesel.rs]] - [[https://diesel.rs/guides/][Diesel Guides]] - [[https://docs.diesel.rs/master/diesel/index.html][Diesel API docs]] - [[https://github.com/diesel-rs/diesel/tree/master/examples][Examples]] (minimalistic) *** Support :PROPERTIES: :BEAMER_col: 0.45 :BEAMER_env: block :END: - [[https://gitter.im/diesel-rs/diesel][Gitter]] - [[https://discourse.diesel.rs/][Discourse Forum]] * Lets code something ** Lets create a small web app A chat box where everybody can post. #+BEGIN_CENTER #+LaTeX:\includegraphics[width = 0.7\textwidth]{img/challenge-accepted-lets-code-it.jpg} #+END_CENTER *** 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 **** Setup Rocket ***** Add Rocket to ~Cargo.toml~ #+BEGIN_SRC toml [dependencies] rocket = "0.4" #+END_SRC ***** Add ~Rocket.toml~ #+BEGIN_SRC toml [development] address = "0.0.0.0" port = 8000 workers = 2 keep_alive = 5 log = "normal" #+END_SRC ***** Modify ~src/main.rs~ for Rockets "~Hello, world!~" program #+BEGIN_SRC rust // #![feature(proc_macro_hygiene, decl_macro)] #![feature(decl_macro)] // #[macro_use] extern crate rocket; // #[macro_use] extern crate diesel; #[get("/")] fn index() -> &'static str { "Hello, world!" } fn main() { rocket::ignite().mount("/", routes![index]).launch(); } #+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" %} {% block content %}

simple text board

create a post

{% if flash %}

{{ flash }}

{% endif %}
{% endblock content %} #+END_SRC ***** Create the ~create_form~ function and add it to routing #+BEGIN_SRC rust #![feature(proc_macro_hygiene, decl_macro)] use std::collections::HashMap; use rocket::{get, request::FlashMessage, routes}; use rocket_contrib::templates::Template; #[get("/create")] fn create_form(flash: Option) -> Template { let mut context = HashMap::new(); 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 **** 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

    {% for post in posts %}
  • {{ loop.index }} - {{ post.title }} - {{ post.author }}
  • {% endfor %}
{% 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 **** 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 - Store a small model with Diesel in a SQLite DB - Create and use Tera templates ** Where to continue - Move DB setup into the project - Add features (List posts by Author, paging, etc..) - (Private) Cookies ** Two small projects *** [[https://git.onders.org/finga/ldap0r][ldap0r]] :PROPERTIES: :BEAMER_col: 0.45 :BEAMER_env: block :END: - LDAP password reset tool - ($\sim$ 300 SloC without tests) - First release - Tests *** [[https://git.onders.org/finga/webhookey][webhookey]] :PROPERTIES: :BEAMER_col: 0.45 :BEAMER_env: block :END: - Execute scripts triggered by http(s) requests - Is becoming more and more robust - ($\sim$ 400 SloC without tests) ** The End #+BEGIN_CENTER #+LaTeX:\includegraphics[width = 0.3\textwidth]{img/goodbye.jpg} \Huge Thanks for your attention!!1! This is the end.. #+END_CENTER