Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rocket

Rocket

Rocket - A web framework for Rust

  • The Rocket web framework for Rust.

  • Rocket starter is a small tool to create a project skeleton.

  • Articles about Rocket with examples.

  • Discussion about Rocket where you can ask questions.

  • TODO: return JSON (in an API)

  • TODO: log into a logfile

  • TODO: Blog engine, map the path to an entry in the database, what if that entry does not exist in the database? How do we return 404 not found. (either return Template or a 404 not found page)

Rocket - Hello World

cargo add rocket
[package]
name = "hello-world"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rocket = "0.5.1"
#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

#[get("/")]
fn index() -> &'static str {
    "Hello, world!"
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index])
}

#[cfg(test)]
mod test {
    use rocket::http::Status;
    use rocket::local::blocking::Client;

    #[test]
    fn hello_world() {
        let client = Client::tracked(super::rocket()).unwrap();
        let response = client.get("/").dispatch();

        assert_eq!(response.status(), Status::Ok);
        assert_eq!(response.into_string(), Some("Hello, world!".into()));
    }
}
}
cargo test
cargo run
$ curl -i http://127.0.0.1:8000/
  • Content-type is text/plain

Rocket - Hello World with separate test file

  • Create a new crate, add rocket as a dependency cargo add rocket
[package]
name = "first"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rocket = "0.5.0"
#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

#[get("/")]
fn index() -> &'static str {
    "Hello, world!"
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index])
}

#[cfg(test)]
mod tests;
}
cargo run

The tests

#![allow(unused)]
fn main() {
use rocket::http::Status;
use rocket::local::blocking::Client;

#[test]
fn hello_world() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/plain; charset=utf-8"
    );
    assert_eq!(response.into_string(), Some("Hello, world!".into()));
}
}
cargo test
curl -i http://localhost:8000/

Rocket - Hello World returning static RawHtml

  • content
  • RawHtml
[package]
name = "hello-world-html"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rocket = "0.5.0"
#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

use rocket::response::content;

#[get("/")]
fn index() -> content::RawHtml<&'static str> {
    content::RawHtml("Hello, <b>world!</b>")
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index])
}

#[cfg(test)]
mod tests;
}
#![allow(unused)]
fn main() {
use rocket::http::Status;
use rocket::local::blocking::Client;

#[test]
fn hello_world() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );
    assert_eq!(response.into_string(), Some("Hello, <b>world!</b>".into()));
}
}
curl -i http://localhost:8000
  • content-type: text/html

Rocket - Generated RawHtml page

  • RawHtml
  • epoch
  • UNIX_EPOCH
#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

use std::time::{SystemTime, UNIX_EPOCH};

use rocket::response::content;

#[get("/")]
fn index() -> content::RawHtml<String> {
    let start = SystemTime::now();
    let since_the_epoch = start
        .duration_since(UNIX_EPOCH)
        .expect("Time went backwards");

    let html = format!(
        "Hello, Rocket at {:?} seconds or {:?} nanoseconds since the epoch",
        since_the_epoch.as_secs(),
        since_the_epoch.as_nanos()
    );
    content::RawHtml(html)
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index])
}
}

Rocket - Hello World with Tera template

  • Tera
[package]
name = "hello-world-tera-template"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rocket = "0.5"
rocket_dyn_templates = { version = "0.1", features = ["tera"] }
#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

use rocket_dyn_templates::{context, Template};

#[get("/")]
fn index() -> Template {
    Template::render(
        "index",
        context! {
            name: "Rocket with Tera"
        },
    )
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .mount("/", routes![index])
        .attach(Template::fairing())
}

#[cfg(test)]
mod tests;
}
#![allow(unused)]
fn main() {
use rocket::http::Status;
use rocket::local::blocking::Client;

#[test]
fn hello_world() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );
    assert_eq!(
        response.into_string(),
        Some("Hello <b>Rocket with Tera!</b>".into())
    );
}
}
Hello <b>{{ name }}!</b>

Rocket - Echo using GET

  • GET
  • Tera
[package]
name = "echo-using-get"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rocket = "0.5"
rocket_dyn_templates = { version = "0.1", features = ["tera"] }

[debug]
port=8001
#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

use rocket_dyn_templates::{context, Template};

#[get("/")]
fn index() -> Template {
    Template::render("index", context! {})
}

#[get("/echo?<text>")]
fn echo(text: String) -> Template {
    println!("text: {:?}", text);

    Template::render(
        "echo",
        context! {
            text: text
        },
    )
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .mount("/", routes![index, echo])
        .attach(Template::fairing())
}

#[cfg(test)]
mod tests;
}
use rocket::http::Status;
use rocket::local::blocking::Client;

#[test]
fn main_page() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );
    assert!(response.into_string().unwrap().contains("<h1>Echo</h1>"));
}

#[test]
fn echo_page() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/echo?text=Foo+Bar").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );
    assert_eq!(
        response.into_string(),
        Some("You typed in <b>Foo Bar</b>".into())
    );
}

#[test]
fn echo_missing_text_param() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/echo").dispatch();

    assert_eq!(response.status(), Status::UnprocessableEntity);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );

    assert!(response
        .into_string()
        .unwrap()
        .contains("<h1>422: Unprocessable Entity</h1>"));
}

#[test]
fn echo_additional_field_does_not_matter() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/echo?text=Foo&other=value").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );
    assert_eq!(
        response.into_string(),
        Some("You typed in <b>Foo</b>".into())
    );
}
You typed in <b>{{ text }}</b>
<h1>Echo</h1>
<form method="GET" action="/echo">
<input name="text">
<input type="submit" value="Echo">
</form>

Rocket - Echo using POST

  • POST
  • Form
  • context
  • Template
  • FromForm
  • Tera
[package]
name = "echo-using-post"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rocket = "0.5"
rocket_dyn_templates = { version = "0.1", features = ["tera"] }

[debug]
port=8001
#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

use rocket::form::Form;
use rocket_dyn_templates::{context, Template};

#[derive(FromForm)]
struct InputText<'r> {
    text: &'r str,
}

#[get("/")]
fn index() -> Template {
    Template::render("index", context! {})
}

#[post("/echo", data = "<input>")]
fn echo(input: Form<InputText<'_>>) -> Template {
    println!("text: {:?}", input.text);

    Template::render(
        "echo",
        context! {
            text: input.text
        },
    )
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .mount("/", routes![index, echo])
        .attach(Template::fairing())
}

#[cfg(test)]
mod tests;
}
use rocket::http::{ContentType, Status};
use rocket::local::blocking::Client;

#[test]
fn main_page() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );
    assert!(response.into_string().unwrap().contains("<h1>Echo</h1>"));
}

#[test]
fn echo_page() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client
        .post("/echo")
        .header(ContentType::Form)
        .body("text=Foo Bar")
        .dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );
    assert_eq!(
        response.into_string(),
        Some("You typed in <b>Foo Bar</b>".into())
    );
}

#[test]
fn echo_page_missing_text() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client
        .post("/echo")
        .header(ContentType::Form)
        //.body("")
        .dispatch();

    assert_eq!(response.status(), Status::UnprocessableEntity);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );
    assert!(response
        .into_string()
        .unwrap()
        .contains("<h1>422: Unprocessable Entity</h1>"));
}
You typed in <b>{{ text }}</b>
<h1>Echo</h1>
<form method="POST" action="/echo">
<input name="text">
<input type="submit" value="Echo">
</form>


<h1>Bad form</h1>
Missing the text field.
<form method="POST" action="/echo">
<input type="submit" value="Echo">
</form>

Rocket - Path parameters

Instead of passing parameters in the Query string in a GET request we can also use the path to pass parameters. This is especially interesgint if we would like to make the pages indexable by search engines.

  • e.g. in a blog engine the path can be mapped to a blog entry
  • In a social site we might want to have a separate page for each users.
#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

use rocket::response::content;

#[get("/")]
fn index() -> content::RawHtml<String> {
    let html = String::from(
        r#"
    <a href="/user/42">User 42</a><br>
    <a href="/user/foo">User foo - not valid</a><br>
    "#,
    );

    content::RawHtml(html)
}

#[get("/user/<uid>")]
fn user(uid: usize) -> content::RawHtml<String> {
    let html = format!("User ID: {uid}");
    content::RawHtml(html)
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index, user])
}

#[cfg(test)]
mod tests;
}
use rocket::http::Status;
use rocket::local::blocking::Client;

#[test]
fn main_page() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );
    let html = response.into_string().unwrap();
    assert!(html.contains(r#"<a href="/user/42">User 42</a><br>"#));
}

#[test]
fn valid_user() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/user/42").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );
    let html = response.into_string().unwrap();
    assert_eq!(html, "User ID: 42");
}

#[test]
fn invalid_user() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/user/foo").dispatch();

    assert_eq!(response.status(), Status::UnprocessableEntity); // 422
}

Rocket - Single hit-counter using a text file

[package]
name = "single-counter-in-text-file"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rocket = "0.5"
tempdir = "0.3"
#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

use std::fs::File;
use std::io::Write;

#[get("/")]
fn index() -> String {
    let file = match std::env::var("COUNTER_PATH") {
        Ok(val) => std::path::PathBuf::from(val),
        Err(_) => {
            let current_dir = std::env::current_dir().unwrap();
            current_dir.join("counter.txt")
        }
    };

    let counter: u32 = if file.exists() {
        std::fs::read_to_string(&file)
            .unwrap()
            .trim_end()
            .parse()
            .unwrap()
    } else {
        0
    };
    let counter = counter + 1;

    let mut fh = File::create(file).unwrap();
    writeln!(&mut fh, "{}", counter).unwrap();

    format!("Counter is {}", counter)
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index])
}

#[cfg(test)]
mod tests;
}
#![allow(unused)]
fn main() {
use rocket::http::Status;
use rocket::local::blocking::Client;
use tempdir::TempDir;

#[test]
fn test_counte() {
    let tmp_dir = TempDir::new("counter").unwrap();
    std::env::set_var("COUNTER_PATH", tmp_dir.path().join("counter.txt"));

    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(response.into_string(), Some("Counter is 1".into()));

    let response = client.get("/").dispatch();
    assert_eq!(response.status(), Status::Ok);
    assert_eq!(response.into_string(), Some("Counter is 2".into()));
}
}
  • Error handling - unwrap.
  • File operations are not atomic.
  • We don't handle variable overflow properly.

Rocket - Logging to the console

  • trace!
  • debug!
  • info!
  • warn!
  • error!
#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

#[get("/")]
fn index() -> &'static str {
    rocket::trace!("Trace from Hello World");
    rocket::debug!("Debug from Hello World");
    rocket::info!("Info from Hello World");
    rocket::warn!("Warning from Hello World");
    rocket::error!("Error from Hello World");
    "Hello, world!"
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index])
}
}
[debug]
# `critical`, `normal`, `debug`, `off`
#log_level = "normal"
#log_level = "debug"

Rocket - Logging to a file using log4rs

  • Add log4rs to the dependencies.
[package]
name = "logging"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
log4rs = "1.3.0"
rocket = "0.5"
  • Create a configuration file:

{% embed include file="src/examples/rocket/logging-with-log4rs-to-file/log4rs.yaml)

  • Initiate the logging
#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

#[get("/")]
fn index() -> &'static str {
    rocket::trace!("Trace from Hello World");
    rocket::debug!("Debug from Hello World");
    rocket::info!("Info from Hello World");
    rocket::warn!("Warning from Hello World");
    rocket::error!("Error from Hello World");
    "Hello, world!"
}

#[launch]
fn rocket() -> _ {
    log4rs::init_file("log4rs.yaml", Default::default()).unwrap();
    rocket::build().mount("/", routes![index])
}
}

