Rocket in Gotham with Diesel, oh my!

TODO Microservice

  • Get all tasks
  • Add a new task
  • Update a task
  • Delete a task

Rust Frameworks

  • Rocket
  • Gotham
  • Diesel

Diesel orm

diesel cli

$ cargo install diesel_cli
$ diesel setup

Migrations

Create the migrations directory

$ diesel migration generate tasks
CREATE TABLE tasks (
  id SERIAL PRIMARY KEY,
  task VARCHAR NOT NULL,
  completed BOOLEAN NOT NULL DEFAULT 'f'
)
DROP TABLE tasks
up.sql
down.sql

adding the models

model.rs
#[derive(Queryable)]
pub struct Task {
    id: i32,
    task: String,
    completed: bool,
}

#[derive(Insertable)]
#[table_name = "tasks"]
pub struct NewTask {
    task: String,
    completed: bool,
}

generate the schema

$ diesel print-schema > src/schema.rs
schema.rs
table! {
    tasks (id) {
        id -> Integer,
        task -> Varchar,
        completed -> Bool,
    }
}

Adding the functionality

impl Task {
    // ...

    pub fn create(conn: &PgConnection, task: NewTask) {
        use schema::tasks;
        diesel::insert_into(tasks::table)
            .values(&task)
            .execute(conn)
            .expect("Unable to insert");
    }

    // ...
}

Create a new task

Adding the functionality

impl Task {
    // ...

   pub fn all(conn: &PgConnection) -> Vec<Task> {
        use schema::tasks::dsl::*;
        tasks.load::<Task>(conn).expect("Could not load tasks")
    }

    
    // ...
}

Retrieve all tasks

Adding the functionality

impl Task {
    // ...

   pub fn update(conn: &PgConnection, task_id: i32, task_update: Task) -> i32 {
        use schema::tasks::dsl::*;
        diesel::update(tasks.find(task_id))
            .set((
                task.eq(task_update.task),
                completed.eq(task_update.completed),
            ))
            .execute(conn)
            .expect("Failed to update");
        task_update.id
    }

    pub fn delete(conn: &PgConnection, task_id: i32) {
        use schema::tasks::dsl::*;
        diesel::delete(tasks.find(task_id))
            .execute(conn)
            .expect("Failed to delete");
    }

    
    // ...
}

Update and delete tasks

Rocket

Managing database connection

type Pool = r2d2::Pool<ConnectionManager<PgConnection>>;
fn init_pool() -> Pool {
    dotenv::dotenv().ok();
    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
    let manager = ConnectionManager::<PgConnection>::new(database_url);
    r2d2::Pool::new(manager).expect("db pool")
}

pub fn start() {
    rocket::ignite()
        .manage(init_pool())
        .launch();
}

Managing database connection

impl<'a, 'r> FromRequest<'a, 'r> for DbConn {
    type Error = ();

    fn from_request(request: &'a Request<'r>) -> request::Outcome<DbConn, ()> {
        let pool = request.guard::<State<Pool>>()?;
        match pool.get() {
            Ok(conn) => Outcome::Success(DbConn(conn)),
            Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())),
        }
    }
}

Adding request handlers

#[get("/")]
fn get_all_tasks(conn: DbConn) -> Json<Vec<Task>> {
    Json(Task::all(&conn))
}
pub fn start() {
    rocket::ignite()
        .mount("/tasks", routes![get_all_tasks])
        .manage(init_pool())
        .launch();
}

Serializing the data

#[derive(Deserialize, Serialize, Queryable)]
pub struct Task {
    id: i32,
    task: String,
    completed: bool,
}

#[derive(Deserialize, Serialize, Insertable)]
#[table_name = "tasks"]
pub struct NewTask {
    task: String,
    completed: bool,
}

Adding the Remaining Requests

#[post("/", data = "<task>")]
fn create_task(task: Json<NewTask>, conn: DbConn) -> Json<String> {
    Task::create(&conn, task.into_inner());
    Json("Task added".to_owned())
}

#[put("/<id>", data = "<task>")]
fn update_task(id: u32, task: Json<Task>, conn: DbConn) -> Json<i32> {
    let id = Task::update(&conn, id as i32, task.into_inner());
    Json(id)
}

#[delete("/<id>")]
fn delete_task(id: u32, conn: DbConn) -> Json<Value> {
    Task::delete(&conn, id as i32);
    Json(json!({"status": "ok"}))
}

Common ground

Getting it started

use gotham::router::Router;
use gotham::router::builder::*;

fn router -> Router {
    build_simple_router(|route|{
        route.get("/tasks").to(get_all_tasks);
    })
}

