Rustful 0.1.0

08 May 2015 — Permalink

The time has finally come, when I feel confident enough to give Rustful (a small web/HTTP framework for Rust) a proper version number and publish it on crates.io. I would therefore like to take the opportunity to tell you about its past, present and future, and show you what it’s all about. You can always take a look at the above links for more hands-on info and a few working examples, if you think this is too much rambling.

In the Beginning There Was Only Git Submodules

I started working on Rustful around the beginning of 2014, as a tool for a university course project. We had quite free hands with the project so I wanted to spice it up a little and build it using Rust. The problem was just that I could not find any up-to-date server libraries back then, except for the now abandoned rust-http by Chris Morgan. My project required some kind of REST-like1 API with routing and stuff, so I decided to build my own framework on top of rust-http.

Note that I do not claim to be or care about being the first or the original or anything. There were other frameworks out there, but those I could find were not up-to-date. I mean, we didn’t even have Cargo to help us and I managed my dependencies using Git submodules, so times were indeed rough.

Anyway, I chose to keep developing Rustful after my course project was completed and did my best to keep pushing features and fixes while the language evolved and even more frameworks popped up. Some of the other frameworks aimed at high abstraction levels, but I decided to stay at a relatively low level because I thought that there would be a niche for that as well.

I kept on tinkering like that until I had to take a long break and focus on my studies. I did eventually regain my motivation and decided that I should fix this thing up, remove unnecessary or half baked features and make it available on cargo.io. This is where we are now.

Computer, Make Me a Website!

The focus of this first development phase was not to make Rustful “feature rich”, but rather to build a solid base. The intention is to create a relatively thin layer on top of Hyper (the current HTTP backend), that should be light, modular and simple to build something with (or on). It should not stand in your way or force your hand if it’s not absolutely necessary to make things work.

Rustful provides a set of common presets which makes it as easy as throwing a closure into a function to fire up a new server:

extern crate rustful;
use rustful::{Server, Context, Response};

#[allow(unused_must_use)]
fn main() {
    Server::new(|_: Context, response: Response| {
        response.into_writer().send("Hello!");
    }).run();
}

But that doesn’t give you much, so I’ll let this serve as the obligatory “Hello world” and instead move on to what it can do. The Server struct you can see in the above example lets you configure your server before starting it. It is completely public, allowing you to simply assign values to the parameters you want to change:

Server {
    host: 8080.into(),
    handlers: router,
    log: Box::new(log),
    server: "My Unmitigatedly Majestic Server".into(),
    ..Server::default()
}

Not too complicated, right? I’ll still go through them, just in case:

  1. host: 8080.into(): The host field contains the address where the server will listen for incoming requests. It’s actually both an IP and a port, but this example shows only how to set the port. Setting the IP is not much harder, so don’t worry. This field can also be excluded if the server should listen to 0.0.0.0:80.

  2. handlers: router: The handlers field contains the request handlers. These will receive the requests after the server has added some more contextual data and tools. This is also what the Server::new(...) method in the previous example expects as parameter.

  3. log: Box::new(log): Each part of the server has access to a shared logger and this allows you to decide where all the printing goes. This logger is included in the Context type, among others.

  4. server: "My Unmitigatedly Majestic Server".into(): I guess you want to give your server a name, right? This is what it will send out in the Server header with each response.

  5. ..Server::default(): And set the rest of the fields to their default values. Done!

You may wonder why there is a Server::new(handlers) function in addition to the Server::default() function. The reason for this is, besides calling new and being done with it, that the type of handlers does not always implement the Default trait. new can then be used instead of default:

Server {
    host: 8080.into(),
    log: Box::new(log),
    server: "My Unmitigatedly Majestic Server".into(),
    ..Server::new(router)
}

The handlers field is not explicitly set here and router is instead used as input to new.

Take Me to Your Handler

One of the initial goals of Rustful was to provide an easy way to set up routing, so that is sort of built into the core, but not with a router built into the server. Not exactly, anyway. The handlers field in Server expects something that implements the Router trait, which has a method that takes a path and an HTTP method and returns a handler. This trait is, as a bonus, also implemented for anything that can be a handler, making it a router with itself as the only handler.

Using traits for routers and handlers makes everything modular and replaceable. You could decide that the TreeRouter, provided by Rustful, is not what you want and replace it with something else. That’s totally OK and will work perfectly as long as it implements the Router trait.

This Router trait does also set a standard for how routes are defined, making it convenient to insert handlers without caring too much about what they go into:

fn insert_some_routes<R: Router<MyHandlerType>>(router: &mut R) {
    //...
    router.insert(Get, "users", list_users);
    router.insert(Get, "users/:user", show_user);
    router.insert(Delete, "users/:user", delete_user);
    router.insert(Get, "users/:user/friends", list_friends);
    router.insert(Post, "users/:user/friends", add_friend);
    router.insert(Delete, "users/:user/friends/:friend", delete_friend);
    router.insert(Get, "about", about_us);
    //...
}

The :something syntax is not standardized, but it’s used in TreeRouter to define variable segments. They will take anything the client replaces them with as input and pass it to the handler, so if the client asks for /users/12, the handler at /users/:user would be called with the variable user set to 12. It’s not uncommon in other routing libraries, so you have probably seen this before.

That’s how you populate a router, but wait! There’s more! Rustful comes with a macro that lets you write your routes as a tree structure:

fn insert_some_routes<R: Router<MyHandlerType>>(router: &mut R) {
    //...
    insert_routes! {
        router => {
            "users" => {
                "/" => Get: list_users,
                ":user" => {
                    "/" => Get: show_user,
                    "/" => Delete: delete_user,
                    "friends" => {
                        "/" => Get: list_friends,
                        "/" => Post: add_friend,
                        ":friend" => Delete: delete_friend
                    }
                }
            },
            "about" => Get: about_us
        }
    }
    //...
}