Rocket - Calculator with GET (passing multiple parameters)

  • RawHtml
[debug]
port=8001
#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

use rocket::response::content;

#[get("/?<a>&<b>&<op>")]
fn index(a: Option<i64>, b: Option<i64>, op: Option<&str>) -> content::RawHtml<String> {
    let mut selected_add = "";
    let mut selected_multiply = "";
    let mut selected_subtract = "";
    let mut selected_divide = "";

    let result = match (a, b, op) {
        (Some(a), Some(b), Some(op)) => match op {
            "add" => {
                selected_add = r#"selected="selected""#;
                (a + b).to_string()
            }
            "subtract" => {
                selected_subtract = r#"selected="selected""#;
                (a - b).to_string()
            }
            "divide" => {
                selected_divide = r#"selected="selected""#;
                (a / b).to_string()
            }
            "multiply" => {
                selected_multiply = r#"selected="selected""#;
                (a * b).to_string()
            }
            _ => String::new(),
        },
        _ => String::new(),
    };

    let a = match a {
        Some(a) => a.to_string(),
        None => String::new(),
    };

    let b = match b {
        Some(b) => b.to_string(),
        None => String::new(),
    };

    let mut html = format!(
        r#"
    <form>
    <input name="a" value="{a}">
    <input name="b" value="{b}">
    <select name="op">
    <option value="add" {selected_add}>+</option>
    <option value="multiply" {selected_multiply}>*</option>
    <option value="subtract" {selected_subtract}>-</option>
    <option value="divide" {selected_divide}>/</option>
    </select>

    <input type="submit" value="Calculate">
    "#
    );

    if !result.is_empty() {
        let res_html = format!("<hr>The result is {result}");
        html.push_str(&res_html);
    }

    content::RawHtml(html)
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index])
}

#[cfg(test)]
mod test {
    use super::rocket;

    use rocket::http::Status;
    use rocket::local::blocking::Client;

    #[test]
    fn test_no_input() {
        let client = Client::tracked(rocket()).unwrap();
        let response = client.get("/").dispatch();
        assert_eq!(response.status(), Status::Ok);
        let html = response.into_string().unwrap();
        assert!(!html.contains("result"));

        assert!(html.contains(r#"<form>"#));
        assert!(html.contains(r#"<input name="a" value="">"#));
        assert!(html.contains(r#"<input name="b" value="">"#));
        assert!(html.contains(r#"<select name="op">"#));
        assert!(html.contains(r#"<option value="add" >+</option>"#));
        assert!(html.contains(r#"<option value="multiply" >*</option>"#));
        assert!(html.contains(r#"<option value="subtract" >-</option>"#));
        assert!(html.contains(r#"<option value="divide" >/</option>"#));
        assert!(html.contains(r#"</select>"#));
        assert!(html.contains(r#"<input type="submit" value="Calculate">"#));
    }

    #[test]
    fn test_add() {
        let client = Client::tracked(rocket()).unwrap();
        let response = client.get("/?a=23&b=19&op=add").dispatch();
        assert_eq!(response.status(), Status::Ok);
        let html = response.into_string().unwrap();
        assert!(html.contains("<hr>The result is 42"));
        assert!(html.contains(r#"<input name="a" value="23">"#));
        assert!(html.contains(r#"<input name="b" value="19">"#));
    }

    #[test]
    fn test_missing_b() {
        let client = Client::tracked(rocket()).unwrap();
        let response = client.get("/?a=23&op=add").dispatch();
        assert_eq!(response.status(), Status::Ok);
        let html = response.into_string().unwrap();
        assert!(!html.contains("result"));
        assert!(html.contains(r#"<input name="a" value="23">"#));
        assert!(html.contains(r#"<input name="b" value="">"#));

        //assert_eq!(html, "");
        // TODO: maybe this should report an error?
    }
}
}

Rocket - Calculator with GET (passing multiple parameters) using enum

  • RawHtml
  • TODO
[debug]
port=8001
#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

//use rocket::form::Form;
use rocket::response::content;
// TODO: This was added to Rocket on 2024.08.06 so it is not released yet https://github.com/rwf2/Rocket/issues/2826
// TODO 0.5.1. was released on 2024.08.22 fix this example when a newer version is released

enum Operation {
    Add,
    Subtract,
    Multiply,
    Divide,
}

#[get("/?<a>&<b>&<op>")]
fn index(a: Option<i64>, b: Option<i64>, op: Option<Operation>) -> content::RawHtml<String> {
    let mut selected_add = "";
    let mut selected_multiply = "";
    let mut selected_subtract = "";
    let mut selected_divide = "";

    let result = match (a, b, op) {
        (Some(a), Some(b), Some(op)) => match op {
            Operation::Add => {
                selected_add = r#"selected="selected""#;
                (a + b).to_string()
            }
            Operation::Subtract => {
                selected_subtract = r#"selected="selected""#;
                (a - b).to_string()
            }
            Operation::Divide => {
                selected_divide = r#"selected="selected""#;
                (a / b).to_string()
            }
            Operation::Multiply => {
                selected_multiply = r#"selected="selected""#;
                (a * b).to_string()
            }
            _ => String::new(),
        },
        _ => String::new(),
    };

    let a = match a {
        Some(a) => a.to_string(),
        None => String::new(),
    };

    let b = match b {
        Some(b) => b.to_string(),
        None => String::new(),
    };

    let mut html = format!(
        r#"
    <form>
    <input name="a" value="{a}">
    <input name="b" value="{b}">
    <select name="op">
    <option value="add" {selected_add}>+</option>
    <option value="multiply" {selected_multiply}>*</option>
    <option value="subtract" {selected_subtract}>-</option>
    <option value="divide" {selected_divide}>/</option>
    </select>

    <input type="submit" value="Calculate">
    "#
    );

    if !result.is_empty() {
        let res_html = format!("<hr>The result is {result}");
        html.push_str(&res_html);
    }

    content::RawHtml(html)
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index])
}
}

Rocket - In-memory counter using State

[package]
name = "in-memory-counter"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rocket = "0.5.0"
rocket_dyn_templates = { version = "0.1", features = ["tera"] }
serde = "1.0.201"
#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;

use serde::Deserialize;

use rocket::{fairing::AdHoc, State};
use rocket_dyn_templates::{context, Template};

struct HitCount {
    count: AtomicUsize,
}

#[derive(Deserialize)]
struct MyConfig {
    title: String,
}

#[get("/")]
fn index(hit_count: &State<HitCount>, config: &State<MyConfig>) -> Template {
    //let current_count = hit_count.count.load(Ordering::Relaxed);
    let current_count = hit_count.count.fetch_add(1, Ordering::Relaxed);

    Template::render(
        "index",
        context! {
            title: &config.title,
            count: current_count,
        },
    )
}

#[catch(404)]
fn not_found() -> Template {
    Template::render(
        "404",
        context! {
            // Currently the title is set in the template
            //title: "404 Not Found"
        },
    )
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .mount("/", routes![index])
        .attach(Template::fairing())
        .attach(AdHoc::config::<MyConfig>())
        .manage(HitCount {
            count: AtomicUsize::new(0),
        })
        .register("/", catchers![not_found])
}

#[cfg(test)]
mod tests;
}
#![allow(unused)]
fn main() {
use rocket::http::Status;
use rocket::local::blocking::Client;

#[test]
fn hello_world() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );
    let html = response.into_string().unwrap();
    assert!(html.contains("<title>Rocket with Tera</title>"));
    assert!(html.contains("<h1>Rocket with Tera</h1>"));
}

#[test]
fn page_not_found() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/other").dispatch();

    assert_eq!(response.status(), Status::NotFound);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );
    let html = response.into_string().unwrap();
    //assert_eq!(html, "");
    assert!(html.contains("<title>404 Not Found</title>"));
    assert!(html.contains("<h1>404 Not Found</h1>"));
    assert!(html.contains("<div>Page not found</div>"));
}
}
[default]
title = "Rocket with Tera"

[debug]

[release]

{% set title = "404 Not Found" %}
{% include "incl/header" %}

    <div>Page not found</div>

{% include "incl/footer" %}
</body>
</html>

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes">

  <title>{{ title }}</title>
</head>
<body>
    <h1>{{title}}</h1>

{% include "incl/header" %}

    <div>
        Welcome to your Rocket project! Counted: {{count}}
    </div>

{% include "incl/footer" %}

Rocket - get, set (add), delete cookies - pending cookies

#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

use rocket::http::CookieJar;
use rocket::response::content;
use std::time::{SystemTime, UNIX_EPOCH};

fn get_time() -> String {
    let start = SystemTime::now();
    let since_the_epoch = start
        .duration_since(UNIX_EPOCH)
        .expect("Time went backwards");

    since_the_epoch.as_micros().to_string()
}

fn get_html(cookies: &CookieJar<'_>, current_time: &str) -> content::RawHtml<String> {
    let saved_time: String = match cookies.get("cookie-demo") {
        Some(cookie) => cookie.value().to_owned(),
        None => String::from("No cookie"),
    };

    content::RawHtml(format!(
        r#"<a href="/">home</a> <a href="/set">set cookie</a> <a href="/delete">delete cookie</a><br>Current time: {current_time}<br>Saved time: {saved_time}<br>"#
    ))
}

#[get("/")]
fn home(cookies: &CookieJar<'_>) -> content::RawHtml<String> {
    let current_time = get_time();
    rocket::info!("home current_time: {}", current_time);
    get_html(cookies, &current_time)
}

#[get("/set")]
fn set_cookie(cookies: &CookieJar<'_>) -> content::RawHtml<String> {
    let current_time = get_time();
    rocket::info!("set_cookie current_time: {}", current_time);
    cookies.add(("cookie-demo", current_time.clone()));
    get_html(cookies, &current_time)
}

#[get("/delete")]
fn delete_cookie(cookies: &CookieJar<'_>) -> content::RawHtml<String> {
    let current_time = get_time();
    rocket::info!("delete_cookie current_time: {}", current_time);
    cookies.remove("cookie-demo");
    get_html(cookies, &current_time)
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![home, delete_cookie, set_cookie])
}
}

Rocket - Multi-counter using cookies (in the client)

#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

use rocket::http::CookieJar;

#[get("/")]
fn index(cookies: &CookieJar<'_>) -> String {
    let counter: u32 = match cookies.get("counter") {
        Some(cookie) => match cookie.value().parse() {
            Ok(val) => val,
            Err(_) => {
                eprintln!(
                    "Invalid value '{}' for the 'counter' cookie.",
                    cookie.value()
                );
                0
            }
        },
        None => 0,
    };
    let counter = counter + 1;
    cookies.add(("counter", counter.to_string()));

    format!("Counter: {}", counter)
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index])
}

#[cfg(test)]
mod tests;
}
#![allow(unused)]
fn main() {
use rocket::http::Status;
use rocket::local::blocking::Client;

#[test]
fn test_without_cookie() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("set-cookie").unwrap(),
        "counter=1; SameSite=Strict; Path=/"
    );

    assert_eq!(response.into_string(), Some("Counter: 1".into()));
}

#[test]
fn test_with_cookie() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/").cookie(("counter", "41")).dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("set-cookie").unwrap(),
        "counter=42; SameSite=Strict; Path=/"
    );

    assert_eq!(response.into_string(), Some("Counter: 42".into()));
}

#[test]
fn test_with_bad_cookie() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/").cookie(("counter", "bla")).dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("set-cookie").unwrap(),
        "counter=1; SameSite=Strict; Path=/"
    );

    assert_eq!(response.into_string(), Some("Counter: 1".into()));
}
}

Rocket - Multi-counter using secure cookies (in the client)

[package]
name = "multi-counter-using-encrypted-cookies"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rocket = { version = "0.5", features = ["secrets"] }
[debug]
secret_key = "qqrqdOg7fX4YNaDFzXf1mu6050BQ9okssS5sKkZFMVsd"

[release]
secret_key = "dOg7fX4YNaDFzXf1mu6050BQ9okssS5sKkZFMVsdpXg="
#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

use rocket::http::CookieJar;

#[get("/")]
fn index(cookies: &CookieJar<'_>) -> String {
    let counter: u32 = match cookies.get_private("counter") {
        Some(cookie) => match cookie.value().parse() {
            Ok(val) => val,
            Err(_) => {
                eprintln!("Invalid value {} for the 'counter' cookie.", cookie.value());
                0
            }
        },
        None => 0,
    };
    let counter = counter + 1;
    cookies.add_private(("counter", counter.to_string()));

    format!("Counter: {}", counter)
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index])
}

#[cfg(test)]
mod tests;
}
#![allow(unused)]
fn main() {
use rocket::http::Status;
use rocket::local::blocking::Client;

#[test]
fn test_without_cookie() {
    let client = Client::tracked(super::rocket()).unwrap();
    let client = client;

    let response = client.get("/").dispatch();

    assert_eq!(response.status(), Status::Ok);

    // "counter=C3YSnaksM52BVeYOPjvcOp7pNRAez5ZB8aq+a+A%3D; HttpOnly; SameSite=Strict; Path=/; Expires=Mon, 15 Jan 2024 06:44:15 GMT"
    let cookie = response.headers().get_one("set-cookie").unwrap();
    assert!(cookie.contains("counter="));
    assert!(cookie.contains("; HttpOnly; SameSite=Strict; Path=/; Expires="));

    assert_eq!(response.into_string(), Some("Counter: 1".into()));
}

#[test]
fn test_with_cookie() {
    let client = Client::tracked(super::rocket()).unwrap();

    let response = client.get("/").private_cookie(("counter", "41")).dispatch();
    assert_eq!(response.status(), Status::Ok);

    assert_eq!(response.into_string(), Some("Counter: 42".into()));
}

#[test]
fn test_with_bad_cookie() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client
        .get("/")
        .private_cookie(("counter", "bla"))
        .dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(response.into_string(), Some("Counter: 1".into()));
}

// I was expecting this too to work, but it does not, I get back "Counter 1" for the second request as well.
// #[test]
// fn test_counter() {
//     let client = Client::tracked(super::rocket()).unwrap();
//     let client = client;

//     let response = client.get("/").dispatch();

//     assert_eq!(response.status(), Status::Ok);

//     // "counter=C3YSnaksM52BVeYOPjvcOp7pNRAez5ZB8aq+a+A%3D; HttpOnly; SameSite=Strict; Path=/; Expires=Mon, 15 Jan 2024 06:44:15 GMT"
//     let cookie = response.headers().get_one("set-cookie").unwrap();
//     assert!(cookie.contains("counter="));
//     assert!(cookie.contains("; HttpOnly; SameSite=Strict; Path=/; Expires="));

//     let (head, _tail) = cookie.split_once(';').unwrap();
//     let (_head, tail) = head.split_once('=').unwrap();
//     let cookie_str = tail.to_string();

//     assert_eq!(response.into_string(), Some("Counter: 1".into()));

//     //assert_eq!(cookie_str, "");
//     let response = client.get("/").cookie(("counter", cookie_str)).dispatch();
//     assert_eq!(response.into_string(), Some("Counter: 2".into()));
// }
}

Rocket - Automatic reload of the application (watch)

  • watch

  • During development it can be usefule to automatically reload the application as we are making changes to the code.

  • cargo-watch

cargo watch -x run

Rocket - Two applications in separate files

  • TODO

  • We created a separate file with its own routes

  • We then mounted it under a path called /blog

  • We provide a function called routes listing all the routes in this applcation and use that in the mount.

Limitation of this solution:

  • in the blog_test we need to use super::super::rocket() instead of super::rocket().
  • in the blog_test we need to access /blog that mean we need to know where it will be mounted.
#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

pub(crate) mod blog;

#[get("/")]
fn index() -> &'static str {
    "Main page"
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .mount("/", routes![index])
        .mount("/blog", blog::routes())
}

#[cfg(test)]
mod tests;
}
#![allow(unused)]
fn main() {
use rocket::http::Status;
use rocket::local::blocking::Client;

#[test]
fn check_main() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/plain; charset=utf-8"
    );
    assert_eq!(response.into_string(), Some("Main page".into()));
}
}
#![allow(unused)]
fn main() {
use rocket::Route;

pub fn routes() -> Vec<Route> {
    routes![index]
}

#[get("/")]
pub fn index() -> &'static str {
    "Blog"
}

