Drink Me.

Vial

~ a micro micro-framework ~

Vial is a small web framework for making small web “sites” in Rust. It includes just a handful of basic features for delivering old school, server-side rendered HTML: request routing, form data parsing, response building, and serving static file assets.

The goal is a small, lean core that compiles quickly and has as few dependencies as possible. Use it for HTML stuff: prototyping ideas, testing out concepts, or, perhaps, even writing tiny personal apps. Nothing serious though, got it?

This manual is an overview of Vial’s built-in features, as well as the few optional features you can enable. It also includes suggestions for some “common tasks”, like using a database to store information.

Hello World

Here’s the bare minimum:

vial::routes! {
    GET "/" => |_| "Greetings, creature.";
}

fn main() {
    vial::run!();
}

That should tell you a lot, in that there isn’t a lot to Vial.

Now here’s a bigger bear, showing off more of Vial’s features:

use vial::prelude::*;

routes! {
    GET "/" => hello_world;
    POST "/" => redirect_to_greeting;
    GET "/:name" => hello_name;
    GET "/*path" => |req|
      Response::from(404).with_body(
        format!("<h1>404 Not Found: {}</h1>",
          req.arg("path").unwrap_or("")));
}

fn hello_world(_req: Request) -> &'static str {
    "<h1>Hello, world!</h1>
    <p><strong>What's your name?</strong></p>
    <form method='POST' action='/'>
        <p><input name='name' type='text'/></p>
        <p><input type='submit'/></p>
    </form>"
}

fn redirect_to_greeting(req: Request) -> Option<impl Responder> {
    let name = req.form("name")?;
    Some(Response::redirect_to(format!("/{}", name)))
}

fn hello_name(req: Request) -> String {
    format!(
        "<h1>Why hello there, {}!</h1>",
        req.arg("name").unwrap()
    )
}

fn main() {
    run!().unwrap();
}

You can run the above example from the root of this repository:

$ cargo run --example manual

Vial comes with a handful of examples in the examples/ directory, so be sure to peruse them skeptically - either alongside or after digesting this manual.

Overview

Like most web library thingy-jingies that only focus on server-side rendering, there are three main parts to a Vial application:

Getting Started

Vial should work on any recent, stable version of Rust on Linux or macOS.

To begin, add Vial to your project’s Cargo.toml:

[dependencies]
vial = "0.1"

Now all you have to do is call vial::routes! to define your routes and vial::run! to start the server in src/main.rs:

vial::routes! {
    GET "/" => |_| "It works!";
}

fn main() {
    vial::run!();
}

This should start a server at http://0.0.0.0:7667 and tell you that it did. Congratulations! You’re on your way.

Routing

Routing is the real gravy and potatoes of any web framework, if you think about it. In Vial, routes are defined with the vial::routes! macro in this format:

HTTP_METHOD ROUTE_PATTERN => ACTION;

The order in which routes are written matters - routes written first will be checked for matches first, meaning you can declare many routes that point to "/", but only the first one defined will ever match.

HTTP Methods

HTTP_METHOD can be one of:

Route Patterns

ROUTE_PATTERN can be an exact match, such as "/user" or "/v2/search.php3", or it can include a named parameter:

  1. "/:name" — This will match anything except paths with / or . in them.
  2. "/:name.md" — Use this format to match on a specific file extension.
  3. "/*name" — This will match everything, including / and .

In the three examples above, calling request.arg("name") in an Action will return Some(&str).

Note that you can have multiple parameters in the same route, as long as the “match all” pattern occurs last:

vial::routes! {
    GET "/:category/:id/*name" => |req| format!(
        "<p>Category: {}</p>
        <p>ID: {}</p>
        <p>Name: {}</p>",
        req.arg("category").unwrap_or("None"),
        req.arg("id").unwrap_or("None"),
        req.arg("name").unwrap_or("None"),
    );
}

fn main() {
    vial::run!();
}

Actions

Actions are what routes actually route to. They are functions or closures take a Request and return either a Response or something that implements the Responder trait:

use vial::prelude::*;

routes! {
    GET "/info" => |req| format!(
        "<p>Name: {}</p>", req.query("name").unwrap_or("None")
    );
    GET "/" => index;
}

fn index(req: Request) -> &'static str {
    "<form method='GET'>
        <p>Enter your name: <input type='text' name='name'/></p>
        <input type='submit'/>
    </form>"
}

fn main() {
    run!();
}

Returning impl Responder is easy - Responder is a Vial trait that defines a single conversion method that returns a Response:

pub trait Responder {
    fn to_response(self) -> Response;
}

These types implement Responder by default:

Filters

