Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#2 Create sample web app using Rocket framework for Rust #6

Merged
merged 13 commits into from
Oct 15, 2024
24 changes: 24 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Rust

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

env:
CARGO_TERM_COLOR: always

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Build
working-directory: ./rocketapp
run: cargo build --verbose
- name: Run tests
working-directory: ./rocketapp
run: cargo test --verbose
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

/demo/target
/demo/.idea
/rocketapp/.idea
/rocketapp/target
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# ironoc-rustt

[Demo read me](demo/README.md)

[Rocket app read me](rocketapp/README.md)
8 changes: 8 additions & 0 deletions rocketapp/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "rocketapp"
version = "0.1.0"
edition = "2021"

[dependencies]
rocket = "0.5.0"
rocket_dyn_templates = { version = "0.1.0", features = ["handlebars", "tera"] }
50 changes: 45 additions & 5 deletions rocketapp/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# rocketapp

## Run steps with Rust package manager 'Cargo'
[![Rust](https://github.com/conorheffron/ironoc-rustt/actions/workflows/rust.yml/badge.svg)](https://github.com/conorheffron/ironoc-rustt/actions/workflows/rust.yml)

## Run steps with Rust package manager 'Cargo':
```shell
[email protected]> cargo build
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.19s
Expand All @@ -11,7 +13,7 @@ note: pass `--verbose` to see 37 unchanged dependencies behind latest
[email protected]> cargo build
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
[email protected]> cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.16s
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.40s
Running `target/debug/rocketapp`
🔧 Configured for debug.
>> address: 127.0.0.1
Expand All @@ -34,23 +36,61 @@ [email protected]> cargo run
>> (hello) GET /hi?<name>
>> (FileServer: public) GET /public/<path..> [10]
📡 Fairings:
>> Shield (liftoff, response, singleton)
>> Templating (ignite, liftoff, request)
>> Shield (liftoff, response, singleton)
🛡️ Shield:
>> Permissions-Policy: interest-cohort=()
>> X-Content-Type-Options: nosniff
>> X-Frame-Options: SAMEORIGIN
>> Permissions-Policy: interest-cohort=()
📐 Templating:
>> directory: templates
>> engines: ["tera", "hbs"]
🚀 Rocket has launched from http://127.0.0.1:8000
GET / text/html:
>> Matched: (root) GET /
>> Outcome: Success(200 OK)
>> Response succeeded.
GET /public/css/pico.min.css text/css:
>> Matched: (FileServer: public) GET /public/<path..> [10]
>> Outcome: Success(200 OK)
>> Response succeeded.
POST / multipart/form-data:
>> Matched: (create) POST /
>> Outcome: Success(303 See Other)
>> Response succeeded.
GET /hi?name=Conor%20Heffron text/html:
>> Matched: (hello) GET /hi?<name>
>> Outcome: Success(200 OK)
>> Response succeeded.
GET /public/css/pico.min.css text/css:
>> Matched: (FileServer: public) GET /public/<path..> [10]
>> Outcome: Success(200 OK)
>> Response succeeded.
GET / text/html:
>> Matched: (root) GET /
>> Outcome: Success(200 OK)
>> Response succeeded.
GET /public/css/pico.min.css text/css:
>> Matched: (FileServer: public) GET /public/<path..> [10]
>> Outcome: Success(200 OK)
>> Response succeeded.
```

## Verify rocket app is running:
### Hi end-point / view that takes name request parameter.
```
http://localhost:8000/hi?name=conair
```

![hi-endp-with-req-param2](../screenshots/rocket/hi-endp-with-req-param2.png?raw=true "Endpoint with Request Parameter")

### Home page / form view.
```
http://localhost:8000/
```
```

![err-form-view](../screenshots/rocket/err-form-view.png?raw=true "Error Form View for Empty Fields")

![err-form-view2](../screenshots/rocket/err-form-view2.png?raw=true "Error Form View for Empty Field")

![form-success-view](../screenshots/rocket/form-success-view.png?raw=true "Form Submit Success View")
4 changes: 4 additions & 0 deletions rocketapp/public/css/pico.min.css

Large diffs are not rendered by default.

54 changes: 54 additions & 0 deletions rocketapp/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
mod models;

use rocket::{get, launch, post, routes, uri};
use rocket::form::{Contextual, Form};
use rocket::fs::{FileServer, Options, relative};
use rocket::request::FlashMessage;
use rocket::response::{Flash, Redirect};
use rocket_dyn_templates::{context, Template};
use crate::models::Person;

#[launch]
fn rocket() -> _ {
rocket::build()
// add templating system
.attach(Template::fairing())
// serve content from disk
.mount("/public", FileServer::new(relative!("/public"), Options::Missing | Options::NormalizeDirs))
// register routes
.mount("/", routes![root, create, hello])
}

#[get("/")]
async fn root() -> Template {
Template::render("root", context! { message: "Hello, Rust"})
}

#[post("/", data = "<form>")]
async fn create(form: Form<Contextual<'_, Person>>) -> Result<Flash<Redirect>, Template> {
if let Some(ref person) = form.value {
let name = format!("{} {}", person.first_name, person.last_name);
let message = Flash::success(Redirect::to(uri!(hello(name))), "It Worked");
return Ok(message);
}

let error_messages: Vec<String> = form.context.errors().map(|error| {
let name = error.name.as_ref().unwrap().to_string();
let description = error.to_string();
format!("'{}' {}", name, description)
}).collect();

Err(Template::render("root", context! {
first_name : form.context.field_value("first_name"),
last_name : form.context.field_value("last_name"),
first_name_error : form.context.field_errors("first_name").count() > 0,
last_name_error : form.context.field_errors("last_name").count() > 0,
errors: error_messages
}))
}

#[get("/hi?<name>")]
async fn hello(name: String, flash: Option<FlashMessage<'_>>) -> Template {
let message = flash.map_or_else(|| String::default(), |msg| msg.message().to_string());
Template::render("hello", context! { name , message })
}
9 changes: 9 additions & 0 deletions rocketapp/src/models.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use rocket::{FromForm};

#[derive(FromForm, Debug)]
pub struct Person {
#[field(validate=len(1..))]
pub(crate) first_name: String,
#[field(validate=len(1..))]
pub(crate) last_name: String,
}
27 changes: 27 additions & 0 deletions rocketapp/templates/hello.html.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark" />
<link rel="stylesheet" href="/public/css/pico.min.css">
<title>Hello {{ name }}!</title>
</head>
<body>
<main class="container">
<dialog open>
<article>
<header>
<a href="/" aria-label="Close" rel="prev"></a>
<p>
<strong>🗓️ {{ message }}!</strong>
</p>
</header>
<p>
Hello {{ name }}
</p>
</article>
</dialog>
</main>
</body>
</html>
46 changes: 46 additions & 0 deletions rocketapp/templates/root.html.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark"/>
<link rel="stylesheet" href="/public/css/pico.min.css">
<title>Hello world!</title>
</head>
<body>
<main class="container">
<h1>Hello Rust!</h1>

{{#if errors }}
<article>
<header>🥺Oh No!</header>
<p>There are some invalid fields in the form</p>
<ul>
{{#each errors}}
<li>{{this}}</li>
{{/each}}
</ul>
</article>
{{/if}}

<form method="POST" enctype="multipart/form-data" action="/">
<input type="text"
name="first_name"
placeholder="First Name"
aria-label="First Name"
value="{{ first_name }}"
aria-invalid="{{ first_name_error}}"
/>

<input type="text" name="last_name"
placeholder="Last Name"
aria-label="Last Name"
value="{{ last_name }}"
aria-invalid="{{ last_name_error}}"
/>

<button type="submit">Say Hello</button>
</form>
</main>
</body>
</html>
Binary file added screenshots/rocket/err-form-view.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/rocket/err-form-view2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/rocket/form-success-view.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/rocket/hi-endp-with-req-param2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.