#[cfg(test)]
mod blog_tests;
}
#![allow(unused)]
fn main() {
use rocket::http::Status;
use rocket::local::blocking::Client;

#[test]
fn check_blog() {
    let client = Client::tracked(super::super::rocket()).unwrap();
    let response = client.get("/blog").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/plain; charset=utf-8"
    );
    assert_eq!(response.into_string(), Some("Blog".into()));
}
}

Rocket - Redirect to another page

  • Redirect

  • uri!

  • Status

  • SeeOther

  • TODO

  • TODO: Implement the same in the separate files case.

  • TODO: Redirect using other Status code

  • TODO: Redirect to an external URL

  • TODO: Optional redirction? (e.g. after successful login we redirect, but if it fails we would like to show the login page)

  • TODO: Dynamic redirect. (e.g. after successful login we go to the page where the user really wanted to go)

#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

use rocket::response::Redirect;

#[get("/")]
fn index() -> &'static str {
    "Hello, world!"
}

#[get("/page")]
fn page() -> &'static str {
    "A page"
}

#[get("/other")]
fn other() -> Redirect {
    Redirect::to(uri!(page))
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index, page, other])
}

#[cfg(test)]
mod tests;
}
#![allow(unused)]
fn main() {
use rocket::http::Status;
use rocket::local::blocking::Client;

#[test]
fn hello_world() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/plain; charset=utf-8"
    );
    assert_eq!(response.into_string(), Some("Hello, world!".into()));

    let response = client.get("/other").dispatch();

    assert_eq!(response.status(), Status::SeeOther);
    assert_eq!(response.headers().get_one("Location").unwrap(), "/page");
    assert_eq!(response.into_string(), None);
    //dbg!(response.headers());
}
}

Rocket - Redirect with parameters

#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

use rocket::response::Redirect;

#[get("/")]
fn index() -> &'static str {
    "Hello, world!"
}

#[get("/user/<id>?<msg>")]
fn user(id: u32, msg: &str) -> String {
    format!("id: {id} msg: {msg}")
}

#[get("/group/<id>?<msg>")]
fn group(id: u32, msg: Option<&str>) -> String {
    format!("id: {id} msg: {msg:?}")
}

#[get("/other")]
fn other() -> Redirect {
    Redirect::to(uri!(user(23, "hello")))
}

#[get("/redir-value")]
fn redir_value() -> Redirect {
    Redirect::to(uri!(group(42, Some("message"))))
}

#[get("/redir-none")]
fn redir_none() -> Redirect {
    // type annotations needed for `std::option::Option<_>`
    //Redirect::to(uri!(group(42, None)))

    let msg: Option<&str> = None;
    Redirect::to(uri!(group(42, msg)))
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount(
        "/",
        routes![index, user, other, group, redir_value, redir_none],
    )
}

#[cfg(test)]
mod tests;
}
#![allow(unused)]
fn main() {
use rocket::http::Status;
use rocket::local::blocking::Client;

#[test]
fn hello_world() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/plain; charset=utf-8"
    );
    assert_eq!(response.into_string(), Some("Hello, world!".into()));

    let response = client.get("/other").dispatch();
    assert_eq!(response.status(), Status::SeeOther);
    assert_eq!(
        response.headers().get_one("Location").unwrap(),
        "/user/23?msg=hello"
    );
    assert_eq!(response.into_string(), None);

    let response = client.get("/redir-value").dispatch();
    assert_eq!(response.status(), Status::SeeOther);
    assert_eq!(
        response.headers().get_one("Location").unwrap(),
        "/group/42?msg=message"
    );
    assert_eq!(response.into_string(), None);

    let response = client.get("/redir-none").dispatch();
    assert_eq!(response.status(), Status::SeeOther);
    assert_eq!(response.headers().get_one("Location").unwrap(), "/group/42");
    assert_eq!(response.into_string(), None);
}
}

Rocket - Serving static files

  • relative

  • FileServer

  • We can use the FileServer to return static files such as images, css files, javascript files etc.

  • We need to mount it to "/".

  • It sets the content type properly for each file.

#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

use rocket::fs::{relative, FileServer};
use rocket::response::content;

#[get("/")]
fn index() -> content::RawHtml<&'static str> {
    content::RawHtml(
        r#"
        <link href="/css/style.css" rel="stylesheet">
        <script src="/js/demo.js"></script>
        Hello, <b>world!</b>
        "#,
    )
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .mount("/", routes![index])
        .mount("/", FileServer::from(relative!("static")))
}

#[cfg(test)]
mod tests;
}
#![allow(unused)]
fn main() {
use rocket::http::Status;
use rocket::local::blocking::Client;

#[test]
fn html_page() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );
    let html = response.into_string().unwrap();
    //println!("{html}");
    assert!(html.contains(r#"<link href="/css/style.css" rel="stylesheet">"#));
    assert!(html.contains(r#"<script src="/js/demo.js"></script>"#));
    assert!(html.contains("Hello, <b>world!</b>"));
}

#[test]
fn css_file() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/css/style.css").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/css; charset=utf-8"
    );

    let content = response.into_string().unwrap();
    assert!(content.contains("background-color: #BBBBBB;"));
}

#[test]
fn javascript_file() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/js/demo.js").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/javascript"
    );

    let content = response.into_string().unwrap();
    assert_eq!(content, r#"console.log("hello");"#);
}

#[test]
fn favicon_ico() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/favicon.ico").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "image/x-icon"
    );
}
}
body {
    background-color: #BBBBBB;
}
console.log("hello");

Rocket - 404 page with static content

#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

use rocket::response::content;

#[get("/")]
fn index() -> content::RawHtml<&'static str> {
    content::RawHtml("Hello, <b>world!</b>")
}

#[catch(404)]
fn not_found() -> content::RawHtml<&'static str> {
    const BODY: &str = include_str!("templates/404.html");
    content::RawHtml(BODY)
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .mount("/", routes![index])
        .register("/", catchers![not_found])
}

#[cfg(test)]
mod tests;
}

{% embed include file="src/examples/rocket/http-404-page-with-static-content/src/templates/404.html)

use rocket::form::validate::Contains;
use rocket::http::Status;
use rocket::local::blocking::Client;

#[test]
fn main_page() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );
    assert_eq!(response.into_string(), Some("Hello, <b>world!</b>".into()));
}

#[test]
fn page_not_found() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/something").dispatch();

    assert_eq!(response.status(), Status::NotFound);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );
    let html = response.into_string().unwrap();
    assert!(html.contains("<title>404 Page not found</title>"));
    assert!(html.contains("<h1>Ooups</h1>"));
}

Rocket - Access custom configuration in the routes

[default]
custom_in_default = "hi"

[debug]
custom_a = "Hello World!"

[release]
log_level = "normal"
custom_b = "Special value for b"
[package]
name = "configuration"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rocket = "0.5"
serde = "1.0.196"

#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

