REST API with Rust + Warp 2: POST
Tags: rust, warp, rest, api
Published: 2021-04-14 12:20:58

Glad to see you back! In this second part, we'll build the first functional method of our API: `POST`.

-----

## Warp 2. Engage!

> The code for this part is available [here](https://github.com/rogertorres/dev.to/tree/main/warp/warp2). 

The `POST` sends a request to the server to insert some data. This data is sent in JSON. This program's job is then to parse (deserialize) the JSON and store this information (in memory only—I will not deal with ORM in this series; maybe in another one).

The holodeck produces simulations (ignore this statement if you're not into Star Trek). With that in mind, I followed the [KISS](https://en.wikipedia.org/wiki/KISS_principle) principle and made it a key-value pair of "simulation id" and "simulation name".

First, once I knew _what_ I was supposed to be listing, I renamed the previous `list()` to `list_sims()` and `handle_list()` to `handle_list_sims()`.

Then, I created a `try_create()` test function, pretty similar to the previous test: it sends the method  (`POST` this time) to the same`/holodeck` path, using a new (still-to-be-coded) filter and expects a good answer (because [what could go wrong in the holodeck?](https://screenrant.com/star-trek-holodeck-accidents-malfunction-every/)).

```rust
// Add "models" in the already existing line "use super::filters;":
use super::{filters,models};


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

    let response = request()
        .method("POST")
        .path("/holodeck")
        .json(&models::Simulation{
            id: 1,
            name: String::from("The Big Goodbye")
        })
        .reply(&api)
        .await;

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

What can go wrong (among other things) is conflict due to duplicated entries; so I created a test for that as well:

```rust
#[tokio::test]
async fn try_create_duplicates() {
    let db = models::new_db();
    let api = filters::post_sim(db);

    let response = request()
        .method("POST")
        .path("/holodeck")
        .json(&models::Simulation{
            id: 1,
            name: String::from("Bride Of Chaotica!")
        })
        .reply(&api)
        .await;

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

    let response = request()
        .method("POST")
        .path("/holodeck")
        .json(&models::Simulation{
            id: 1,
            name: String::from("Bride Of Chaotica!")
        })
        .reply(&api)
        .await;

    assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
```

The `Simulation` struct that's being passed as JSON is still to be defined (which will require a `use super::models` here in the tests mod—be aware of these `use` statements; hopefully I haven't forgotten to mention any).

Before coding `post_sim()`, I needed three things: [1] types to handle and persist the data (just in memory), [2] a JSON body, and [3] a way to bundle it all up.

Starting with [1]: I first needed the `serde` crate to handle (de)serialization, so I added this line under `[dependencies]` in `Cargo.toml`

```toml
serde = { version = "1", features = ["derive"]}
```

With that settled I went back to `lib.rs` and created a `mod` with a `type` and a `struct` that can _automagically_ (de)serialize the data that will come as JSON, thanks to the serde macros. Ah, and there's also the omnipresent `new()` function (which is not inside an implementation as the manuals usually tell you to do because I used `type` for `Db`, as I didn't want to nest it within a `struct`).

```rust
mod models {
    use serde::{Deserialize, Serialize};
    use std::collections::HashSet;
    use std::sync::Arc;
    use tokio::sync::Mutex;

    #[derive(Clone, Debug, Hash, PartialEq, Eq, Deserialize, Serialize)]
    pub struct Simulation {
        pub id: u64,
        pub name: String,
    }

    pub type Db = Arc<Mutex<HashSet<Simulation>>>;

    pub fn new_db() -> Db {
        Arc::new(Mutex::new(HashSet::new()))
    }
}
```
Now, why did I chose these smart pointers? 
- Short answer: because the [cool kids](https://github.com/seanmonstar/warp/blob/master/examples/todos.rs) did so.
- Smart answer: I don't think I had a good alternative for `Arc`, as I needed a thread-safe reference to the HashSet; regarding `Mutex`, I could have used `RwLock` instead, to allow concurrent reads (Mutex make you hold the lock for both read and write alike), but it didn't seem necessary given the context.

But what about what is inside the Mutex? "Why a HashSet?", you ask. Well, I agree that a HashMap feels like the obvious choice, and I also agree that a Vector would be the easier one to implement, but I chose HashSet because it allowed me to gain an important benefit from HashMap alongside some of the Vector advantages; but the actual explanation is something that I'm keeping for the second part of this series, so you'll have to trust me on this one.

[2]: The JSON body function was just copied from the aforementioned cool kids' example and placed inside the `filters` mod. It accepts any JSON body, insofar it ain't too big.

```rust
// This line already exists; I just added "models"
use super::{handlers, models}; 

fn json_body() -> impl Filter<Extract = (models::Simulation,), Error = warp::Rejection> + Clone {
    warp::body::content_length_limit(1024 * 16).and(warp::body::json())
}
```

To bundle it all up [3], we need another new function under `filters`. This function, `post_sim()`, will receive a JSON body and a Db (our Arc\<Mutex\<HashSet\>>), and then send both to the handler `handle_create_sim()`.

```rust
pub fn post_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")
        .and(warp::post())
        .and(json_body())
        .and(db_map)
        .and_then(handlers::handle_create_sim)
}
```

The line that might be harder to grasp here is the `let db_map` one. The `warp::any` is a [catch all](https://docs.rs/warp/0.3.1/warp/filters/any/fn.any.html); that is, it is a filter that filters nothing. So all we are doing here is making sure our Db is "wrapped" in a Filter, so we can stick it into the `.and(db_map)` you see up there.

The `handle_create_sim()` that is triggered by the _and_then_ goes inside the `handlers` mod looks like this:

```rust
// Add this "use" below the others
use super::models;

pub async fn handle_create_sim(sim: models::Simulation, db: models::Db) -> Result<impl warp::Reply, Infallible> {
    let mut map = db.lock().await;

    if let Some(result) = map.get(&sim){
        return Ok(warp::reply::with_status(
            format!("Simulation #{} already exists under the name {}", result.id, result.name), 
            StatusCode::BAD_REQUEST,
        ));
    }

    map.insert(sim.clone());
    Ok(warp::reply::with_status(format!("Simulation #{} created", sim.id), StatusCode::CREATED))
}
```

It takes the JSON (_sim: Simulation_) and the HashSet (_db: Db_), returns an error if the entry is already there, or inserts the JSON data into the HashSet and return success otherwise.

> Here having a HashMap would clearly be the better solution, as it would allow us to compare keys. We'll solve this in part 2.

I could've just used `Ok(StatusCode::CREATED)` and `Ok(StatusCode::BAD_REQUEST)` instead of wrapping it inside a `warp::reply::with_status`, but since I am not going to handle the errors, I thought it was the least I could do.

```
$ cargo test

running 3 tests
test tests::try_create ... ok
test tests::try_create_duplicates ... ok
test tests::try_list ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
```

_Alright, alright, alright._

-----

## In the next episode of **_Engaging Warp_**...

Next in line is the `GET` method, which means we'll see parameter handling and (finally) deal with this HashSet thing.
 
🖖