10

Basic CRUD with rust using tide - refactoring

 3 years ago
source link: https://dev.to/pepoviola/basic-crud-with-rust-using-tide-refactoring-2meb
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Basic CRUD with rust using tide - refactoring

Jul 11 Originally published at javierviola.com

・7 min read

Basic CRUD api with Rust and Tide (7 Part Series)

In the last post I started a basic crud using tide and we end up with a simple api that allow us to store dinosaurs information.

Starting from there, let's clean the code a little bit to be more organized. First, we had a closure in every route (let's call it endpoint from here) and will be more clear if we extract that to functions.

async fn dinos_create(mut req: Request<State>) -> tide::Result {
    let dino: Dino = req.body_json().await?;
    // let get a mut ref of our store ( hashMap )
    let mut dinos = req.state().dinos.write().await;
    dinos.insert(String::from(&dino.name), dino.clone());
    let mut res = Response::new(201);
    res.set_body(Body::from_json(&dino)?);
    Ok(res)
}

async fn dinos_list(req: tide::Request<State>) -> tide::Result {
    let dinos = req.state().dinos.read().await;
    // get all the dinos as a vector
    let dinos_vec: Vec<Dino> = dinos.values().cloned().collect();
    let mut res = Response::new(200);
    res.set_body(Body::from_json(&dinos_vec)?);
    Ok(res)
}


( ... )

    app.at("/dinos")
        .post(dinos_create)
        .get(dinos_list);


Enter fullscreen modeExit fullscreen mode

We moved the closures for this two endpoints to its own functions, let's run the tests to make sure we didn't break anything.

$ cargo test

    Finished test [unoptimized + debuginfo] target(s) in 12.48s
     Running target/debug/deps/tide_basic_crud-3d6db2bae3cd08a5

running 5 tests
test delete_dino ... ok
test index_page ... ok
test list_dinos ... ok
test create_dino ... ok
test update_dino ... ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Enter fullscreen modeExit fullscreen mode

Great! works as expected. We can move now the rest of the endpoints

    app.at("/dinos/:name")
        .get( dinos_get )
        .put( dinos_update )
        .delete( dinos_delete );
Enter fullscreen modeExit fullscreen mode

Awesome, but now we have five dinos_ functions in the main file. Let's refactor this to be more organized

First, let's create a new struct to represent a rest entity with a base_path field.

struct RestEntity {
    base_path: String,
}
Enter fullscreen modeExit fullscreen mode

And implement the same methods we had earlier

impl RestEntity {
    async fn create(mut req: Request<State>) -> tide::Result {
        let dino: Dino = req.body_json().await?;
        // let get a mut ref of our store ( hashMap )
        let mut dinos = req.state().dinos.write().await;
        dinos.insert(String::from(&dino.name), dino.clone());
        let mut res = Response::new(201);
        res.set_body(Body::from_json(&dino)?);
        Ok(res)
    }

    async fn list(req: tide::Request<State>) -> tide::Result {
        let dinos = req.state().dinos.read().await;
        // get all the dinos as a vector
        let dinos_vec: Vec<Dino> = dinos.values().cloned().collect();
        let mut res = Response::new(200);
        res.set_body(Body::from_json(&dinos_vec)?);
        Ok(res)
    }

    async fn get(req: tide::Request<State>) -> tide::Result {
        let mut dinos = req.state().dinos.write().await;
        let key: String = req.param("id")?;
        let res = match dinos.entry(key) {
            Entry::Vacant(_entry) => Response::new(404),
            Entry::Occupied(entry) => {
                let mut res = Response::new(200);
                res.set_body(Body::from_json(&entry.get())?);
                res
            }
        };
        Ok(res)
    }

    async fn update(mut req: tide::Request<State>) -> tide::Result {
        let dino_update: Dino = req.body_json().await?;
        let mut dinos = req.state().dinos.write().await;
        let key: String = req.param("id")?;
        let res = match dinos.entry(key) {
            Entry::Vacant(_entry) => Response::new(404),
            Entry::Occupied(mut entry) => {
                *entry.get_mut() = dino_update;
                let mut res = Response::new(200);
                res.set_body(Body::from_json(&entry.get())?);
                res
            }
        };
        Ok(res)
    }

    async fn delete(req: tide::Request<State>) -> tide::Result {
        let mut dinos = req.state().dinos.write().await;
        let key: String = req.param("id")?;
        let deleted = dinos.remove(&key);
        let res = match deleted {
            None => Response::new(404),
            Some(_) => Response::new(204),
        };
        Ok(res)
    }
}
Enter fullscreen modeExit fullscreen mode