use rocket::{fairing::AdHoc, State};
use serde::Deserialize;

#[derive(Deserialize)]
struct MyConfig {
    #[serde(default = "get_default_custom_in_default_section")]
    custom_in_default: String,

    #[serde(default = "get_default_custom_a")]
    custom_a: String,

    #[serde(default = "get_default_custom_b")]
    custom_b: String,
}

fn get_default_custom_in_default_section() -> String {
    String::from("some other default")
}

fn get_default_custom_a() -> String {
    String::from("some default for a")
}

fn get_default_custom_b() -> String {
    String::from("some default for b")
}

#[get("/")]
fn index(config: &State<MyConfig>) -> &'static str {
    rocket::info!(
        "profile is debug: {:?}",
        rocket::Config::default().profile == "debug"
    );

    rocket::info!("custom_a {:?}", config.custom_a);

    rocket::info!("custom_b {:?}", config.custom_b);

    rocket::info!("custom_in_default {:?}", config.custom_in_default);

    "See the console"
}

#[get("/bad")]
fn bad() -> &'static str {
    rocket::info!(
        "profile is debug: {:?}",
        rocket::Config::default().profile == "debug"
    );

    let custom_a: String = rocket::Config::figment()
        .extract_inner("custom_a")
        .unwrap_or(String::from("some default in a"));
    rocket::info!("custom_a {:?}", custom_a);

    let custom_b = rocket::Config::figment()
        .extract_inner::<String>("custom_b")
        .unwrap_or(String::from("some default in b"));
    rocket::info!("custom_b {:?}", custom_b);

    let custom_in_default: String = rocket::Config::figment()
        .extract_inner("custom_in_default")
        .unwrap_or(String::from("some other default"));
    rocket::info!("custom_in_default {:?}", custom_in_default);

    "See the console"
}

#[get("/defaults")]
fn defaults() -> &'static str {
    rocket::info!("default: {:#?}", rocket::Config::default());

    "See the console"
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .mount("/", routes![index, bad, defaults])
        .attach(AdHoc::config::<MyConfig>())
}

#[cfg(test)]
mod test {
    use rocket::http::Status;
    use rocket::local::blocking::Client;

    #[test]
    fn home() {
        let client = Client::tracked(super::rocket()).unwrap();
        let response = client.get("/").dispatch();

        assert_eq!(response.status(), Status::Ok);
    }
}
}

Rocket - Custom configuration and injecting (overriding) values in tests

  • We would like to have some custom configuration values for the application. (e.g. database address, API key for email sending service, folder to save uploaded imagesetc.)

  • During testing we would like to set these values separately. e.g. for each test we create a temporary folder and then set the custom variable of the upload_folder to that value.

  • This will keep our environment clean and the test will be independent.

  • We can add those custom configuration values to Rocket.toml:


[debug]
custom = "In Rocket.toml"

  • We can then create a struct describing these parameters (MyConfig in our example) and we can use .attach(AdHoc::config::<MyConfig>()) to make Rocket read these values.

  • In each route we can use config: &State<MyConfig> to get the configuration values.

  • In the tests we can override specific configuration values before we create the client.

let provider = Config::figment().merge(("custom", "In Test 1"));
let app = super::rocket().configure(provider);

let client = Client::tracked(app).unwrap();
#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

use rocket::response::content;
use rocket::{fairing::AdHoc, State};
use serde::Deserialize;

#[derive(Deserialize)]
struct MyConfig {
    custom: String,
}

#[get("/")]
fn index(config: &State<MyConfig>) -> content::RawHtml<String> {
    content::RawHtml(format!("Custom: {}", config.custom))
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .mount("/", routes![index])
        .attach(AdHoc::config::<MyConfig>())
}

#[cfg(test)]
mod test {
    use rocket::config::Config;
    use rocket::http::Status;
    use rocket::local::blocking::Client;

    #[test]
    fn home() {
        let client = Client::tracked(super::rocket()).unwrap();
        let response = client.get("/").dispatch();

        assert_eq!(response.status(), Status::Ok);
        let html = response.into_string().unwrap();
        assert_eq!(html, "Custom: In Rocket.toml");
    }

    #[test]
    fn test_1() {
        let provider = Config::figment().merge(("custom", "In Test 1"));
        let app = super::rocket().configure(provider);

        let client = Client::tracked(app).unwrap();
        let response = client.get("/").dispatch();

        assert_eq!(response.status(), Status::Ok);
        let html = response.into_string().unwrap();
        assert_eq!(html, "Custom: In Test 1");
    }

    #[test]
    fn test_2() {
        let provider = Config::figment().merge(("custom", "In Test 2"));
        let app = super::rocket().configure(provider);

        let client = Client::tracked(app).unwrap();
        let response = client.get("/").dispatch();

        assert_eq!(response.status(), Status::Ok);
        let html = response.into_string().unwrap();
        assert_eq!(html, "Custom: In Test 2");
    }
}
}
  • This will ensure that each test will have its own value for this custom field.

Rocket - Request guard - FromRequest

  • FromRequest

  • Request

  • Outcome

  • async_trait

  • These guards don't check anything yet, they just either accept or reject the reuqest, but this can be a good skeleton.

#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

// #[cfg(test)] mod tests;

use rocket::http::Status;
use rocket::outcome::Outcome;
use rocket::request::{self, FromRequest, Request};

struct GoodGuard;
struct BadGuard;

#[rocket::async_trait]
impl<'r> FromRequest<'r> for GoodGuard {
    type Error = ();

    async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, ()> {
        rocket::info!("from_request in GoodGuard");
        rocket::info!("client_ip: {:?}", req.client_ip());
        rocket::info!("uri: {:?}", req.uri());

        Outcome::Success(Self)
    }
}

#[rocket::async_trait]
impl<'r> FromRequest<'r> for BadGuard {
    type Error = ();

    async fn from_request(_req: &'r Request<'_>) -> request::Outcome<Self, ()> {
        rocket::info!("from_request in BadGuard");
        Outcome::Error((Status::BadRequest, ()))
    }
}

#[get("/")]
fn index(_g: GoodGuard) -> &'static str {
    "Hello, world!"
}

#[get("/good")]
fn good(_g: GoodGuard) -> &'static str {
    "Hello, world!"
}

#[get("/bad")]
fn bad(_g: BadGuard) -> &'static str {
    "Hello, world!"
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index, good, bad])
}
}

Rocket - Blog using request guard - FromRequest

  • FromRequest
  • Status
#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

#[cfg(test)]
mod tests;

use rocket::fs::relative;
use rocket::http::Status;
use rocket::outcome::Outcome;
use rocket::request::{self, FromRequest, Request};
use rocket::response::content;

struct MyGuard;

#[rocket::async_trait]
impl<'r> FromRequest<'r> for MyGuard {
    type Error = ();

    async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, ()> {
        rocket::info!("from_request");
        rocket::info!("ip: {:?}", req.real_ip());
        let slug: std::path::PathBuf = req.segments(1..).unwrap();
        rocket::info!("path: {:?}", slug);
        let mut filepath = std::path::PathBuf::from(relative!("pages")).join(slug);
        filepath.set_extension("md");
        rocket::info!("filepath: {:?}", filepath);

        if filepath.exists() {
            Outcome::Success(MyGuard)
        } else {
            Outcome::Error((Status::NotFound, ()))
        }
    }
}

#[get("/")]
fn index() -> content::RawHtml<String> {
    let html = String::from(
        r#"
    <a href="/blog/main">main</a><br>
    <a href="/blog/missing">missing</a><br>
    "#,
    );
    content::RawHtml(html)
}

#[get("/blog/<slug>")]
fn blog(slug: &str, _g: MyGuard) -> content::RawHtml<String> {
    let mut filepath = std::path::PathBuf::from(relative!("pages")).join(slug);
    filepath.set_extension("md");
    let content = std::fs::read_to_string(filepath).unwrap();

    let html = format!("slug: {:?} content: {}", slug, content);

    content::RawHtml(html)
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index, blog])
}
}
#![allow(unused)]
fn main() {
use rocket::http::Status;
use rocket::local::blocking::Client;

#[test]
fn check_index() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );
    let html = response.into_string().unwrap();
    assert!(html.contains(r#"<a href="/blog/main">main</a><br>"#));
    assert!(html.contains(r#"<a href="/blog/missing">missing</a><br>"#));
}

#[test]
fn check_main() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/blog/main").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );
    let html = response.into_string().unwrap();
    //println!("{html}");
    assert!(html.contains(r#"slug: "main" content: Main page"#));
}

#[test]
fn check_about() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/blog/about").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );
    let html = response.into_string().unwrap();
    //println!("{html}");
    assert!(html.contains(r#"slug: "about" content: About page"#));
}

#[test]
fn check_missing_page() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/blog/other").dispatch();

    assert_eq!(response.status(), Status::NotFound);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );
    let html = response.into_string().unwrap();
    println!("{html}");
    assert!(html.contains(r#"<title>404 Not Found</title>"#));
}
}
About page
Main page

Rocket - Blog with FromParam - selectively accept pathes

#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

#[cfg(test)]
mod tests;

use rocket::fs::relative;
use rocket::response::content;

use rocket::request::FromParam;
#[derive(Debug)]
struct MyPath {
    filepath: std::path::PathBuf,
}

impl<'r> FromParam<'r> for MyPath {
    type Error = &'r str;

    fn from_param(param: &'r str) -> Result<Self, Self::Error> {
        rocket::info!("from_param: {:?}", param);
        let mut filepath = std::path::PathBuf::from(relative!("pages")).join(param);
        filepath.set_extension("md");
        rocket::info!("filepath: {:?}", filepath);

        if filepath.exists() {
            Ok(Self { filepath })
        } else {
            Err("bad")
        }
    }
}

#[get("/")]
fn index() -> content::RawHtml<String> {
    let html = String::from(
        r#"
    <a href="/blog/main">main</a><br>
    <a href="/blog/missing">missing</a><br>
    "#,
    );
    content::RawHtml(html)
}

#[get("/blog/<slug>")]
fn blog(slug: MyPath) -> content::RawHtml<String> {
    rocket::info!("slug: {:?}", slug);

    let content = std::fs::read_to_string(slug.filepath).unwrap();

    let html = format!("content: {}", content);

    content::RawHtml(html)
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index, blog])
}
}
#![allow(unused)]
fn main() {
use rocket::http::Status;
use rocket::local::blocking::Client;

#[test]
fn check_index() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );
    let html = response.into_string().unwrap();
    assert!(html.contains(r#"<a href="/blog/main">main</a><br>"#));
    assert!(html.contains(r#"<a href="/blog/missing">missing</a><br>"#));
}

#[test]
fn check_main() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/blog/main").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );
    let html = response.into_string().unwrap();
    //println!("{html}");
    assert!(html.contains(r#"content: Main page"#));
}

#[test]
fn check_about() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/blog/about").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );
    let html = response.into_string().unwrap();
    assert!(html.contains(r#"content: About page"#));
}

#[test]
fn check_missing_page() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/blog/other").dispatch();

    assert_eq!(response.status(), Status::UnprocessableEntity);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );
    let html = response.into_string().unwrap();
    println!("{html}");
    assert!(html.contains(r#"<h1>422: Unprocessable Entity</h1>"#));
}
}
About page
Main page

Rocket - Return Status (arbitrary HTTP code)

  • Returning a Status allows us to either return some content or return an arbitrary HTTP status code
  • Then we can - if we want - setup a catcher for that error code to show content we would like to show.
#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

use rocket::http::Status;
use rocket::response::content;

#[get("/")]
fn index() -> content::RawHtml<&'static str> {
    content::RawHtml(r#"<a href="/language/rust">rust</a> <a href="/language/python">python</a>"#)
}

#[get("/language/<answer>")]
fn question(answer: &str) -> std::result::Result<content::RawHtml<&'static str>, Status> {
    if answer == "rust" {
        Ok(content::RawHtml("correct"))
    } else {
        Err(Status::BadRequest)
    }
}

#[catch(400)]
fn http_400() -> content::RawHtml<&'static str> {
    content::RawHtml("This is a 400 error")
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .mount("/", routes![index, question])
        .register("/", catchers![http_400])
}

#[cfg(test)]
mod tests;
}
use rocket::http::Status;
use rocket::local::blocking::Client;

#[test]
fn main_page() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );

    assert_eq!(
        response.into_string(),
        Some(r#"<a href="/language/rust">rust</a> <a href="/language/python">python</a>"#.into())
    );
}

#[test]
fn language_rust() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/language/rust").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );

    assert_eq!(response.into_string(), Some("correct".into()));
}