Which one is the best? You decide. The macro may feel more verbose, but the point of it is to prevent typos and make it easier to read. You will only have to write each path segment once and it may also give you a better overview. Both alternatives lets you do the same thing, so the only difference is that the path in the macro is broken into pieces.

Handle with Ease

A handler in Rustful is a quite modest being. The idea, right from the start, is that it should be possible to just throw a function into a router and it should just work. This is still the case2, as can be seen in the first example above, where a closure is used as a handler.

Here is a simple handler function, taken from the hello_world.rs example:

fn say_hello(context: Context, response: Response) {
    //Get the value of the path variable `:person`.
    let person = match context.variables.get("person") {
        Some(name) => &name[..],
        None => "stranger"
    };

    //Use the value of the path variable to say hello.
    if let Err(e) = response.into_writer().send(format!("Hello, {}!", person))  {
        context.log.note(&format!("could not send hello: {}", e.description()));
    }
}

The comments should explain it quite well, and what it responds with is Hello, stranger! if you visit / and Hello, Name! if you visit /Name.

The Context contains the request data and some more. It comes with all the contextual tools and data that may be useful for the handler, except for the prepared response. You can, as an example, see how context is used to log failed send(...) calls3.

The fact that the Context is owned by the handler makes it possible to deconstruct it and move values out of it. The Response, on the other hand, is completely closed, and the reason for this is the strict procedure of writing data to the client, so it’s not supposed to be picked apart.

The Response is used in two stages; Response where status code and headers can be set, and ResponseWriter where the body data is written. This makes it impossible to send body data to the client before the headers have been sent. Transforming the Response into a ResponseWriter causes the headers to become unaccessible, as they have already been sent to the client.

#Filter

Everything mentioned so far has been pretty much standard server stuff, so let’s finish this off with something slightly less standard before we move on to the future.

Rustful comes with something called filters that allows you to change the incoming and outgoing data before and after it passes through the handlers. The idea is that there are some situations where something is supposed to be done no matter what the handler does and these operations can be implemented as filters. This will also give you control over a larger part of the data flow.

One simple example is surrounding JSON data with a function call, like in the JSONP standard. You can have a handler that checks for a query variable with a function name and notifies a ResponseFilter. The filter will then inject the function call into the body when it’s written. Such a filter may look like this:

struct JsonpFn(String);

struct Jsonp;

impl ResponseFilter for Jsonp {
    fn begin(&self, ctx: FilterContext, status: StatusCode, headers: Headers)
    -> (StatusCode, Headers, ResponseAction) {
        //Check if a JSONP function is defined and write the beginning of the call.
        let output = if let Some(&JsonpFn(ref function)) = ctx.storage.get() {
            Some(format!("{}(", function))
        } else {
            None
        };

        (status, headers, ResponseAction::next(output))
    }

    fn write<'a>(&'a self, _ctx: FilterContext, bytes: Option<Data<'a>>) -> ResponseAction {
        ResponseAction::next(bytes)
    }

    fn end(&self, ctx: FilterContext) -> ResponseAction {
        //Check if a JSONP function is defined and write the end of the call.
        let output = ctx.storage.get::<JsonpFn>().map(|_| ");");
        ResponseAction::next(output)
    }
}

…and the handler could look something like this:


fn say_hello(mut context: Context, mut response: Response) {
    //Take the name of the JSONP function from the query variables.
    if let Some(jsonp_name) = context.query.remove("jsonp") {
        response.filter_storage().insert(JsonpFn(jsonp_name));
    }

    let person = match context.variables.get("person") {
        Some(name) => &name[..],
        None => "stranger"
    };

    //Send some simple JSON data.
    let data = format!("{{\"message\": \"Hello, {}!\"}}", person);
    if let Err(e) = response.into_writer().send(data)  {
        context.log.note(&format!("could not send hello: {}", e.description()));
    }
}

Note the use of context.query.remove("jsonp"). No one else should be interested in that query variable so this saves us a clone.

Filters are stored in two Vecs in the server and the one containing the response filters is then borrowed by the response structures. They can pass data to each other and the handler using an AnyMap, and it’s all local to each request.

Filters are still a bit experimental and I haven’t had time to properly test them in any more “real” situations than the examples. I will therefore not take us any deeper into this subject, but you can always take a look at the filters.rs example where the above filter and a couple of context filters are demonstrated.

So, What Happens Next?

This is only the beginning, and there are still a lot of things to do (especially when it comes to documentation) and still some internal cruft to take care of before I would even dare giving Rustful the 1.0.0 version. The main thing to do is to test it, fix bugs and polish it, based on your and other users’ feedback. How do you use it? What is good? What is missing? What should change? Every piece of constructive feedback is absolutely necessary to make this good!

There are also a number of changes and additions on the list, like a central storage and taking the library as close to RESTfulness as it can reasonably get without sacrificing too much usability. I don’t think it will be too hard, but those are also some famous last words. I would also like to add some more utilities and make the server even more modular (if possible).

Over and Out!

I hope that this post has sparked some interest and that you will give Rustful a try. Here is the link to crates.io again, in case you missed it at the top, and you can also find links to the repository and the documentation there. Thank you for your time and interest, and have fun building servers!

  1. Buzzword, I know, but that was my reasoning back then. 

  2. There is, unfortunately, a certain compiler bug that prevents us from doing this in all cases, but it can easily be worked around by wrapping the function in a struct. 

  3. That line is only there for this example. It’s not necessarily a good idea to log every failed use of send(...)