A Simple Crud on Rust (With Rocket.rs and Diesel.rs)
Luís Von Müller
Senior Software Developer (Rust/TS/JS/PHP). Open Source enthusiast;
?? If you want, you can have a better time reading this not on LinkedIn (Here we don`t have cool features as we have in medium, so heres the medium link pay-wall free: https://medium.com/swlh/a-simple-crud-on-rust-with-rocket-rs-and-diesel-rs-e885672cb23d?sk=a78a73580666e0ba7ea858d0d556d20e )
This tutorial shows you how to create a simple CRUD on Rust Language (with rocket.rs as our web server and diesel.rs being our ORM for PostgreSQL.)
?? To follow this, you must have:
- Rust nightly version & cargo and as well it must be on your path (you can check it by tipping on any terminal: $ cargo — version )
- PostgreSQL (the LATEST stable version is recommended)
- PgAdmin, because it has a nice interface for the DataBase management System and will make things a lot easier to debug..
After setting all this up, we’re able to start, first:
$ cargo new heroes
Then change to the directory of the project into this new folder by typing: “cd heroes/”
And you will see something like this:
After that, open this folder in your favorite text editor, like vscode. If you do have vs-code too, you can open it fast by typing: code ./
The project inside VSCODE
Let’s install the diesel_cli. The diesel_cli will manage our migration process. On your terminal, do:
$ cargo install diesel_cli --no-default-features --features postgres
This will make the diesel_cli available for use. Since we have diesel installed, let’s create a “.env” file that will hold our environment information, like our database connection. To enter the database connection you must run the following command:
$ echo DATABASE_URL=postgres://username:password@localhost/heroes > .env
After that, a new file will pop on your folder (.env). If everything is ok inside it, now we can run:
$ diesel setup
After that, on our PgAdmin session, we’ll be able to see our new database, “heroes”:
Since we have a database, the only thing missing is our table that will hold heroes information. For the sake of good practices we will use diesel migration system to instance it:
$ diesel migration generate heroes
By running it, another two files will pop on our project folder inside migrations: the “up.sql” that will bring our table structure up and the “down.sql” that will drop our table (it must be the inverse of up).
For up.sql:
CREATE TABLE heroes ( id SERIAL PRIMARY KEY, fantasy_name VARCHAR NOT NULL, real_name VARCHAR NULL, spotted_photo TEXT NOT NULL, strength_level INT NOT NULL DEFAULT 0
);
And for down.sql, just:
DROP TABLE heroes;
And to migrate it to our database… just:
$ diesel migration run
You also must need to create a folder named: “templates” on your project root, and for sure, there must be a folder named “imgs” to hold our leaked heroes photos. Your folder tree should look like this:
So far so good, we can start with rust stuff.
?? First things first.
Let import what we will be needing to follow up inside Cargo.toml (our dependencies).
[package] name = "heroes" version = "0.1.0" authors = ["luisvonmuller <[email protected]>"] edition = "2018" [dependencies] rocket = "0.4.5" rocket_codegen = "0.4.5" diesel = { version = "1.4.5", features = ["postgres"] } dotenv = "0.15.0" rocket-multipart-form-data = "0.9.5" serde = { version = "1.0", features = ["derive"] } [dependencies.rocket_contrib] version = "0.4.5" default-features = false features = ["handlebars_templates"]
- dotenv stands for the library that makes easier to read our .env file.
- rocket provides our webserver, the rocket code gen and the multipart will give us the libraries needed to use images and templates via rocket.
- diesel (ORM)
Our main.rs
#![feature(proc_macro_hygiene, decl_macro)] /* Our extern crates */ #[macro_use] extern crate diesel; #[macro_use] extern crate rocket; extern crate dotenv; /* Importing functions */ use diesel::pg::PgConnection; use diesel::Connection; use dotenv::dotenv; use std::env; use rocket_contrib::templates::Template; /* Static files imports */ use std::path::{Path, PathBuf}; use rocket::response::NamedFile; /* Declaring a module, just for separating things better */ pub mod heroes; /* Will hold our data structs */ pub mod models; /* auto-generated table macros */ pub mod schema; /* This will return our pg connection to use with diesel */ pub fn establish_connection() -> PgConnection { dotenv().ok(); let database_url = env::var("DATABASE_URL") .expect("DATABASE_URL must be set"); PgConnection::establish(&database_url) .expect(&format!("Error connecting to {}", database_url)) } /* Static files Handler, will give back our heroes images */ #[get("/imgs/<file..>")] fn assets(file: PathBuf) -> Option<NamedFile> { NamedFile::open(Path::new("imgs/").join(file)).ok() } fn main() { rocket::ignite().mount("/", routes![ assets, heroes::list, heroes::new, heroes::insert, heroes::update, heroes::process_update, heroes::delete ]).attach(Template::fairing()).launch();
}
This file declares a lot of stuff, we must take a closer look at our modules declarations:
- “schema.rs” : The auto-generated table macro that diesel gives us when we run migrations. (You don’t need to create it, diesel will.)
- “models.rs”: This one you have to manually create, will store our database related data structures.
/* Import macros and others */ use crate::schema::*; /* For beeing able to serialize */ use serde::Serialize; #[derive(Debug, Queryable, Serialize)] pub struct Hero { pub id: i32, pub fantasy_name: String, pub real_name: Option<String>, pub spotted_photo: String, pub strength_level: i32, } #[derive(Debug, Insertable, AsChangeset)] #[table_name="heroes"] pub struct NewHero<'x> { pub fantasy_name: &'x str, pub real_name: Option<&'x str>, pub spotted_photo: String, pub strength_level: i32,
}
- “heroes.rs”: This one will hold our back-end processing (CRUD)
/* To be able to return Templates */ use rocket_contrib::templates::Template; use std::collections::HashMap; /* Diesel query builder */ use diesel::prelude::*; /* Database macros */ use crate::schema::*; /* Database data structs (Hero, NewHero) */ use crate::models::*; /* To be able to parse raw forms */ use rocket::http::ContentType; use rocket::Data; use rocket_multipart_form_data::{ MultipartFormData, MultipartFormDataField, MultipartFormDataOptions, }; /* Flash message and redirect */ use rocket::request::FlashMessage; use rocket::response::{Flash, Redirect}; /* List our inserted heroes */ #[get("/")] pub fn list(flash: Option<FlashMessage>) -> Template { let mut context = HashMap::new(); /* Get all our heroes from database */ let heroes: Vec<Hero> = heroes::table .select(heroes::all_columns) .load::<Hero>(&crate::establish_connection()) .expect("Whoops, like this went bananas!"); /* Insert on the template rendering context our new heroes vec */ if let Some(ref msg) = flash { context.insert("data", (heroes, msg.msg())); } else { context.insert("data", (heroes, "Listing heroes...")); } /* Return the template */ Template::render("list", &context) } #[get("/new")] pub fn new(flash: Option<FlashMessage>) -> Template { let mut context = HashMap::new(); if let Some(ref msg) = flash { context.insert("flash", msg.msg()); } Template::render("new", context) } #[post("/insert", data ?= "<hero_data>")] pub fn insert(content_type: &ContentType, hero_data: Data) -> Flash<Redirect> { /* File system */ use std::fs; /* First we declare what we will be accepting on this form */ let mut options = MultipartFormDataOptions::new(); options.allowed_fields = vec![ MultipartFormDataField::file("spotted_photo"), MultipartFormDataField::text("fantasy_name"), MultipartFormDataField::text("real_name"), MultipartFormDataField::text("strength_level"), ]; /* If stuff matches, do stuff */ let multipart_form_data = MultipartFormData::parse(content_type, hero_data, options); match multipart_form_data { Ok(form) => { /* If everything is ok, we will move the image and the insert into our datatabase */ let hero_img = match form.files.get("spotted_photo") { Some(img) => { let file_field = &img[0]; let _content_type = &file_field.content_type; let _file_name = &file_field.file_name; let _path = &file_field.path; /* Lets split name to get format */ let format: Vec<&str> = _file_name.as_ref().unwrap().split('.').collect(); /* Reparsing the fileformat */ /* Path parsing */ let absolute_path: String = format!("imgs/{}", _file_name.clone().unwrap()); fs::copy(_path, &absolute_path).unwrap(); Some(format!("imgs/{}", _file_name.clone().unwrap())) } None => None, }; /* Insert our form data inside our database */ let insert = diesel::insert_into(heroes::table) .values(NewHero { fantasy_name: match form.texts.get("fantasy_name") { Some(value) => &value[0].text, None => "No Name.", }, real_name: match form.texts.get("real_name") { Some(content) => Some(&content[0].text), None => None, }, spotted_photo: hero_img.unwrap(), strength_level: match form.texts.get("strength_level") { Some(level) => level[0].text.parse::<i32>().unwrap(), None => 0, }, }) .execute(&crate::establish_connection()); match insert { Ok(_) => Flash::success( Redirect::to("/"), "Success! We got a new Hero on our database!", ), Err(err_msg) => Flash::error( Redirect::to("/new"), format!( "Houston, We had problems while inserting things into our database ... {}", err_msg ), ), } } Err(err_msg) => { /* Falls to this patter if theres some fields that isn't allowed or bolsonaro rules this code */ Flash::error( Redirect::to("/new"), format!( "Houston, We have problems parsing our form... Debug info: {}", err_msg ), ) } } } #[get("/update/<id>")] pub fn update(id: i32) -> Template { let mut context = HashMap::new(); let hero_data = heroes::table .select(heroes::all_columns) .filter(heroes::id.eq(id)) .load::<Hero>(&crate::establish_connection()) .expect("Something happned while retrieving the hero of this id"); context.insert("hero", hero_data); Template::render("update", &context) } #[post("/update", data ?= "<hero_data>")] pub fn process_update(content_type: &ContentType, hero_data: Data) -> Flash<Redirect> { /* File system */ use std::fs; /* First we declare what we will be accepting on this form */ let mut options = MultipartFormDataOptions::new(); options.allowed_fields = vec![ MultipartFormDataField::file("spotted_photo"), MultipartFormDataField::text("id"), MultipartFormDataField::text("fantasy_name"), MultipartFormDataField::text("real_name"), MultipartFormDataField::text("strength_level"), ]; /* If stuff matches, do stuff */ let multipart_form_data = MultipartFormData::parse(content_type, hero_data, options); match multipart_form_data { Ok(form) => { /* If everything is ok, we will move the image and the insert into our datatabase */ let hero_img = match form.files.get("spotted_photo") { Some(img) => { let file_field = &img[0]; let _content_type = &file_field.content_type; let _file_name = &file_field.file_name; let _path = &file_field.path; /* Lets split name to get format */ let format: Vec<&str> = _file_name.as_ref().unwrap().split('.').collect(); /* Reparsing the fileformat */ /* Path parsing */ let absolute_path: String = format!("imgs/{}", _file_name.clone().unwrap()); fs::copy(_path, &absolute_path).unwrap(); Some(format!("imgs/{}", _file_name.clone().unwrap())) } None => None, }; /* Insert our form data inside our database */ let insert = diesel::update( heroes::table.filter( heroes::id.eq(form.texts.get("id").unwrap()[0] .text .parse::<i32>() .unwrap()), ), ) .set(NewHero { fantasy_name: match form.texts.get("fantasy_name") { Some(value) => &value[0].text, None => "No Name.", }, real_name: match form.texts.get("real_name") { Some(content) => Some(&content[0].text), None => None, }, spotted_photo: hero_img.unwrap(), strength_level: match form.texts.get("strength_level") { Some(level) => level[0].text.parse::<i32>().unwrap(), None => 0, }, }) .execute(&crate::establish_connection()); match insert { Ok(_) => Flash::success( Redirect::to("/"), "Success! We got a new Hero on our database!", ), Err(err_msg) => Flash::error( Redirect::to("/new"), format!( "Houston, We had problems while inserting things into our database ... {}", err_msg ), ), } } Err(err_msg) => { /* Falls to this patter if theres some fields that isn't allowed or bolsonaro rules this code */ Flash::error( Redirect::to("/new"), format!( "Houston, We have problems parsing our form... Debug info: {}", err_msg ), ) } } } #[get("/delete/<id>")] pub fn delete(id: i32) -> Flash<Redirect> { diesel::delete(heroes::table.filter(heroes::id.eq(id))) .execute(&crate::establish_connection()) .expect("Ops, we can't delete this."); Flash::success(Redirect::to("/"), "Yey! The hero was deleted.")
}
??????
And we will have our templates too, since they’re 3 files, and this small post will turn into a big one if I throw them here too, I’ll give the link to the repo then you can get them at: https://github.com/luisvonmuller/heroes-crud-rust/tree/master/templates
??????
After all this, you can just run:
$ cargo run
And you will be able to see the result on your favorite web browser, located at: https://locahost:8000/.
This is it — Strokes, the.
?? Would be great if you could follow me on twitter: @luisvonmuller
Soon I'll post it on portuguese too. ??
?? I'm open to new opportunities, mail me at: [email protected]
Analista de conteúdo na Von Muller Software
4 年Muito show...