#[test]
fn language_bad() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/language/python").dispatch();

    assert_eq!(response.status(), Status::BadRequest);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );

    assert_eq!(response.into_string(), Some("This is a 400 error".into()));
}

Rocket - catch panic in the route handle

  • catch
  • catchers!
  • 500
#[macro_use]
extern crate rocket;

use rocket::http::Status;
use rocket::response::content;
use rocket::Request;

#[get("/")]
fn index() -> content::RawHtml<&'static str> {
    content::RawHtml(
        r#"
    <form action="/divide" method="GET">
    Divide <input name="a"> by <input name="b"> <input type="submit" value="Divide">
    </form>
    "#,
    )
}

#[get("/divide?<a>&<b>")]
fn divide(a: i32, b: i32) -> content::RawHtml<String> {
    let res = a / b;
    content::RawHtml(format!(r#"{a} / {b} = {res}"#))
}

#[catch(500)]
fn internal_error(status: Status, req: &Request) -> content::RawHtml<String> {
    let reason = status.reason().unwrap_or_default();
    rocket::error!("Error: {reason} in {}", req.uri());
    content::RawHtml(format!(
        r#"
    Internal error: '{reason}' {}
    "#,
        req.uri()
    ))
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .mount("/", routes![index, divide])
        .register("/", catchers![internal_error])
}

#[cfg(test)]
mod tests {
    use rocket::form::validate::Contains;
    use rocket::http::Status;
    use rocket::local::blocking::Client;

    #[test]
    fn main_page() {
        let client = Client::tracked(super::rocket()).unwrap();
        let response = client.get("/").dispatch();

        assert_eq!(response.status(), Status::Ok);
        assert_eq!(
            response.headers().get_one("Content-Type").unwrap(),
            "text/html; charset=utf-8"
        );
        let html = response.into_string().unwrap();
        assert!(html.contains(r#"<form action="/divide" method="GET">"#));
    }

    #[test]
    fn divide_good() {
        let client = Client::tracked(super::rocket()).unwrap();
        let response = client.get("/divide?a=10&b=5").dispatch();

        assert_eq!(response.status(), Status::Ok);
        assert_eq!(
            response.headers().get_one("Content-Type").unwrap(),
            "text/html; charset=utf-8"
        );
        let html = response.into_string().unwrap();
        assert_eq!(html, "10 / 5 = 2");
    }

    #[test]
    fn divide_by_zero() {
        let client = Client::tracked(super::rocket()).unwrap();
        let response = client.get("/divide?a=10&b=0").dispatch();

        assert_eq!(response.status(), Status::InternalServerError);
        assert_eq!(
            response.headers().get_one("Content-Type").unwrap(),
            "text/html; charset=utf-8"
        );
        let html = response.into_string().unwrap();
        println!("{html}");
        assert!(html.contains("Internal error"));
    }
}

Simple TODO list with Rocket and SurrealDB

  • TODO: why is the item.id.id shown as [object] in the web page while printing to the log shows the ID only.

Setup server:

docker volume create my-surreal-db
docker run --detach --restart always --name surrealdb -p 127.0.0.1:8000:8000 --user root -v my-surreal-db:/database surrealdb/surrealdb:v2.0.1 start --user root --pass root --log trace file://database

Dependencies

[package]
name = "todo"
version = "0.1.0"
edition = "2021"

[dependencies]
rocket = { version = "0.5", features = ["secrets", "uuid"] }
rocket_dyn_templates = { version = "0.1", features = ["tera"] }
surrealdb = "2.0"
tokio = { version = "1.35", features = ["macros", "rt-multi-thread"] }
chrono = "0.4.38"
serde = { version = "1.0", features = ["derive"] }

[debug]
port=8001
#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

use rocket::form::Form;
use rocket::State;
use rocket_dyn_templates::{context, Template};

use surrealdb::engine::remote::ws::Client;
use surrealdb::Surreal;

pub mod db;
use crate::db::Item;

#[derive(FromForm)]
struct AddForm<'r> {
    title: &'r str,
}

#[derive(FromForm)]
struct UpdateForm<'r> {
    id: &'r str,
    title: &'r str,
    text: &'r str,
}

async fn form_and_list(dbh: &State<Surreal<Client>>) -> Template {
    let items: Vec<Item> = db::get_items(dbh).await.unwrap();
    for item in &items {
        rocket::info!("{} {}", item.id.id, item.title);
    }

    let pairs = items
        .iter()
        .map(|item| {
            let id = item.id.id.to_string();
            (id, item)
        })
        .collect::<Vec<_>>();

    Template::render(
        "index",
        context! {
            title: "TODO",
            items: pairs,
        },
    )
}

#[get("/")]
async fn get_index(dbh: &State<Surreal<Client>>) -> Template {
    form_and_list(dbh).await
}

#[get("/clear")]
async fn clear_db(dbh: &State<Surreal<Client>>) -> Template {
    db::clear(dbh).await.unwrap();
    form_and_list(dbh).await
}

#[post("/", data = "<input>")]
async fn post_index(dbh: &State<Surreal<Client>>, input: Form<AddForm<'_>>) -> Template {
    rocket::info!("Add '{}'", input.title);
    let title = input.title.trim();
    db::add_item(dbh, title).await.unwrap();
    form_and_list(dbh).await
}

#[get("/item/<id>")]
async fn get_item(dbh: &State<Surreal<Client>>, id: String) -> Option<Template> {
    if let Some(item) = db::get_item(dbh, &id).await.unwrap() {
        return Some(Template::render(
            "item",
            context! {
                title: item.title.clone(),
                id: item.id.clone().id.to_string(),
                item: item,
            },
        ));
    }

    None
}

#[post("/update", data = "<input>")]
async fn update_item(dbh: &State<Surreal<Client>>, input: Form<UpdateForm<'_>>) -> Template {
    rocket::info!("Update '{}'", input.id);
    let id = input.id.trim();
    let title = input.title.trim();
    let text = input.text.trim();
    db::update_item(dbh, id, title, text).await.unwrap();

    form_and_list(dbh).await
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .mount(
            "/",
            routes![clear_db, get_index, post_index, get_item, update_item],
        )
        .attach(Template::fairing())
        .attach(db::fairing())
}
}
#![allow(unused)]
fn main() {
use chrono::{DateTime, Utc};
use rocket::fairing::AdHoc;
use serde::{Deserialize, Serialize};
use surrealdb::engine::remote::ws::Client;
use surrealdb::engine::remote::ws::Ws;
use surrealdb::opt::auth::Root;
use surrealdb::opt::Resource;
use surrealdb::sql::{Id, Thing};
use surrealdb::Surreal;

#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub struct Item {
    pub id: Thing,
    pub title: String,
    pub text: Option<String>,
    pub date: DateTime<Utc>,
}

/// # Panics
///
/// Panics when it fails to connect to the database
#[must_use]
pub fn fairing() -> AdHoc {
    AdHoc::on_ignite("Managed Database Connection", |rocket| async {
        let address = "127.0.0.1:8000";
        let username = "root";
        let password = "root";
        let db_namespace = "todo";
        let db_database = "todo";

        let dbh = Surreal::new::<Ws>(address).await.unwrap();

        dbh.signin(Root { username, password }).await.unwrap();

        dbh.use_ns(db_namespace).use_db(db_database).await.unwrap();

        rocket.manage(dbh)
    })
}

pub async fn clear(dbh: &Surreal<Client>) -> surrealdb::Result<()> {
    dbh.query("DELETE FROM items;").await?;
    Ok(())
}

pub async fn add_item(dbh: &Surreal<Client>, title: &str) -> surrealdb::Result<()> {
    let utc: DateTime<Utc> = Utc::now();

    let id = Id::ulid();

    let entry = Item {
        id: Thing::from(("items", id)),
        title: title.to_owned(),
        text: None,
        date: utc,
    };

    dbh.create(Resource::from("items")).content(entry).await?;

    Ok(())
}

pub async fn update_item(
    dbh: &Surreal<Client>,
    id: &str,
    title: &str,
    text: &str,
) -> surrealdb::Result<()> {
    rocket::info!("Update '{}' '{}'", id, title);

    #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
    struct UpdateItem {
        title: String,
        text: String,
    }

    let _item: Option<Item> = dbh
        .update(("items", id))
        .merge(UpdateItem {
            title: title.to_owned(),
            text: text.to_owned(),
        })
        .await?;

    Ok(())
}

pub async fn get_items(dbh: &Surreal<Client>) -> surrealdb::Result<Vec<Item>> {
    let mut response = dbh.query("SELECT * FROM items;").await?;
    let entries: Vec<Item> = response.take(0)?;
    Ok(entries)
}

pub async fn get_item(dbh: &Surreal<Client>, id: &str) -> surrealdb::Result<Option<Item>> {
    dbh.select(("items", id)).await
}
}
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{title}}</title>
  </head>
  <body>
      <h1><a href="/">{{title}}</a></h1>
       <form method="POST" action="/">
           <input name="title"><br>
           <input type="submit" value="Add"><br>
       </form>
       <ul>
       {% for item in items %}
          <li><a href="/item/{{item.0}}">{{item.1.title}}</a></li>
       {% endfor %}
       </ul>

      <hr>
      <a href="/clear">remove all the items</a>
  </body>
</html>
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{title}}</title>
  </head>
  <body>
      <h1><a href="/">home</a></h1>
      <h2>Title: {{item.title}}</h2>
      <div>Date: {{item.date}}</div>

      <form method="POST" action="/update">
        <input type="hidden" name="id" value="{{id}}">
        <input name="title" value="{{item.title}}"><br>
        <textarea name="text">{{item.text}}</textarea><br>
        <input type="submit" value="Update"><br>
      </form>

  </body>
</html>

Use Tera filters (length)

this is a simple example using the length filter:

Hello <b>{{ text }}!</b> length: <b>{{ text | length }}</b>
#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

use rocket_dyn_templates::{context, Template};

#[get("/")]
fn index() -> Template {
    let text = "Rocket with Tera";
    Template::render(
        "index",
        context! {
            text
        },
    )
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .mount("/", routes![index])
        .attach(Template::fairing())
}

#[cfg(test)]
mod tests;
}
#![allow(unused)]
fn main() {
use rocket::http::Status;
use rocket::local::blocking::Client;

#[test]
fn hello_world() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );
    assert_eq!(
        response.into_string(),
        Some("Hello <b>Rocket with Tera!</b> length: <b>16</b>".into())
    );
}
}
[debug]
port=8001

Create Tera filters

[package]
name = "tera-filter"
version = "0.1.0"
edition = "2021"

[dependencies]
rocket = "0.5"
rocket_dyn_templates = { version = "0.2.0", features = ["tera"] }
serde_json = "1.0.128"
tera = "1.20.0"

