Roger Torres (he/him/ele)
Posted on April 14, 2021
There are a few good web framework options for Rust: actix, rocket, tide, warp... And they all offer some sort of trade-off; but instead of carefully analyzing these intricate aspects (as Luca did here), you simply decided to go with the one that has an explicit reference to Start Trek, right? Not ideal, mafriend, not ideal... Will I blame you? No. You will get nothing but support from me.
And since you're already down this path, I think it might be a good idea for me to show you how I used warp myself to create a REST API (for learning purposes, just like a holodeck battle).
Please note that this is not a tutorial; this is me sharing a process in the hopes that it can help you. Imagine you ask a friend to help you build something. This friend can either point you to the guides she someday used or show you how she actually went about it, not only sharing the code but explaining the whys and hows. This text is a simulation of the latter. In other words, if examples like these are enough to get you going, considering using them instead of reading this somewhat long text (unless you are easily amused by Star Trek references and puns, then stay).
Still up for the ride? Great! Grab your Earl Gray tea (hot!) and let's make it so!
Getting started
First, the playground.
$ cargo new --lib holodeck
Now, there are (at least) two ways to go about it. The first is to serve the API and test it via curl, as Bastian did here; and for this, I would use a binary crate. The other way is to use warp's built-in test functionalities, for which I think it is best to get a library. As one of my main goals was to actually try warp's test module, I chose the second path.
I started by editing the Cargo.toml
file and adding two dependencies.
[dependencies]
tokio = { version = "1", features = ["full"] }
warp = "0.3"
Then I wrote a very simple test case. I chose to test a dull GET
method. By dull I mean that it will not actually return any data, it will only [1] assume that there will be a mod called filters
where the filters will be added (filters are the dorsal spine of warp usage, as shown here); [2]; use the request to make make a GET
on the path /holodeck
using the assumed filters; [3] and finally compare the answer with the enum StatusCode::ACCEPTED
. And because the whole thing is going to be asynchronous, the #[tokio::test]
is needed.
#[cfg(test)]
mod tests {
use warp::http::StatusCode;
use warp::test::request;
use super::filters;
#[tokio::test]
async fn try_list() {
let api = filters::list();
let response = request()
.method("GET")
.path("/holodeck")
.reply(&api)
.await;
assert_eq!(response.status(), StatusCode::ACCEPTED);
}
}
You will notice that I chose not to write
use super::filters::*
with the *, which would allow me to write things likelist()
instead offilters::list()
; I did so because it makes it easier for me (and for you) to know what is coming from where.
Then, I created the missing filter, which is designed to expect nothing but an empty GET
and handle it over to what I call a "Matthew McConaughey handler", because all it has to do is to say alright, alright, alright (a.k.a. StatusCode::ACCEPTED
).
mod filters{
use warp::Filter;
use super::handlers;
pub fn list() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone{
warp::path!("holodeck")
.and(warp::get())
.and_then(handlers::handle_list)
}
}
mod handlers{
use warp::http::StatusCode;
use std::convert::Infallible;
pub async fn handle_list() -> Result<impl warp::Reply, Infallible> {
// "Alright, alright, alright", Matthew said.
Ok(StatusCode::ACCEPTED)
}
}
Why Infallible
in the handler's Result? Because if something goes wrong (that's not coded yet), the problem will also be sent as Ok (e.g. Ok(StatusCode::BAD_GATEWAY)
).
It might seem like a lot of wasted time, but let me tell you the benefits of doing this (and I'm not even going to get fancy with TDD). For starters, sometimes (maybe every time) it is easier to know what we wanna do than how we're supposed to do it, and tests are a good way to force you to answer this "what" before jumping into the myriad of possible answers to the "how" question. Besides that, these baby steps allowed me to have a clear picture of which function was responsible for which task, who got to be async and who didn't, and so on.
$ cargo test
running 1 test
test tests::try_list ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
It worked; not much happened but worked nonetheless.
In the next episode of Engaging Warp...
The next step is to build the POST
method. That will require proper coding of both filters and handlers, as well as deserializing the JSON and storing it somewhere.
Anyway, that's all for now. If I said something wrong or made things more complicated than they should be, let me know in the comments.
š
Posted on April 14, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.