#3: Conspiracies, Dissemination, and REST APIs

Welcome to the third post in a series where I share my experience learning Rust. I’m building out a conspiracy theories API to help me get more familiar with Rust and have a little fun. Since I am new to Rust, I welcome all feedback, especially from developers who have been using Rust for quite some time. Leave a comment below or send your feedback via Twitter. Now it’s time to draw the blackout curtains, put on your foil-lined hat on and start serving up conspiracies from a REST API.

Previous posts in the series

In this post, I create HTTP GET routes to fetch a page full of conspiracies, a single conspiracy or tags, more on tags later. I am using the crate actix-web to implement my web service. The actix-web crate offers the ability to handle requests asynchronously. After adding a new binary, the conspiracies_api, to the Cargo.toml file I am ready to get started on creating the REST API. For those of you who haven’t read the previous post, I explain how to add binaries to a project in that post.

While you were away

Creating a Web Service

A simple HTTP GET Request

Since the index function is pretty straightforward, I’ll walk you through the main function instead, starting with the server::new call. The only parameter server::new takes is a closure function. The closure starts off by creating a new App without passing in any state. The chained resource call maps the / path to another function that implements the core::ops::FnOnce trait. I started writing an overview of what the FnOnce trait is but the documents do a better job of it so if you are curious about what it is, check out the docs.

Adding Middleware

I run the service and make a HTTP GET /tags call and see the following output.

Running `target/debug/conspiracies_api`
INFO 2018-06-06T21:43:40Z: actix_web::server::srv: Starting 8 http workers
INFO 2018-06-06T21:43:40Z: actix_web::server::srv: Starting server on http://127.0.0.1:8088
Started http server: 127.0.0.1:8088
INFO 2018-06-06T21:43:46Z: actix_web::middleware::logger: 127.0.0.1:51337 [06/Jun/2018:17:43:46 -0400] "GET /tags HTTP/1.1" 200 240 "-" "insomnia/5.16.6" 0.001722

My middleware is ready to go, its time to move on to discuss house actix-web uses actors to handle the requests from clients.

Actors & Messages

  • Send a message to other actors
  • Do work based on the message it receives
  • Start other actors

For my API I am sending messages to actors, that make queries against the table that is specified by the message sent. Let’s take a look at get_conspiracies_by_id(req: HttpRequest<State>) -> impl Future<Item=HttpResponse, Error=Error> the handler for the /conspiracies/{page_id} route.

The function’s signature has a generic version of the HttpRequest. It expects an instance of the State struct defined in main.rs. I use the State object to pass around the DbExecutor which contains the database connection and is the actor I am using throughout the API service. More on the DbExecutor when we get to the actor code.

The return value of the function uses something new to Rust 1.26 the impl trait return value. What that means that get_conpsiracies_by_id returns something that implements the trait Future where the Item is an HttpResponse and the Error is an actix_web::Error

The first line of the body of the function grabs the page_id value from the URL. The rest of the function deals with sending the message and handling the returned data from the actor. Let’s take a look at the DbExecutor and the GetConspiracy code.

The DbExecutor is the actor, and GetConspiracy is a message that the handler sends to DbExecutor. Thankfully, the names of the traits that the two structs must implement make it clear which role each struct is playing. Each route handler sends a different message to DbExecutor. The DbExecutor implements a Handler trait for each message type DbExecutor wants to support.

The GetConspiracy message has one public field, page_id. The message is how the handler sends the page_id of the conspiracy the user has requested. To use this struct as a message, I have to implement the actix::Message trait, which requires the definition of a Result type. For the GetConspiracy type, I’ve defined the type to be Result<Conspiracy, String> where Conspiracy is the object that contains the record from the database and the String is where an error message would return if an error occurs.

The DbExecutor actor indicates it wants to support the GetConspiracy message by implementing the actix:: Handler<GetConspiracy> trait. The only requirement of the trait is to define the Result type and a handle function.

The handle function takes a reference to the DbExecuter instance that is receiving the message, the GetConspiracy message and a third parameter that I am ignoring. It returns the Result that defined in the trait implementation. Acting on this message is simple, I call the db::get_conspiracy_by_id function passing it a reference to the database connection that is ‘in’ the DbExecory object and the page_id that is a field in the GetConspiracy message. The return value from that call ends up in the route handler, in this case, it’s the get_conspiracy_by_id route handler back in the src/conspiracies_api/main.rs file.