[default]
port = 8001
#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

mod mytera;

#[cfg(test)]
mod tests;

use rocket::response::content::RawHtml;
use rocket_dyn_templates::{context, Template};

#[get("/")]
fn index() -> RawHtml<&'static str> {
    RawHtml(r#"See <a href="/hello/Foo Bar">Foo Bar</a>."#)
}

#[get("/hello/<name>")]
pub fn hello(name: &str) -> Template {
    Template::render(
        "index",
        context! {
            title: "Hello",
            name: Some(name),
        },
    )
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .mount("/", routes![index, hello])
        .attach(Template::custom(|engines| {
            mytera::customize(&mut engines.tera);
        }))
}
}
#![allow(unused)]
fn main() {
use std::collections::HashMap;

use rocket_dyn_templates::tera::Tera;
use tera::{to_value, Result, Value};

fn my_len(val: &Value, _map: &HashMap<String, Value>) -> Result<Value> {
    let s = val.as_str().unwrap();
    Ok(to_value(s.len()).unwrap())
}

pub fn customize(tera: &mut Tera) {
    tera.register_filter("my_len", my_len);
}
}
#![allow(unused)]
fn main() {
use super::rocket;

use rocket::http::Status;
use rocket::local::blocking::Client;

#[test]
fn test_name() {
    let client = Client::tracked(rocket()).unwrap();
    let response = client.get("/hello/Tera%20Rocket").dispatch();
    assert_eq!(response.status(), Status::Ok);
    let html = response.into_string().unwrap();
    assert!(html.contains("Hi Tera Rocket!"));
    assert!(html.contains("The name is 11 characters long."));
    assert!(html.contains("My number is 11."));
}

#[test]
fn test_index() {
    let client = Client::tracked(rocket()).unwrap();
    let response = client.get("/").dispatch().into_string().unwrap();
    assert!(response.contains(r#"See <a href="/hello/Foo Bar">Foo Bar</a>."#));
}
}
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Tera Demo - {{ title }}</title>
  </head>
  <body>

<a href="/">Home</a> | <a href="/hello/Unknown">Hello</a>
    <h1>Hi {{ name }}!</h1>
    <div>The name is {{ name | length }} characters long.</div>
    <div>My number is {{ name | my_len }}.</div>
    <p>Try going to <a href="/hello/Your%20Name">/hello/Your Name</a></p>
  </body>
</html>

Skip route by returning None

  • Option}

  • None}

  • Unfortunately this does not work as I expected. see my question

#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

use rocket::response::content;

#[get("/")]
fn index() -> content::RawHtml<String> {
    let html = String::from(
        r#"
    <a href="/number/23">23</a>
    <a href="/number/42">42</a>
    "#,
    );
    content::RawHtml(html)
}

#[get("/number/<num>", rank = 1)]
fn odd_number(num: u32) -> Option<content::RawHtml<String>> {
    if num % 2 == 1 {
        let html = format!("Odd number: {num:?}");
        return Some(content::RawHtml(html));
    }

    None
}

#[get("/number/<num>", rank = 2)]
fn any_number(num: u32) -> content::RawHtml<String> {
    let html = format!("Any number: {num:?}");
    content::RawHtml(html)
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index, odd_number, any_number])
}

#[cfg(test)]
mod test {
    use super::rocket;

    use rocket::http::Status;
    use rocket::local::blocking::Client;

    #[test]
    fn test_main() {
        let client = Client::tracked(rocket()).unwrap();
        let response = client.get("/").dispatch();
        assert_eq!(response.status(), Status::Ok);
        let html = response.into_string().unwrap();
        assert!(html.contains(r#"<a href="/number/23">23</a>"#));
        assert!(html.contains(r#"<a href="/number/42">42</a>"#));
    }

    #[test]
    fn test_23() {
        let client = Client::tracked(rocket()).unwrap();
        let response = client.get("/number/23").dispatch();
        assert_eq!(response.status(), Status::Ok);
        let html = response.into_string().unwrap();
        assert_eq!(html, r#"Odd number: 23"#);
    }

    #[test]
    fn test_42() {
        let client = Client::tracked(rocket()).unwrap();
        let response = client.get("/number/42").dispatch();
        assert_eq!(response.status(), Status::Ok);
        let html = response.into_string().unwrap();
        assert_eq!(html, r#"Any number: 42"#);
    }
}
}
[default]
port = 8001

Skip route using a guard

  • FromRequest
  • Outcome::Success
  • Outcome::Forward
  • Outcome::Error
#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

use rocket::http::Status;
use rocket::outcome::Outcome;
use rocket::request::{self, FromRequest, Request};
use rocket::response::content;

#[derive(Debug)]
struct OddNumberU32;

#[rocket::async_trait]
impl<'r> FromRequest<'r> for OddNumberU32 {
    type Error = ();

    async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, ()> {
        println!("from_request in OddNumber '{req}'");
        match req.param::<u32>(1) {
            None => {
                println!("none");
                Outcome::Success(Self)
            }
            Some(val) => {
                println!("val: {val:?}");
                match val {
                    Ok(num) => {
                        if num % 2 == 1 {
                            println!("success");
                            Outcome::Success(Self)
                        } else {
                            println!("BadRequest");
                            Outcome::Forward(Status::NotFound)
                        }
                    }
                    Err(err) => {
                        println!("err: {err}");
                        Outcome::Error((Status::BadRequest, ()))
                    }
                }
            }
        }
    }
}

#[get("/")]
fn index() -> content::RawHtml<String> {
    let html = String::from(
        r#"
    <a href="/number/23">23</a>
    <a href="/number/42">42</a>
    "#,
    );
    content::RawHtml(html)
}

#[get("/number/<num>", rank = 1)]
fn odd_number(num: u32, _x: OddNumberU32) -> Option<content::RawHtml<String>> {
    let html = format!("Odd number: {num:?}");
    Some(content::RawHtml(html))
}

#[get("/number/<num>", rank = 2)]
fn any_number(num: u32) -> content::RawHtml<String> {
    println!("any_number");
    let html = format!("Any number: {num:?}");
    content::RawHtml(html)
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index, odd_number, any_number])
}

#[cfg(test)]
mod test {
    use super::rocket;

    use rocket::http::Status;
    use rocket::local::blocking::Client;

    #[test]
    fn test_main() {
        let client = Client::tracked(rocket()).unwrap();
        let response = client.get("/").dispatch();
        assert_eq!(response.status(), Status::Ok);
        let html = response.into_string().unwrap();
        assert!(html.contains(r#"<a href="/number/23">23</a>"#));
        assert!(html.contains(r#"<a href="/number/42">42</a>"#));
    }

    #[test]
    fn test_23() {
        let client = Client::tracked(rocket()).unwrap();
        let response = client.get("/number/23").dispatch();
        assert_eq!(response.status(), Status::Ok);
        let html = response.into_string().unwrap();
        assert_eq!(html, r#"Odd number: 23"#);
    }

    #[test]
    fn test_42() {
        let client = Client::tracked(rocket()).unwrap();
        let response = client.get("/number/42").dispatch();
        assert_eq!(response.status(), Status::Ok);
        let html = response.into_string().unwrap();
        assert_eq!(html, r#"Any number: 42"#);
    }
}
}
[default]
port = 8001

Rocket Guards - experiment

  • TODO
[package]
name = "guards"
version = "0.1.0"
edition = "2021"

[dependencies]
rocket = "0.5.1"
[debug]
port=8001
#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

use rocket::http::Status;
use rocket::outcome::Outcome;
use rocket::request::{self, FromRequest, Request};

#[derive(Debug)]
struct GuardA;

#[rocket::async_trait]
impl<'r> FromRequest<'r> for GuardA {
    type Error = ();

    async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, ()> {
        println!("from_request in GuardA '{req}'");
        Outcome::Success(GuardA)
    }
}

#[derive(Debug)]
struct OddGuard;

#[rocket::async_trait]
impl<'r> FromRequest<'r> for OddGuard {
    type Error = ();

    async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, ()> {
        println!("from_request in GuardA '{req}'");
        match req.param::<u32>(0) {
            None => {
                println!("none");
                Outcome::Success(OddGuard)
            }
            Some(val) => {
                println!("{val:?}");
                match val {
                    Ok(num) => {
                        if num % 2 == 1 {
                            Outcome::Success(OddGuard)
                        } else {
                            Outcome::Error((Status::BadRequest, ()))
                        }
                    }
                    Err(err) => {
                        println!("{err}");
                        Outcome::Error((Status::BadRequest, ()))
                    }
                }
            }
        }
    }
}

// #[derive(Debug)]
// struct Alfa {
//     number: i32,
// }

// #[rocket::async_trait]
// impl<'r> FromRequest<'r> for Alfa {
//     type Error = ();

//     async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, ()> {
//         Outcome::Success(Alfa { number: 23})
//     }
// }

// #[derive(Debug)]
// struct Beta {
//     number: i32,
// }

// #[rocket::async_trait]
// impl<'r> FromRequest<'r> for Beta {
//     type Error = ();

//     async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, ()> {
//         let out = req.guard::<Alfa>().await;
//         match out {
//             Outcome::Success(val) => println!("Success: {val:?}"),
//             Outcome::Error(_) => (),
//             Outcome::Forward(_) => (),
//         };

//         Outcome::Success(Beta { number: 42})
//     }
// }

#[get("/")]
async fn index() -> String {
    String::from("Hello, world!")
}

#[get("/guarded")]
async fn guarded(g: GuardA) -> String {
    rocket::info!("{g:?}");
    String::from("Guarded")
}

#[get("/number?<num>")]
async fn number(num: u32, g: OddGuard) -> String {
    rocket::info!("{num} {g:?}");
    String::from("Guarded")
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index, guarded, number])
}

#[cfg(test)]
mod test {
    use rocket::http::Status;
    use rocket::local::blocking::Client;

    #[test]
    fn hello_world() {
        let client = Client::tracked(super::rocket()).unwrap();
        let response = client.get("/").dispatch();

        assert_eq!(response.status(), Status::Ok);
        assert_eq!(response.into_string(), Some("Hello, world!".into()));
    }
}
}

Rocket return user-id

  • TODO
[package]
name = "return-result"
version = "0.1.0"
edition = "2021"

[dependencies]
rocket = "0.5.1"
#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

use rocket::form::Form;
use rocket::http::Status;
use rocket::response::{content, Redirect};

#[derive(FromForm)]
struct LoginForm<'r> {
    username: &'r str,
    password: &'r str,
}

#[get("/")]
fn index() -> content::RawHtml<&'static str> {
    content::RawHtml(
        r#"
    <form method="POST" action="/login">
    Username: <input name="username"> Password: <input type="password" name="password"> <input type="submit" value="Login">
    </form>
    "#,
    )
}

#[post("/login", data = "<input>")]
fn login(input: Form<LoginForm>) -> std::result::Result<Redirect, Status> {
    if input.username == "foo" && input.password == "secret" {
        Ok(Redirect::to(uri!(home)))
    } else {
        //Outcome::Error(Status::BadRequest)
        Err(Status::BadRequest)
    }
}

#[get("/home")]
fn home() -> content::RawHtml<&'static str> {
    content::RawHtml("Home")
}

#[catch(400)]
fn http_400() -> content::RawHtml<&'static str> {
    content::RawHtml("This is a 400 error")
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .mount("/", routes![index, login, home])
        .register("/", catchers![http_400])
}

#[cfg(test)]
mod tests;
}
use rocket::http::Status;
use rocket::local::blocking::Client;