Now we can create a helper function that allow us to register rest like entities to our server, registering five different endpoints to handle the list/create/read/update/delete operations.

fn register_rest_entity(app: &mut Server<State>, entity: RestEntity) {
    app.at(&entity.base_path)
        .get(RestEntity::list)
        .post(RestEntity::create);

    app.at(&format!("{}/:id", entity.base_path))
        .get(RestEntity::get)
        .put(RestEntity::update)
        .delete(RestEntity::delete);
}
Enter fullscreen modeExit fullscreen mode

And in the server we just need to create a new instance of the struct with the desired base_path and call the helper fn

    let dinos_endpoint = RestEntity {
        base_path: String::from("/dinos"),
    };

    register_rest_entity(&mut app, dinos_endpoint);
Enter fullscreen modeExit fullscreen mode

Great, let's just run the test to ensure that all the operations are still working...

cargo test
   Compiling tide-basic-crud v0.1.0
     Running target/debug/deps/tide_basic_crud-3d6db2bae3cd08a5

running 5 tests
test delete_dino ... ok
test list_dinos ... ok
test create_dino ... ok
test index_page ... ok
test update_dino ... ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Enter fullscreen modeExit fullscreen mode

Awesome, we just create a nice abstraction that allow us to create easily more rest like entities and implement the basic operations.

That's all for today, in the next iteration I will try to move away from the HashMap and persist the entities information in a db.

As always, I write this as a learning journal and there could be another more elegant and correct way to do it and any feedback is welcome.

I leave here the repo of this example and the pr of the refactor.

Thanks!

blog/content/post on  blog [$!?] on ☁️ (us-east-1)

blog/content/post on  blog [$!?] on ☁️ (us-east-1)

blog/content/post on  blog [$!?] on ☁️ (us-east-1)

blog/content/post on  blog [$!?] on ☁️ (us-east-1)

blog/content/post on  blog [$!?] on ☁️ (us-east-1)

blog/content/post on  blog [$!?] on ☁️ (us-east-1)

blog/content/post on  blog [$!?] on ☁️ (us-east-1)

blog/content/post on  blog [$!?] on ☁️ (us-east-1)

blog/content/post on  blog [$!?] on ☁️ (us-east-1)

blog/content/post on  blog [$!?] on ☁️ (us-east-1)

blog/content/post on  blog [$!?] on ☁️ (us-east-1)

blog/content/post on  blog [$!?] on ☁️ (us-east-1)

blog/content/post on  blog [$!?] on ☁️ (us-east-1)

blog/content/post on  blog [$!?] on ☁️ (us-east-1)

❯ cat basic-crud-with-rust-using-tide-refactoring.md

layout: blog
title: "Basic CRUD with rust using tide - refactoring"
tags:

  • notes
  • http_rs
  • tide-basic-crud date: 2020-10-03T19:20:36.901Z --- In the last post I started a basic crud using tide and we end up with a simple api that allow us to store dinosaurs information.

Starting from there, let's clean the code a little bit to be more organized. First, we had a closure in every route (let's call it endpoint from here) and will be more clear if we extract that to functions.

async fn dinos_create(mut req: Request<State>) -> tide::Result {
    let dino: Dino = req.body_json().await?;
    // let get a mut ref of our store ( hashMap )
    let mut dinos = req.state().dinos.write().await;
    dinos.insert(String::from(&dino.name), dino.clone());
    let mut res = Response::new(201);
    res.set_body(Body::from_json(&dino)?);
    Ok(res)
}

async fn dinos_list(req: tide::Request<State>) -> tide::Result {
    let dinos = req.state().dinos.read().await;
    // get all the dinos as a vector
    let dinos_vec: Vec<Dino> = dinos.values().cloned().collect();
    let mut res = Response::new(200);
    res.set_body(Body::from_json(&dinos_vec)?);
    Ok(res)
}


( ... )

    app.at("/dinos")
        .post(dinos_create)
        .get(dinos_list);


Enter fullscreen modeExit fullscreen mode

We moved the closures for this two endpoints to its own functions, let's run the tests to make sure we didn't break anything.

$ cargo test

    Finished test [unoptimized + debuginfo] target(s) in 12.48s
     Running target/debug/deps/tide_basic_crud-3d6db2bae3cd08a5