pub fn start() {
    let addr = "127.0.0.1:8000";
    println!("Listening for requests at http://{}", addr);
    gotham::start(addr, router())
}

Starting to make a handler

extern crate hyper;
extern crate mime;
extern crate serde_json;

use gotham::state::State;
use gotham::http::response::create_response;
use hyper::{Response, StatusCode};

impl IntoResponse for TaskList {
    fn into_response(self, state: &State) -> Response {
        create_response(
            &state,
            StatusCode::Ok,
            Some((
                serde_json::to_string(&self.list)
                    .expect("serialized product")
                    .into_bytes(),
                mime::APPLICATION_JSON,
            )),
        )
    }
}
// ...

#[derive(Deserialize, Serialize)]
pub struct TaskList {
    pub list: Vec<Task>,
}

// ...
model.rs

making the handler

fn get_all_tasks(state: State) -> (State, TaskList) {
    let tasks = vec![
        Task {
            id: 1,
            task: "Do homework".to_owned(),
            completed: false,
        },
    ];
    (state, tasks)
}

manage the database connection

#[derive(StateData)]
struct PoolState(Pool);
  • Manage through the state
  • Handler signature
  • Middleware injects this into the state

middleware

use gotham::middleware::Middleware;

#[derive(Clone, NewMiddleware)]
struct DbConnMiddleware;

impl Middleware for DbConnMiddleware {
    fn call<Chain>(self, mut state: State, chain: Chain) -> Box<HandlerFuture>
    where
        Chain: FnOnce(State) -> Box<HandlerFuture>,
    {
        if !state.has::<PoolState>() {
            // Initialize it
            state.put(PoolState(init_pool()));
        }
        chain(state)
    }
}

Add to the router

use gotham::pipeline::single::single_pipeline;

fn router() -> Router {
    let (chain, pipelines) = single_pipeline(new_pipeline().add(DbConnMiddleware).build());
    build_router(chain, pipelines, |route| {
        route.get_or_head("/").to(index);
        route.get("/tasks").to(get_all_tasks);
    })
}

Retrieving tasks

fn db_conn(state: &State) -> Option<DbConn> {
    state.borrow::<PoolState>().get().ok().map(|x| DbConn(x))
}

// ...

fn get_all_tasks(state: State) -> (State, TaskList) {
    let conn = db_conn(&state).expect("Failed with DB connection");
    let tasks = TaskList {
        list: Task::all(&conn),
    };
    (state, tasks)
}

Using path variables

#[derive(Deserialize, StateData, StaticResponseExtender)]
struct PathId {
    id: u32,
}
// ... 
route
    .put("/task/:id")
    .with_path_extractor::<PathId>()
    .to(update_task);
route
    .delete("/task/:id")
    .with_path_extractor::<PathId>()
    .to(delete_task);
// ...

Router

Delete a task


fn delete_task(mut state: State) -> (State, Response) {
    let PathId { id } = PathId::take_from(&mut state);
    let conn = db_conn(&state).expect("Failed with DB connection");
    Task::delete(&conn, id as i32);
    let resp = create_response(&state, StatusCode::Ok, None);
    (state, resp)
}

request body

use hyper::Body;

let body = Body::take_from(&mut state)
        .concat2()
        .then(// Do something with the body);
fn body_handler<F>(mut state: State, f: F) -> Box<HandlerFuture>
where
    F: 'static + Fn(String, &State) -> Response,
{
    let body = Body::take_from(&mut state)
        .concat2()
        .then(move |full_body| match full_body {
            Ok(valid_body) => {
                let body_content = String::from_utf8(valid_body.to_vec()).unwrap();
                let res = f(body_content, &mut state);
                future::ok((state, res))
            }
            Err(e) => return future::err((state, e.into_handler_error())),
        });
    Box::new(body)
}

create and update

fn create_task(state: State) -> Box<HandlerFuture> {
    body_handler(state, |s, state| {
        let task = serde_json::from_str(&s).expect("Failed to deserialize");
        let conn = db_conn(state).expect("Failed with DB connection");
        Task::create(&conn, task);
        create_response(state, StatusCode::Ok, None)
    })
}

fn update_task(mut state: State) -> Box<HandlerFuture> {
    let PathId { id } = PathId::take_from(&mut state);
    body_handler(state, move |s, state| {
        let task = serde_json::from_str(&s).expect("Failed to deserialize");
        let conn = db_conn(&state).expect("Failed with DB connection");
        Task::update(&conn, id as i32, task);
        create_response(state, StatusCode::Ok, None)
    })
}

thank you

maccoda

Rocket in Gotham with Diesel, oh my!

By maccoda

Rocket in Gotham with Diesel, oh my!

  • 299