DEV Community

Cover image for REST API with Rust + Warp 4: PUT & DELETE
Roger Torres (he/him/ele)
Roger Torres (he/him/ele)

Posted on

REST API with Rust + Warp 4: PUT & DELETE

That's it, the last methods. In the beginning, I thought it would be the end of the series, but then I realized it needed an additional post, regarding how to manually test it using curl. However, before that, there are still two more methods to be coded. But don't worry, both combined are probably easier than the single ones we handled so far.


Warp 4, Mr. Sulu.

The code for this part is available here.

The PUT method is a mix between insert and change: it creates an entry when the data is not there and updates it when it is.

This behavior is already met by our HashSet; this is exactly how the insert() function works. However, we got to know if we are inserting or changing because the status that is returned got to be different:

  • Code 201 when it is created
  • Code 200 when it is updated

Where did I get this from? It is written here.

With that in mind, I wrote this code:

#[tokio::test]
async fn try_update() {
    let db = models::new_db();
    let api = filters::update_sim(db);

    let response = request()
        .method("PUT")
        .path("/holodeck/1")
        .json(&models::Name{ new_name: String::from("The Big Goodbye")})
        .reply(&api)
        .await;

    assert_eq!(response.status(), StatusCode::CREATED);

    let response = request()
        .method("PUT")
        .path("/holodeck/1")
        .json(&models::Name{ new_name: String::from("The Short Hello")})
        .reply(&api)
        .await;

    assert_eq!(response.status(), StatusCode::OK);
}
Enter fullscreen mode Exit fullscreen mode

How did I knew that 201 was StatusCode::CREATED and 200 was StatusCode::OK? Here.

As you can see, the request is made by sending the parameter id ("1", in this case). Different from GET, this parameter is mandatory. And because the id is already being sent in the URI, the body only contains the name. The reasoning behind this is also in the aforementioned rfc.

Because of this, I implemented a new struct and a new function to get the JSON body.

#[derive(Debug, Deserialize, Serialize)]
pub struct NewName{ pub name: String }
Enter fullscreen mode Exit fullscreen mode
// This code is inside the mod "filters"
fn json_body_put() -> impl Filter<Extract = (models::NewName,), Error = warp::Rejection> + Clone {
    warp::body::content_length_limit(1024 * 16).and(warp::body::json())
}
Enter fullscreen mode Exit fullscreen mode

This is certainly a suboptimal way of doing this. But let's move on anyway; I am saving the excuses about the poor execution of things to the last part.

Now, the filter and the handler.

// This is inside the mod "filters"
pub fn update_sim(db: models::Db) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
    let db_map = warp::any()
        .map(move || db.clone());

    warp::path!("holodeck" / u64)
        .and(warp::put())
        .and(json_body_put())
        .and(db_map)
        .and_then(handlers::handle_update_sim)
}
Enter fullscreen mode Exit fullscreen mode
pub async fn handle_update_sim(id: u64, new: models::NewName, db: models::Db) -> Result<impl warp::Reply, Infallible> {
    // Replaced entry
    if let Some(_) = db.lock().await.replace(Simulation{id, name: new.name}){
        return Ok(warp::reply::with_status(
            format!("Simulation #{} was updated.\n", id), 
            StatusCode::OK,
        ));
    }

    // Create entry
    Ok(warp::reply::with_status(
        format!("Simulation #{} was inserted.\n", id), 
        StatusCode::CREATED,
    ))
}
Enter fullscreen mode Exit fullscreen mode

And that's it!


Red alert

To warp wrap things up, the DELETE method.

As usual, the request is quite simple: it sends the id as a parameter and no body. As a response, we expect code 200 (OK) including a "representation describing the status".

#[tokio::test]
async fn try_delete() {
    let simulation = models::Simulation{
        id: 1, 
        name: String::from("The Big Goodbye!"),
    };

    let db = models::new_db();
    db.lock().await.insert(simulation);

    let api = filters::delete_sim(db);

    let response = request()
        .method("DELETE")
        .path("/holodeck/1")
        .reply(&api)
        .await;

        assert_eq!(response.status(), StatusCode::OK);
}
Enter fullscreen mode Exit fullscreen mode

Hopefully, nothing about the filter implementation seems strange to you:

pub fn delete(db: models::Db) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
    let db_map = warp::any()
        .map(move || db.clone());

    warp::path!("holodeck" / u64)
        .and(warp::delete())
        .and(db_map)
        .and_then(handlers::handle_delete_sim)
}
Enter fullscreen mode Exit fullscreen mode

Since it is the first time we're deleting data, the handler has a unique behavior, but also nothing very different from what has been done so far.

pub async fn handle_delete_sim(id: u64, db: models::Db) -> Result<impl warp::Reply, Infallible> {
    if db.lock().await.remove(&Simulation{id, name: String::new(),}){
        return Ok(warp::reply::with_status(
            format!("Simulation #{} was deleted", id), 
            StatusCode::OK,
        ))
    };

    Ok(warp::reply::with_status(
        format!("No data was deleted."),
        StatusCode::OK,
    ))
}
Enter fullscreen mode Exit fullscreen mode

That should do...

$ cargo test

running 5 tests
test tests::try_delete ... ok
test tests::try_create ... ok
test tests::try_list ... ok
test tests::try_create_duplicates ... ok
test tests::try_update ... ok

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

And it did!


In the next episode of Engaging Warp...

Finally, the last part. We will serve what has been built and curl against it.

πŸ––

Top comments (0)