The GET /conspiracies/{page_id} route

If I try and POST to that route a 405 error, Method Not allowed error, is returned.

That works as you’d expect given the fact that I specifically said HTTP GET is the only supported method. Let’s go back to the ‘/’ route; I haven’t specified what methods are available for the route. I wonder what happens when I do a POST /.

I can post that route without an issue. I probably should fix that. Thankfully it is pretty straightforward. First, I’ll add the r.method(http::Method::GET). I also add a few lines so that if someone tries to do a POST to an HTTP GET route, they should receive a 405 Method Not Allowed error instead of the 404 Not Found error they would receive without the updated code.

The default_resource throws a 405 Error for all non-GET requests. After fixing that oversite and adding the 405 code, I’d like to go over the route handler responsible for retrieving the requested conspiracy from the database.

Meanwhile, Back in the Route Handler

The and_then() method takes a FnOnce to allow us to work with the results of the message call. In this case, I’m merely matching on the Ok or Error states. If it found the conspiracy, it returns a JSON representation of the conspiracy. If the call returns an error, a NotFound error. Both responses are sent via the responder() call. Now that we’ve discussed how actors and messaging works, there are a few lines in the main function that I’d like to go over so that you can understand how the actix system gets started.

Starting the actix::System

SyncArbiter::start starts up an actor, the DbExecutor actor to be specific. The function takes two parameters. The first one is the number of threads to start for the actor. The second parameter is a function, here, I use a closure that instantiates DbExecutor actors, creating a connection to the database. The start call returns an which is an address to the actor.

The last step is to tell the system its time to ‘run.’ You do that by calling run() on the returned Addr from the start() call. The service is now ready to handle web requests.

The GET /conspiracies route

let page_num = req.query()
.get("page")
.unwrap_or("0")
.parse::<i64>().unwrap();

req.state().db.send(Conspiracies{page_num: page_num})

The calls above, get the parameter by name, unwraps the value if the page parameter is present or sets the value of page_num to zero if the page parameter is not is there. The parse call converts the string value into an i64 value. The parsed value is used to set the value of the Conspiracies.page_num field.

Handling HTTP Posts

.resource("/tags/new", |r| r.method(http::Method::POST).with(add_tag))

For the post routes, I’m using the with function to assign a handler to the route. The reason for using the with call is it allows me to use extractors to parse the body of the POST request. An extractor is a type-safe way of retrieving data. The handler’s signature looks a little different than the GET routes, here’s the add_tags function signature.

fn add_tag((req, tag): (HttpRequest<State>, Json<models::NewTag>)) -> Box<Future<Item=HttpResponse, Error=Error>>

Since I need both the State object and the request body, I am using a tuple. The first entry in the tuple is the request itself and the second is a struct that supports the deserialization of the JSON into an object. The NewTag struct is a simple struct and is used to parse the body and insert a new tag into the tags table.

#[derive(Insertable, Debug, Serialize, Deserialize)]
#[table_name="tags"]
pub struct NewTag {
pub name: String
}

The last difference between the GET and POST handlers is the way that I set the message field’s value.

req.state().db.send(AddTag{tag: tag.into_inner()})

The tag object returned from the JSON extractor is used to set the value of the AddTag.tag field. To get the NewTag object, I need to call the into_inner() method. If I needed the value of the name field in the NewTag object, I could use tag.name without a problem. However, if I try to set the message’s tag field to the tag object without the into_inner() method I get the following error:

--> src/conspiracies_api/main.rs:34:37
|
34 | req.state().db.send(AddTag{tag: tag})
| ^^^ expected struct `conspiracies::models::NewTag`, found struct `actix_web::Json`

To get the NewTag out of the JSON extractor, I need to make a call to into_inner() which returns the NewTag object. The remaining part of the handler follows the same pattern as the routes we’ve seen earlier. I now have a functioning API! What am I going to do with that?

In this post, I’ve shown how I set up a REST API using the actix-web crate. We took a deep dive into how the /conspiracies/{page_id} route is handled using messages and actors, how to parse path and query parameters, add middleware, as well as start up the actix system.

Reading

Crates of Interest

Me

Originally published at www.myprogrammingadventure.org.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store