Filters are functions that are run before actions. They can either modify the existing request before it’s sent to your action, or they can return a Response that will be delivered to the client without any actions being called:

fn(req: &mut Request) -> Option<Response>;

Like Rust’s attributes, they can either apply to all routes defined in the same vial::routes! macro call or just a specific route:

use std::sync::atomic::{AtomicUsize, Ordering};
use vial::prelude::*;

routes! {
    // `count` will run before all routes in this block
    #![filter(count)]

    GET "/" => |_| "Hey there!";
    GET "/hits" => hits;

    // `count` will run again when /double is visited
    #[filter(count)]
    GET "/double" => double;

    // `echo` will be called when /echo is visited
    #[filter(echo)]
    GET "/echo" => |_| "Is there an echo in here?";
}

fn hits(req: Request) -> impl Responder {
    format!("Hits: {}", req.counter().count())
}

fn double(req: Request) -> impl Responder {
    "Double trouble."
}

fn echo(req: &mut Request) -> Option<Response> {
    println!("{:#?}", req);
    None
}

fn count(req: &mut Request) -> Option<Response> {
    req.counter().incr();
    None
}

#[derive(Debug, Default)]
struct Counter(AtomicUsize);

impl Counter {
    fn count(&self) -> String {
        self.0.load(Ordering::Relaxed).to_string()
    }

    fn incr(&self) {
        self.0.fetch_add(1, Ordering::Relaxed);
    }
}

trait WithCounter {
    fn counter(&self) -> &Counter;
}

impl WithCounter for Request {
    fn counter(&self) -> &Counter {
        self.state::<Counter>()
    }
}

fn main() {
    use_state!(Counter::default());
    run!().unwrap();
}

Route Modules

Routes can be defined in different modules and combined together with vial::run!:

mod blog;

mod wiki {
    vial::routes! {
        GET "/wiki" => |_| "This is the wiki.";
    }
}

vial::routes! {
    GET "/" => |_| "Index page.";
}

fn main() {
    vial::run!(self, blog, wiki);
}

Requests

When a route matches and an Action is called, it’s passed a Request object. Request contains information about the request itself, as well as a number of helper methods.

Route Parameters

As mentioned in the Routing section above, you can define parameters in a route and access their value for a given request using request.arg():

vial::routes! {
    GET "/:animal" => |req| format!(
        "Animal: {}", req.arg("animal").unwrap_or("None")
    );
}

Query Parameters

In addition to route parameters, Vial will also parse good ol’ fashioned query string parameters for you:

vial::routes! {
    GET "/info" => |req| format!(
        "Version: v{}",
        req.query("version").unwrap_or("?")
    );
}

fn main() {
    vial::run!();
}

Running this and visiting /info will show:

Version: v?

But visiting /info?version=1.0 will show:

Version: v1.0

Like arg(), query() returns Option<&str>.

Form Data

What’s the web without open ended <textarea>s? Perish the thought.

POSTed form data follows the same pattern as query and route parameters: use request.form() to access a form parameter:

use vial::prelude::*;
use db;

routes! {
    GET "/show/:id" => show;
    GET "/new" => new;
    POST "/new" => create;
}

fn new(_req: Request) -> impl Responder {
    "<form method='POST'>
        <p>Name: <input type='text' name='name'/></p>
        <p>Location: <input type='text' name='location'/></p>
        <p><input type='submit'/></p>
    </form>"
}

fn create(req: Request) -> Result<impl Responder, io::Error> {
    let id = db::insert!(
        "name" => req.form("name").unwrap(),
        "location" => req.form("location").unwrap()
    )?;
    Ok(Response::redirect_to(format!("/show/{}", id)))
}

fn show(req: Request) -> Option<impl Responder> {
    let record = db::query!("id" => id).ok()?;
    format!(
        "<p>Name: {}</p>
        <p>Location: {}</p>",
        record.get("name").unwrap_or("None"),
        record.get("location").unwrap_or("None"),
    )
}

fn main() {
    run!();
}

Request Headers

Headers are available without any of the peksy conveniences of type safety. Just give request.header() a string and hope you get one back!

use vial::prelude::*;
use std::{fs, path::Path};

routes! {
    GET "/:file" => show;
}