#[test]
fn main_page() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );

    assert_eq!(
        response.into_string(),
        Some(r#"<a href="/language/rust">rust</a> <a href="/language/python">python</a>"#.into())
    );
}

#[test]
fn language_rust() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/language/rust").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );

    assert_eq!(response.into_string(), Some("correct".into()));
}

#[test]
fn language_bad() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/language/python").dispatch();

    assert_eq!(response.status(), Status::BadRequest);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );

    assert_eq!(response.into_string(), Some("This is a 400 error".into()));
}

Rocket People and groups

  • TODO
#![allow(unused)]
fn main() {
use rocket::fairing::AdHoc;
use serde::{Deserialize, Serialize};
use surrealdb::engine::remote::ws::Client;
use surrealdb::engine::remote::ws::Ws;
use surrealdb::opt::auth::Root;
use surrealdb::opt::Resource;
use surrealdb::sql::{Id, Thing};
use surrealdb::Surreal;

const NAMESPACE: &str = "rust-slides";
const PERSON: &str = "person";
const GROUP: &str = "group";
const MEMBERSHIP: &str = "membership";

#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub struct Person {
    pub id: Thing,
    pub name: String,
}

#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub struct Group {
    pub id: Thing,
    pub name: String,
    pub owner: Thing,
}

#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub struct Membership {
    pub id: Thing,
    pub person: Thing,
    pub group: Thing,
}

#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub struct MembershipWithPersonAndGroup {
    pub id: Thing,
    pub person: Person,
    pub group: Group,
}

#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub struct GroupWithOwner {
    pub id: Thing,
    pub name: String,
    pub owner: Person,
}

/// # Panics
///
/// Panics when it fails to connect to the database
#[must_use]
pub fn fairing(database: String) -> AdHoc {
    AdHoc::on_ignite("Managed Database Connection", |rocket| async {
        let address = "127.0.0.1:8000";
        let username = "root";
        let password = "root";

        let dbh = Surreal::new::<Ws>(address).await.unwrap();

        dbh.signin(Root { username, password }).await.unwrap();

        dbh.use_ns(NAMESPACE).use_db(database).await.unwrap();

        let query = format!(
            "DEFINE INDEX membership_ids ON TABLE {MEMBERSHIP} COLUMNS {PERSON}, {GROUP} UNIQUE"
        );
        dbh.query(query).await.unwrap();

        rocket.manage(dbh)
    })
}

pub async fn clear(dbh: &Surreal<Client>, database: String) -> surrealdb::Result<()> {
    rocket::info!("Clearing database");
    let result = dbh
        .query(format!("REMOVE DATABASE IF EXISTS `{database}`;"))
        .await?;
    result.check()?;

    Ok(())
}

pub async fn add_person(dbh: &Surreal<Client>, name: &str) -> surrealdb::Result<Person> {
    let entry = Person {
        id: Thing::from((PERSON, Id::ulid())),
        name: name.to_owned(),
    };

    dbh.create(Resource::from(PERSON))
        .content(entry.clone())
        .await?;

    Ok(entry)
}

pub async fn add_group(dbh: &Surreal<Client>, name: &str, uid: &str) -> surrealdb::Result<Group> {
    let entry = Group {
        id: Thing::from((GROUP, Id::ulid())),
        name: name.to_owned(),
        owner: Thing::from((PERSON, Id::from(uid))),
    };

    dbh.create(Resource::from(GROUP))
        .content(entry.clone())
        .await?;

    Ok(entry)
}

pub async fn add_member(dbh: &Surreal<Client>, uid: &str, gid: &str) -> surrealdb::Result<()> {
    let entry = Membership {
        id: Thing::from((MEMBERSHIP, Id::ulid())),
        person: Thing::from((PERSON, Id::from(uid))),
        group: Thing::from((GROUP, Id::from(gid))),
    };

    dbh.create(Resource::from(MEMBERSHIP))
        .content(entry.clone())
        .await?;

    Ok(())
}

pub async fn get_memberships(
    dbh: &Surreal<Client>,
) -> surrealdb::Result<Vec<MembershipWithPersonAndGroup>> {
    let mut response = dbh
        .query("SELECT * FROM type::table($table) FETCH person, group")
        .bind(("table", MEMBERSHIP))
        .await?;

    let entries: Vec<MembershipWithPersonAndGroup> = response.take(0)?;
    rocket::info!("Response: {:?}", entries);
    Ok(entries)
}

pub async fn get_memberships_of_person(
    dbh: &Surreal<Client>,
    uid: &str,
) -> surrealdb::Result<Vec<MembershipWithPersonAndGroup>> {
    let mut response = dbh
        .query("SELECT * FROM type::table($table) WHERE person=type::thing($person_table, $uid) FETCH person, group")
        .bind(("table", MEMBERSHIP))
        .bind(("person_table", PERSON))
        .bind(("uid", uid.to_owned()))
        .await?;

    let entries: Vec<MembershipWithPersonAndGroup> = response.take(0)?;
    rocket::info!("Response: {:?}", entries);
    Ok(entries)
}

pub async fn delete_membership(dbh: &Surreal<Client>, id: &str) -> surrealdb::Result<()> {
    let res = dbh
        .query("DELETE type::table($table) WHERE id=type::thing($table, $id)")
        .bind(("table", MEMBERSHIP))
        .bind(("id", id.to_owned()))
        .await?;
    res.check()?;

    Ok(())
}

pub async fn get_people(dbh: &Surreal<Client>) -> surrealdb::Result<Vec<Person>> {
    let mut response = dbh
        .query("SELECT * FROM type::table($table);")
        .bind(("table", PERSON))
        .await?;
    let entries: Vec<Person> = response.take(0)?;
    Ok(entries)
}

pub async fn get_person(dbh: &Surreal<Client>, id: &str) -> surrealdb::Result<Option<Person>> {
    dbh.select((PERSON, id)).await
}

pub async fn get_person_with_groups(
    dbh: &Surreal<Client>,
    id: &str,
) -> surrealdb::Result<Option<(Person, Vec<Group>, Vec<MembershipWithPersonAndGroup>)>> {
    let person = get_person(dbh, id).await?;
    match person {
        None => Ok(None),
        Some(person) => {
            let groups = get_groups_by_owner(dbh, id).await?;
            let memberships = get_memberships_of_person(dbh, id).await?;
            Ok(Some((person, groups, memberships)))
        }
    }
}

pub async fn get_groups_by_owner(
    dbh: &Surreal<Client>,
    uid: &str,
) -> surrealdb::Result<Vec<Group>> {
    rocket::info!("Getting groups for {}", uid);
    let mut response = dbh
        .query(
            "SELECT * FROM type::table($group_table) WHERE owner=type::thing($user_table, $uid);",
        )
        .bind(("group_table", GROUP))
        .bind(("uid", uid.to_owned()))
        .bind(("user_table", PERSON))
        .await?;
    let entries: Vec<Group> = response.take(0)?;
    Ok(entries)
}

pub async fn get_groups(dbh: &Surreal<Client>) -> surrealdb::Result<Vec<Group>> {
    let mut response = dbh.query(format!("SELECT * FROM {GROUP};")).await?;
    let entries: Vec<Group> = response.take(0)?;
    Ok(entries)
}

#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
struct GroupId {
    group: Thing,
}

pub async fn get_groups_not(dbh: &Surreal<Client>, uid: &str) -> surrealdb::Result<Vec<Group>> {
    let mut response = dbh
        .query(format!(
            "SELECT group FROM {MEMBERSHIP} WHERE person = type::thing($person_table, $uid)"
        ))
        .bind(("person_table", PERSON))
        .bind(("uid", uid.to_owned()))
        .await?;

    let entries: Vec<GroupId> = response.take(0)?;
    println!("ids: {:?}", entries);

    let mut response = dbh.query(format!("SELECT * FROM {GROUP} WHERE {GROUP}.id IN (SELECT group FROM {MEMBERSHIP} WHERE person = type::thing($person_table, $uid))"))
    .bind(("person_table", PERSON))
    .bind(("uid", uid.to_owned())).await?;
    let entries: Vec<Group> = response.take(0)?;
    Ok(entries)
}

pub async fn get_group(dbh: &Surreal<Client>, id: &str) -> surrealdb::Result<Option<Group>> {
    dbh.select((GROUP, id)).await
}

pub async fn get_group_with_owner(
    dbh: &Surreal<Client>,
    id: &str,
) -> surrealdb::Result<Option<GroupWithOwner>> {
    let mut response = dbh
        .query("SELECT * FROM type::table($group_table) WHERE id=type::thing($id) FETCH owner")
        .bind(("group_table", GROUP))
        .bind(("id", format!("{GROUP}:{id}")))
        .await?;

    let entries: Vec<GroupWithOwner> = response.take(0)?;
    rocket::info!("Response: {:?}", entries);
    Ok(entries.first().cloned())
}
}
#![allow(unused)]
fn main() {
use std::collections::HashMap;

use rocket_dyn_templates::tera::Tera;
use tera::{to_value, Result, Value};

fn object2id(val: &Value, _map: &HashMap<String, Value>) -> Result<Value> {
    let s = val["id"]["String"].as_str().unwrap();
    Ok(to_value(s).unwrap())
}

pub fn customize(tera: &mut Tera) {
    tera.register_filter("object2id", object2id);
}
}
#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

use std::vec;

use rocket::form::Form;
use rocket::State;
use rocket_dyn_templates::{context, Template};

use surrealdb::engine::remote::ws::Client;
use surrealdb::Surreal;

struct Config {
    database: String,
}
pub mod db;

mod mytera;

#[derive(FromForm)]
struct AddPerson<'r> {
    name: &'r str,
}

#[derive(FromForm)]
struct AddGroup<'r> {
    name: &'r str,
    uid: &'r str,
}

#[derive(FromForm)]
struct AddMember<'r> {
    gid: &'r str,
    uid: &'r str,
}

// ----------------------------------

#[get("/")]
async fn get_index(dbh: &State<Surreal<Client>>) -> Template {
    let people = db::get_people(dbh).await.unwrap();
    let groups = db::get_groups(dbh).await.unwrap();
    Template::render(
        "index",
        context! {
            title: "People and Groups",
            people: people,
            groups: groups,
        },
    )
}

#[get("/clear")]
async fn clear_db(dbh: &State<Surreal<Client>>, config: &State<Config>) -> Template {
    rocket::info!("Clearing database {}", config.database);
    db::clear(dbh, config.database.clone()).await.unwrap();
    Template::render(
        "database_cleared",
        context! {
            title: "Database cleared",
        },
    )
}

#[get("/people")]
async fn get_people(dbh: &State<Surreal<Client>>) -> Template {
    let people = db::get_people(dbh).await.unwrap();
    rocket::info!("People: {:?}", people);

    Template::render(
        "people",
        context! {
            title: "People",
            people,
        },
    )
}

#[post("/add-person", data = "<input>")]
async fn post_add_person(dbh: &State<Surreal<Client>>, input: Form<AddPerson<'_>>) -> Template {
    let name = input.name.trim();
    rocket::info!("Add  person called '{name}'");
    let person = db::add_person(dbh, name).await.unwrap();

    Template::render(
        "person_added",
        context! {
            title: "Person added",
            person,
        },
    )
}

#[get("/person/<id>")]
async fn get_person(dbh: &State<Surreal<Client>>, id: String) -> Option<Template> {
    if let Some((person, owned_groups, memberships)) =
        db::get_person_with_groups(dbh, &id).await.unwrap()
    {
        return Some(Template::render(
            "person",
            context! {
                title: person.name.clone(),
                person,
                owned_groups,
                memberships,
            },
        ));
    }

    None
}

