REST API with Rust + Warp 4: PUT & DELETE
Tags: rust, warp, rest, api
Published: 2021-04-14 12:21:26

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](https://github.com/rogertorres/dev.to/tree/main/warp/warp4).

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](https://tools.ietf.org/html/rfc7231#section-4.3.4).

With that in mind, I wrote this code:

```rust
#[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);
}
```

How did I knew that 201 was `StatusCode::CREATED` and 200 was `StatusCode::OK`? [Here](https://tools.ietf.org/html/rfc7231#section-8.2.3).

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](https://tools.ietf.org/html/rfc7231#section-4.3.4).

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

```rust
#[derive(Debug, Deserialize, Serialize)]
pub struct NewName{ pub name: String }
```

```rust
// 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())
}
```

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.

```rust
// 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)
}
```
```rust
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,
    ))
}
```

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"](https://tools.ietf.org/html/rfc7231#section-4.3.5).

```rust
#[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);
}
```

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

```rust
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)
}
```

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.

```rust
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,
    ))
}
```

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
```

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.

🖖