fn show(req: Request) -> Option<impl Responder> {
    let path = format!("./{}", req.arg("file")?);
    if Path::new(&path).exists() {
        if req.header("Accept").unwrap_or("?").starts_with("text/plain") {
            Some(Response::from_header("Content-Type", "text/plain")
                .with_file(&path))
        } else {
            let file = fs::read_to_string(&path).unwrap();
            Some(Response::from_body(format!(
                "<html><body>
                <pre style='width:50%;margin:0 auto'>{}</pre>
                </body></html>", file)))
        }
    } else {
        None
    }
}

fn main() {
    run!();
}

Header names are case insensitive, though, so at least you don’t have to worry about that.

Other Info

Beyond the headers, Request also surfaces a few more basic bits of information such as the request.method() and request.path():

impl Request {
    // "GET", "POST", etc. Always uppercase.
    fn method(&self) -> &str;
    // Always starts with "/"
    fn path(&self) -> &str;
}

Responses

Every Action returns either a Response or a type that implements the Responder trait’s single method:

pub trait Responder {
    fn to_response(self) -> Response;
}

Common types like &str and Option<String> already implement this, so you are free to be lazy and return simple types in your Actions. If, however, you want to set headers and do other fancy jazz, you’ll need to build and return a Response directly.

Rather than use the “Builder” pattern like more mature and better designed libraries, Vial’s Response lets you set properties either directly or using Builder-style methods:

vial::routes! {
    GET "/404" => |_| Response::from(404).with_text("404 Not Found");
}

Each Response defaults to a Content-Type of text/html; charset=utf8, so you can build HTML with your bare hands:

fn index(_req: Request) -> impl Responder {
    Response::from("<marquee>Coming soon!</marquee>")
}

To produce plain text, set the header using with_header() or set_header(), or use with_text() instead of with_body() or from_body():

fn readme(_req: Request) -> Response {
    // This will be rendered as plain text.
    Response::from_file("README.md")
        .with_header("Content-Type", "text/plain")
}

Building Responses

The Response documentation contains more information on all the methods available, but here are some of the properties you can set on a Response in your actions:

Redirect

To issue a 302 redirect, use the redirect_to static method:

fn search(req: Request) -> Option<impl Responder> {
    let url = format!(
        "https://en.wikipedia.org/wiki/Special:Search?search={}",
        req.arg("search")?
    )
    Some(Response::redirect_to(url))
}

Status Codes

Empty responses with status codes can be created from usize:

fn fourohno(_req: Request) -> impl Responder {
    Response::from(404)
}

fn pay_me(_req: Request) -> impl Responder {
    402
}

Headers

Headers can be set Builder-style using with_header or imperative-style using set_header:

fn not_found(_req: Request) -> Response {
    Response::from(404)
        .with_header("Content-Type", "text/plain")
        .with_body("404 Not Found")
}

fn download(req: Request) -> Option<impl Responder> {
    Response::from_file(req.arg("file")?)
        .with_header("Content-Type", "application/octet-stream")
}

fn perm_redirect(url: &str) -> Response {
    Response::from(301).with_header("Location", url)
}

Serving Static Files

Vial can automatically serve static files out of an asset directory, complete with proper ETag handling, if you tell it which directory to use with the vial::asset_dir! macro. It can also optionally bundle those assets them into your application in --release mode, producing a single binary that was developed as if it used separate CSS and JS files.

vial::asset_dir!

To get started, put all your .js and .css and other static assets into a directory in the root of your project, such as assets/. In your HTML, include those files as if the “assets” directory were the root of your application. So if you have “assets/app.js”, in your <script> tag you would include just “/app.js”.

Next call vial::asset_dir!() with the path to your asset directory (maybe assets/?) before starting your application with vial::run!:

If we had a directory structure like this:

.
├── README.md
├── assets
│   └── img
│       ├── banker.png
│       └── doctor.png
└── src
    └── main.rs

We could serve our images like so:

vial::routes! {
    GET "/" => |_| "
        <p><img src='/img/doctor.png'/></p>
        <p><img src='/img/banker.png'/></p>
    ";
}
fn main() {
    vial::asset_dir!("assets/");
    vial::run!().unwrap();
}

asset::methods()

By setting an asset directory, either through the vial::asset_dir!() or [vial::bundle_assets!()][bundle_assets api] macro, you can then use the methods in the asset:: module to work with them:

Bundling Assets

Vial is meant to be small and swift, like a ninja star. Part of that means Vial apps should be able to compile into standalone binaries that don’t rely on the filesystem. In order to accomplish this, Vial can bundle your assets into your final --release binary for you. As long as you the asset::() API described above to access them, all your code will work the same whether your assets are bundled or not.

Combined with the ETag support, this means all assets will be reloaded by your browser whenever they’re modified in dev mode for a smoother developmental experience.

To bundle your assets into your final binary in release mode, you must:

  1. REMOVE any calls to the vial::asset_dir!() macro.

  2. Add vial as to [build-dependencies] in your Cargo.toml:

[build-dependencies]
vial = "0.1"

(Yes, you should now have vial in there twice. Once for build-dependencies and once for dependencies. Only the one under dependencies needs to list any optional features you want to use, however.)

  1. Create a build.rs in the root of your project and call vial::bundle_assets!() in it, passing your asset directory as the sole argument:
fn main() {
    vial::bundle_assets!("assets/").unwrap();
}

⚠️ Note: Bundling assets and setting an asset path using vial::asset_dir!() are mutually exclusive - you can’t do both, as enabling bundling will set the asset path for you. Therefor if you are making the transition from using-assets-but-not-bundling to using-assets-and-bundling-them, make sure to remove your call to vial::asset_dir!.

Other than that, you’re all set! Your application will now bundle your assets in --release mode and use the disk in debug and test mode.

All calls to functions in the assets module should work with the files in your asset directory. Add more and get to it!

State

There are two types of state available in Vial:

  1. Local State - Built-in to Request. Allows caching of expensive algorithms (like DB lookups) on a per-request basis.

  2. Global State - Allows you to share any Send + Sync + 'static types (like database connections) across all requests.

Local State

Local state lives for only a single Request, but can be useful to prevent looking up the same data over and over. The cache is based on the return type of the function or closure you pass to cache(), so make sure to create little wrapper structs if you want different functions to return the same type, like Vec<String>:

struct PageNames(Vec<String>);
struct UserNames(Vec<String>);

Here’s an example:

use vial::prelude::*;
use page::Page;
use db;

routes! {
    GET "/" => list;
}

struct PageNames(Vec<String>);

fn all_pages(_: &Request) -> Vec<Page> {
    db::lookup("select * from pages")
}

fn page_names(req: &Request) -> PageNames {
    PageNames(req.cache(all_pages)
        .iter()
        .map(|page| page.name.clone())
        .collect::<Vec<_>>())
}

fn list_of_names(req: &Request) -> String {
    req.cache(page_names)
        .0
        .iter()
        .map(|name| format!("<li>{}</li>", name))
        .collect::<Vec<_>>()
        .join("\n")
}

fn list(req: Request) -> impl Responder {
    format!(
        "<html>
            <head><title>{title}</title></head>
            <body>
                <h1>{title}</h1>
                <h3>There are {page_count} pages:</h3>
                <ul>
                    {pages}
                </ul>
            </body>
        </html>",
        title = "List Pages",
        page_count = req.cache(all_pages).len(),
        pages = req.cache(list_of_names),
    )
}

fn main() {
    run!().unwrap();
}

Global State

There are two steps involved in setting up shared, global state in Vial:

  1. Create a struct that is Send + Sync to hold your application’s shared state:
use vial;
use std::sync::{Arc, Mutx, atomic::{AtomicUsize, Ordering}};
use some_db_crate::DB;

struct MyConfig {
    db: Arc<Mutex<DB>>,
    counter: AtomicUsize,
}

impl MyConfig {
    pub fn new(db: DB) {
        MyConfig {
            db: Arc::new(Mutex::new(db)),
            counter: AtomicUsize::new(0),
        }
    }
}
  1. Tell Vial about your state object before calling run!:
fn main() {
    let db = DB::new();
    vial::use_state!(MyConfig::new(db));
    vial::run!();
}

Now your actions and filters can access MyConfig by calling the state() method on Request:

use vial::prelude::*;

routes! {
    GET "/list" => list;
}

fn find_names(db: Arc<Mutex<DB>>) -> Result<Vec<String>, db::Error> {
    Ok(db.lock()?.query("SELECT name FROM names")?
        .map(|row| row.get("name")?)
        .collect::<Vec<_>>())
}

fn list(req: Request) -> Result<String> {
    Ok(
        find_names(req.state::<MyConfig>().db.clone())?
            .map(|name| format!("<li>{}</li>", name))
            .join("\n")
    )
}

You might find it more convenient to define and implement a local trait on the Request struct instead of calling request.state() directly:

use vial::prelude::*;

routes! {
    GET "/list" => list;
}

// ...

trait WithConfig {
    fn config(&self) -> &MyConfig;
}

impl WithConfig for vial::Request {
    fn config(&self) -> &MyConfig {
        self.state::<MyConfig>()
    }
}

fn list(req: Request) -> Result<String> {
    Ok(
        find_names(req.config().db.clone())?
            .map(|name| format!("<li>{}</li>", name))
            .join("\n")
    )
}

Templates

Optional Feature: Coming soon.

Cookies

Extremely basic cookie support is available by enabling the cookies feature in your Cargo.toml:

[Dependencies]
vial = { version = "*", features = ['cookies'] }

Once it’s enabled you can access cookies the client sent using req.cookie(name), set cookies using a similar API to the Request Headers API, or remove cookies using remove_cookie() or without_cookie():

use vial::prelude::*;

routes! {
    GET "/" => show;
    GET "/clear" => clear;
    GET "/set/:count" => set;
}

fn show(req: Request) -> impl Responder {
    let count: usize = req.cookie("count").unwrap_or("0").parse().unwrap();
    let new_count = count + 1;
    Response::from(format!("count: {}", count))
        .with_cookie("count", new_count.to_string())
}

fn clear(req: Request) -> impl Responder {
    Response::redirect_to("/").without_cookie("count")
}

fn set(req: Request) -> Option<impl Responder> {
    let val: usize = req.arg("count")?.parse().unwrap();
    Response::redirect_to("/")
        .with_cookie("count", val.to_string()).into()
}

fn main() {
    run!();
}

Like all HTTP key/value pairs, cookie names are case insensitive.

Sessions

Sessions are essentially encrypted cookies that live until the user closes their browser. As a result, they can be treated as semi- trustworthy and store information like a username or preference.

To enable sessions, enable the sessions feature in your Cargo.toml:

[Dependencies]
vial = { version = "*", features = ['sessions'] }

Note that this also turns on the cookies feature, as sessions depend on cookies.

Once it’s enabled you can access the client’s session using an API similar to cookie’s: req.session(name). You can set session values using a similar API to the Request Headers API. Removing session values is done using remove_session() or without_session():

use vial::prelude::*;

routes! {
    GET "/" => show;
    GET "/clear" => clear;
    GET "/set" => set;
}

fn show(req: Request) -> impl Responder {
    let count: usize = req.session("count").unwrap_or("0").parse().unwrap();
    let new_count = count + 1;
    Response::from_session("count", &new_count.to_string()).with_body(format!(
        r#"
<h1> Count: {} </h1>
<p><a href="/clear">Clear Count</a></p>
<form action="/set" method="GET">
    <input type="text" name="count" />
    <input type="submit" value="Set Count" />
</form>
    "#,
        new_count
    ))
}

fn clear(_req: Request) -> impl Responder {
    Response::redirect_to("/").without_session("count")
}

fn set(req: Request) -> impl Responder {
    let mut res = Response::redirect_to("/");
    if let Some(val) = req.query("count").map(|c| c.parse::<usize>().unwrap_or(0)) {
        let val = val.to_string();
        res.set_session("count", &val);
    }
    res
}

fn main() {
    run!();
}

Like all HTTP key/value pairs, session names are case insensitive.

JSON

Vial supports JSON requests and responses via Serde and nanoserde.

First, enable either json_serde or json_nano in your Cargo.toml:

[Dependencies]
vial = { version = "*", features = ['json_serde'] }

Now, you can use Request::json to deserialize a JSON request body, and Response::with_json to serialize a JSON response body:

// serde example
use vial::prelude::*;

routes! {
    POST "/json" => post;
}

fn post(req: Request) -> impl Responder {
    match req
        .json::<serde_json::Value>()
        .ok()
        .as_ref()
        .and_then(|val| val.as_object())
        .and_then(|obj| obj.get("message"))
        .and_then(|val| val.as_str())
        .map(|message| message.to_string())
    {
        Some(message) => Response::from(200).with_json(serde_json::json!({
            "message": format!("Echo: {}", message)
        })),
        None => Response::from(400).with_body("json request parse error"),
    }
}

fn main() {
    vial::run!().unwrap();
}

Serde’s derive macro can conveniently generate code to serialize and deserialize structs and enums, including helpful error messages. To use it, add a dependency on serde with the derive feature enabled in your Cargo.toml:

[Dependencies]
serde = { version = "*", features=["derive"] }

Now, you can use Request::json and Response::with_json with any type implementing serde::Deserialize and serde::Serialize, respectively:

use vial::prelude::*;

routes! {
    POST "/json" => post;
}

#[derive(serde::Serialize, serde::Deserialize)]
struct Echo {
    message: String,
}

fn post(req: Request) -> impl Responder {
    match req.json::<Echo>() {
        Ok(echo) => Response::from(200).with_json(Echo {
            message: format!("Echo: {}", echo.message),
        }),
        Err(e) => Response::from(400).with_body(e.to_string()),
    }
}

fn main() {
    vial::run!().unwrap();
}

nanoserde has a similar derive macro available:

#[derive(SerJson, DeJson)]
struct Message {
    message: String,
}

Database

“Pro Tip”: Coming soon.

Markdown

“Pro Tip”: Coming soon.