#[get("/groups")]
async fn get_groups(dbh: &State<Surreal<Client>>) -> Template {
    let groups = db::get_groups(dbh).await.unwrap();
    rocket::info!("Groups: {:?}", groups);

    Template::render(
        "groups",
        context! {
            title: "Groups",
            groups,
        },
    )
}

#[get("/add-group?<uid>")]
async fn get_add_group(dbh: &State<Surreal<Client>>, uid: String) -> Template {
    let person = db::get_person(dbh, &uid).await.unwrap().unwrap();

    Template::render(
        "add_group",
        context! {
            title: format!("Add Group owned by {}", person.name),
            uid: uid.to_string(),
        },
    )
}

#[post("/add-group", data = "<input>")]
async fn post_add_group(dbh: &State<Surreal<Client>>, input: Form<AddGroup<'_>>) -> Template {
    let name = input.name.trim();
    let uid = input.uid.trim();
    rocket::info!("Add  group called '{name}' to user '{uid}'");
    let group = db::add_group(dbh, name, uid).await.unwrap();

    let person = db::get_person(dbh, uid).await.unwrap().unwrap();

    Template::render(
        "group_added",
        context! {
            title: format!("Group called {} owned by {} was added", name, person.name),
            uid: uid.to_string(),
            group,
        },
    )
}

#[get("/add-membership?<uid>")]
async fn get_add_membership(dbh: &State<Surreal<Client>>, uid: String) -> Template {
    let person = db::get_person(dbh, &uid).await.unwrap().unwrap();

    //let groups = db::get_groups(dbh).await.unwrap();
    let groups = db::get_groups_not(dbh, &uid).await.unwrap();
    //let memberships = db::get_memberships_of_person(dbh, &person.id.to_string()).await.unwrap();

    // remove the groups that the person already owns or is a member of
    let groups = groups
        .into_iter()
        .filter(|group| group.owner.id != person.id.id)
        .collect::<Vec<_>>();
    Template::render(
        "add_membership",
        context! {
            title: format!("Add {} to one of the groups as a member", person.name),
            uid: uid.to_string(),
            groups
        },
    )
}

#[post("/add-membership", data = "<input>")]
async fn post_add_membership(dbh: &State<Surreal<Client>>, input: Form<AddMember<'_>>) -> Template {
    let gid = input.gid.trim();
    let uid = input.uid.trim();
    rocket::info!("Add  person '{uid}' to group '{gid}'");
    let group = db::get_group(dbh, gid).await.unwrap().unwrap();
    let person = db::get_person(dbh, uid).await.unwrap().unwrap();
    db::add_member(dbh, uid, gid).await.unwrap();

    Template::render(
        "added_to_group",
        context! {
            title: format!("'{}' was added to group '{}'", person.name, group.name),
            person,
            group,
        },
    )
}

#[get("/delete-membership?<id>")]
async fn delete_membership(dbh: &State<Surreal<Client>>, id: String) -> Template {
    db::delete_membership(dbh, &id).await.unwrap();

    Template::render(
        "membership_deleted",
        context! {
            title: "Membership deleted",
        },
    )
}

#[get("/memberships")]
async fn get_membership(dbh: &State<Surreal<Client>>) -> Template {
    let memberships = db::get_memberships(dbh).await.unwrap();

    Template::render(
        "memberships",
        context! {
            title: "Memberships",
            memberships
        },
    )
}

#[get("/group/<id>")]
async fn get_group(dbh: &State<Surreal<Client>>, id: String) -> Option<Template> {
    if let Some(group) = db::get_group_with_owner(dbh, &id).await.unwrap() {
        rocket::info!("Group: {}", group.owner.id);
        rocket::info!("Group: {:?}", group);
        return Some(Template::render(
            "group",
            context! {
                title: group.name.clone(),
                group,
            },
        ));
    }

    None
}

#[launch]
fn rocket() -> _ {
    start(String::from("people-and-groups"))
}

fn start(database: String) -> rocket::Rocket<rocket::Build> {
    rocket::build()
        .mount(
            "/",
            routes![
                clear_db,
                delete_membership,
                get_add_membership,
                get_index,
                get_people,
                get_person,
                get_groups,
                get_group,
                get_membership,
                get_add_group,
                post_add_group,
                post_add_membership,
                post_add_person,
            ],
        )
        .manage(Config {
            database: database.clone(),
        })
        .attach(Template::custom(|engines| {
            mytera::customize(&mut engines.tera);
        }))
        .attach(db::fairing(database))
}

#[cfg(test)]
mod tests {
    use super::start;

    use rocket::http::{ContentType, Status};
    use rocket::local::blocking::Client;
    //use scraper::{Html, Selector};

    #[test]
    fn test_main() {
        let client = Client::tracked(start(String::from("test-people-and-groups"))).unwrap();

        let response = client.get("/clear").dispatch();
        assert_eq!(response.status(), Status::Ok);

        // Make sure we can clear the database even if it does not exist
        let response = client.get("/clear").dispatch();
        assert_eq!(response.status(), Status::Ok);

        let response = client.get("/").dispatch();
        assert_eq!(response.status(), Status::Ok);
        let html = response.into_string().unwrap();
        assert!(html.contains(r#"<title>People and Groups</title>"#));
        assert!(html.contains(r#"<div>Number of people: 0</div>"#));
        assert!(html.contains(r#"<div>Number of groups: 0</div>"#));

        let response = client
            .post("/add-person")
            .header(ContentType::Form)
            .body("name=John")
            .dispatch();
        assert_eq!(response.status(), Status::Ok);
        let html = response.into_string().unwrap();
        assert!(html.contains(r#"Person added:"#));
        let document = scraper::Html::parse_document(&html);
        let selector = scraper::Selector::parse("#added").unwrap();
        assert_eq!(
            &document.select(&selector).next().unwrap().inner_html(),
            "John"
        );
        let john_path = &document
            .select(&selector)
            .next()
            .unwrap()
            .attr("href")
            .unwrap();
        let john_id = john_path.split('/').nth(2).unwrap();

        let response = client
            .post("/add-person")
            .header(ContentType::Form)
            .body("name=Mary Ann")
            .dispatch();
        assert_eq!(response.status(), Status::Ok);
        let html = response.into_string().unwrap();
        assert!(html.contains(r#"Person added:"#));
        let document = scraper::Html::parse_document(&html);
        let selector = scraper::Selector::parse("#added").unwrap();
        assert_eq!(
            &document.select(&selector).next().unwrap().inner_html(),
            "Mary Ann"
        );
        let mary_ann_path = &document
            .select(&selector)
            .next()
            .unwrap()
            .attr("href")
            .unwrap();
        let mary_ann_id = mary_ann_path.split('/').nth(2).unwrap();

        let response = client.get("/people").dispatch();
        assert_eq!(response.status(), Status::Ok);
        let html = response.into_string().unwrap();
        assert!(html.contains(r#">John</a></li>"#));
        assert!(html.contains(r#">Mary Ann</a></li>"#));

        let response = client.get("/").dispatch();
        assert_eq!(response.status(), Status::Ok);
        let html = response.into_string().unwrap();
        assert!(html.contains(r#"<title>People and Groups</title>"#));
        assert!(html.contains(r#"<div>Number of people: 2</div>"#));
        assert!(html.contains(r#"<div>Number of groups: 0</div>"#));

        let response = client.get(john_path.to_string()).dispatch();
        assert_eq!(response.status(), Status::Ok);
        let html = response.into_string().unwrap();
        assert!(html.contains(r#"<h2>Name: John</h2>"#));
        let expected = format!(r#"<div><a  href="/add-group?uid={john_id}">add group</a></div>"#);
        assert!(html.contains(&expected));

        let response = client.get(format!("/add-group?uid={john_id}")).dispatch();
        assert_eq!(response.status(), Status::Ok);
        let html = response.into_string().unwrap();
        assert!(html.contains(r#"<title>Add Group owned by John</title>"#));

        let response = client
            .post("/add-group")
            .header(ContentType::Form)
            .body(format!("name=group one&uid={mary_ann_id}"))
            .dispatch();
        assert_eq!(response.status(), Status::Ok);
        let html = response.into_string().unwrap();
        println!("html: {}", html);
        assert!(html.contains(r#"<h1>Group called group one owned by Mary Ann was added</h1>"#));
        assert!(html.contains(r#"Group added:"#));

        let document = scraper::Html::parse_document(&html);
        let selector = scraper::Selector::parse("#added").unwrap();
        assert_eq!(
            &document.select(&selector).next().unwrap().inner_html(),
            "group one"
        );
        let group_one_path = &document
            .select(&selector)
            .next()
            .unwrap()
            .attr("href")
            .unwrap();
        let _group_one_id = group_one_path.split('/').nth(2).unwrap();

        let response = client.get(group_one_path.to_string()).dispatch();
        assert_eq!(response.status(), Status::Ok);
        let html = response.into_string().unwrap();
        assert!(html.contains(r#"<h2>Name: group one</h2>"#));
        let expected =
            format!(r#"<h2>Owner name: <a href="/person/{mary_ann_id}">Mary Ann</a></h2>"#);
        assert!(html.contains(&expected));
    }
}
}
[debug]
port=8001

Rocket Userid in path

  • TODO
#![allow(unused)]
fn main() {
#[macro_use]
extern crate rocket;

#[cfg(test)]
mod tests;

use rocket::response::content;

use rocket::request::FromParam;
#[derive(Debug)]
struct User {
    uid: usize,
}

impl<'r> FromParam<'r> for User {
    type Error = &'r str;

    fn from_param(param: &'r str) -> Result<Self, Self::Error> {
        rocket::info!("from_param: {:?}", param);
        match param.parse::<usize>() {
            Ok(uid) => {
                if uid < 10000 {
                    Ok(Self { uid })
                } else {
                    Err("bad uid")
                }
            }
            Err(_) => Err("not a usize"),
        }
    }
}

#[get("/")]
fn index() -> content::RawHtml<String> {
    let html = String::from(
        r#"
    <a href="/user/42">id 42</a><br>
    <a href="/user/10001">id 10001</a> (not in database)<br>
    <a href="/user/text">text</a><br>
    "#,
    );
    content::RawHtml(html)
}

#[get("/user/<user>")]
fn user(user: User) -> content::RawHtml<String> {
    rocket::info!("slug: {:?}", user);

    let html = format!("uid: {}", user.uid);

    content::RawHtml(html)
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index, user])
}
}
#![allow(unused)]
fn main() {
use rocket::http::Status;
use rocket::local::blocking::Client;

#[test]
fn check_index() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );
    let html = response.into_string().unwrap();
    assert!(html.contains(r#"<a href="/blog/main">main</a><br>"#));
    assert!(html.contains(r#"<a href="/blog/missing">missing</a><br>"#));
}

#[test]
fn check_main() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/blog/main").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );
    let html = response.into_string().unwrap();
    //println!("{html}");
    assert!(html.contains(r#"content: Main page"#));
}

#[test]
fn check_about() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/blog/about").dispatch();

    assert_eq!(response.status(), Status::Ok);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );
    let html = response.into_string().unwrap();
    assert!(html.contains(r#"content: About page"#));
}

#[test]
fn check_missing_page() {
    let client = Client::tracked(super::rocket()).unwrap();
    let response = client.get("/blog/other").dispatch();

    assert_eq!(response.status(), Status::UnprocessableEntity);
    assert_eq!(
        response.headers().get_one("Content-Type").unwrap(),
        "text/html; charset=utf-8"
    );
    let html = response.into_string().unwrap();
    println!("{html}");
    assert!(html.contains(r#"<h1>422: Unprocessable Entity</h1>"#));
}
}