running 5 tests
test delete_dino ... ok
test index_page ... ok
test list_dinos ... ok
test create_dino ... ok
test update_dino ... ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Enter fullscreen modeExit fullscreen mode

Great! works as expected. We can move now the rest of the endpoints

    app.at("/dinos/:name")
        .get( dinos_get )
        .put( dinos_update )
        .delete( dinos_delete );
Enter fullscreen modeExit fullscreen mode

Awesome, but now we have five dinos_ functions in the main file. Let's refactor this to be more organized

First, let's create a new struct to represent a rest entity with a base_path field.

struct RestEntity {
    base_path: String,
}
Enter fullscreen modeExit fullscreen mode

And implement the same methods we had earlier

impl RestEntity {
    async fn create(mut req: Request<State>) -> tide::Result {
        let dino: Dino = req.body_json().await?;
        // let get a mut ref of our store ( hashMap )
        let mut dinos = req.state().dinos.write().await;
        dinos.insert(String::from(&dino.name), dino.clone());
        let mut res = Response::new(201);
        res.set_body(Body::from_json(&dino)?);
        Ok(res)
    }

    async fn list(req: tide::Request<State>) -> tide::Result {
        let dinos = req.state().dinos.read().await;
        // get all the dinos as a vector
        let dinos_vec: Vec<Dino> = dinos.values().cloned().collect();
        let mut res = Response::new(200);
        res.set_body(Body::from_json(&dinos_vec)?);
        Ok(res)
    }

    async fn get(req: tide::Request<State>) -> tide::Result {
        let mut dinos = req.state().dinos.write().await;
        let key: String = req.param("id")?;
        let res = match dinos.entry(key) {
            Entry::Vacant(_entry) => Response::new(404),
            Entry::Occupied(entry) => {
                let mut res = Response::new(200);
                res.set_body(Body::from_json(&entry.get())?);
                res
            }
        };
        Ok(res)
    }

    async fn update(mut req: tide::Request<State>) -> tide::Result {
        let dino_update: Dino = req.body_json().await?;
        let mut dinos = req.state().dinos.write().await;
        let key: String = req.param("id")?;
        let res = match dinos.entry(key) {
            Entry::Vacant(_entry) => Response::new(404),
            Entry::Occupied(mut entry) => {
                *entry.get_mut() = dino_update;
                let mut res = Response::new(200);
                res.set_body(Body::from_json(&entry.get())?);
                res
            }
        };
        Ok(res)
    }

    async fn delete(req: tide::Request<State>) -> tide::Result {
        let mut dinos = req.state().dinos.write().await;
        let key: String = req.param("id")?;
        let deleted = dinos.remove(&key);
        let res = match deleted {
            None => Response::new(404),
            Some(_) => Response::new(204),
        };
        Ok(res)
    }
}
Enter fullscreen modeExit fullscreen mode

Now we can create a helper function that allow us to register rest like entities to our server, registering five different endpoints to handle the list/create/read/update/delete operations.

fn register_rest_entity(app: &mut Server<State>, entity: RestEntity) {
    app.at(&entity.base_path)
        .get(RestEntity::list)
        .post(RestEntity::create);

    app.at(&format!("{}/:id", entity.base_path))
        .get(RestEntity::get)
        .put(RestEntity::update)
        .delete(RestEntity::delete);
}
Enter fullscreen modeExit fullscreen mode

And in the server we just need to create a new instance of the struct with the desired base_path and call the helper fn

    let dinos_endpoint = RestEntity {
        base_path: String::from("/dinos"),
    };

    register_rest_entity(&mut app, dinos_endpoint);
Enter fullscreen modeExit fullscreen mode

Great, let's just run the test to ensure that all the operations are still working...

cargo test
   Compiling tide-basic-crud v0.1.0
     Running target/debug/deps/tide_basic_crud-3d6db2bae3cd08a5

running 5 tests
test delete_dino ... ok
test list_dinos ... ok
test create_dino ... ok
test index_page ... ok
test update_dino ... ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Enter fullscreen modeExit fullscreen mode

Awesome, we just create a nice abstraction that allow us to create easily more rest like entities and implement the basic operations.

That's all for today, in the next iteration I will try to move away from the HashMap and persist the entities information in a db.

As always, I write this as a learning journal and there could be another more elegant and correct way to do it and any feedback is welcome.

I leave here the repo of this example and the pr of the refactor.

Thanks!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK