Mastering Roda

Federico M. Iachetti

Acknowledgments

This book was originally written as a collaboration between myself (Federico) and Avdi Grimm (from RubyTapas/Graceful.dev)

It is now an Open Source effort maintained by myself and Jeremy Evans (Roda's creator and maintainer).

Creative Commons License

Mastering Roda by Federico Iachetti is licensed under a Creative Commons Attribution 4.0 International License .

You can contribute and propose changes to this book by submitting a merge request to the repository at https://gitlab.com/fiachetti/mastering-roda .

How to support the project

If you would like to support this project monetarily, you can do so via be a direct donation through PayPal. Although, you wouldn't get anything in return, except for my eternal gratitude towards you.

Another way to support the project is to share it. More eyes on an open source book means more ideas and more feedback.

Thank you very much for helping!

Full disclosure
The funds obtained via direct donation go directly to me (Federico M. Iachetti). I've offered Jeremy to share profits as an affiliate, but he's in no position to accept the deal. So, either if you're planning on contributing or not, you should thank him in any way you can, because it's an awesome effort he's putting into Roda. Thanks Jeremy!

Introduction

I've been a Ruby on Rails developer since 2011. Rails is a great framework. It's also a big and opinionated framework. It allows us to do almost anything out of the box (whether we need to or not), with little or no extra configuration needed. Unless we want to do something that's "off the rails"… in which case, we're on our own.

After a while using Rails I decided I wanted to move on to smaller frameworks, taking a more minimalistic approach. After some time trying a number of gems I landed on Roda, a small library created by Jeremy Evans, which I really liked. In fact, I liked so much that I wanted to share my knowledge to how to use it.

About Roda

Roda is a routing tree web toolkit. The Roda philosophy prefers simplicity, reliability, extensibility, and performance, with only the most basic features enabled by default. While only the most basic features are enabled by default, Roda ships with an extensive set of features. All other features are enabled separately using a very powerful plugins library.

Each of the features (plugins) that Roda ships with can be thought of as a tool, and depending on the type of web application we are building, we may need different tools. Roda lets us choose the tools that we use to build our web application. In general, Roda operates more like a library than a web framework, though it is often compared with other web frameworks.

Roda performs routing by implementing what its developer calls a routing tree. As we'll see in the pages ahead, this routing tree approach is what gives Roda a lot of its flexibility and power. The primary advantage of the routing tree is that request handling and routing are integrated, so that we can handle a request while routing it. This can remove a lot of duplication inherent in web frameworks that separate routing from request handling.

Roda is a lightweight library. The basic features enabled by default are implemented in fewer than 800 lines of code. However, Roda ships with over 100 plugins that can handle the needs of most web applications.

Roda was designed with performance in mind, and it is widely considered the fastest Ruby web framework. While some of the optimizations that Roda uses make the code more difficult to understand, most of Roda's code is easy to understand. Applications built using Roda are also easy to understand, since it is possible to trace the logic and see exactly how a request will be routed/handled.

Roda takes special care to avoid polluting the application scope with many instance variables, constants or methods. This helps avoid unexpected name clashes. All internal instance variables that Roda uses are prefixed with an underscore, and all constants are prefixed with Roda. There are only a handful of methods defined in the application scope.

In this book, we'll cover the basic concepts and tools Roda provides, along with conventions and good practices that will help us get started using this amazing library.

How to read this book

This book is completely driven by examples. Every concept introduced is described by providing a problem or situation to solve.

You're welcome to simply read this book from cover to cover. Each example is written in a way that you can follow along at home, and I suggest you do. Applying examples to code is a great way to cement the concepts in your mind.

I'd also suggest that you go further and experiment on the code. If there's something you didn't completely understand, before contacting us, modify the code in order to discover what's missing. If you find something that you think should be changed, please submit a merge request to change the book.

A quick introduction to lucid_http

Before starting with Roda itself, I'd like to introduce lucid_http, a gem I created for showing off HTTP interactions. This gem is used in the rest of the book for the example code for submitting requests and receiving responses. I won't go into much detail here. For a more detailed explanation, please consult the appendix called "The lucid_http gem".

lucid_http is a thin wrapper over the http.rb library, which provides a very simple and consistent API for performing HTTP requests. What lucid_http adds is a higher level presentation abstraction.

I created a small app that will allow me to show how the gem works. The code for this app is included in the Mastering Roda repository, in the appendix_lucid_http_app.ru file. In the appendix I go into detail about how to get it running.

So, we start by making a GET request to the hello path.

To do this, we need to call the GET method, passing the required path as an argument.

require "lucid_http"

GET "/hello"
# => "<h1>Hello World!</h1>"

The method will return the rendered body, which is shown as the string after the # => comment marker.

By default, the base URL we're targeting is http://localhost:9292. Notice that there's not a trailing slash on that string, which means that we need to include it on the path we want to request, hence the /hello argument path.

By calling the GET method we're returning the body of our response. However, what about other relevant information? Well, lucid_http also provides the following methods:

require "lucid_http"

GET "/hello/you"
status                          # => "200 OK"
status.to_i                     # => 200
content_type                    # => "text/html"
url                             # => "http://localhost:9292/hello/you"
path                            # => "/hello/you"

When we decide to make the next request, the current information gets cleaned up, and the new request starts with a clean slate.

require "lucid_http"

GET "/hello/you"
status                          # => "200 OK"
content_type                    # => "text/html"
url                             # => "http://localhost:9292/hello/you"
path                            # => "/hello/you"
body[/\>(.+)\</, 1]             # => "Hello, You!"

GET "/403"
status                          # => "403 Forbidden"
content_type                    # => "text/html"
url                             # => "http://localhost:9292/403"
path                            # => "/403"
body                            # => "The request returned a 403 status."

We can follow redirections passing the follower: :follow attribute

require "lucid_http"

GET "/redirect_me"
status                          # => "302 Found"

GET "/redirect_me", 
status                          # => "200 OK"
body                            # => "You have arrived here due to a redirection."

If we get an error status (500), we can see what happened by calling error, which will return the first line of the body in order to show a succinct message.

require "lucid_http"

GET "/500"
status                          # => "500 Internal Server Error"
error                           # => "ArgumentError: wrong number of arguments (given 0, expected 2+)"

However, if the request doesn't return a 500 code, the library will be nice enough to let us know.

require "lucid_http"

GET "/not_500"
status                          # => "200 OK"
error                           # => "No error found"

If we have a json endpoint, the string output might not be the best way to show it

require "lucid_http"

GET "/hello_world"
# => "You said: hello_world"

GET "/hello_world.json"
# => "{\"content\":\"You said: hello_world\",\"keyword\":\"hello_world\",\"timestamp\":\"2016-12-31 15:00:42 -0300\",\"method\":\"GET\",\"status\":200}"

However, passing the formatter: :json attribute, we see it as a Hash, which is much nicer to look at. That's better.

require "lucid_http"

GET "/hello_world"
# => "You said: hello_world"

GET "/hello_world.json", 
# => {"content"=>"You said: hello_world",
#     "keyword"=>"hello_world",
#     "timestamp"=>"2016-12-31 15:00:42 -0300",
#     "method"=>"GET",
#     "status"=>200}

lucid_http also support a number of other HTTP verbs we can use.

require "lucid_http"

GET     "/verb"                  # => "<GET>"
verb                             # => "GET"

POST    "/verb"                  # => "<POST>"
verb                             # => "POST"

PUT     "/verb"                  # => "<PUT>"
verb                             # => "PUT"

PATCH   "/verb"                  # => "<PATCH>"
verb                             # => "PATCH"

DELETE  "/verb"                  # => "<DELETE>"
verb                             # => "DELETE"

OPTIONS "/verb"                  # => "<OPTIONS>"
verb                             # => "OPTIONS"

Finally, we can submit a form with the request using the :form option.

require "lucid_http"

POST "/params?item=book", 
# => {"item"=>"book"}

POST "/params", , { "book", 1, 50.0, "The complete guide to doing absolutely nothing at all."  }
# => {"item"=>"book",
#     "quantity"=>"1",
#     "price"=>"50.0",
#     "title"=>"The complete guide to doing absolutely nothing at all."}

Now that we have a basic understanding of how results will be displayed throughout the book, we can begin.

Core Roda

In this section we'll explore Roda's core classes, and the behavior and capabilities that Roda offers by default. This is referred to as core Roda. However, when discussing many core Roda features, we'll also bring up related plugins.

We'll learn the basic structure of a Roda application, how it routes requests from the user to the target code, how to return the appropriate response, and how to handle sessions.

A very small hello world

We'll kick things off by creating a very small web application to help get a grasp of what it looks like. We first need to create a new Roda project.

Some users that are used to working in a heavyweight web framework like Rails might be expecting Roda to include a command to generate a new project. However, Roda is more of a library than a framework, and does not come with such a command.

So our first step is simply to create a new, empty directory.

mkdir my_app

Let's add a Gemfile, to control the Rubygems this project will use.

Naturally, our first gem to add is roda. Then we need a web application server. We'll go with puma, which is a simple, mature, and fast choice. So our Gemfile will look like this.

source "https://rubygems.org"

gem "roda"
gem "puma"

Now we run bundle install, to install the gems we've added.

bundle install

For the rest of the book, unless noted otherwise, we'll be using this configuration for every example. The gems we installed are going to be present in every Gemfile from here on. Now we're ready to start writing some code.

Every major Ruby web application framework is built on top of a universal compatibility layer called Rack. Roda too is Rack-compatible, so we start out by creating a "rackup file", using the standard file name config.ru.

In it we require roda , and then create a new class to represent our application. This app will inherit from the Roda class.

Roda is built around the idea of a routing tree, which implies creating branches by adding routes. So we start by defining a route block. This block will receive a request as an argument, which, by convention we abbreviate as r.

Our first route checks to see if the HTTP request is for the /hello path. If so, the block that we provide will be executed. We'll just return the string "hello!" from our block.

By inheriting from the Roda class, our App class is implicitly a Rack application. In order to tell Rack (and the web server) to execute our app for HTTP requests, we have to tell it to run the App class.

require "roda"

class App < Roda
  route do |r|
    r.get "hello" do
      "hello!"
    end
  end
end

run App

Then from the command line we run the rackup command to start up the web server and start serving requests.

rackup

If we now load http://localhost:9292/hello in our browser, we see our first message. Yay!

Now let's make a change to our tiny application. We'll change the return value of the block.

require "roda"

class App < Roda
  route do |r|
    r.get "hello" do
      "Hello, world!"
    end
  end
end

run App

If we navigate again to http://localhost:9292/hello, we see … nothing has changed. Why not? Because the server is still running our original code.

This is another reminder that we're not building on a fancy web framework. If we want bells and whistles like automatic code reloading, we can have them! … but we have to ask for them.

For a very simple way to reload the code every time we change it, we'll use the rerun gem (RubyTapas Episode #320 provides a quick introduction to the rerun gem).

rerun rackup

Now we can navigate to http://localhost:9292/hello, and see the updated output.

Again, for this moment on assume that, for every example, we'll be running rackup using rerun in order to avoid having to start and stop the server by hand.

We have a working web application, but we'd like to be able to interact with it. Let's give the user the ability to specify who or what is being greeted. For this, let's create a second route. As before, we'll match again the string "hello". However, this time, we'll follow the "hello" with String, which indicates that any string segment will match. This means that /hello/Frank and /hello/Nancy will both match.

When we specify the String class as a matcher, the block we provide will receive a corresponding block argument for the string that matched. Let's give the block argument an appropriate name, and then interpolate it into the output.

require "roda"

class App < Roda
  route do |r|
    r.get "hello", String do |name|
      "<h1>Hello #{name}!</h1>"
    end
  end
end

run App

When we navigate to http://localhost:9292/hello/Roda we see that the string Roda is taken from the path and inserted into the page.

So far, we've been working out of the config.ru rackup file. However, a rackup file is really intended just for configuring the app server startup process, not for including a whole application. Let's organize our app a little better, and move the application code out of the config.ru file.

We remove the entire App class, along with the require statement, and in its place, we require a file named app.rb.

require "./app"

run App

Then we proceed to create the app.rb file and paste the code in.

require "roda"

class App < Roda
  route do |r|
    r.get "hello", String do |name|
      "<h1>Hello #{name}!<h1>"
    end
  end
end

We can check that everything still works.

So there we have it, our very first Roda app.

Now let's imagine we are writing a site that tells us about a mystery guest. The user doesn't know who the mystery guest is, but they should be able to see it by browsing to the /mystery_guest path.

For reasons beyond the scope of this book, we are going to have the mystery guest be a mozzarella pizza. We'll be using Ruby's Struct class to create a Pizza class, and then create an instance of Pizza. Then we'll use that Pizza instance in our mystery_guest route. Just for whimsy, we'll misspell Guest on the page.

require "roda"
Pizza = Struct.new()

class App < Roda
  mystery_guest = Pizza.new("Mozzarella")

  route do |r|
    r.get 'mystery_guest' do
      "The Mystery Gest is: #{mystery_guest}"
    end
  end
end

That's not very helpful. What's going on here? If we use LucidHttp to fetch the page, we see that the result is coming back correctly, it's a Pizza!

require "lucid_http"

GET "/mystery_guest"
# => "The Mystery Gest is: #<struct Pizza flavor=\"Mozzarella\">"

Why can't we see that on the browser if the response is getting properly returned? If we take a closer look, everything breaks after the #. The next string is <struct Pizza flavor="Mozzarella">, which a lot like at HTML tag. What's the solution for this?, well, one solution would be to implement a to_s method in our pizza class and everything would be fine. Unfortunately, this approach is too naive. Even though for this particular case is the actual solution, it excludes at least two major considerations:

For both cases, the h plugin provides a solution. We can load it into our Roda application using plugin :h. After loading it, we can pass the string we need to escape to the h method

require "roda"
Pizza = Struct.new()

class App < Roda
  plugin 

  mystery_guest = Pizza.new("Mozzarella")

  route do |r|
    r.get 'mystery_guest' do
      "The Mystery Gest is: #{h mystery_guest}"
    end
  end
end

and the response will be properly escaped

require "lucid_http"

GET "/mystery_guest"
# => "The Mystery Gest is: #&lt;struct Pizza flavor=&quot;Mozzarella&quot;&gt;"

It makes the raw HTML look ugly, but it allows for correctly displaying the result in the browser

Basic routing

At a basic level, HTTP is just a bunch of requests and responses. If we only ever needed to respond to one kind of request, we could handle it all with one big hunk of code.

However, web applications usually support a lot more than one type of request. To keep ourselves sane as developers, we usually try to separate out the different bits of code which handle different types of request. Likewise, we try to avoid duplicating the bits of code that are the same across requests.

Taking a client's request and directing it to the bit of code that should handle it is called routing and in this section we'll take a look how Roda does it.

As we saw in the previous section's examples, to create a Roda app we first define a class which inherits from Roda.

Now, in order to actually add our routes, we go into our Roda app class and call the route class method. We pass a block, which will receive as a parameter an object representing the current request.

Let's stop right here and take a look at what the contents of the r parameter look like. We'll use the Kernel#p method for this. Since we are only using p for debugging, we will not indent it, so that it is easier to spot to remove later. We'll also return an empty string in order to avoid cluttering our output with error messages (we'll explain why this step is necessary later).

  require "roda"

  class App < Roda
    route do |r|
p r
      ""
    end
  end

When we browse any path on the app, we see that our block has been passed an App::RodaRequest instance.

#<App::RodaRequest GET />
127.0.0.1 - - [07/Apr/2023:10:57:40 -0300] "GET / HTTP/1.1" 200 - 0.0009

Roda::RodaRequest is a subclass of the Rack::Request class, with methods added to handle routing. App::RodaRequest is a subclass of Roda::RodaRequest::, which is automatically created when App is created, which allows for customization of App request instances in plugins.

We can see from the p output that the request is a GET request for the / path.

We can call methods on the request object (r) to handle routing, and we're going to explore some of them in the next few sections. For now, let's start by defining a single route.

We call the on method on our request object, passing it a string parameter and a block. From the block, we return a string.

class App < Roda
  route do |r|
    r.on "hello" do
      "Hello Roda!"
    end
  end
end

The r.on method is one of the methods that Roda calls a match method, which is a method that accepts arguments (called matchers), and sees if the matchers match the request. If the match is successful, the match method yields to the block (called a match block), and the request handling ends when the match block exits. If the match was not successful, the match method does not yield to the match block, and execution continues.

The r.on method is the simplest match method. It only checks that the matchers match the request, and does not do any additional checks.

Let's take a look at what it produced. The page body is the exact string the match block returned. When Roda yields to a match block, and the match block returns a string, that string is used as the body of the response. We can also see that the response's status code is 200 and the response's content type is text/html.

require "lucid_http"

GET "/hello"

body                            # => "Hello Roda!"
status                          # => "200 OK"
content_type                    # => "text/html"

If the block returns nil or false,

class App < Roda
  route do |r|
    r.on "hello" do
      nil
    end
  end
end

Roda interprets it as an unhandled route, and returns an empty body, with a 404 response status code.

require "lucid_http"

GET "/hello"

body                      # => ""
status                    # => "404 Not Found"

If we return something that Roda does not know how to handle, such as an Integer

class App < Roda
  route do |r|
    r.on "hello" do
      1
    end
  end
end

Roda raises an Roda::RodaError exception, which most webservers will treat as an internal server error, with some body prepared by the webserver:

require "lucid_http"

GET "/hello"

error              # => "Roda::RodaError: unsupported block result: 1"
status             # => "500 Internal Server Error"

Roda's default behavior is that only strings, nil, and false are supported return values of match blocks. Roda ships with plugins that support additional return values for match blocks, and we will discuss those later.

The story doesn't end there. Let's go back to our previous example. If we look closely, we see that the actual code that generates the content is wrapped inside a block.

Let's put a debugging statement inside the route block and another one inside the hello block,

  require "roda"

  class App < Roda
    route do |r|
p "ROUTE block"

      r.on "hello" do

p "HELLO block"

        "Hello Lucid!"
      end
    end
  end

When we browse to the root path of the application (http://localhost:9292/), we can see that the string corresponding to the route block was printed.

When we browse to http://localhost:9292/hello, both strings are printed.

"ROUTE block"
127.0.0.1 - - [07/Apr/2023:11:02:05 -0300] "GET / HTTP/1.1" 404 - 0.0009
"ROUTE block"
"HELLO block"
127.0.0.1 - - [07/Apr/2023:11:02:16 -0300] "GET /hello/ HTTP/1.1" 200 12 0.0006

This demonstrates one of the core principles of Roda's design. In many web frameworks, the routing definitions are executed when the program starts, and compiled into some kind of internal data structure. Roda is different, in that the route block is executed every time a request is received.

This is a really powerful idea. For one thing, it makes Roda's routing really easy to understand. We don't have to mentally map the route definition to some kind of internal representation. What we see is what we get: if we can read Ruby code, we can tell what each step of the route will do.

Some users might be worried that Roda's design could lead to slower routing of requests as the route definitions grow in size. After all, Roda is executing the route block for every single request. However, as branches not taken are skipped, and routes inside skipped branches are not considered, Roda's design generally results in very fast routing! In fact, when it comes to routing, Roda is the fastest Ruby web framework with significant production usage.

From a Big-O perspective, Roda's default routing design requires an O(N) lookup at each branch of the routing tree (including the root), with N being the number of possible branches at that level. However, in most applications this results in roughly O(log(N)) routing for an application, where N is the total number of routes in the application. We'll see later that Roda ships with a plugin that allows for O(1) routing at each branch of the routing tree, allowing for O(N) routing performance, where N is the number of segments in the request (regardless of the total number of routes in the application).

As we'll see in future sections, the incoming request works its way through the Roda routing definition like a caterpillar crawling from a tree's trunk out to the very tip of a twig. Only the parts of the routing code that match the request are ever run.

We can see it happen by adding a second route with debugging statements.

  require "roda"

  class App < Roda
    route do |r|
p "ROUTE block"
      r.on "hello" do
p "HELLO block"
        "hello"
      end

      r.on "goodbye" do
p "GOODBYE block"
        "goodbye"
      end
    end
  end

If we now browse to http://localhost:9292/goodbye, we might expect both hello and goodbye strings to appear in the output, but they don't. Only the route that matched was executed, even though that the definition of the hello route came first.

"ROUTE block"
"GOODBYE block"
127.0.0.1 - - [07/Apr/2023:11:04:56 -0300] "GET /goodbye HTTP/1.1" 200 7 0.0009

In fact, this tree-style execution is why Roda is called a "Routing Tree Web Toolkit".

Another way to think about this is as a lazy evaluation of the routes. A route (match block) doesn't get executed unless it's actually needed.

There is another advantage to this tree-ish style of routing. In most other frameworks, if we need to perform some setup for handling a request, we must wait until after routing has been completed. With Roda, setup for handling a group of routes can happen while we are routing. This eliminates the need for "filter" or "hook" mechanisms to perform actions before or after every one of a group of routes.

What do I mean with that? Well, say we have users in our system and both routes either say hello or goodbye to them, but for that to happen, we first need to find the user somehow. We can retrieve it from a database, from the session or any other mechanism we can think of. Let's simulate this by just assigning a local variable. Then we insert it into the output string. Now we need to do the same for the second route.

require "roda"

class App < Roda
  route do |r|
    r.on "hello" do
      name = "Roda"
      "Hello, #{name}!"
    end

    r.on "goodbye" do
      name = "Roda"
      "Goodbye, #{name}!"
    end
  end
end

If we kept adding sub-routes that need to access this user, we'd need to keep fetching it, right? Well, actually no. Here's when the shared scope nature of code blocks in Ruby pays off in this structure. Instead of retrieving the user each time we need it, we can actually retrieve it once inside the route block (or inside any match block) and it will be available in all of the nested routes.

require "roda"

class App < Roda
  route do |r|
    name = "Roda"

    r.on "hello" do
      "Hello, #{name}!"
    end

    r.on "goodbye" do
      "Goodbye, #{name}!"
    end
  end
end

Now we can browse to http://localhost:9292/hello and we get the expected output. We get similar output if we go to http://localhost:9292/goodbye.

require "lucid_http"

GET "/hello"                    # => "Hello, Roda!"
GET "/goodbye"                  # => "Goodbye, Roda!"

Roda's routing advantages are due to the fact that Roda stores the routes in a block, and not in a data structure. However, this design requires a trade-off, which is that it limits the ability to do route introspection, such as printing a list of possible routes. There are workarounds that allow for route introspection, but they require additional work to setup.

Match methods

r.on and r.is

In the previous section, we got our first taste of routing in Roda. Now let's explore the different match methods that Roda provides us out of the box.

In order to do it, we'll begin with a simple example Roda blogging app (very original, right?). We want a /posts route that returns the list of all of our posts. We create this route using the r.on match method. We add a variable to hold our data repository which will just be a hash with all of our posts. From the block, we return a string representing our post list and we just join them using pipe characters for simplicity.

class App < Roda
  route do |r|
    r.on "posts" do
      post_list = {
        1 => "Post[1]",
        2 => "Post[2]",
        3 => "Post[3]",
        4 => "Post[4]",
        5 => "Post[5]",
      }

      post_list.values.join(" | ")
    end
  end
end

If we now browse to http://localhost:9292/posts, we see our posts list.

If we browse to http://localhost:9292/posts/ or http://localhost:9292/posts/whatever, we see the same list. That's something we probably don't want in a real application, and we'll go over how to fix this in a little bit.

require "lucid_http"

GET "/posts"
body
# => "Post[1] | Post[2] | Post[3] | Post[4] | Post[5]"

GET "/posts/"
body
# => "Post[1] | Post[2] | Post[3] | Post[4] | Post[5]"

GET "/posts/whatever"
body
# => "Post[1] | Post[2] | Post[3] | Post[4] | Post[5]"

r.on tries to match each given matcher ("posts" in this case) to the request. As the remaining path at the point for routing for all three request starts with /posts, the match is successful.

Let's see how routing works in more complex situations, by assuming we want the http://localhost:9292/posts/1 to return the post with id 1. To do so, we nest a call to r.is inside the r.on "posts" match block. r.is is a match method similar to r.on, but in addition to the matching done by r.on, it will also require that the path has been completely consumed after handling the matchers, in order for the match to be considered successful (we'll explain what consumed means in a little bit).

In other words, r.on is a non-terminal match method, and r.is is a terminal match method. A non-terminal match method doesn't require the remaining path to be completely consumed in order to successfully match. A terminal match method requires the remaining path to be completely consumed to successfully match.

So, if we add a sub-route that matches specifically the path /posts/1, we can have it return the first post.

class App < Roda
  route do |r|
    r.on "posts" do
      post_list = {
        1 => "Post[1]",
        2 => "Post[2]",
        3 => "Post[3]",
        4 => "Post[4]",
        5 => "Post[5]",
      }

      r.is "1" do 
        post_list[1]
      end

      post_list.values.map { |post| post }.join(" | ")
    end
  end
end

And this would work as expected for the first post.

require "lucid_http"

GET "/posts/1"
body                            # => "Post[1]"

But this approach doesn't scale. In order to make it more flexible, we can pass the Integer matcher to the r.is match method.

The Integer matcher operates similarly to the String matcher, but instead of matching any path segment, it only matches segments that consist solely of decimal characters (0-9). Like the String matcher, on a successful match, Roda will yield the matching path segment to the match block. However, the Integer matcher will convert the matching path segment to an Integer before yielding it.

class App < Roda
  route do |r|
    r.on "posts" do
      post_list = {
        1 => "Post[1]",
        2 => "Post[2]",
        3 => "Post[3]",
        4 => "Post[4]",
        5 => "Post[5]",
      }

      r.is Integer do |id|
        post_list[id]
      end

      post_list.values.map { |post| post }.join(" | ")
    end
  end
end

When we try it out, we see only the post we requested. Note that if we request a post that doesn't exist, post_list[id] will be nil, resulting in a 404 response. This is a desired behavior, so that a request for a post that does not exist will result in a not found response.

require "lucid_http"

GET "/posts/1"
body                            # => "Post[1]"
status                          # => "200 OK"

GET "/posts/5"
body                            # => "Post[5]"
status                          # => "200 OK"

GET "/posts/6"
body                            # => ""
status                          # => "404 Not Found"

Adding this r.is call did not affect other routes. The handling of /posts, /posts/, and /posts/whatever remain the same. However, it's not generally a good idea to allow arbitrary routes to a resource. We should probably change it so that /posts returns all posts, but /posts/ and /posts/whatever return 404 responses. We can make that change by wrapping the returning of all posts in an r.is block.

class App < Roda
  route do |r|
    r.on "posts" do
      post_list = {
        1 => "Post[1]",
        2 => "Post[2]",
        3 => "Post[3]",
        4 => "Post[4]",
        5 => "Post[5]",
      }

      r.is Integer do |id|
        post_list[id]
      end

      r.is do
        post_list.values.map { |post| post }.join(" | ")
      end
    end
  end
end

We can then check that this has the desired effect.

require "lucid_http"

GET "/posts"
body
# => "Post[1] | Post[2] | Post[3] | Post[4] | Post[5]"

GET "/posts/"
body                            # => ""
status                          # => "404 Not Found"

GET "/posts/whatever"
body                            # => ""
status                          # => "404 Not Found"

Going back to our example, it may seem kind of strange, as we are calling r.is with no matchers. However, this will become natural as we use Roda. We can think of matchers as filters. Roda's default behavior is that a match method will match, unless there is a matcher or other requirement causing it not to match. If there are no matchers, then r.on always matches, and r.is only matches if the remaining path has already been completely consumed.

Path, consuming and capturing

We've mentioned that the path can be consumed during the normal process of a request.

Now, what do we mean by consumed when talking about the request path? Lets take a look at the path during a request-response cycle. We'll add a couple of debugging statements at the beginning of the route, just after r.on "posts", r.is Integer and r.is.

  class App < Roda
    route do |r|
p [0, r.path]
      r.on "posts" do
p [1, r.path]
        r.is Integer do |id|
p [2, r.path]
          ""
        end

        r.is do
p [3, r.path]
          ""
        end
      end
    end
  end

When we look at the output produced, we note that it doesn't change during one request cycle.

[0, "/"]
127.0.0.1 - - [07/Apr/2023:18:58:07 -0300] "GET / HTTP/1.1" 404 - 0.0012
[0, "/posts"]
[1, "/posts"]
[3, "/posts"]
127.0.0.1 - - [07/Apr/2023:18:58:13 -0300] "GET /posts HTTP/1.1" 200 - 0.0013
[0, "/posts/1"]
[1, "/posts/1"]
[2, "/posts/1"]
127.0.0.1 - - [07/Apr/2023:18:58:19 -0300] "GET /posts/1 HTTP/1.1" 200 - 0.0007

When matchers match against the path, they consume the part of the path that they match against. But Roda doesn't change the path of a request while routing (r.path and r.path_info remain constant).

To keep track of the path consumption, Roda stores the part of the path that is yet to be consumed, calling it the remaining path (available at r.remaining_path). Using this information and the the requests's path, Roda can determine the part of the path that has already been routed, called the matched path (available at r.matched_path). As matchers successfully match, they consume that part of the remaining path (assuming they are matching against the path). The remaining path gets smaller during routing, and if it is empty, the remaining path has been fully consumed.

It's easier to understand how this works by showing the effect that matching has on the remaining path and the matched path. So let's output the remaining path and the matched path in different sections of the routing tree.

  class App < Roda
    route do |r|
p [0, r.matched_path, r.remaining_path]
      r.on "posts" do
p [1, r.matched_path, r.remaining_path]
        r.is Integer do |id|
p [2, r.matched_path, r.remaining_path]
          ""
        end

        r.is do
p [3, r.matched_path, r.remaining_path]
          ""
        end
      end
    end
  end

Here are the results for requests for /, /posts, and /posts/1, showing how the remaining path is consumed by the matchers, and what the matched path is at each step.

[0, "", "/"]
127.0.0.1 - - [19/Sep/2016:19:46:26 -0300] "GET / HTTP/1.1" 404 - 0.0016
[0, "", "/posts"]
[1, "/posts", ""]
[3, "/posts", ""]
127.0.0.1 - - [19/Sep/2016:19:46:36 -0300] "GET /posts HTTP/1.1" 200 47 0.0016
[0, "", "/posts/1"]
[1, "/posts", "/1"]
[2, "/posts/1", ""]
127.0.0.1 - - [19/Sep/2016:19:46:46 -0300] "GET /posts/1 HTTP/1.1" 200 7 0.0016

While most of the calls to match methods in previous examples have used a single matcher, be aware that these methods accept an arbitrary number of matchers. If we wanted to show all the posts that were posted on some date, we could capture the year, month and day, create a Date object and pass it to a Post.posts_for_date method.

# ...

r.on "posts", "date", Integer, Integer, Integer do |year, month, day|
  date = Date.new(year, month, day)
  posts = Post.posts_for_date(date)

  # ...
end

#...

The word capture here is tied to consume. In Roda, when matchers consume a segment of the path, they may also capture it and yield it to the match block. In the above example, "posts", "date", and Integer are all matchers, and all of them consume a segment of the path. However, the "posts" and "date" matchers do not capture the segments they consume, but the Integer matcher does. As there are three Integer matchers that consume a path segment, three arguments are yielded to the match block.

Let's expand the earlier example, and focus on handling /posts/1/show and /posts/1/show/detail routes. They both display the post, but the detail route includes information on the last accessed time. As we need to handle routes under /posts/1, we cannot use r.is Integer, we need to switch to r.on Integer, because we no longer want a terminal match at this point in the routing tree.

Inside the r.on Integer match block, we find the post and store it in a local variable. Then we call r.on "show". Inside r.on "show", we have two r.is calls, one for the case where the remaining path has been fully consumed, and another for where the remaining path is "/detail".

require "roda"

class App < Roda
  route do |r|
    r.on "posts" do
      # ...
      r.on Integer do |id|
        post = post_list[id]

        r.on "show"  do
          r.is do
            "Showing #{post}"
          end

          r.is "detail" do
            "Showing #{post} | Last access: #{Time.now.strftime("%H:%M:%S")}"
          end
        end
      end
      # ...
    end
  end
end

As a quick reminder, r.on is used for handling path branches in the routing tree, where there are multiple paths to handle inside the branch. r.is is used for handling path leafs in the routing tree, where the remaining path has been fully consumed by the routing tree.

r.on and r.is are probably the most common match methods in Roda. However, an HTTP request is not solely a request for a path, it is a request for a path using a specific request method, and the response should depend upon the request method used. We'll look at how to handle request methods in the next section.

r.get and r.post

Now that we know how to route requests for various paths, let's discuss how to handle various request methods. For browsers, there are only two request methods that we need to worry about, GET and POST. In general, GET is used for idempotent requests such as navigating to a page, and POST is used for forms that may modify state.

In general, successfully handling an request should require routing the full request path (consuming the entire remaining path), as well as be specific to the request method. Roda by default includes two match methods for handling requests for specific request methods, r.get for handling GET requests, and r.post for handling POST requests.

Both r.get and r.post have the same behavior other than the request method they match against. If not passed any matchers, both r.get and r.post operate as non-terminal match methods. However, if they are passed any arguments, both r.get and r.post operate as terminal match methods. Why the difference in behavior depending on whether arguments are passed? Well, in general use, Roda's behavior, while seemingly inconsistent, does exactly what we want.

In general, passing no arguments is used to check the request method after the path has been fully routed using r.is, in which case there is no reason to do a duplicate check for a terminal match.

require "roda"

class App < Roda
  route do |r|
    r.on "posts" do
      r.is Integer do |id|
        r.get do
          # Handle GET /posts/$ID
        end

        r.post do
          # Handle POST /posts/$ID
        end
      end
    end
  end
end

Alternatively, if the request method is being checked before the path, to branch first by request method and then by path, we would not want a terminal match.

require "roda"

class App < Roda
  route do |r|
    r.get do
      r.on "posts" do
        r.is Integer do |id|
          # Handle GET /posts/$ID
        end
      end
    end

    r.post do
      r.on "posts" do
        r.is Integer do |id|
          # Handle POST /posts/$ID
        end
      end
    end
  end
end
Note
Routing first by path and then by HTTP request method often leads to less code duplication. In real world applications there are few instances where routing by request method first makes things easier. An example of such an instance would be an application where the vast majority of routes require one request method (probably GET), and few routes use another request method.

However, when we pass matchers to r.get and r.post, that is usually an indication that the remaining path to route is only handled by a specific request method.

require "roda"

class App < Roda
  route do |r|
    r.on "posts" do
      r.on Integer do |id|
        r.get "show" do
          # Handle GET /posts/$ID/show
        end

        r.post "update" do
          # Handle POST /posts/$ID/update
        end
      end
    end
  end
end

If r.get and r.post did not do a terminal match if passed a matcher, then we would need to wrap all such calls with r.is

require "roda"

class App < Roda
  route do |r|
    r.on "posts" do
      r.on Integer do |id|
        r.get "show" do
          r.is do
            # Handle GET /posts/$ID/show
          end
        end

        r.post "update" do
          r.is do
            # Handle POST /posts/$ID/update
          end
        end
      end
    end
  end
end

As shown above, that would just lead to redundant code. By making r.get and r.post operate as terminal match methods if passed any matchers, Roda increases usability at the expense of consistency.

What if we want a r.get or r.post to use a terminal match? We would pass a matcher that is always matches, which is true.

require "roda"

class App < Roda
  route do |r|
    r.on "posts" do
      r.on Integer do |id|
        r.get true do
          # Handle GET /posts/$ID
        end

        r.is "manage" do
          r.get do
            # Handle GET /posts/$ID/manage
          end

          r.post do
            # Handle POST /posts/$ID/manage
          end
        end
      end
    end
  end
end

So far, we've just discussed the GET and POST request methods. What about the other request methods? In order to keep Roda small, only the methods that browsers support (GET and POST) are included by default. However, Roda ships with an all_verbs plugin that adds other request methods such as r.head, r.put, r.patch, and r.delete for handling the other HTTP request methods. The behavior for these methods is the same as r.get and r.post, other than the request method they use as a filter.

require "roda"

class App < Roda
  plugin 

  route do |r|
    r.on "posts" do
      r.is Integer do |id|
        r.head do
          # Handle HEAD /posts/$ID
        end

        r.get do
          # Handle GET /posts/$ID
        end

        r.post do
          # Handle POST /posts/$ID
        end

        r.put do
          # Handle PUT /posts/$ID
        end

        r.patch do
          # Handle PATCH /posts/$ID
        end

        r.delete do
          # Handle DELETE /posts/$ID
        end
      end
    end
  end
end

Additionally, if we want to treat HEAD requests the same as GET requests, except omit the response body, we can use the head plugin:

require "roda"

class App < Roda
  plugin 

  route do |r|
    r.on "posts" do
      r.is Integer do |id|
        r.get do
          # Handle HEAD /posts/$ID (response body will be empty)
          # Handle GET /posts/$ID
        end

        r.post do
          # Handle POST /posts/$ID
        end
      end
    end
  end
end

When hosting a public website, using the head plugin is recommended, unless the application is handling HEAD requests separately. Otherwise, web crawlers that use HEAD will probably think the related pages no longer exist, since a HEAD request for them would result in a 404 response.

r.root

So far, we've been looking at how to route requests for various paths and request methods. What about requests that are for the root path of a website?

If we take a look at the previous section, we can figure out one of the ways Roda allows us to define it: using the r.get match method, with an empty string matcher (which match GET / requests).

For now we'll just return a dummy string.

class App < Roda
  route do |r|
    r.get "" do
      "Root Path"
    end

    r.get "posts" do
      posts = (0..5).map {|i| "Post #{i+1}"}
      posts.join(" | ")
    end
  end
end

When requesting the page, we get that string as the request body.

require "lucid_http"

GET "/"
path                            # => "http://localhost:9292/"
body                            # => "Root Path"

Now, this is a route that we'll end up writing for every single app we work on, and the current syntax is not very pretty. Luckily, Roda gives us a convenience match method for this particular route called, r.root.

Let's change this example to use it.

class App < Roda
  route do |r|
    r.root do
      "Root Path"
    end

    # ...
  end
end

If we make the same request again, the r.root match method will match and the result will be the same.

require "lucid_http"

GET "/"
path                            # => "http://localhost:9292/"
body                            # => "Root Path"

As mentioned above, r.root is the same as r.get "", not the same as r.is "". By design, it only matches GET requests. If we would like to handle requests for other HTTP request methods at the root path, we will have to stick to r.is "". One advantage of using r.root is that it expresses our intention more clearly.

Now, say that we want to be able to fetch a post using the /posts/:id path. We need to add a route for that. Notice that we changed the match method using the "posts" matcher from r.get to r.on because now it's not handling a terminal route anymore. Inside r.on "posts", we have two routes, one that displays all posts (for GET /posts/, note the trailing slash) and one that only displays the requested post (e.g. for GET /posts/1).

require "roda"

class App < Roda
  route do |r|
    # ...
    r.on "posts" do
      posts = (0..5).map {|i| "Post #{i}"}

      r.get "" do
        posts.join(" | ")
      end

      r.get Integer do |id|
        posts[id]
      end
    end
  end
end

We can then check that everything works as expected.

require "lucid_http"

GET "/posts/"
# => "Post 1 | Post 2 | Post 3 | Post 4 | Post 5"

GET "/posts/1"
# => "Post 1"

Now, as explained earlier, passing an empty string to the r.get match method is the same as using r.root. Let's make this change,

r.on "posts" do
  posts = (0..5).map {|i| "Post #{i}"}

  r.root do
    posts.join(" | ")
  end
end

and see if it works here.

require "lucid_http"

GET "/posts/"
# => "Post 1 | Post 2 | Post 3 | Post 4 | Post 5"

Yes, that worked, even though we're not at the root of our application. Why is that? This has to do with the way matchers work in Roda. As it turns out, all of the match methods that ship with Roda and match on the remaining path, so any part of the path that has already been consumed will not be considered when matching.

OK, the r.root match method works here but, does it make sense to use the root abstraction in a sub-route? While it's a matter of taste, in general it only makes sense if we want to support routes with trailing slash, and only handle GET requests for those routes. For the root route, a trailing slash is forced and GET is often the only request method used, but for all other routes, we would have to go out of our way to design a path structure for our application that used trailing slashes. In general, it's probably better to avoid that. So instead of /posts/ and /posts/1/, we would use /posts and /posts/1. In that case, we would not use r.root. Instead, we would use r.get true.

Custom match methods

In the previous sections, we looked at all of the matchers and match methods that are available by default with Roda. In this section, we'll write a simple custom match method.

As we've seen, the matchers and match methods that are available by default focus on matching on the remaining path or on the request method. While those are the primary parts of the request used for matching, there are cases when we may want to match on other parts of the request, or maybe on aspects unrelated to the request (such as the time or date).

Let's say we want to write a match method that expects a hash argument, and only matches if each key in the argument has a submitted parameter with a matching value.

require 'roda'

class App < Roda
  route do |r|
    r.with_params "secret"=>"Um9kYQ==\n" do
    end
  end
end

This code won't work because the r.with_params match method doesn't exist yet. Where should we define the r.with_params method?, Well, in a previous section, we learned that the r variable is an instance of a App::RodaRequest, so we can just add it to that class directly. However, we probably don't know how to write the with_params method yet.

require 'roda'

class App < Roda
  class RodaRequest
    def with_params(hash, &block)
      #
    end
  end

  route do |r|
    r.with_params "secret"=>"Um9kYQ==\n" do
    end
  end
end

First, we should decide how we want to write this method. Since the argument we passed in is a hash of expected parameters, and not a matcher, we wouldn't want to pass it to another method that expects a matcher. It's probably best to check whether the hash matches the expected parameters. If not, we don't have to do anything. If it does match, then we want to treat it as a match block.

It's probably helpful to know that the submitted parameters are available by calling the params method. This method actually comes from Rack::Request (remember that App::RodaRequest descends from Rack::Request). So a simple way to check if the parameters match is to iterate the supplied hash, and return from the method unless the value matches the value of submitted parameter.

def with_params(hash, &block)
  hash.each do |key, value|
    return unless params[key] == value
  end

  #
end

This only handles the case where the match fails. How do we handle a successful match? In this case, if after the hash.each call, we are still executing the method, then we want to treat the given block as a match block. This can be handled by passing the block to on with no arguments (remember that on is r.on, because r is an instance of App::RodaRequest).

def with_params(hash, &block)
  hash.each do |key, value|
    return unless params[key] == value
  end

  on(&block)
end

on will treat the block as a match block, passing control to it, and after the block executes, the response will be returned.

Conditionals in match blocks

Let's start with an example similar to one used in an earlier section.

require "roda"

class App < Roda
  route do |r|
    # ...
    r.on "posts" do
      posts = (0..5).map {|i| "Post #{i}"}

      r.get true do
        posts.join(" | ")
      end

      r.get Integer do |id|
        posts[id]
      end
    end
  end
end

We'll make a small modification. We want the /posts/:id route to render a string showing the post name and the access time.

class App < Roda
  route do |r|
    # ...
    r.on "posts" do
      # ...

      r.get Integer do |id|
        post        = posts[id]
        access_time = Time.now.strftime("%H:%M")

        "Post: #{post} | Accessing at #{access_time}"
      end
    end
  end
end

Now, when we access the post with an id of 2, we get more information.

require "lucid_http"

GET "/posts/2"
body                             # => "Post: Post 2 | Accessing at 09:53"

In our attempt to add a feature, we've introduced a bug. What would happen if we looked up a post that doesn't exist? Let's try it out.

require "lucid_http"

GET "/posts/12"
body                            # => "Post:  | Accessing at 09:55"

Oops, we're rendering an empty post name, which is far from desirable.

To fix this, we can add a conditional to handle the case where the post doesn't exist.

r.get Integer do |id|
  if post = posts[id]
    access_time = Time.now.strftime("%H:%M")
    "Post: #{post} | Accessing at #{access_time}"
  end
end

If the post exists, the if expression returns the last value, which is the string with the post and the access time. If the post doesn't exists, then the if expression returns nil, and Roda will use a 404 response with an empty body.

require "lucid_http"

GET "/posts/12"
status                            # => "404 Not Found"

Or alternatively, we can skip the rest of the block using next, just like in any Ruby block. next is usually used to skip the current iteration of the block and move to the next iteration, but as each route block is only executed at most once, it has the equivalent behavior of an early block return.

r.get Integer do |id|
  next unless post = posts[id]
  access_time = Time.now.strftime("%H:%M")
  "Post: #{post} | Accessing at #{access_time}"
end

As no argument is given to next, it is the equivalent of the block returning nil, so a request for a post that doesn't exist will result in a 404 response (the same as in the example above using if). However, we can provide an argument to next, which is the equivalent of forcing an early return of the block using the argument.

r.get Integer do |id|
  next "No matching post" unless post = posts[id]
  access_time = Time.now.strftime("%H:%M")
  "Post: #{post} | Accessing at #{access_time}"
end

This does work, but because the block returned a string, it is treated as a successful response.

require "lucid_http"

GET "/posts/12"
body                            # => "No matching post"
status                          # => "200 OK"

To include a body but use a 404 response code, we need to set the response status code manually before using next.

r.get Integer do |id|
  unless post = posts[id]
    response.status = 404
    next "No matching post"
  end

  access_time = Time.now.strftime("%H:%M")
  "Post: #{post} | Accessing at #{access_time}"
end

Then we can check that the expected body and status code are used.

require "lucid_http"

GET "/posts/12"
body                            # => "No matching post"
status                          # => "404 Not Found"

Metaprogramming routes

Due to Roda's design, we have full control over how routing works, which allows multiple ways to reduce duplication via approaches that are similar to metaprogramming.

Consider the following routing tree. Assume that the view here returns a response body string based on the argument given (view is added by the render plugin, which we'll be discussing later). This has a fair amount of duplication here, with each of these r.get lines looking similar.

route do |r|
  r.get("about")      { view("about") }
  r.get("contact_us") { view("contact_us") }
  r.get("license")    { view("license") }
end

We can remove this duplication similar to how we remove other duplication in Ruby, by moving the repetitive code into a loop.

route do |r|
  %w[about contact_us license].each do |route_name|
    r.get(route_name) { view(route_name) }
  end
end

Let's think about what's going on here. We created a bunch of routes in one go at runtime. It might look like magic, or even very advanced metaprogramming, but it isn't. It's just a each call on an array. This is the kind of power that Roda gives us. Note that I put emphasis on the words at runtime. As with every route in Roda, that loop won't get triggered at all if it's not on a branch we reach in our routing tree.

We'll see later in the section on array matchers that there is a built-in way to simplify the loop above.

Matchers

So far, we've discussed all of the match methods that Roda includes by default. However, we've only covered a few of the matchers that Roda includes by default. Let's first briefly discuss the matchers we have already been exposed to, followed by some new matchers.

String matchers

The string matcher is the most common matcher. It matches against the next segment in the remaining path.

route do |r|
  r.get "posts" do
    # GET /posts
  end
end

We can handle multiple segments in the same string if we include a slash.

route do |r|
  r.get "posts/today" do
    # GET /posts/today
  end
end

Including a slash to handle multiple segments is basically a shortcut for using separate string matchers.

route do |r|
  r.get "posts", "today" do
    # GET /posts/today
  end
end

When a string matcher is used, the segment it matches is consumed, including the preceding slash, but it is not captured (it will not yield arguments to the match block).

String matchers will match only complete segments, they are not simple prefix matches.

route do |r|
  r.on "posts" do
    # Requests for /posts and any path starting with /posts/
    # Would not match /posts-today
  end
end

A string matcher will not match if remaining path does not start with a slash.

Class matchers

Roda only supports two class matchers by default, both of which we've seen previously, String and Integer.

As a reminder, the String matcher consumes and captures the next non-empty segment in the remaining path. It will not match if the remaining path does not start with a slash, or if the next segment is empty.

route do |r|
  r.on "posts" do
    r.on String do |seg|
      "0 #{seg} #{r.remaining_path}"
    end
  end

  r.on String do |seg|
    "1 #{seg} #{r.remaining_path}"
  end
end

Here are some examples using different request paths.

require "lucid_http"

GET "/posts"
status                          # => "404 Not Found"

GET "/posts/"
status                          # => "404 Not Found"

GET "/posts/new"
body                            # => "0 new "

GET "/posts/new/"
body                            # => "0 new /"

GET "/posts/new/recent"
body                            # => "0 new /recent"

GET "/topics"
body                            # => "1 topics "

GET "/topics/"
body                            # => "1 topics /"

GET "/topics/new"
body                            # => "1 topics /new"

The Integer matcher consumes and captures the next segment in the remaining path if the segment only includes decimal characters (0-9). When capturing, it converts the segment to an integer, so the block argument it yields will be an integer.

route do |r|
  r.on Integer do |seg|
    "#{seg.inspect} #{r.remaining_path}"
  end
end

Here are some examples using different request paths.

require "lucid_http"

GET "/"
status                          # => "404 Not Found"

GET "/posts"
status                          # => "404 Not Found"

GET "/1a"
status                          # => "404 Not Found"

GET "/1"
body                            # => "1 "

GET "/2/"
body                            # => "2 /"

GET "/3/b"
body                            # => "3 /b"

Custom class matchers

While Roda only includes the String and Integer class matchers by default, it also ships with a class_matchers plugin which allows using any class as a class matcher. After loading the plugin, we need to call class_matcher with the class we want to register, a regexp for the segment it should match, and a block that accepts the regexp's captures and returns an array of captures to yield to the match block on a successful match (which can be empty to not yield anything).

class_matcher(Date, /(\d\d\d\d)-(\d\d)-(\d\d)/) do |y, m, d|
 [Date.new(y.to_i, m.to_i, d.to_i)]
end

route do |r|
  r.on Date do |date|
    date.strftime('%m/%d/%Y')
  end
end

This will take a date in year-month-day format and return the date in month/day/year format.

require "lucid_http"

GET "/2020-04-23"
body                           # => "04/23/2020"

Boolean matchers

We've already seen one of the boolean matchers, true, usually used to force a terminal match when using r.get or r.post. There are two other boolean matchers, false and nil. As we might expect, similar to use in Ruby conditionals, true always matches, and false and nil never match.

While there are reasons to use a literal true in order to force a terminal match when using r.get or r.post, there is no reason to use a literal false or nil. However, they are supported so we can call methods or access local variables that may return one of the values:

def allow?
  request.ip == '127.0.0.1'
end

def allowed_prefix
  "let-me-in" if allow?
end

route do |r|
  r.on allowed_prefix do
    "Allowed #{r.remaining_path}"
  end

  r.on allow? do
    "Also Allowed #{r.remaining_path}"
  end
end

If the request comes from IP address 127.0.0.1, then we will get the following results:

require "lucid_http"

GET "/"
body                            # => "Also Allowed /"

GET "/posts"
body                            # => "Also Allowed /posts"

GET "/let-me-in"
body                            # => "Allowed "

GET "/let-me-in/please"
body                            # => "Allowed /please"

If the request does not come from IP address 127.0.0.1, then all requests will result in a 404 response, since allowed_prefix will return nil and allow? will return false.

Regexp matchers

Roda supports the use of regexps as matchers. Regexp matchers must match complete segments, and they consume the segments they match, and capture whatever the regexp captures (which could be nothing if the regexps do not have any captures).

route do |r|
  r.on /posts/ do
    # Same as "posts" string matcher
  end

  r.on /posts/i do
    # Similar to a case insensitive string matcher
  end

  r.on /(posts|topics)/ do |seg|
    # Match either of the two segments and yield the matched segment
  end

  r.on /posts(?:\.html)/ do
    # Match with or without .html extension
  end

  r.on /(\d\d\d\d)-(\d\d)-(\d\d)/ do |year, month, day|
    # Handle multiple captures (arguments yielded are strings)
  end
end

Like string matchers, regexp matchers will not match if remaining path does not start with a slash.

Array matchers

Array matchers must contain other matchers, and will match if any of the matchers it contains match. Each member of the array is tried in order, and as soon as any member matches, the array matcher matches. If the member of the array that matches captures, then the captures are respected.

route do |r|
  r.get "posts", [Integer, true] do |id|
    # GET /posts/1 matches as:
    # * the Integer matcher matches
    # * the remaining path is fully consumed
    # * id is 1
    #
    # GET /posts matches as
    # * the true matcher matches
    # * the remaining path is fully consumed
    # * no argument is yielded (id is nil)
    #
    # GET /posts/new does not match as
    # * the true matcher matches
    # * but the remaining path is not fully consumed
  end

  r.on [/members/, /topics/] do
    # Match either of the regexp matchers
  end
end

One small inconsistency for better usability is that if the member matcher is a string, that string is yielded. This is an alternative approach to using a capturing regexp for multiple possible segments.

route do |r|
  r.on ['posts', 'topics'] do |seg|
    # Match either of the two segments and yield the matched segment
  end
end

Hash matchers

The behavior of a hash matcher depends on the keys of the matcher. Each key should be a symbol registered as a hash matcher. By default, there are two supported symbols, :all and :method.

:all

The :all hash matcher should have a value that is an Enumerable, and has the same behavior as the arguments given to r.on, r.is, r.get, and r.post, in that all members of the value must match for the :all hash matcher to match. It is exposed as a separate matcher as it can be combined with the array matcher.

route do |r|
  r.get ['post', {['posts', Integer]}] do |id|
    # GET /post matches as
    # * the first array member matches
    # * the remaining path is fully consumed
    # * no argument is yielded (id is nil)
    #
    # GET /posts/1 matches as
    # * the first array member does not match
    # * the second array member is then tried
    #   * the second array member is a :all hash matcher
    #   * all members of the :all hash matcher match
    # * the remaining path is fully consumed
    # * id is 1
    #
    # GET /posts/new does not match as
    # * the first array member does not match
    # * the second array member is then tried
    #   * the second array member is a :all hash matcher
    #   * the second member of the :all hash matcher does not match
  end
end

:method

The :method hash matcher matches the HTTP request method. It can be used when we want to do a non-terminal match while providing other arguments to r.on.

route do |r|
  r.on "posts",  do
    # POST requests for /posts or starting with /posts/
  end
end

Additionally, we can provide an array to match any of the given request methods:

route do |r|
  r.on "posts", ['put', 'patch'] do
    # PUT or PATCH requests for /posts or starting with /posts/
  end
end

As shown above, we can use symbols or strings, and the values are not case sensitive (they are always converted to uppercase).

Custom hash matchers

While Roda only includes the :all and :method hash matchers by default, it also ships with a hash_matcher plugin that allows for setting up our own custom hash matchers.

In the earlier section on setting up custom match methods, we used this example.

require 'roda'

class App < Roda
  route do |r|
    r.with_params "secret"=>"Um9kYQ==\n" do
    end
  end
end

Let's change this to use a custom hash matcher instead of a custom match method. For simplicity, instead of checking for arbitrary parameters, we'll create a custom hash_matcher that only handles a parameter named secret.

require 'roda'

class App < Roda
  route do |r|
    r.on("Um9kYQ==\n") do
    end
  end
end

As before, this code doesn't work because we haven't added the secret hash matcher. Let's do that now. After loading the plugin, we need to call the hash_matcher method with the related hash key symbol, and a block that should return nil or false to not match, and any other value to match.

require 'roda'

class App < Roda
  plugin 

  hash_matcher() do |v|
    params['secret'] == v
  end

  route do |r|
    r.on("Um9kYQ==\n") do
    end
  end
end

The hash_matcher plugin does not handle capturing. However, we can manually add captures by appending them to the request's captures. For example, if we wanted to yield the value of the key parameter if the secret parameter matches, we can do this:

require 'roda'

class App < Roda
  plugin 

  hash_matcher() do |v|
    if params['secret'] == v
      captures << params['key']
    end
  end

  route do |r|
    r.on("Um9kYQ==\n") do |key|
    end
  end
end

Additional hash matchers in plugins

Roda ships with multiple plugins that add their own hash matchers. path_matchers includes :prefix, :suffix, and :extension hash matchers. header_matchers includes :header, :host, :user_agent, and :accept hash matchers. param_matchers includes :param, :param!, :params, and :params! hash matchers.

Symbol matchers

Symbol matchers are identical to the String class matcher. They are a historical form not recommended in new Roda applications, as the String class matcher is more intuitive and less redundant.

route do |r|
  r.on  do |seg|
    # same as r.on String do |seg|
  end
end

Custom symbol matchers

While it is recommended to use the String class matcher instead of a symbol matcher by default, Roda ships with a symbol_matchers plugin that allows different symbols to match different segments. Let's say we have many routes that accept a username, and our application only allows usernames that are alphanumeric and between 6 and 20 characters. We can use a custom symbol matcher that we will be sure will only match if the username format is valid.

require 'roda'

class App < Roda
  plugin 

  symbol_matcher , /([a-z0-9]{6,20})/

  route do |r|
    r.on  do |username|
    end
  end
end

The symbol_matchers plugin also includes some default symbol matchers, such as :d for decimal segments (similar to the Integer class matcher), :w for alphanumeric segments, and :rest for the rest of the remaining path. All of these consume and capture the matching segment (or rest of the remaining path in the case of :rest).

Proc matchers

Roda allows us to use Ruby procs as matchers. The proc will be called, and if it returns nil or false, the matcher will not match. If it returns any other value, it will match. We can use proc matchers as a substitute for using conditionals.

r.get Integer do |id|
  post = posts[id]
  r.on(proc { post }) do
    access_time = Time.now.strftime("%H:%M")

    "Post: #{post} | Accessing at #{access_time}"
  end
end

The code still works as it did in the section describing conditionals.

require "lucid_http"

GET "/posts/2"
body                            # => "Post: Post 2 | Accessing at 10:09"

GET "/posts/10"
status                          # => "404 Not Found"

This approach makes the code more complex and doesn't add value, only complexity. In general, we would only want to use proc matchers if we have a proc from an external source that could return an arbitrary value, and we want to use it for matching.

Anything else

By default, if we use any other value as a matcher, Roda will raise a Roda::RodaError. This is to prevent undefined behavior when an unexpected matcher type is used.

However, if you want to support other types of matchers, you can use the custom_matchers plugin. This plugin adds support for using arbitrary objects as matchers, as long as the Roda application has been configured to accept them.

For example, if you want the application to accept Set instances as matchers, and have them match any value in the set (similar to how the array matcher works, but without the special behavior for string values), you can load the custom_matchers plugin and call the custom_match class method to register a matcher for Set instances, using a block to determine how these Set instances should be matched. Then you can use Set instances as matchers. Any captures added by the block you pass to the custom_matcher method are yielded to the match block that you passed when calling the match method with a Set matcher.

require 'set'

class App < Roda
  plugin 

  custom_matcher(Set) do |matcher|
    matcher.any?{|v| match(v)}
  end

  set = Set.new([/(a)(\d+)/, /(b)(\w+)/, /(c)(\h+)/])

  route do |r|
    r.on set do |prefix, id|
      case prefix
      when 'a'
        # ...
      when 'b'
        # ...
      when 'c'
        # ...
      end
    end
  end
end

Other RodaRequest methods

r.redirect

Say we actually want to render the post list when we browse to the root path. We could definitely copy and paste the code we have on the posts routing block. However, we don't like code duplication, and try to avoid it when possible.

class App < Roda
  route do |r|
    r.root do
      posts = (0..5).map {|i| "Post #{i}"}
      posts.join(" | ")
    end

    r.get "posts" do
      posts = (0..5).map {|i| "Post #{i}"}
      posts.join(" | ")
    end
  end
end

What we can do instead is redirect to that path. We do so by calling the conveniently named r.redirect method.

class App < Roda
  route do |r|
    r.root do
      r.redirect "/posts/"
    end

    r.get "posts" do
      posts = (0..5).map {|i| "Post #{i}"}
      posts.join(" | ")
    end
  end
end

Let's try this out. We request the root route from our browser. Immediately, the browser forwards to the post list URL.

The only difference from the client's point of view is that now, instead of returning a 200 status, we're returning a 302, corresponding to a redirect.

require "lucid_http"

GET "/"
path                            # => "http://localhost:9292/"
body                            # => ""
status                          # => "302 Found"

If we follow the redirect, we see that we're rendering the desired list.

require "lucid_http"

GET "/", 
path                            # => "http://localhost:9292/"
body                            # => "Post 1 | Post 2 | Post 3 | Post 4 | Post 5"
status.to_s                     # => "200 OK"

Redirect paths

In general, if we want to redirect to a different path, we would give a separate path to r.redirect. However, if we follow a URL design such that a GET request for a path shows a form to submit to the same path via POST, and after a POST we want to perform a GET request to the same page to see the current state of the page after the update, Roda allows us to omit the path.

class App < Roda
  route do |r|
    r.is "posts", Integer do |id|
      @post = Post[id]

      r.get do
        @post.inspect 
      end

      r.post do
        @post.update(Time.now)
        r.redirect
      end
    end
  end
end

Redirect status codes

By default, r.redirect will use a 302 status code. We can specify an explicit status code when redirecting by using a second argument.

class App < Roda
  route do |r|
    r.root do
      r.redirect "/posts/", 303
    end

    r.get "posts" do
      posts = (0..5).map {|i| "Post #{i}"}
      posts.join(" | ")
    end
  end
end

If we want to change the default status when redirecting to 303, we can use the status_303 plugin.

r.halt

Roda allows returning a response at any point during routing. This is necessary because of Roda's design, so that whenever a match block exits, the response is returned. To stop (or halt) processing a request, we can call r.halt at any point in our routing tree. By default, we would call r.halt with no arguments, which will use the existing response. So we can set the status, headers, or body of the response, and then call r.halt to halt request processing and return the response.

route do |r|
  r.get "posts" do
    if r.params['forbid']
      response.status = 403
      response.headers['My-Header'] = 'header value'
      response.write 'response body'
      r.halt
    end

    # not reached if forbid parameter submitted
  end
end

While r.halt uses the request's current response by default, we can pass a rack response to r.halt to have it use the given response instead of the request's current response. A rack response is an array with 3 elements, status code (integer), headers (hash), body (array of strings).

route do |r|
  r.get "posts" do
    if r.params['forbid']
      r.halt [
        403,
        {
          'Content-Type'=>'text/html',
          'Content-Length'=>'13',
          'My-Header'=>'header value',
        },
        ['response body']
      ]
    end

    # not reached if forbid parameter submitted
  end
end

The default behavior of r.halt is to only support using the current response (no argument) or the given rack response (1 argument that is an array of 3 elements). Roda also ships with a halt plugin that expands r.halt to partially modify the current response before returning it.

If we load the halt plugin, we can call r.halt with a integer to change the response status code before returning.

r.halt 403

Or call r.halt with a string to update the response body before returning.

r.halt 'response body'

Or call r.halt with 2 arguments to change the response status code and update the response body before returning.

r.halt 403, 'response body'

Or call r.halt with 3 arguments to change the response status code, update the response headers, and update the response body before returning.

r.halt(403, {'My-Header'=>'header value'},  'response body')

The difference between calling r.halt with 3 arguments and with a single rack response (array of 3 elements) is that the rack response will be returned directly, where using 3 arguments will update the request's current response's headers and response body before returning the request's current response.

r.run

Roda has support for directly calling other rack applications at any point during routing. This allows for taking any rack application (including another Roda application) and mounting it inside in the Roda application, generally under a subpath.

Let's say we have already developed an administrative front end to our application as a separate rack application. We want to mount that application under the /admin route of the current application. We would setup a branch for /admin, and any requests for that branch would be sent to the admin application, and the admin application's response would be returned as the response.

route do |r|
  r.on "admin" do
    r.run AdminApp
  end

  # rest of application
end

RodaResponse

Earlier, we discussed that if we have a Roda application (subclass of Roda) named App, then Roda will automatically setup App::RodaRequest to be the class of App's requests. Likewise, Roda will automatically setup App::RodaResponse to be the class of App's responses. Just as App::RodaRequest is a subclass of Roda::RodaRequest, App::RodaResponse is a subclass of Roda::RodaResponse. The reason for creating the response subclass is the same as the reason for creating the request subclass, so that plugins can offer custom response behavior.

A Roda::RodaResponse instance is simpler than a Roda::RodaRequest instance. There are a lot fewer methods added. A Roda::RodaResponse instance has accessors for the status (response status code integer), headers (response headers), and body (response body). In the section on r.halt, we saw that we can set the status for a response and set a header in the response. We can also replace the response body, keeping in mind that the response body must be a valid rack response body (an object that response to each and yields strings).

route do |r|
  response.status = 403
  response.headers['My-Header'] = "header value"
  response.body = ["response body"]
end

Roda::RodaResponse has a few helper methods. It supports getting and setting the headers using the array reference operator:

route do |r|
  response['Other-Header'] = response['My-Header']
end

As shown in the r.halt section, it supports writing to the body. However, if the body is written to manually, Roda will ignore the result of blocks and will use the already written body when returning a response.

route do |r|
  response.write 'response body'
  'ignored'
end

Roda::RodaResponse also supports a redirect method for setting the location to redirect and an optional status (302 by default). Note that we don't generally call redirect directly on the response. It is usually called on the request, where it has the same behavior (as it calls redirect on the response) but also halts request processing after.

route do |r|
  r.is 'old-path'
    response.redirect '/new-path' # 302 status used
  end

  r.is 'other-old-path'
    response.redirect '/other-new-path', 303
  end
end

route block scope

Now that we've talked about the request and response, we can discuss the other major object we'll be doing with in Roda, which is the scope of the route block. The route block is executed in the context of a new instance of the Roda app class.

class App < Roda
  route do |r|
    self.class # App
  end
end

So for the Roda application given above (App), the route block scope is an instance of App. By design, the Roda class (and therefore, the App class) has few public instance methods, so that the scope of the route block will not be polluted. There are some internal methods prefixed with _roda_, but other than those, there are only a few methods added:

The Roda class

Now that we know the basics of what happens at the instance level, let's discuss the Roda class itself.

app, the rack application

As we've seen in earlier examples, Roda can operate as a rack application. This is necessary so that we can use run Roda (or run App if our Roda subclass is App) in config.ru. However, while Roda can operate as a rack application, what actually happens is that Roda creates a rack application internally, and then if it is called as a rack application, it passes the request environment to the actual rack application. It is faster to skip this step, and run the underlying rack application directly. We can access the underlying rack application using app. So the config.ru file should be changed to:

require "./app"

run App.app

There is no difference in behavior if we leave off the .app, but adding the .app will speed our application up slightly.

freeze, to prevent unexpected modification

Roda applications are recommended to be frozen in production and when testing, so that if something accidentally tries to change the Roda application in an unexpected way, it will fail. For development, we should also freeze the Roda application unless we expect to be modifying it. Certain code reloading libraries depend on modifying the class. However, rerun does not, so if we are using rerun in development, we can freeze the application in all cases. Often the freezing is done in the config.ru file.

require "./app"

run App.freeze.app

If we want to freeze the application except during development, we can check the RACK_ENV environment variable.

require "./app"

unless ENV["RACK_ENV"] == "development"
  App.freeze
end

run App.app

opts, the class and plugin options

Instead of storing state in multiple instance variables, Roda stores all the class level state in a single hash, which we can access via opts. Plugins that need to handle state generally store their state in opts as well.

There are a few options we can consider setting when creating a Roda application, as they effect the behavior of Roda itself or multiple plugins that ship with Roda.

plugin, to load plugins

As we've shown earlier, plugin is used to load plugins into the Roda application. Some plugins do not accept arguments:

class App < Roda
  plugin 
  plugin 
end

Many plugins can be loaded without arguments, but will accept an options hash for arguments:

class App < Roda
  plugin 
  plugin , true
end

Few plugins require arguments:

class App < Roda
  plugin , 
  plugin , "", /(?:\/\z|(?=\/|\z))/
end

We can pass a block when loading a plugin. In general, we should only use this if the plugin expects to be passed a block, as otherwise the block will be ignored (the default behavior in Ruby):

class App < Roda
  plugin  do
    "File Not Found"
  end

  plugin  do |e|
    "Internal Server Error"
  end
end

route, to set the route block

As earlier examples have shown, route sets the route block to use. What we haven't discussed directly yet is that the route block is also treated as a match block. Just like any match block, if the response body has not been written to, and the return value of the route block is a string, it is used as the response body. So if we want to use the same response to all requests, we don't need to use a separate r.on call, we can just have our route block return the value.

class App < Roda
  route do |r|
    "Response body for all requests"
  end
end

Internally, for performance reasons, Roda uses the block we pass to route and creates an instance method from it, which is called when the rack application is called. If we want to access the route block for some reason, we can use route_block.

middleware handling

While we can load middleware in config.ru via use, Roda also supports use to load middleware. This can be useful if the Roda application depends on the middleware in some way, and it can be used by multiple or arbitrary config.ru files.

require 'logger'

class App < Roda
  use Rack::CommonLogger, Logger.new($stdout)
end

By default, middleware are inherited by subclasses, but we can turn this off by setting inherit_middleware to false. Additionally, if we want to clear the middleware stack, we can use clear_middleware!. For more advanced middleware handling, such as removing particular middleware or inserting middleware before or after other middleware, we can use the middleware_stack plugin.

Be aware that rack middleware work differently from plugins. Each rack middleware we use has a performance cost, as each rack middleware wraps the application. So if we have 3 rack middleware loaded in the application, the first middleware executes, then the second, then the third, before finally being dispatched to the application. In cases where both rack a middleware and Roda plugin can handle the same need, it is usually better for performance to use the Roda plugin.

Accessing request data

So far, we've learned how to take a client's request and handle routing for it. What if we wanted to pass data along with the request? Roda allows us to receive data from a client in several ways. The first one we'll explore is one we've already seen: passing information in the request path.

In the request path

Variable data in the request path often serves a dual role: we want to route requests based on the structure of the path, but we also want to capture parts of the path for later use.

Let's use an example where we are matching on a request with 3 segments. The first segment is posts, the second is some numeric ID, and the third is some string that represents an action. In the match block, we'll return the inspect values of ID and the action to see what we got.

class App < Roda
  route do |r|
    r.get "posts", Integer, String do |id, action|
      "#{id.inspect} - #{action.inspect}"
    end
  end
end

If we try to access the route, we see that it extracts the ID segment as an integer, and the action segment as a string:

require "lucid_http"

GET "/posts/1/show"
body                            # => "1 - \"show\""

GET "/posts/2/update"
body                            # => "2 - \"update\""

In the above example, we have three segments, handled by a single r.get statement. However, in many cases, it is preferable to use a separate match block for each segment, as we may have other routes at each level of the tree.

Let's add a routing API for a group of models. First, we have a route that will match a model name and pass it to the block. We only want to match an allowed model name, as allowing any name could result in a name being passed in that we do not expect. We should consider all forms of input into our application as a possible method of attack, and limit the allowed input whenever possible. We'll limit the model names using an array, though in general for a large number of models we would probably want a more efficient approach. If we remember from the section about array matchers, when the member that matches is a string, that string is both captured. We'll use that to find which string was submitted.

One thing to note about the array of models is that we defined it as a local variable in the App class, and reference it inside the route block. We could have inlined it in the call to r.on, or used a local variable inside the route block, but that would cause additional work for every request. We could also have used a constant instead of a local variable. That would work fine, but in general a local variable is a simpler approach, and we should only use a constant if we need to.

Inside the match block, we will take the model's name and find the appropriate model class. This uses Object.const_get, which takes a string and returns the class for the string. Object.const_get is something that should only be called with trusted input, which is why we have made sure to limit the class names that are allowed. Notice that as soon as we know the model name, we were able to use it. We don't have to wait until the path is fully routed in order to get a reference to the model. This allows us to use this model in any of the nested routes, and that's what we usually want.

class App < Roda
  models = ["account", "post"]

  route do |r|
    r.on models do |model_name|
      model_class = Object.const_get(model_name.capitalize)

      # ...
    end
  end
end

Then we have the index route, that will match GET requests for /post/index, and will return the list of posts.

class App < Roda
  models = ["account", "post"]

  route do |r|
    r.on models do |model_name|
      model_class = Object.const_get(model_name.capitalize)

      r.get "index" do
        model_class.all.join(" | ")
      end
    end
  end
end

If we got to here, but the remaining path wasn't /index, or the request method wasn't GET, we skip that block, and try to match to the model ID. If the next segment is numeric, we can assume that is the model ID. That means that we're interested in doing something with a particular instance of the previously selected model, so the next logical step would be to access that particular instance.

class App < Roda
  models = ["account", "post"]

  route do |r|
    r.on models do |model_name|
      model_class = Object.const_get(model_name.capitalize)

      r.get "index" do
        model_class.all.join(" | ")
      end

      r.on Integer do |id|
        model = model_class[id]

        # ...
      end
    end
  end
end

Finally, once we have the appropriate object on our hands, if it is a GET request for the show action, we can either display the model information. displ. If it is a POST request for the update action, we can update the model instance (in this case we'll just return that we are updating it without actually doing so).

class App < Roda
  models = ["account", "post"]

  route do |r|
    r.on models do |model_name|
      model_class = Object.const_get(model_name.capitalize)

      r.get "index" do
        model_class.all.join(" | ")
      end

      r.on Integer do |id|
        model = model_class[id]

        r.get "show" do
          model.to_s
        end

        r.post "update" do
          "Updating #{model}"
        end
      end
    end
  end
end

This is a contrived example, but it illustrates how to go about designing a routing tree when we are routing by path. If possible, we should include request data in the path when we need it for routing.

In the query string parameters

We can also pass information to a request using the query string. The query string is a string that goes at the end of the URL, after a question mark, and it consist of a series of key=value pairs separated by ampersands.

Here's an example: we set the post parameter to have a value of 42 and the action parameter to have a value of show

http://localhost:9292?post=42&action=show

This kind of approach is commonly used to pass data when submitting a form using the GET request method. One of the more common uses is to pass search terms to a search feature. Let's assume we have an array of articles and we want to perform a search based on a q parameter passed through the query string.

We add a route to match GET requests for the /search path, and then we're ready to perform our search. Now we can take a look at the r.query_string instance method on the request just by returning it as the response body (the query_string method comes from Rack::Request).

class App < Roda
  route do |r|
    r.get "search" do
      r.query_string
    end
  end
end

When we browse to http://localhost:9292/search?q=article, we see that r.query_string returns the actual string.

require "lucid_http"

GET "/search?q=article"
body                    # => "q=article"

We could parse it by hand and extract each key and value, but there is a helper method that already does that, r.params (which also comes from Rack::Request)

class App < Roda
  route do |r|
    r.on "search" do
      r.params.inspect
    end
  end
end

When we browse to http://localhost:9292/search, passing q as an argument to the query string, we see the parsed parameters as a hash on the terminal. If we add another attribute to the query string, for example, we see the new hash with both keys and their respective values.

require "lucid_http"

GET "/search?q=article"
body                    # => "{\"q\"=>\"article\"}"

GET "/search?q=article&category=video"
body                   # => "{\"q\"=>\"article\", \"category\"=>\"video\"}"

Now, we can actually populate our route. We search through the articles for the ones that include the value of the q attribute on the request. Finally, we join them in order to return a string.

class App < Roda
  ARTICLES = [
    "This is an article",
    "This is another article",
    "This is a post",
    "And this is whatever you want it to be",
  ]

  route do |r|
    r.on "search" do
      ARTICLES.filter do |article|
        article.include?(r.params["q"])
      end.join(" | ")
    end
  end
end

When we search for the article keyword, we get all the articles that include the text article.

require "lucid_http"

GET "/search?q=article"
body                            # => "This is an article | This is another article"

Former Rails developers might be wondering if we can fetch parameters using symbols, and the answer is no. The r.params method returns a Ruby hash, and hashes differentiate between symbols and strings. This is another example of how Roda is non-intrusive. It mostly uses Ruby core classes and does not modify their behaviors to suit its needs. So if we know Ruby and how Ruby's core classes work, we should find Roda easy to understand.

Unfortunately, there is a small problem with the above example. What happens if we go to the search page directly, without specifying the q parameter? Well, if we try it, we'll see that it raises a TypeError, because r.params["q"] is nil, and String#include? doesn't accept nil as an argument. There is a similar issues if the q parameter is parsed as an array or hash (both are possible).

There are a couple of approaches to fix this. One is to just convert r.params["q"] to a string. This has the advantage that it handles all input without errors. However, it will result in going to the search page without specifying the q parameter as showing all articles, since nil converts to the empty string, and every string includes the empty string.

class App < Roda
  route do |r|
    r.on "search" do
      ARTICLES.filter do |article|
        article.include?(r.params["q"].to_s)
      end.join(" | ")
    end
  end
end

An alternative approach would be using a standard Ruby conditional, such as a case statement.

class App < Roda
  route do |r|
    r.on "search" do
      case q = r.params["q"]
      when String
        ARTICLES.filter do |article|
          article.include?(q)
        end.join(" | ")
      else
        "Invalid q parameter"
      end
    end
  end
end

In the request body parameters

Another way of handling data submitted in the request is when submitting a form via the POST method. When browsers submit forms via POST, instead of including data in the query string, they include the data in the request body.

We'll add a small example for this one. We want to route a POST request to http://localhost:9292/articles and accept a content parameter with the article to create.

We get a 404 status because this route doesn't exist yet.

require "lucid_http"

POST "/articles", {Time.now.strftime("%H:%M:%S") }
body                            # => ""
status                          # => "404 Not Found"

To handle this route, we'll use r.post with the "articles" matcher. Inside the match block, we need to we extract the data from the request. We can do it the same exact same way we did for the search feature, by using the r.params method. r.params merges the parameters from the query string with the parameters from the request body, which is why it works in both cases.

If the route matches, we can add the content parameter submitted in the request as a new article. We can then return a string showing the last article (which should the one we just added unless there was another request between the execution of the two lines), and a count of all the existing ones.

r.post "articles" do
  ARTICLES << r.params["content"]
  "Latest: #{ARTICLES.last} | Count: #{ARTICLES.count}"
end

Now, when we try it, we see our new articles being appended.

require "lucid_http"

POST "/articles", {Time.now.strftime("%H:%M:%S")}
# => "Latest: 12:13:38 | Count: 5"

sleep 2

POST "/articles", {Time.now.strftime("%H:%M:%S")}
# => "Latest: 12:13:40 | Count: 6"

This example has the same issues as the previous example, in that submitting a form with no content parameter would result in unexpected behavior (in this case, nil added to the array). We would want to make sure that r.params["content"] is a string before adding it as an article.

We mentioned earlier that r.params combines the query string parameters and the request body parameters. While there is not a general reason to access them separately, if we would like to do so, we can use r.GET for the query string parameters and r.POST for the request body parameters (both of these methods come from Rack::Request).

Roda lies on rack's parsing of the body, and as such it only handles parsing of request content types that rack itself supports, including application/x-www-form-urlencoded and multipart/form-data. HTTP supports other content types for requests, and a request content type that is commonly used is application/json, for JSON requests. Roda can handle these requests if we use the json_parser plugin.

In the request headers

The final way to access request data is as part of the request headers. The built-in way to access the request headers is through the request environment hash (env). The request environment hash includes all headers, but the rack specification requires that the header keys be converted to uppercase, have dashes converted to underscores, and have HTTP_ prepended (except for exceptions such as Content-Type and Content-Length). So if we want to access the My-Foo header, we would use env["HTTP_MY_FOO"].

If we don't want to remember all of the conversion rules, we can use the request_headers plugin, which allows us to use r.headers['My-Foo'].

Roda Plugins

Roda has a philosophy of being un-opinionated out of the box. If we followed along up to this point, we already know probably 95% of core Roda.

However, Roda is much more than just core Roda. In terms of lines of code, core Roda less than 15% the size of Roda. The remaining 85% of Roda is implemented via plugins. Some plugins are used by most applications. Other plugins are used in some types of applications (such as APIs) but not in others. Other plugins have specialized uses and will not be needed by most applications. This section will cover all of the Roda plugins that are commonly used in applications, as well as many of the plugins that have application-specific usage. It will not cover most of the plugins that have specialized uses.

To find the list of plugins, we can go to the Roda site at http://roda.jeremyevans.net/ and click on the Docs link. Here we can find a list of plugins. Clicking on any of the plugins will take us to a RDoc-generated page with detailed information on that plugin.

We'll start our discussion of plugins with plugins focused on rendering, or generating response bodies for requests.

Rendering

public

We'll start our review of rendering plugins with the public plugin. On the top of its documentation page, we see a title, followed by a discussion of the plugin itself. This discussion usually contains enough information for basic plugin use. However, if we need more information, after this section there is a section for classes and modules. That will have a link to each of the plugin's modules, so we can see what methods it adds to the scope, request, and response (or their classes). After that section it lists the constants the plugin defines, which can generally be ignored. Following that is a section on public class methods, which will document the plugin's configure method if it has one. The configure method is called when the plugin is loaded, to configure the Roda application based on the arguments we passed when loading the plugin.

If we want to see how any plugin is implemented, either out of curiosity or in order to debug an issue, we can find the source code for any of them in Roda's GitHub repository, in the lib/roda/plugins/<plugin name>.rb file.

Let's try the public plugin out, so we can see it in action. We have an app without any routes.

class App < Roda
  route do |r|
  end
end

With this Roda application, any request we make will result in a 404 status. We'll try http://localhost:9292/dave.html and confirm it doesn't currently exist.

require "lucid_http"

GET "/dave.html"
status                          # => "404 Not Found"

Now, we will see how behavior changes when we load and use the public plugin. As mentioned earlier in the section on core Roda, to add a plugin, we need to invoke the plugin class method inside our app class and pass the plugin name (:public in this case) as a symbol.

Notice that we didn't have to add any new gems to our project, since this is one of plugins that ships with Roda, and the public plugin does not have any external dependencies other than rack (which Roda itself depends on). We also didn't need to add a require statement. Roda automatically requires the plugin file based on the name of the plugin.

The public plugin adds an r.public method. When we call the r.public method inside the route block, it will serve requests for static files inside a specific directory if they exist.

class App < Roda
  plugin 

  route do |r|
    r.public
  end
end

By default, the public plugin will serve files from the directory <app root>/public (where <app root> is the applications :root option or the current working directory). Let's create the public directory and add some files to it.

  Dir.mkdir("public")
  Dir.chdir("public")

  %w[chris dave matt pete].each_with_index do |doc_name, i|
    doc_num = i + 9
    File.write("#{doc_name}.html", <<CONTENT)
<h2>My name is #{doc_name.capitalize} <h2>
<h3>and I'm ##{doc_num}</h3>
CONTENT
    end
  end

Now that we have added files to the public directory, we retry http://localhost:9292/dave.html, and we see that now we get a rendered page and a 200 status.

require "lucid_http"

GET "/dave.html"
body               # => "<h2>My name is Dave <h2>\n<h3>and I'm #10</h3>\n"
status             # => "200 OK"

If we want to serve files from the static directory instead of the public directory, we need to set the :root option when loading the plugin.

class App < Roda
  plugin , "static"

  route do |r|
    r.public
  end
end

Now, when the app is restarted, the configuration for the plugin changed. Instead of looking into the public directory, it will look into static. When we make this change, the application will return a 404 status because the directory doesn't exist yet.

require "lucid_http"

GET "/dave.html"
body                          # => ""
status                        # => "404 Not Found"

When we rename the public directory to static,

File.rename('public', 'static')

and retry, it works again.

require "lucid_http"

GET "/dave.html"
body               # => "<h2>My name is Dave <h2>\n<h3>and I'm #10</h3>\n"
status             # => "200 OK"

One nice feature of using the public plugin is that we can serve files from a subpath. So if we wanted to serve files from the static directory, but only want to check the static directory for files if the path starts with /static/, we can call r.public inside an r.on "static" block.

class App < Roda
  plugin , "static"

  route do |r|
    r.on "static" do
      r.public
    end
  end
end

If we try the same request, it'll return a 404 status because the directory doesn't exist yet. However, if we prefix the request with /static, it will return the content.

require "lucid_http"

GET "/dave.html"
body                          # => ""
status                        # => "404 Not Found"

GET "/static/dave.html"
body               # => "<h2>My name is Dave <h2>\n<h3>and I'm #10</h3>\n"
status             # => "200 OK"

Another nice feature of the public plugin is that it has built-in support for serving files already compressed with gzip or brotli if the request indicates that gzip or brotli transfer encoding is supported. For file types that are not already compressed or encrypted, this generally can increase performance by decreasing the amount of bytes needed to transfer the file. Because it works on files that have already been compressed with gzip or brotli, and doesn't compress or decompress files at runtime, this doesn't require additional processing work.

Let's create a gzipped version of the file, and remove the original version of the file.

require 'zlib'

Zlib::GzipWriter.open('static/dave.html.gz') do |gz|
  gz.write(File.read('static/dave.html'))
end
File.delete('static/dave.html')

Then we change the plugin options to use the :gzip option.

class App < Roda
  plugin , "static", true

  route do |r|
    r.on "static" do
      r.public
    end
  end
end

Then we can check that requests for the file still work correctly, even though only the gzipped version exists.

require "lucid_http"

GET "/static/dave.html"
body               # => "<h2>My name is Dave <h2>\n<h3>and I'm #10</h3>\n"
status             # => "200 OK"

Note that in a production setting, we'll want to keep both the compressed and uncompressed file, so that we can still serve files to clients that do not accept gzip encoding.

multi_public

The public plugin is great if you keep all static files your application serves in a single directory. However, there may be reasons you would like to store different types of files in different directories. For example, maybe you want to make sure that certain files are only available to admins, while other files are available to everyone. Roda supports multiple separate directories for static files using the multi_public plugin. This plugin is loaded similar to the public plugin, except that you pass in a hash of directory information. The keys of the hash you use in calls to the r.multi_public method, and the values of the hash are paths to the directory to serve.

Let's create an admin_static directory, and add a dave.html file to it.

  Dir.mkdir("admin_static")

  File.write("admin_static/dave.html", <<CONTENT)
<h2>My admin name is Evad<h2>
CONTENT
    end
  end

Then we can modify our Roda app to use the multi_public plugin, serving the existing static directory under the /files path for all requests, and serving the new admin_static directory under the /admin/files path for admin requests. For simplicity in this example, we'll consider all requests as admin requests.

class App < Roda
  plugin ,
    'static',
    'admin_static'

  route do |r|
    r.on "files" do
      r.multi_public()
    end

    if admin?
      r.on "admin", "files" do
        r.multi_public()
      end
    end
  end

  def admin?
    true
  end
end

Then we can check that requests for both types of files can work correctly.

require "lucid_http"

GET "/files/dave.html"
body               # => "<h2>My name is Dave <h2>\n<h3>and I'm #10</h3>\n"
status             # => "200 OK"

GET "/admin/files/dave.html"
body               # => "<h2>My admin name is Evad<h2>\n"
status             # => "200 OK"

Generating HTML

So far, we've had Roda return short strings as response bodies. However, that's not a very realistic approach. Most applications are either going to return full HTML pages, or HTML fragments, or JSON. Core Roda doesn't offer good support for those cases, mostly because many applications are either returning HTML or JSON and not both. Core Roda tries to only offer a minimal set of capabilities that are needed by most web applications, with plugins to handle more advanced needs.

In most production web applications, we will not be preparing the response bodies in the routing tree itself. Most applications will handle the creation of response bodies using dedicated methods. In this section we'll show to move from creating response bodies in the routing tree to using a separate method that will use a template file to create the response body. This process is of taking a template and create a response body from it is often called rendering.

Let's assume we need to write the list of tasks in a to-do list application. We'll start by returning a string with the list of tasks one after the other, separated by a new line.

require "roda"
require "./models"

class App < Roda
  route do |r|
    r.root do
      Task.all.map(&).join("\n")
    end
  end
end

When we print it on the command line, we get what we expected.

require "lucid_http"

GET "/"
puts body

# Play Battletoads
# Learn how to force-push
# Find radioactive spider
# Rescue April
# Add red setting to sonic screwdriver
# Fix lightsaber
# Shine claws
# Saw cape
# Buy Blue paint
# Repaint TARDIS

However, this is going to look ugly in the browser, because the returned Content-Type will be text/html (Roda's default), but we are returning plain text. So the newlines will be converted into spaces.

If we want something that looks nice in the browser, we want to output something in HTML format. We can use a unordered list with a checkbox telling us whether the task is done or not.

One approach to doing this is to create an empty string, then append to it for each item, and return the string as the response body. This works, but is fairly ugly and will make maintenance more difficult.

route do |r|
  r.root do
    result = String.new
    result << "<ul>"
    Task.all.each do |task|
      result << "<li class=\"#{task.done? ?  : }\">"
      result << "  <input type=\"checkbox\"#{" checked" if task.done?}>"
      result << "    #{task.title}"
      result << "</li>"
    end
    result << "</ul>"
    result
  end
end

We could extract this logic into a separate method, or add some helpers, but ultimately building responses by explicitly appending to a string is going to be more verbose than most developers desire. Roda handles this issue similarly to many other frameworks, by supporting templates using the render plugin.

render

Using the render plugin, we can remove the messy code that uses explicit string appending, and replace it with a call to the render method, passing a template name.

To get started using the render plugin, we first need to install tilt, the gem that the render plugin uses for rendering templates. So we need to add tilt to the Gemfile, then run bundle install.

source "https://rubygems.org"

gem "roda"
gem "puma"
gem "tilt"

After installing the gem, we can use the render plugin. We'll load it into our application, then try rendering a template.

class App < Roda
  plugin 

  route do |r|
    r.root do
      render "tasks"
    end
  end
end

Then we try it out to see what we get. Since we haven't created the template file, we get an error telling us that our template does not exist. The error message is useful, because now we know where the plugin is looking for the template file. As the file extension indicates, the default template language that the plugin expects is erb (embedded Ruby), and it expects files to be in the views directory.

require "lucid_http"

GET "/"
error
# => "Errno::ENOENT: No such file or directory @ " \
#    "rb_sysopen - /home/lucid/code/my_app/views/tasks.erb"

So we create the views directory and views/tasks.erb file. In the views/tasks.erb file, we can add the template code. We'll also return a full HTML page instead of just an HTML fragment.

We add a title to the HTML file, then, for each task in the list, we use a list item. The item class switches on the state of the li element between done and todo. Inside the list item we see the expected checkbox. It will be checked if the task is done.

<html>
  <head>
    <title>To-Do or not To-Do</title>
  </head>
  <body>
    <h1>To-Do or not To-Do</h1>
    <h2>Tasks</h2>
    <ul>
      <% @tasks.each do |task| %>
        <li class="<%= task.done? ?  :  %>">
          <input type="checkbox"<%= " checked" if task.done? %>>
          <%= task.title %>
        </li>
      <% end %>
    </ul>
  </body>
</html>

When we try this again, we get a NoMethodError. This is expected, because we haven't set the @tasks instance variable yet.

require "lucid_http"

GET "/"
error
# => "NoMethodError: undefined method `each' " \
#    "for nil:NilClass for #<App:0x00000001bd6ba8>"

We can set the @tasks instance variable before rendering the template file (also called a view), and that will fix the issue.

class App < Roda
  plugin 

  route do |r|
    r.root do
      @tasks = Task.all
      render "tasks"
    end
  end
end

When we visit the page, we see our plain looking list.

Setting instance variables in the routing tree allows the view to access them because the view is executed in the same scope as the routing tree. By setting instance variables in the routing tree, you do not need to pass them explicitly to the view.

There's a second way for the view to access data set in the routing tree, and that's an explicit approach of passing local variables when rendering. We can also mix the two approaches, setting some instance variables and passing other data explicitly as local variables.

We can pass local variables to the template by passing the :locals option to the render method with a hash, where the keys are symbols of the names of the local variables we want to define, and each value in the hash is the value of that local variable in the view.

class App < Roda
  plugin 

  route do |r|
    r.root do
      render "tasks", { Task.all }
    end
  end
end

We would then need to change @tasks in the template to tasks, since we are now passing it as a local variable instead of an instance variable.

<html>
  <head>
    <title>To-Do or not To-Do</title>
  </head>
  <body>
    <h1>To-Do or not To-Do</h1>
    <h2>Tasks</h2>
    <ul>
      <% tasks.each do |task| %>
        <li class="<%= task.done? ?  :  %>">
          <input type="checkbox"<%= " checked" if task.done? %>>
          <%= task.title %>
        </li>
      <% end %>
    </ul>
  </body>
</html>

In general, using instance variables instead of passing local variables explicitly is recommended as it offers better performance and requires less verbose code. However, some developers prefer the explicitness of passing data to the view using local variables. Most of the rest of the book will use instance variables, though a later section will describe one case when using local variables is recommended.

Now, we could add some styling to our list to make it a bit more colorful. We'll start with the simple way of using an inline style tag inside the head element. We'll also remove the list item bullets, which don't look great considering we are also using checkboxes.

<html>
  <head>
    <title>To-Do or not To-Do</title>
    <style>
      ul { list-style: none; }
      ul .todo { color: red;}
      ul .done { color: green;}
    </style>
  </head>
  <body>
    <h1>To-Do or not To-Do</h1>
    <ul>
      <% @tasks.each do |task| %>
        <li class="<%= task.done? ?  :  %>">
          <input type="checkbox"<%= " checked" if task.done? %>>
          <%= task.title %>
        </li>
      <% end %>
    </ul>
  </body>
</html>

When we reload, we can see our style applied.

Now that the list is slightly prettier, we move to our next feature: showing details for a particular task. So we add the new route, set the instance variable we plan to use, and then render the new view.

route do |r|
  r.root do
    @tasks = Task.all
    render "tasks"
  end

  r.get "tasks", Integer do |id|
    next unless @task = Task[id]
    render "task"
  end
end

We can then add the views/task.erb file. In this file, we need to show the task title, whether it was done or not, and its due date. We'll copy the erb code from the previous template and modify it.

<html>
  <head>
    <title>To-Do or not To-Do</title>
    <style>
      ul { list-style: none; }
      ul .todo { color: red;}
      ul .done { color: green;}
    </style>
  </head>
  <body>
    <h1>To-Do or not To-Do</h1>
    <h2><%= @task.title %></h2>
    <% if @task.done? %>
      <span class="done">[DONE]</span>
    <% else %>
      <span class="todo">[TODO]</span>
    <% end %>
    <h3>Due Date: <%= @task.due.strftime("%v") %></h3>
  </body>
</html>

We try this out and we see the new template rendered. Here's an example of a task that has been completed.

Here's an example of a task we have not completed yet.

Layouts

We now have our two different templates, but something doesn't feel right about this code. We are using the same HTML structure, CSS style, and title, and we're duplicating it for every new page we add.

If we wanted to change anything on our basic layout, say the color of the done tasks to a different shade of green, we'd need to change it in every view.

We can fix this issue and remove the duplication issue by replacing the render method with a call to view.

route do |r|
  r.root do
    @tasks = Task.all
    view "tasks"
  end

  r.get "tasks", Integer do |id|
    next unless @task = Task[id]
    view "task"
  end
end

When we load any of the two pages we currently have, we see that view is not finding a file called layout.erb in the same path as the previous templates.

require "lucid_http"

GET "/"
error
# => "Errno::ENOENT: No such file or directory @ " \
#    "rb_sysopen - /home/lucid/code/my_app/views/layout.erb"

We want to move the shared content from both of the other views into the layout view, and in the layout view where the page-specific content goes, we will call yield.

<html>
  <head>
    <title>To-Do or not To-Do</title>
    <style>
      ul { list-style: none; }
      ul .todo { color: red;}
      ul .done { color: green;}
    </style>
  </head>
  <body>
    <h1>To-Do or not To-Do</h1>
    <%= yield %>
  </body>
</html>

We can then update the tasks.erb view.

<h2>Tasks</h2>
<ul>
  <% tasks.each do |task| %>
    <li class="<%= task.done? ?  :  %>">
      <input type="checkbox"<%= " checked" if task.done? %>>
      <%= task.title %>
    </li>
  <% end %>
</ul>

We can also update the task.erb view.

<h2><%= @task.title %></h2>
<% if @task.done? %>
  <span class="done">[DONE]</span>
<% else %>
  <span class="todo">[TODO]</span>
<% end %>
<h3>Due Date: <%= @task.due.strftime("%v") %></h3>

We can check and make sure everything works correctly again.

One thing to be aware of when using layouts with the view method is that the page-specific view is rendered before the layout view. This is helpful as it allows us to set instance variables in the page-specific view, and access them in the layout view. This is commonly used to set the HTML page title on a per-page basis. Let's make that change first to the layout, to use a custom page title from the @page_title instance variable, with the previous page title as a fallback.

<html>
  <head>
    <title><%= @page_title || "To-Do or not To-Do" %></title>
    <style>
      ul { list-style: none; }
      ul .todo { color: red;}
      ul .done { color: green;}
    </style>
  </head>
  <body>
    <h1>To-Do or not To-Do</h1>
    <%= yield %>
  </body>
</html>

We can then update the tasks.erb view to set the @page_title.

<% @page_title = "All Tasks" %>

<h2>Tasks</h2>
<ul>
  <% tasks.each do |task| %>
    <li class="<%= task.done? ?  :  %>">
      <input type="checkbox"<%= " checked" if task.done? %>>
      <%= task.title %>
    </li>
  <% end %>
</ul>

We can also update the task.erb view to set the @page_title.

<% @page_title = "Task: #{@task.title}" %>

<h2><%= @task.title %></h2>
<% if @task.done? %>
  <span class="done">[DONE]</span>
<% else %>
  <span class="todo">[TODO]</span>
<% end %>
<h3>Due Date: <%= @task.due.strftime("%v") %></h3>

content_for

In some cases, there can be multiple places where the layout uses content provided by the page-specific view, instead of just a single place. While we can pass content from the page-specific view to the layout by setting an instance variable, in some cases that results in ugly code. This is particularly true of cases where large blocks of template code that are page-specific are designed to be displayed outside of the main content in the layout.

As an example, let's say we wanted to add a footer to our pages. We can render a separate template for the footer, unless the page-specific view has overridden the footer. Let's change our layout to support this. We'll assume we have a footer.erb view for the default footer. We can use render("footer") in the layout to render the footer view.

<html>
  <head>
    <title><%= @page_title || "To-Do or not To-Do" %></title>
    <style>
      ul { list-style: none; }
      ul .todo { color: red;}
      ul .done { color: green;}
    </style>
  </head>
  <body>
    <h1>To-Do or not To-Do</h1>
    <%= yield %>
    <div class="footer">
      <%= render("footer") %>
    </div>
  </body>
</html>

We'll use a simple footer.erb view.

This is the footer.

This works and adds the footer to all pages. However, let's say we wanted the footer on the page that displays all tasks to instead show the total number of tasks. We need to store the content for the footer in the tasks.erb view, and then modify the layout to use it. To do this, we'll use the content_for plugin, which allows storing the content in one template, and displaying it in another template. Let's first enable the plugin.

class App < Roda
  plugin 
  plugin 

  route do |r|
    r.root do
      @tasks = Task.all
      view "tasks"
    end

    r.get "tasks", Integer do |id|
      next unless @task = Task[id]
      view "task"
    end
  end
end

Next, let's update the tasks.erb view to store the custom footer showing how many total tasks there are. In this view, we'll call the content_for method with a block, and the result of the block is the content to store.

<% @page_title = "All Tasks" %>

<h2>Tasks</h2>
<ul>
  <% tasks.each do |task| %>
    <li class="<%= task.done? ?  :  %>">
      <input type="checkbox"<%= " checked" if task.done? %>>
      <%= task.title %>
    </li>
  <% end %>
</ul>
<% content_for() do %>
  There are <%= tasks.length %> tasks total.
<% end %>

Finally, let's update the layout.erb file to use the custom footer if one is given, and fall back to the default footer if not. In the layout, we'll call content_for without a block, which will return the stored content, or nil if no content was stored. So if the page-specific view set the content for the footer, it will be used. If it did not set the content for the footer, the default of rendering the footer template will be used.

<html>
  <head>
    <title><%= @page_title || "To-Do or not To-Do" %></title>
    <style>
      ul { list-style: none; }
      ul .todo { color: red;}
      ul .done { color: green;}
    </style>
  </head>
  <body>
    <h1>To-Do or not To-Do</h1>
    <%= yield %>
    <div class="footer">
      <%= content_for() || render("footer") %>
    </div>
  </body>
</html>

capture_erb

If you want to capture content from an erb template, without the desire to access it elsewhere later using the content_for method, then you can directly use the capture_erb plugin (the content_for plugin uses capture_erb internally):

<% footer_content = capture_erb do %>
  There are <%= tasks.length %> tasks total.
<% end %>

Injecting content with inject_erb

Roda uses ERB syntax, which means that the following template code does not work:

<%= some_method do %>
  content
<% end %>

This is invalid ERB syntax, though it is supported by Rails (Rails attempts to parse the code inside the tag using a regexp, and changes behavior based on the results of the regexp parsing). With Roda, you have to use valid ERB syntax, such as the following template code:

<% some_method do %>
  content
<% end %>

However, this means that some_method in the above template cannot update the template output, other than by yielding to the block passed to it. This makes it hard to write template methods that accept blocks that want to wrap the blocks and inject content before and after the blocks. Thankfully, Roda has an inject_erb plugin to allow for this. With the inject_erb plugin loaded, you can call the inject_erb method to inject content into the template currently being rendered. For example, if some_method should include <form> tags around the content, you could define it similar to the following:

def some_method
  inject_erb '<form>'
  yield
  inject_erb '</form>'
end

With the above approach, some_method injects <form> into the template output, then yields to the block, then injects </form> into the template output after the block returns.

What if you want to modify the content inside the block passed to the method before injecting it into the template? To do that, you can combine the inject_erb plugin with the capture_erb plugin discussed above. You capture the content of the block, and then can modify it before injecting it. For example, if you want some_method to capitalize the content inside the block, in addition it to wrapping it with <form> tags:

def some_method(&block)
  inject_erb '<form>'
  inject_erb capture_erb(&block).capitalize
  inject_erb '</form>'
end

Escaping content

One issue with the current templates is that they do not correctly escape content before displaying it on the page. If the content can come from sources beyond our control, this could open up a vulnerability called cross-site scripting (XSS). To prevent this type of vulnerability, we need to escape output before using it.

Early in the book, we learned about the h plugin, and how it can be used to escape content before displaying it. We could use that here, with explicit calls to h every time we want to escape output. For example, we could switch the task.erb file to use the h plugin.

<% @page_title = "Task: #{h @task.title}" %>

<h2><%=h @task.title %></h2>
<% if @task.done? %>
  <span class="done">[DONE]</span>
<% else %>
  <span class="todo">[TODO]</span>
<% end %>
<h3>Due Date: <%= @task.due.strftime("%v") %></h3>

Unfortunately, this approach is fairly error-prone, as in a large application it is likely that we will miss a place that needs to be escaped (notice that h was used in two places in the above template). Roda has a better approach. Instead of only escaping stuff we know is bad, we can escape everything except what we know is good (by good, we mean we know the output is already escaped HTML).

We need to enable this support in the render plugin using the :escape option. Be aware that this automatic escaping support requires the erubi gem, so we need to add that to our Gemfile and then run bundle install.

source "https://rubygems.org"

gem "roda"
gem "puma"
gem "tilt"
gem "erubi"

After installing the erubi gem, we can use the render plugin's :escape option.

class App < Roda
  plugin , true

  # ...
end

This approach allows us to remove the manual escaping in the task.erb view. The @task.title in the h2 tag will be automatically escaped. This will also escape the @task.due.strftime("%v") output in the h3 tag, even though that doesn't need escaping.

<% @page_title = "Task: #{@task.title}" %>

<h2><%= @task.title %></h2>
<% if @task.done? %>
  <span class="done">[DONE]</span>
<% else %>
  <span class="todo">[TODO]</span>
<% end %>
<h3>Due Date: <%= @task.due.strftime("%v") %></h3>

In the layout view, we only need to make a single change, and that is to mark that the yield call returns already escaped output. We can do that by adding an extra equal sign to the output tag.

<html>
  <head>
    <title><%= @page_title || "To-Do or not To-Do" %></title>
    <style>
      ul { list-style: none; }
      ul .todo { color: red;}
      ul .done { color: green;}
    </style>
  </head>
  <body>
    <h1>To-Do or not To-Do</h1>
    <%== yield %>
  </body>
</html>

This will not escape the output of the page-specific template (as that should already be escaped), but it will add escaping of the title tag. When using automatic escaping, we just need to remember to use the double equals output tag instead of the single equals if we want to output a value that has already been escaped.

The render plugin includes more advanced configuration options, beyond the :escape option already shown. We can learn more about the options on the render plugin documentation page.

View subdirectories

Using a single views folder with all template files inside of it works fine for small applications, but larger applications are probably going to want to organize their views using subdirectories. We'll look at how to do that when applied to the previous example. Let's create a views/tasks subdirectory and move the views into it.

Dir.mkdir('views/tasks')
File.rename('views/tasks.erb', 'views/tasks/index.erb')
File.rename('views/task.erb', 'views/tasks/task.erb')

Then update our application to render a template from the subdirectory, by prepending the subdirectory to the view name.

route do |r|
  r.root do
    @tasks = Task.all
    view "tasks/index"
  end

  r.get "tasks", Integer do |id|
    next unless @task = Task[id]
    view "tasks/task"
  end
end

view_options

The above example works, but results in duplication of the subdirectory name for the related routes. Let's fix that using the view_options plugin. First, let's use a more realistic example. In general, we are only going to use view subdirectories when we have multiple branches in the routing tree. It's fairly common to use a view subdirectory per branch. So we'll expand our example to have a branch for tasks and a branch for posts.

class App < Roda
  plugin , true

  route do |r|
    r.on "tasks" do
      r.get true do
        @tasks = Task.all
        view "tasks/index"
      end

      r.get Integer do |id|
        next unless @task = Task[id]
        view "tasks/task"
      end
    end

    r.on "posts" do
      r.get true do
        @posts = Post.all
        view "posts/index"
      end

      r.get Integer do |id|
        next unless @post = Post[id]
        view "posts/post"
      end
    end
  end
end

As the previous example shows, this duplicates the view subdirectory name in every route. Let's fix this using the view_options plugin. It includes a set_view_subdir method for setting the view subdirectory. When using this, we need to set the :layout option when loading the render plugin to include a slash. This way the views/layout.erb file will be used for the layout, instead of looking for the layout in each view subdirectory.

class App < Roda
  plugin , true, './layout'
  plugin 

  route do |r|
    r.on "tasks" do
      set_view_subdir "tasks"

      r.get true do
        @tasks = Task.all
        view "index"
      end

      r.get Integer do |id|
        next unless @task = Task[id]
        view "task"
      end
    end

    r.on "posts" do
      set_view_subdir "posts"

      r.get true do
        @posts = Post.all
        view "index"
      end

      r.get Integer do |id|
        next unless @post = Post[id]
        view "post"
      end
    end
  end
end

The behavior remains the same, but now we don't need to specify the view subdirectory explicitly in every route. The view_options plugin also has the ability to change any view or layout options on a per-branch basis. This can be used if templates in one of the subdirectories use a different enging than the templates used by default.

Reducing duplication in views

In the section on layout, we learned about separating the layout view from the page-specific view to reduce duplication. However, on many complex applications, we'll run into the same duplication issue in other areas, where we want to reuse code without duplicating it in multiple separate views. In this section, we'll learn how to split up a template into partials.

We have a To-Do app where we show the tasks list on the root route.

We noticed that when the list grows too long, it starts to get annoying having to scroll up and down to locate the undone tasks. To solve this, we could sort the list by putting the undone tasks first. However, we don't want to loose the actual ordering of the list.

What we decide to do is to show a list containing only the undone tasks. In order to do that, we create the /todo route. To keep the example simple, we'll go back to a single views folder instead of using view subdirectories. So we'll use a todo view, and set an @tasks instance variable that the view will be able to access.

route do |r|
  r.root do
    @tasks = Task.all
    view "index"
  end

  r.get "todo" do
    @tasks = Task.todo
    view "todo"
  end
end

Then we need to create the views/todo.erb template file. Since this is just the first pass through this implementation and we already have a stylized version of our tasks, we decide to use the same code for our list items. We can always come back and simplify this code later.

<h2>Undone Tasks</h2>
<ul>
  <% @tasks.each do |task| %>
    <li class="<%= task.done? ?  :  %>">
      <input type="checkbox"<%= " checked" if task.done? %>>
      <%= task.title %>
    </li>
  <% end %>
</ul>

We can check and see that this works.

Now that it works, we can come back to our code. To simplify the views, we decide to extract the duplicated code into it's own template file. We create a views/task.erb view, and copy code for handling a specific task to it.

<li class="<%= task.done? ?  :  %>">
  <input type="checkbox"<%= " checked" if task.done? %>>
  <%= task.title %>
</li>

Now, we need a way to render this view for each of the tasks. Luckily, we already learned how to do this, though it might not be obvious. We just need to reuse the render method for rendering the template. We use render instead of view because we don't want to include the layout when rendering the views/task.erb view.

We are going to pass the task to the view as a local variable in this case, because we are calling the view in a loop, and it is already a local variable passed to the block. This is a case where it is recommended to use a local variable and not an instance variable when rendering.

<h2>Undone Tasks</h2>
<ul>
  <% tasks.each do |task| %>
    <%== render("task", locals: { task: task }) %>
  <% end %>
</ul>

We can try it and confirm that it still works.

render_each

Rendering the same template for each object in an enumerable of objects is actually a common need in web applications, and Roda offers a render_each plugin to support that need. We can load that plugin into our application.

class App < Roda
  plugin , true
  plugin 

  # ...
end

Then we can update our views/task view to use it. This is significantly simpler than manually iterating over each task and rendering a template for it. It is also better for performance due to optimizations in the render_each plugin.

<h2>Undone Tasks</h2>
<ul>
  <%== render_each(tasks, "task") %>
</ul>

As the template filename is task.erb, the render_each method will automatically set a local variable of task for the current task being rendered. We can choose a different local variable name using the :local option.

partials

If we look at the views/tasks directory for the previous example, we can see that tasks.erb and todo.erb are a meant to render a whole page on our site. They are what we usually call views in most web frameworks. On the other hand, task.erb is meant just to render a portion of the page, and possibly is called multiple times per request. In web development, this kind of file is commonly known as a partial (short for partial template).

Ruby on Rails provides a convention to make the view/partial difference at the file name level. If the file is meant to be a partial, we prepend an underscore to the filename. The partials plugin allows for similar behavior as Rails. Here's an example of using it. First, we need to rename the partial to prepend the filename with an underscore.

File.rename('views/task.erb', 'views/_task.erb')

Then we need to update our application to use the partials plugin.

class App < Roda
  plugin , true
  plugin 

  # ...
end

Then we can update the views/todo.erb view to use the partials plugin. We can either use the long way of manual iteration and calling the partial method.

<h2>Undone Tasks</h2>
<ul>
  <% tasks.each do |task| %>
    <%== partial("task", locals: { task: task }) %>
  <% end %>
</ul>

Or the shortcut of using each_partial. Both cases result in the same code.

<h2>Undone Tasks</h2>
<ul>
  <%== each_partial(tasks, "task") %>
</ul>

Both partial and each_partial just add an underscore to the template filename. They do not change other behavior, so they are only useful if we want to use a convention that prefixes filenames for partials with an underscore.

symbol_views

Roda's default behavior is that match blocks should return either a string for the response body, or nil or false. However, that's only the default behavior, the behavior can be modified in a plugin. In the next couple sections, we'll learn how to use a couple of those plugins.

Let's take an earlier example discussing tasks, without the view subdirectories. So our app code looks like this.

class App < Roda
  plugin , true

  route do |r|
    r.root do
      @tasks = Task.all
      view "index"
    end

    r.get "todo" do
      @tasks = Task.todo
      view "todo"
    end
  end
end

There is some duplication here, in that we are calling view method multiple times. It's actually fairly common in a large routing tree that most of the r.get calls will end with a call to view. To reduce duplication and make things prettier (and less explicit), we are going to use the symbol_views plugin. The symbol_views plugin allows match blocks to return symbols, and if a match block returns a symbol, the symbol is passed to the view method and the output is used as the response body. So the above app code can be changed to:

class App < Roda
  plugin , true
  plugin 

  route do |r|
    r.root do
      @tasks = Task.all
      
    end

    r.get "todo" do
      @tasks = Task.todo
      
    end
  end
end

It's possible to use symbol_views with view subdirectories, but it looks a bit less pretty:

class App < Roda
  plugin , true
  plugin 

  route do |r|
    r.root do
      @tasks = Task.all
      :"tasks/index"
    end

    r.get "todo" do
      @tasks = Task.todo
      :"tasks/todo"
    end
  end
end

However, most applications using view subdirectories are generally going to use the view_options plugin as discussed earlier, so it is rare to need this syntax.

json

symbol_views isn't the only plugin that adds support for additional types to be returned by match blocks. symbol_views helps reduce duplication in applications that return HTML. Roda offers a similar way to reduce duplication in applications that return JSON, using the json plugin.

We're going to use an example application that shows movie showtimes. We'll assume the movies local variable is defined that is an array of hashes, with each hash representing a movie showtime. We'll use two routes. One is /movies route for listing our movies. Another is a /movies/<slug> route for displaying the title, times and description for a specific movie.

class App < Roda
  movies = [
    {
      "infinity-war",
      "Avengers Infinity War",
      ["15:30", "18:40", "21:45"],
      "The Avengers fight Thanos."
    },
    {
      "the-usual-suspects",
      "The Usual Suspects",
      ["11:10", "15:45"],
      "A random police lineup leads to something deadly."
    },
    {
      "the-matrix",
      "The Matrix",
      ["17:15", "22:10"],
      "Computer hacker finds he lives in a simulation."
    },
  ]

  route do |r|
    r.on "movies" do
      r.get true do
        movies.map do |movie|
          "#{movie[]}: /movies/#{movie[]}"
        end.join("\n")
      end

      r.get String do |slug|
        movie = movies.find { |m| m[] == slug }

        <<~EOF
          #{movie[]}
          Times: [ #{movie[].join(" ")} ]
          Description: #{movie[]}
        EOF
      end
    end
  end
end

Unfortunately, a manager comes along and decides that our simple app should now be powered by the frontend javascript framework du jour. So our Roda application must switch to returning JSON that will be consumed by the frontend.

We first convert the /movies route to return JSON. We aren't insane enough to construct the JSON by hand, so we'll convert our movies array into the appropriate format, and then call to_json on the result. We also need to set the Content-Type header of the response to let the client know that we're returning JSON.

r.get true do
  response['Content-Type'] = 'application/json'

  movies.map do |movie|
    {movie[], "/movies/#{movie[]}"}
  end.to_json
end

Now, when we take a look at the end result, we see our JSON response.

require "lucid_http"
require "json"

GET "/movies", 
# => [{"title"=>"Avengers Infinity War", "url"=>"/movies/infinity-war"},
#     {"title"=>"The Usual Suspects", "url"=>"/movies/the-usual-suspects"},
#     {"title"=>"The Matrix", "url"=>"/movies/the-matrix"}]

Now we move on to the /movies/<slug> route. We first need to find the correct movie. If we can't find the movie, we use next to return a 404 response. If we can find the movie, we set the Content-Type, then create a new hash with the movie information, and convert that hash to JSON using to_json.

Here's the full routing tree for the application.

route do |r|
  r.on "movies" do
    r.get true do
      response['Content-Type'] = 'application/json'

      movies.map do |movie|
        {movie[], "/movies/#{movie[]}"}
      end.to_json
    end

    r.get String do |slug|
      next unless movie = movies.find { |m| m[] == slug }
      response['Content-Type'] = 'application/json'

      {
              movie[],
              movie[],
        movie[]
      }.to_json
    end
  end
end

We can then check that the /movies/<slug> route works.

require "lucid_http"
require "json"

GET "/movies/infinity-war", 
# => {"title"=>"Avengers Infinity War",
#     "times"=>["15:30", "18:40", "21:45"],
#     "description"=>
#      "The Avengers fight Thanos."}

This approach has two cases that result in duplication. First, we need to set the Content-Type header so the frontend will handle the response as JSON. We only want to set the Content-Type header if we are sure we are returning JSON, not in cases where we are returning an empty 404 response. Second, we need to call to_json on the hash or array that we want to use as the JSON response body.

Roda's json plugin is designed to remove the duplication in both of these cases. We first need to load the plugin. After loading it, match blocks can return an array or a hash, and the array or hash will be converted to JSON and the JSON output will be returned as the response. The json plugin handles setting the Content-Type header, so we don't need to do that in every route.

In this case, our r.get true route returns an array, and our r.get String route returns a hash. Both are converted to JSON.

class App < Roda
  plugin 

  movies = [
    # ...
  ]

  route do |r|
    r.on "movies" do
      r.get true do
        movies.map do |movie|
          {movie[], "/movies/#{movie[]}"}
        end
      end

      r.get String do |slug|
        next unless movie = movies.find { |m| m[] == slug }

        {
                movie[],
                movie[],
          movie[]
        }
      end
    end
  end
end

We can check the output again to make sure everything works.

require "lucid_http"
require "json"

GET "/movies", 
# => [{"title"=>"Avengers Infinity War", "url"=>"/movies/infinity-war"},
#     {"title"=>"The Usual Suspects", "url"=>"/movies/the-usual-suspects"},
#     {"title"=>"The Matrix", "url"=>"/movies/the-matrix"}]

GET "/movies/infinity-war", 
# => {"title"=>"Avengers Infinity War",
#     "times"=>["15:30", "18:40", "21:45"],
#     "description"=>
#      "The Avengers fight Thanos."}

Hopefully the symbol_views and json plugins have shown how Roda's default behavior can be extended by plugins to make certain uses cases simpler.

Assets

So far, our discussion of rendering has focused on rendering views to generate response bodies for requests. We'll now extend the discussion to rendering both the CSS stylesheet and the javascript files used by the views. The layout in our examples inlined the CSS. In general, we are not going to want to do that. We would want to use an separate stylesheet, and if we use javascript, we'll also want to separate the javascript file.

Static assets

The simplest way to handle stylesheet and javascript assets is to use static assets. In general, static asset files would go under the public folder, and would be served directly by the content delivery network, front-end web-server, or by the public plugin. In development, we'll probably want to use the public plugin, so that's what we'll use in this example.

Let's create a directory structure and empty files for the static assets.

Dir.mkdir("public/css")
Dir.mkdir("public/js")
File.write("public/css/app.css", "")
File.write("public/js/app.js", "")

We'll then open up the public/css/app.css file and add the CSS code from the layout, formatting it to make it a little nicer.

ul {
  list-style: none;
}
ul .todo {
  color: red;
}
ul .done {
  color: green;
}

We'll then open up the public/js/app.js file and add some javascript code that changes the class of the item when the checkbox is modified.

(function() {
  Array.from(document.getElementsByTagName("input")).
    forEach(function(element) {
      element.onchange = function() {
          element.parentNode.classList.toggle("done");
          element.parentNode.classList.toggle("todo");
        }
    });
})();

We need to update our routing tree to serve files under the public directory.

class App < Roda
  plugin , true
  plugin 
  plugin 

  route do |r|
    r.public

    r.root do
      @tasks = Task.all
      
    end

    r.get "todo" do
      @tasks = Task.todo
      
    end
  end
end

We can then edit our views/layout.erb file to link to the CSS and JS files.

<html>
  <head>
    <title><%= @page_title || "To-Do or not To-Do" %></title>
    <link rel="stylesheet"  href="/css/app.css" />
  </head>
  <body>
    <h1>To-Do or not To-Do</h1>
    <%== yield %>
    <script type="text/javascript" src="/js/app.js"></script>
  </body>
</html>

This works and the page displays the same as it did previously when using the inline stylesheet. It functions slightly nicer as clicking on a checkbox modifies the style of the related item.

We can get fairly far with using static assets. If we are sufficiently stubborn, we can handle even a large website using static assets. However, the static asset approach has some issues:

Thankfully, Roda ships with an assets plugin that handles all of these concerns.

Introduction to assets

The assets plugin handles compiling assets, as well as combining and compressing assets in production. Roda tries to make setting up assets as easy as possible. By default, Roda expects assets will be stored in an assets directory, with assets/css storing CSS files (or files that compile to CSS) and assets/js storing javascript files (or files that compile to javascript). Let's first create the assets directory tree and then move our static assets to them.

Dir.mkdir("assets")
Dir.mkdir("assets/css")
Dir.mkdir("assets/js")
File.rename("public/css/app.css", "assets/css/app.css")
File.rename("public/js/app.js", "assets/js/app.js")

Then we can update our routing tree to serve the assets. Note how the only changes are replacing the public plugin with the assets plugin, and using r.assets instead of r.public in the routing true.

class App < Roda
  plugin , true
  plugin 
  plugin , ["app.css"], ["app.js"]

  route do |r|
    r.assets

    r.root do
      @tasks = Task.all
      
    end

    r.get "todo" do
      @tasks = Task.todo
      
    end
  end
end

We can then edit our views/layout.erb file to call the assets methods instead of hardcoding the links. The assets method returns an already escaped HTML tag (or multiple tags), so just as with yield, we use the double equals when outputing to avoid double escaping the output. If we want to use the assets plugin for both CSS and javascript, we'll want to call the assets method twice, once for each type of asset.

<html>
  <head>
    <title><%= @page_title || "To-Do or not To-Do" %></title>
    <%== assets() %>
  </head>
  <body>
    <h1>To-Do or not To-Do</h1>
    <%== yield %>
    <%== assets() %>
  </body>
</html>

This works and the page displays the same as it did before the changes to use the assets plugin.

Asset compilation

With the previous example, the assets plugin is serving the static file directly without doing any processing on it. It's likely slower than the static asset approach that used the public plugin, and doesn't offer an advantage. Let's actually start using the assets plugin features so we can see what the advantages are.

First, we are going to take the assets.css file and change it to assets.scss. This changes it from a CSS file to an SCSS file.

File.rename("public/css/app.css", "assets/css/app.scss")

Then we can update the assets plugin options we used to reflect the file renaming.

plugin , ["app.scss"], ["app.js"]

Converting the SCSS format to CSS format requires the sassc gem (or the older and now deprecated sass gem), so let's add the sassc gem to our Gemfile, then run bundle install.

source "https://rubygems.org"

gem "roda"
gem "puma"
gem "tilt"
gem "erubi"
gem "sassc"

After installing the sassc gem, we can check and the page displays the same as it did before we renamed the file. That may appear to be a little weird, because the file format did change. The reason it works is that valid CSS code is automatically valid SCSS code, it just doesn't use any of the SCSS features. Let's start using an SCSS feature to show what SCSS can do. We'll edit the assets/css/app.scss file, and modify it to use SCSS format. This is invalid CSS, as CSS doesn't support nested style tags like this. However, SCSS does support such tags.

ul {
  & {
    list-style: none;
  }
  .todo {
    color: red;
  }
  .done {
    color: green;
  }
}

We can check again and see that the page still looks the same. If we make a request for the /assets/css/app.scss file, we can see that our SCSS file has been compiled into CSS, and the CSS content has been returned as the body, with the correct Content-Type.

require "lucid_http"

GET "/assets/css/app.scss"
status                          # => 200 OK
content_type                    # => "text/css; charset=UTF-8"
body
# >> ul {
# >>   list-style: none; }
# >> ul .todo {
# >>   color: red; }
# >> ul .done {
# >>   color: green; }

SCSS doesn't support just style tag nesting, but also supports variables, mixins, inheritance, and mathematical operators. Any CSS file of moderate size can probably be simplified by switching it to SCSS and using SCSS features to reduce the amount of duplication in it.

In addition to supporting compilation for CSS, the assets plugin also supports compilation for javascript. For both CSS and javascript, the assets plugin can use any template engine supported by tilt, the gem that powers the render plugin. For CSS, this includes the sass and less formats in addition to scss. For javascript, this can include coffee (CoffeeScript), ts (TypeScript), babel (Babel Transpiler), and rb (Opal, allowing us to write our frontend code in Ruby).

Asset combination

In the previous section, we discussed one improvement that the assets plugin offers, compiling a different format to CSS or javascript. Now we will look at another advantage the assets plugin offers, which is combining multiple CSS and/or javascript files.

We'll update our example to use multiple CSS and multiple javascript assets. For the CSS, we'll assume we are including a modified version of the Bootstrap CSS library as our base, in addition to the app.scss file we used earlier. For the javascript, we'll assume we are adding a TypeScript file that will handle dynamic page actions on one of the task pages. Note that we are using multiple different formats for both CSS and javascript files.

plugin ,
  ["bootstrap.css", "app.scss"],
  ["app.js", "tasks.ts"]

If we view the page content, we'll see how the assets(:css) and assets(:js) calls result in multiple link and script tags, respectively. We can infer from this that the assets plugin does not combine assets by default.

<html>
  <head>
    <title>To-Do or not To-Do</title>
    <link rel="stylesheet"  href="/assets/css/bootstrap.css" />
    <link rel="stylesheet"  href="/assets/css/app.scss" />
  </head>
  <body>
    <h1>To-Do or not To-Do</h1>
    # ...
    <script type="text/javascript" src="/assets/js/app.js"></script>
    <script type="text/javascript" src="/assets/js/tasks.ts"></script>
  </body>
</html>

Let's turn on the asset compilation to see how things have changed. After loading the assets plugin, we'll call the compile_assets method.

plugin ,
  ["bootstrap.css", "app.scss"],
  ["app.js", "tasks.ts"]
compile_assets

When we call compile_assets, Roda will convert all of the CSS assets to CSS and will concatenate them together. Roda will also convert all of the javascript assets to javascript and will concatenate them together. After making this change, if we request the page again, we can the content has changed, and now a single link and script tag are present. Note that in this example, the string in the middle was truncated to make it fit on one line.

<html>
  <head>
    <title>To-Do or not To-Do</title>
    <link rel="stylesheet"
      integrity="sha256-5uh7i+PDFHCJmpP9ef8B8TTTS8x7A2jeM/IvQH9Togs="
      href="/assets/app.e6e87b8be3c31470899a93fd79ff01f134d.css" />
  </head>
  <body>
    <h1>To-Do or not To-Do</h1>
    # ...
    <script type="text/javascript"
      integrity="sha256-M1Y2mm2BRge3II4oUCyNKCQDthqZ3YTCQlfGTKP7bW8="
      src="/assets/app.3356369a6d814607b7208e28502c8d282403.js">
    </script>
  </body>
</html>

In the link tag href attribute and in the script tag src attribute, we see that the paths used have a basic format of /assets/app.*.css and /assets/app.*.js. What does the string in the middle of the path represent? The assets plugin uses the SHA-256 hash (in hex encoding) of the content of the asset as part of the combined asset filename. This ensures that if there is a change in any of the assets, a new filename will be used, and HTTP clients will not use an old cached filename.

The integrity attribute in both tags is the same SHA-256 hash, but in base64 encoding. The integrity attribute allows us to host the compiled asset files on a content delivery network or other server that we do not control, and ensures that the files will only be loaded if they have not been modified.

Asset precompilation

The compile_assets call is great for production. However, we wouldn't want to use it in development, since once we call compile_assets, the assets plugin will only serve the combined asset files, and will not pick up changes to the source asset files. We could skip the compiling of assets in development mode fairly easily by using a conditional.

plugin ,
  ["bootstrap.css", "app.scss"],
  ["app.js", "tasks.ts"]
compile_assets unless ENV["RACK_ENV"] == "development"

However, there are cases where compile_assets needs to be called before the application starts. One reason for this is if the application runs on a read-only filesystem. The assets plugin can handle this using a process called asset precompilation. With asset precompilation, the assets are compiled before the application starts, and the metadata related to the compiled assets is stored in a JSON file. When the application starts, it checks for the existence of the JSON file. If the JSON file exists, the assets plugin assumes the assets have already been combined and that the precompiled asset metadata is valid. If the JSON file doesn't exist, the assets plugin operates in the default mode, using a separate link and script tag per asset file.

To use asset precompilation, we need to include the :precompiled option when loading the assets plugin, with the value being the filename containing the precompiled asset metadata. The convention for this file is to use compiled_assets.json in the same directory as the application. Additionally, we should remove any calls to the compile_assets method.

plugin ,
  ["bootstrap.css", "app.scss"],
  ["app.js", "tasks.ts"],
  File.expand_path('../compiled_assets.json', __FILE__)

To precompile the assets, it is common to use a rake task. This rake task can be used on Heroku to precompile the assets when building the application.

namespace  do
  desc "Precompile the assets"
  task  do
    require './app'
    App.compile_assets
  end
end

When calling compile_assets, if the :precompiled option was given to the assets plugin, the metadata related to the compiled assets will be written in JSON format to that file, so that the next time the application starts, the assets plugin will operate in compiled mode.

Asset compression

The last major feature of the assets plugin is the ability to compress assets after combining them. There's actually two separate types of compression. The first type is often called minimization. This requires custom gems such as yuicompressor, which will inspect the generated CSS and javascript code, and make it smaller while still behaving the same. If we have yuicompressor installed, the assets plugin will automatically pick it up and use it to compress the assets.

The other type of compression used is gzip compression, which happens after any asset minimization. This makes a gzip compressed version of the asset available. For requests for the asset that accept gzip encoding, the gzip version will be transferred directly, which can save substantial amounts of bandwidth. We can enable this using the :gzip option to the assets plugin.

plugin ,
  ["bootstrap.css", "app.scss"],
  ["app.js", "tasks.ts"],
  File.expand_path('../compiled_assets.json', __FILE__),
  true

Routing

In the previous section, we discussed many plugins that Roda includes that handle rendering response bodies. In this section, we'll discuss some plugins that extend Roda's routing capabilities.

hash_branches

Most applications start out small. If we are lucky (from a maintenance perspective), our application will stay small. However, there may be cases where the application needs to grow substantially. We may have started out with 5 routes, but after adding features, our application now has hundreds of routes.

With core Roda, all routes must be in the same block, and blocks cannot span files in Ruby, so that means that all routes must be in a single file. That's not a practical approach for an application with a large number of routes. Roda recognizes that, and has included plugins for managing large numbers of routes since its initial release. In this section, we'll go over the use of the plugin recommended for handling large routing trees, which is called hash_branches.

Let's assume our application that originally handled only tasks has now expanded to handle blog posts, and that we've also added an online store. There are now many routes in each section, and it is becoming hard to manage all routes inside the app.rb file.

class App < Roda
  route do |r|
    r.on "tasks" do
      # task routes
    end

    r.on "posts" do
      # post routes
    end

    r.on "store" do
      # store routes
    end
  end
end

We decide we want to split this up, and have separate files for the routing branches for tasks, posts, and store. We'll add a routes directory and create separate files for the three routing branches.

Dir.mkdir("routes")
File.write("routes/tasks.rb", "")
File.write("routes/posts.rb", "")
File.write("routes/store.rb", "")

Then we'll open the routes/tasks.rb file and move the tasks routing branch into it, using hash_branch. We pass the hash_branch the segment for the branch and a block to handle requests for that branch. Each of the blocks you pass to hash_branch acts as its own routing tree (similar to the Roda route method):

class App
  hash_branch "tasks" do |r|
    # /tasks routes
  end
end

Similarly, we'll open the routes/posts.rb file and move the posts routing branch into it.

class App
  hash_branch "posts" do |r|
    # /posts routes
  end
end

Then we'll open the routes/store.rb file and move the store routing branch into it.

class App
  hash_branch "store" do |r|
    # /store routes
  end
end

Finally, we'll modify our app.rb file to use the hash_branches plugin. We'll also need to require the files containing the routes. We'll use a string that will also include subdirectories of the routes directory, even though we are not using subdirectories yet. Then we can remove the separate routing branches, and replace them with a single call to r.hash_branches.

class App < Roda
  plugin 

  Dir["routes/**/*.rb"].each do |route_file|
    require_relative route_file
  end

  route do |r|
    r.hash_branches
  end
end

With this routing tree, all requests that come in will call r.hash_branches. r.hash_branches will look at the first segment in the request. If the request starts with /tasks, it will call the hash_branch "tasks" block, which we specified in the routes/tasks.rb file. If the request starts with /posts, it will call the hash_branch "posts" block. If the request starts with /store, it will call the hash_branch "store" block. If the request starts with something else, it won't call any block and will return nil, resulting in the expected 404 response.

The reason the plugin is named hash_branches is that the per-branch route blocks that we specify with the hash_branch method are stored in a hash table. When the request comes in, the r.hash_branches call can take the first segment in the remaining path, look for a matching entry in the hash table, and then dispatch to the value of the entry if the entry exists. In addition to allowing for better code organization, this approach is also much faster if we have a large number of routing branches.

hash_branch namespaces

Let's say our application keeps growing, and now our store is very popular. The routes/store.rb file is now getting as large as the app.rb file was before we split it up:

class App
  hash_branch "store" do |r|
    r.on "items" do
      # routes for viewing items
    end

    r.on "cart" do
      # routes for managing shopping cart
    end

    r.on "checkout" do
      # routes for checking out
    end
  end
end

Thankfully, we can use the same approach for splitting the routing branch for store as we used originally for splitting up the root routing tree. We add a routes/store directory and create separate files for the three routing branches under the store branch.

Dir.mkdir("routes/store")
File.write("routes/store/items.rb", "")
File.write("routes/store/cart.rb", "")
File.write("routes/store/checkout.rb", "")

Then we can move the routes for viewing items into routes/store/items.rb. We again call hash_branch, but this time we provide an additional first argument, the "/store" namespace. The reason we are using this namespace is that it is the already routed path at this point in the routing tree. This is the recommended approach if the already routed path does not contain any placeholders. If the already routed path contains placeholders, the recommendation is to use a symbol as the namespace.

class App
  hash_branch("/store", "items") do |r|
    # routes for viewing items
  end
end

We can move the routes for managing the shopping cart into routes/store/cart.rb.

class App
  hash_branch("/store", "cart") do |r|
    # routes for managing shopping cart
  end
end

We can move the routes for checking out into routes/store/checkout.rb.

class App
  hash_branch("/store", "checkout") do |r|
    # routes for checking out
  end
end

Then we can update the routing branch in the routes/store.rb file to use r.hash_branches. We don't provide any arguments to r.hash_branches, but it still correctly delegates to the three routing branches for in the "/store" namespace. The reason for this is that r.hash_branches takes an optional argument, the namespace in which to look for routes. The default value for the argument is the matched path (r.matched_path). When r.hash_branches is called in the app.rb file, the matched path is the empty string "", which is the default namespace that hash_branches uses if we do not provide an argument. That's why it dispatches to the hash_branch "store" block. When r.hash_branches is called inside the hash_branch "store" block, the matched path is "/store", which is why the call the dispatches to the one of the routing branches in the "/store" namespace.

class App
  hash_branch "store" do |r|
    r.hash_branches
  end
end

With the hash_branches plugin, we can split the root routing tree or any routing branch into separate routing branches, which can be stored in separate files. This allows for easy route organization on sites with hundreds or thousands of routes, with very little routing overhead.

hash_branches with placeholders

In addition to the store routing branch getting large, the tasks routing branch is now also getting large. However, the tasks routing branch is a little different from the store routing branch, in that most of the routes occur after finding the related task. Here is what the tasks routing branch looks like.

class App
  hash_branch "tasks" do |r|
    r.get true do
      # page showing all tasks
    end

    r.on Integer do |id|
      next unless @task = Task[id]

      r.is do
        r.get do 
          # page for editing task
        end

        r.post do
          # action for updating task information
        end
      end

      r.on "dependencies" do
        # routes for managing task dependencies
      end

      r.on "related" do
        # routes for managing related tasks
      end
    end
  end
end

Let's say the sub-branches for dependencies and related are large and we want to move them into their own routing files. First, we'll prepare the directory structure and create the empty files.

Dir.mkdir("routes/tasks")
File.write("routes/tasks/dependencies.rb", "")
File.write("routes/tasks/related.rb", "")

We'll first edit the routes/tasks/dependencies.rb file. This will look somewhat different from the previous routing branch files. We'll still call hash_branch, but we can't use the already routed path as the namespace. The already routed path could be /tasks/1, /tasks/24601, or similar. However, because it is not static, it is not possible to use it as a namespace. In this case, it is recommended to pick an appropriate symbol as the namespace, in this case :task.

We can then move the routes for task dependencies into routes/tasks/dependencies.rb.

class App
  hash_branch(, "dependencies") do |r|
    # routes for managing task dependencies
  end
end

Likewise, we can then move the routes for related tasks into routes/tasks/related.rb.

class App
  hash_branch(, "related") do |r|
    # routes for managing related tasks
  end
end

Finally, we can edit the routes/tasks.rb file to use these new routing branches, by replacing them with a call to r.hash_branches(:task).

class App
  hash_branch "tasks" do |r|
    r.get true do
      # page showing all tasks
    end

    r.on Integer do |id|
      next unless @task = Task[id]

      r.is do
        r.get do 
          # page for editing task
        end

        r.post do
          # action for updating task information
        end
      end

      r.hash_branches()
    end
  end
end

type_routing

It's easiest from a maintenance perspective if our web application only has to return one type of data. For example, we may have a standard web application, where response bodies are HTML, or we may have a JSON API, where response bodies are JSON. However, in some cases, web applications need to vary the type of information they are returning per request. For example, we can use the same application to serve HTML response bodies to clients requesting HTML, and JSON response bodies to clients who request JSON.

We can handle multiple response types with core Roda, though it may not be as pretty as we would like it to be. We can use a regular expression, and make the file extension optional. We can then use a case statement based on the file type requested. If the .html extension is used, or no extension is given, we can return an HTML response body using the render plugin. If the .json extension is used, we can return an array of hashes, which the json plugin will convert into a JSON response body.

require "roda"

class App < Roda
  plugin , true
  plugin 

  route do |r|
    r.get /tasks(.html|.json)?/ do |type|
      @tasks = Task.all

      case type
      when nil, '.html'
        view("tasks")
      when '.json'
        @tasks.map do |task|
          {task.id, task.title}
        end
      end
    end
  end
end

That approach works just fine, and for a single route is probably the best approach. However, if we need to do this for many routes, using regexps and case statements for every route where we want to vary the response body type is a lot of extra work. It would be better if there were a simpler way.

Thankfully, there is a simpler way. Roda includes a type_routing plugin that offers a nicer way to do this. We don't need to use regexps to handle the file extension, as the type_routing plugin makes routing ignore the extension. So we can use a simpler string matcher instead of a regexp matcher. The type_routing plugin adds r.html and r.json match methods. r.html will only yield to the block if the request uses an .html extension, or if the request does not use an extension. r.json will only yield to the block if the request uses a .json extension.

require "roda"

class App < Roda
  plugin , true
  plugin 
  plugin 

  route do |r|
    r.get "tasks" do |type|
      @tasks = Task.all

      r.html do
        view("tasks")
      end

      r.json do
        @tasks.map do |task|
          {task.id, task.title}
        end
      end
    end
  end
end

By default, if no extension is given, the type_routing plugin will analyze the request's Accept header, and will choose the first type in the Accept header that the plugin knows it is able to handle. If the Accept header is not present or does not contain any types that the type_routing plugin knows it is able to handle, then the type_routing plugin will assume the requested format is HTML.

While only r.html and r.json are shown in the above example, the type_routing plugin also supports r.xml for XML format by default. It also supports the use of user-specified formats. Additionally, we can configure it to determine the requested type using only the extension (and not the Accept header), or using only the Accept header (and not the extension). See the plugin documentation for more details.

not_found

By default, when we don't handle a route in Roda, Roda will return an empty 404 response. In some browsers, that results in a blank white page, though most popular browsers handle it by showing a browser-specific 404 page.

In the typical case, we want to customize what the 404 page looks like, and for that Roda offers a not_found plugin. We pass this plugin a match block, and for empty 404 responses, such as when we do not handle a route in the routing tree, the block is called. As with any match block, the return value of the block is used as the response body. In this example, we want all responses that are not handled to use the application's normal layout, and render the views/404.erb file.

require "roda"

class App < Roda
  plugin , true

  plugin  do
    view('404')
  end
end

The example above omits the route block. That is allowed with Roda, it is the same as having an empty route block, resulting in every request returning a 404 response.

status_handler

The not_found plugin is actually just a specific use of the status_handler plugin. The status_handler plugin allows us to define match blocks for specific status codes, and an empty response with that status code results in calling the block. The not_found plugin just sets up a handler for the 404 status code.

For example, let's say we want to show the same page for all responses where access has been forbidden. This uses the 403 status code. We can use the status_handler plugin and set up a handler for the 403 code. As in the not_found plugin example, we can render the views/403.erb file to handle all 403 responses. In the routing tree, we can check the IP address of the request, and if it doesn't include 127.0.0.1, we can set the status to 403, then use next to skip further processing (we could also use r.halt).

require "roda"

class App < Roda
  plugin , true

  plugin 

  status_handler(403) do
    view('403')
  end

  route do |r|
    unless r.ip =~ /127.0.0.1/
      response.status = 403
      next
    end

    # rest of app
  end
end

empty_root

Say we have an application with a route that matches exactly /is, and returns a trivial string.

require "roda"

class App < Roda
  route do |r|
    r.is "is" do
      "IS"
    end
  end
end

Let's try this out. We go to http://localhost:9292/is and everything works as expected. However, if we add a slash at the end, we get a 404 status.

require "lucid_http"

GET "/is"                       # => "IS"
GET "/is/"                      # => "ERROR: 404 Not Found"

What if we use r.on instead of r.is?

class App < Roda
  route do |r|
    r.s "is" do
      "IS"
    end

    r.on "on" do
      "ON"
    end
  end
end

Well, that just works. Even with the trailing slash, or actually any trailing information in the path.

require "lucid_http"

GET "/is"                       # => "IS"
GET "/is/"                      # => "STATUS: 404 Not Found"
GET "/is/anything"              # => "STATUS: 404 Not Found"

GET "/on"                       # => "ON"
GET "/on/"                      # => "ON"
GET "/on/anything"              # => "ON"

It's generally a bad idea to handle arbitrary paths as references the same path. In general, it's also a bad idea to treat /on and /on/ as the same path. They are separate paths and should not be treated as the same path. One issue with treating them as the same path is that relative paths work differently in such a resource, as /on uses a relative base of / and /on/ uses a relative base of /on/.

However, if we do want to treat these routes the same, we would need to use separate route blocks for them, one where the remaining path is empty (r.get true), and one where the remaining path is / (r.root).

class App < Roda
  route do |r|
    r.is "is" do
      "IS"
    end

    r.on "on" do
      r.get true do
        "ON ROOT"
      end

      r.root do
        "ON ROOT"
      end
    end
  end
end

This duplicates the code to the route, which is also a bad idea. One way we can eliminate the duplication is a single route that matches both paths.

class App < Roda
  route do |r|
    r.is "is" do
      "IS"
    end

    r.on "on" do
      r.get ["", true] do
        "ON ROOT"
      end
    end
  end
end

The above works, but looks a little strange. It first tests if the next segment in the path is /. If so, it consumes it. If not, it goes to the next matcher, true, which always matches and doesn't consume. In both the empty remaining path case and the / remaining path case, the remaining path is fully consumed after the matchers have been processed.

The empty_root plugin allows us to simplify the r.get ["", true] call, by making r.root match both the / remaining path and the empty remaining path.

class App < Roda
  plugin 

  route do |r|
    r.is "is" do
      "IS"
    end

    r.on "on" do
      r.root do
        "ON ROOT"
      end
    end
  end
end

Remember that it's probably a antipattern to use empty_root. It should only be used if we want to treat two distinct paths (trailing slash and no trailing slash) as the same path.

slash_path_empty

The slash_path_empty plugin is very similar to the empty_root plugin, and a bad idea to use for the same reason. The difference between the two plugins is that empty_root only affects the r.root method, allowing empty paths in addition to the / path, while slash_path_empty treats a remaining_path of / as empty after processing the matchers in the r.is, r.get, and r.post match methods.

So another way of handling the previous example is to switch from empty_root to slash_path_empty, and then use r.get "on". The slash_path_empty plugin will then make r.get "on" match requests for both /on and /on/.

class App < Roda
  plugin 

  route do |r|
    r.is "is" do
      "IS"
    end

    r.get "on" do
      "ON ROOT"
    end
  end
end

Security

Roda has a strong focus on security. However, it doesn't enable all security features by default because security features necessary in some applications, such as CSRF for a site using HTML forms, don't make sense in other applications such as JSON APIs. Just like most of Roda's other features, most of Roda's security features are implemented via plugins.

In the section on rendering, we discussed the :escape option of the render plugin, for preventing cross site scripting. In this section, we'll look at the most important security-related plugins that Roda includes.

route_csrf

We have Backpack application with a form that allows us to add items to an imaginary backpack.

Today we're preparing our backpack for a plane trip to Gotham. We pack the plane tickets, a book for reading, our cellphone, and our wallet.

While we are doing this, someone manages to sneak in a knife in our backpack using just a simple ruby script.

require "http"

response = HTTP.post "http://localhost:9292/add",
  {"KNIFE"}

response
# => #<HTTP::Response/1.1 302 Found {"Location"=>"/form", "Content-Type"=>"text/html", "Content-Length"=>"0"}>

Then we add our headphones. However, when the page reloads, we find out that an item that we never put into our backpack, that pesky knife, that will trigger all the alarms on the airport.

This kind of sneaky request represents a great safety threat for our app. This security issue is called Cross Site Request Forgery or CSRF for short. Roda has a plugin that prevents this issue, called the route_csrf plugin. To prevent CSRF issues, we load the route_csrf plugin into our application, then have our routing tree call check_csrf!. If the request is a POST request, the check_csrf! call will check that the request was the submission of a form that the site generated.

CSRF protection requires the support for sessions, so we'll load the sessions plugin for that (the sessions plugin will be discussed in the later section). In general, if we are not using sessions, we don't need to worry about CSRF.

class App < Roda
  plugin , true
  plugin , "some_long_secret"
  plugin 

  route do |r|
    check_csrf!

    # Rest of application
  end
end

Now, how does this prevent an attacker to add a knife to our backpack? Well, once we add the route_csrf plugin and make sure that the routing tree calls check_csrf!, Roda will check this token's presence for every POST request. If a POST request is received that doesn't include the token, or the token isn't valid, the check_csrf! call will raise an exception (by default).

So if the attacker submits the same request to add an item, he will now get Roda::RodaPlugins::RouteCsrf::InvalidToken exception, meaning that the CSRF token didn't match the expected value.

require "http"

response = HTTP.post "http://localhost:9292/add",
  { "KNIFE" }

response.body.to_s
# => "Roda::RodaPlugins::RouteCsrf::InvalidToken: " \
#    "encoded token is not a string"
#    ...

If we try to add an item to the backpack, we get the same error in our browser. This is because the check_csrf! method cannot verify that the request was a submission of one of the site's forms.

In order to make sure that the check_csrf! call knows that the request was a submission of one of the site's forms, we need to make sure that all forms on the site submit a parameter that identifies the request as valid. This parameter is called the CSRF token. We can generate the HTML tag for this parameter using the csrf_tag method. We pass this method the path that the form will submit to. The csrf_tag method will return the HTML to use for a hidden input tag, so we need to make sure to use the double equals when escaping (assuming we are using the render plugin :escape option as shown above).

<form action="/add" method="post">
  <%== csrf_tag('/add') %>
  <div class="input-group">
    <input type="text" name="item" class="form-input"/>
    <input type="submit" value="Add item!"
      class="btn btn-primary input-group-btn"/>
  </div>
</form>

If we take a look at the page source, we see the generated hidden field.

require "lucid_http"

GET "/form"
puts body

# >> <html>
# >>   <head>
# >>     ...
# >>   </head>
# >>   <body>
# >>   ...
# >>   <form action="/add" method="post">
# >>     <input type="hidden" name="_csrf"
# >>       value="aDaSZ18CNWqpIZd1...KBxn2bWmIj8rni0" />
# >>     <div class="input-group">
# >>       <input type="text" name="item" class="form-input"/>
# >>       <input type="submit" value="Add item!"
# >>         class="btn btn-primary input-group-btn"/>
# >>     </div>
# >>   </form>
# >>
# >> <h1> Backpack contents:</h1>
# >> ...

Now our application is protected against Cross Site Request Forgery!

typecast_params

One of the most common security issues in Ruby web applications stem from unexpected parameter types being submitted by an attacker. For example, let's say we are trying to implement a search feature in our application. We can use r.params['field'] to get the value of the submitted parameter named field, and pass that to our search method.

class App < Roda
  plugin , true

  route do |r|
    r.get "task", "search" do
      field = r.params['field']
      next unless field

      @tasks = Task.where(field).all
      view('tasks')
    end
  end
end

However, before doing that, we should probably consider what the type could be returned by r.params['field']. With Rack's default parameter parsing, it could be nil if no value was submitted, or a string if the value was submitted. The above code handles both of those cases. However, it could also be an array or a hash, which we probably did not expect. In most cases, we know what type of value we expect to receive, and it is safest if we only handle that type.

The typecast_params plugin allows for making sure that submitted parameters are of the expected type, or handling conversions to the expected type. So if we expect the field parameter to be submitted as a string, we can use the typecast_params plugin to enforce that. Using typecast_params.str('field'), we ensure that if the field parameter is submitted as an array or a hash, that an error is raised.

class App < Roda
  plugin , true
  plugin 

  route do |r|
    r.get "task", "search" do
      field = typecast_params.str('field')
      next unless field

      @tasks = Task.where(field).all
      view('tasks')
    end
  end
end

One of the problems with the above examples is if the field parameter is submitted but it is the empty string. As the empty string is treated as true and not false in Ruby, this will result in searching for the empty string. That may be what we want, but it may not be. As handling the empty string differently than a nonempty string is a fairly common case, typecast_params has a shortcut, nonempty_str. This will return nil if the parameter is submitted but it is empty, treating an empty parameter the same as not submitting a parameter at all.

class App < Roda
  plugin , true
  plugin 

  route do |r|
    r.get "task", "search" do
      field = typecast_params.nonempty_str('field')
      next unless field

      @tasks = Task.where(field).all
      view('tasks')
    end
  end
end

If we have a lot of fields, manually checking each parameter value to see if it is nil is tedious. Assuming we are using the HTML required attribute on our inputs, browsers shouldn't be submitting empty parameters. The code is much easier to read and write if we assume things will be submitted correctly, and raise an exception if they aren't. The typecast_params plugin allows this style by having a ! counterpart for all conversion methods. The ! method will raise an exception instead of returning nil.

We can switch to using nonempty_str!, and remove the next unless field call. If a empty field is submitted, the code will raise an exception, which we can handle using an exception handler (we'll discuss using an exception handler in a later section).

class App < Roda
  plugin , true
  plugin 

  route do |r|
    r.get "task", "search" do
      field = typecast_params.nonempty_str!('field')

      @tasks = Task.where(field).all
      view('tasks')
    end
  end
end

Integer parameters

In many cases, we expect submitted parameters to represent integers. However, by default, Rack's parameter parsing only supports string, array, and hash parameters, it does not support integer parameters. So parsing parameters as integers can be a manual process. If we expect the field parameter in the above example to be an integer, then we might manually convert the parameter to an integer using to_i. Note that nil.to_i and "".to_i are both 0, and we probably only care about positive integers, so we'll assume the field isn't submitted unless to_i returns a positive integer.

class App < Roda
  plugin , true

  route do |r|
    r.get "task", "search" do
      field = r.params['field'].to_i
      next unless field > 0

      @tasks = Task.where(field).all
      view('tasks')
    end
  end
end

This approach has a couple of issues. It's a little verbose, but more importantly it also results in a NoMethodError if field is submitted as an array or a hash. As this is also a common need in web applications, typecast_params has a shortcut to handle this case, the pos_int method. Using it, we can clean up this code.

class App < Roda
  plugin , true
  plugin 

  route do |r|
    r.get "task", "search" do
      field = typecast_params.pos_int('field')
      next unless field

      @tasks = Task.where(field).all
      view('tasks')
    end
  end
end

Nested parameters

HTTP query string parameters do not support nesting, just string keys and values, so why do we have to worry about array and hash parameters? This is because Rack will automatically parse certain key values specially, turning them into nested parameters. For example, Rack will take an HTTP query string such as:

?task[id]=1&task[title]=Study

and turn it into the following parameter hash:

{
  "task" => {
    "id" => 1,
    "title" => "Study"
  }
}

Let's say we had a form that used nested parameters, and we were handling them using r.params. We may do something like:

class App < Roda
  plugin , true

  route do |r|
    r.is "tasks" do
      r.get do
        view "tasks"
      end

      r.post do
        title = r.params['task']['title']
        Task.create(title)
        r.redirect 
      end
    end
  end
end

In addition to having the same issues discussed previous, that r.params['task']['title'] could be nil, or a string, hash, or array, this also can raise a NoMethodError, because it assumes that r.params['task'] is a hash, when it could be nil or a string or array. typecast_params can also handle this issue. If we expect the task parameter to be a hash and the task['title'] parameter to be a nonempty str, we can use a similar approach as shown previously.

We use typecast_params['task'], which will return an object similar to the object that typecast_params returns, but scoped to the nested parameter task. We then call the nonempty_str! method on that object to get the value for the task['title'] parameter, or raise an error if there was no parameter submitted, the parameter is not in the correct format, or the parameter is empty.

class App < Roda
  plugin , true

  route do |r|
    r.is "tasks" do
      r.get do
        view "tasks"
      end

      r.post do
        title = typecast_params['task'].
          nonempty_str!('title'])
        Task.create(title)
        r.redirect 
      end
    end
  end
end

typecast_params has many other features for handling submitted parameters, such as handling arrays, dates, boolean values, and uploaded files. We can also easily add support for application-specific types. It can also transform submitted parameters into a hash with typecasted values, suitable for passing to internal methods that expect the correct type to be passed. Please see the typecast_params plugin documentation for more details.

default_headers

Roda has a default_headers plugin that changes the response headers that are added by default. By default, the only header added is the Content-Type header, which is set to text/html. However, there are some security features we can enable in browsers by specifying headers. So if we are writing an application that will be used by browsers (as opposed to an API), we may want to specify these headers.

The most important of these headers is Strict-Transport-Security. We should set this header if we would like our application to only be reachable on secure (https://) connections and not on insecure (http://) connections. There are a few other headers such as X-Content-Type-Options, X-Frame-Options, and X-XSS-Protection that we may want to enable, but they mostly help older browsers.

Note that the default_headers plugin overrides Roda's default headers, and as such we should specify the Content-Type header when using it. This plugin takes a hash of headers to use as the default headers.

class App < Roda
  plugin ,
    'Content-Type'=>'text/html',
    'Strict-Transport-Security'=>'max-age=16070400;',
    'X-Content-Type-Options'=>'nosniff',
    'X-Frame-Options'=>'deny',
    'X-XSS-Protection'=>'1; mode=block'
end

content_security_policy

There is one other security-related header that we should probably use in our applications (if the application is used by browsers), called Content-Security-Policy. However, this header is complex to setup and is much more likely to vary per request, so Roda offers a content_security_policy plugin to handle it. The Content-Security-Policy header helps prevent attacks in browsers, such as cross site scripting and click-jacking. We can use it to specify what servers are allowed to serve specific types of content on the requested page, where forms on the page can submit, if the page is allowed to appear inside a frame, and other features.

Basic configuration of the content_security_policy plugin happens by passing a block when loading the plugin. The block is yielded a content security policy object, and we can call methods on that object to configure the default policy. By convention, the block argument is named csp. The recommended way to use the plugin is to use csp.default_src :none at the top of the block, which specifies that by default, nothing is allowed. Everything that is needed is then specified separately after that, such as style_src for stylesheets, script_src for javascript, and img_src for images. Here's an example of a restrictive default for Content-Security-Policy.

class App < Roda
  plugin  do |csp|
    csp.default_src  # deny everything by default
    csp.style_src 
    csp.script_src 
    csp.connect_src 
    csp.img_src 
    csp.font_src 
    csp.form_action 
    csp.base_uri 
    csp.frame_ancestors 
    csp.block_all_mixed_content
  end
end

This restrictive policy may work for some pages. However, maybe there is a section of the site that needs to use some external javascript and wants to disallow form submissions. The content_security_policy plugin allows for specifying changes to the policy on a per-route or per-branch basis. We can get access to the content security policy object in the routing tree by calling content_security_policy, and call methods on that to either replace existing settings or to append to existing settings.

r.on "special-section" do
  content_security_policy.add_script_src \
    "https://external-javascript-site.com"
  content_security_policy.form_action 

  # rest of branch
end

We can also use the block configuration form in routing branches, which is likely to be preferable if we are changing more than one setting.

r.on "special-section" do
  content_security_policy do |csp|
    csp.add_script_src "https://external-javascript-site.com"
    csp.form_action 
  end

  # rest of branch
end

Handling Email

Hopefully this book has shown how using a routing tree for handling web requests makes the request handling code simpler and more understandable, in addition to adding flexibility. It turns out that we can use a similar approach for handling non-web requests, and get much of the same benefits. One of the more common needs in web applications is dealing with email, and Roda includes plugins both for sending email and processing received email.

mailer

Roda's mailer plugin allows us to process requests to send email, similar to how core Roda processes web requests. In most cases, we would use a separate Roda subclass to handle requests to send email, since email requests would be generated internally, they wouldn't come from external sources.

Let's assume we wanted to add emailing to our tasks application, so after the task is updated, we email the user who created the task. We also want to email the user who created the task when the task is marked finished. We'll add a Roda subclass called App::Mailer that uses the mailer plugin, and it will handle the email sending.

Be aware that the mailer plugin requires the mail gem, so we need to add that to our Gemfile and then run bundle install.

source "https://rubygems.org"

gem "roda"
gem "puma"
gem "tilt"
gem "erubi"
gem "mail"

We'll store the App::Mailer code in mailer.rb.

class App::Mailer < Roda
  plugin 

  route do |r|
    r.on "tasks", Integer do |id|
      no_mail! unless @task = Task[id]

      from "tasks@example.com"
      to task.user.email

      r.mail "updated" do
        subject "Task ##{id} Updated"
        "Task #{task.name} has been updated!"
      end

      r.mail "finished" do
        subject "Task ##{id} Finished"
        "Task #{task.name} has been finished!"
      end
    end
  end
end

Note how the above routing tree for handling requests to send mail is very similar to the routing tree for handling web requests. The same r.on match method is used for handling branches in the mail routing tree, and the "tasks", and Integer matchers work the same way. The r.mail match method is very similar to the r.get match method, in that it requires the remaining path be fully consumed. Match blocks work similarly, with the return value of the match block being used as the body of the email to send.

Since the mail routing tree should only be called internally, it is expected that all requests to send mail will be successful. If we don't want to send an email, we need to call no_mail!. If we don't call no_mail! and the routing finishes without using a body for the email, then an exception will be raised. This is why the above example uses no_mail! instead of next if it cannot find the related Task.

The from, to, and subject methods set the From, To, and Subject headers for the email to send. As the above example shows, we can call these methods at any level of the routing tree. In the above example, the same From and To addresses are used for all emails for a specific task. So the from and to methods are called in the r.on "tasks", Integer branch. The subjects for the task updated and task finished emails differ, so subject is called separately in each of those routes. Both r.mail blocks return the body for the email.

One of the advantages of Roda's mailer plugin is that we can use most of Roda's other plugins with it. In most cases, we are not going to want to specify email bodies in the routing tree, as they are often quite large. Just as for web request bodies, we are going to want to use a separate method to create them, and we can use the same plugin in both cases, the render plugin. We can also use a plugin like symbol_views to DRY up the rendering code, just like for a routing tree that handles web requests.

So we could move the task updated mail body template to mailer_views/task_updated.erb and the task finished mail body template to mailer_views/task_finished.erb, then update our mail routing tree to use those templates.

class App::Mailer < Roda
  plugin , 'mailer_views', nil
  plugin 
  plugin 

  route do |r|
    r.on "tasks", Integer do |id|
      no_mail! unless @task = Task[id]

      from "tasks@example.com"
      to task.user.email

      r.mail "updated" do
        subject "Task ##{id} Updated"
        
      end

      r.mail "finished" do
        subject "Task ##{id} Finished"
        
      end
    end
  end
end

Now that we have a mailer that will handle routes to send mail, how do we actually use it? In general, we can just require the mailer file in our app that handles web requests, and use the sendmail method to send the email. Let's look at an example web routing tree that handles this. It's basically the same as a standard Roda routing tree, except for the Mailer.sendmail calls in the cases where we would like an email sent.

class App < Roda
  require_relative 'mailer'

  plugin 
  plugin , true
  plugin 

  route do |r|
    r.on "tasks", Integer do |id|
      next unless @task = Task[id]

      r.is do
        r.get do
          
        end

        r.post do
          status = typecast_params.nonempty_str('status')
          @task.update(status)
          Mailer.sendmail("/tasks/#{id}/updated")
          r.redirect
        end
      end

      r.post "close" do
        @task.update(false)
        Mailer.sendmail("/tasks/#{id}/finished")
        r.redirect "/tasks/#{id}"
      end
    end
  end
end

In this example, we are providing explicit paths when sending mail. However, if our mail paths are the same as our web paths, we can use Mailer.sendmail(r.path_info), which can be simpler than constructing the path ourselves.

Using a path to send mail allows the Mailer object to handle the routing using a tree, with all of advantages this book has discussed. One other advantage about using a path to send mail is that it makes it easy to introduce a job system that will send the email later. This has advantages at scale, so that the response to the web request does not have to wait for the email to be sent. Using a single path string to represent the email to send ensures that there are no serialization issues when switching to use a job system.

In addition to the from, to, and subject methods, the mailer plugin also has cc and bcc methods for setting the CC and BCC recipients. It also supports adding attachments to emails, using the add_file method. The mailer plugin also supports multipart emails, allowing us to send both a plain text part and an HTML part via the text_part and html_part methods. For cases where we need to pass objects from the web request handler to the mailer that we cannot serialize into a string, we can provide additional arguments to the sendmail call, which results in the related r.mail block yielding the additional arguments. See the mailer plugin documentation for information on these features.

mail_processor

The mailer plugin handles sending email, but what if we want to handle receiving email and taking actions based on received email? Thankfully, Roda has a plugin for that, called mail_processor. Like core Roda and the mailer plugin, this plugin also uses the Roda routing tree to handle mail, but because received mail doesn't have a path to operate on, routing it and handling it are a bit different, and use a different set of methods.

With the mail_processor plugin, we have routing methods on the request instance, just like in core Roda. However, the routing methods are named based on the parts of the email, so the most common ones are r.from, r.rcpt, r.subject, and r.text. As mail processing need not return a result, the mail_processor plugin uses an explicit approach for marking a message as being handled, using the r.handle method. As a shortcut, all of the routing method have a handle equivalent that combines handling and matching in the same method call (e.g. r.handle_subject).

Let's look at a simple example routing tree for handling a single type of email. We'll assume we are working on the same application as in the previous example. In the email to the user that a task is updated, we inform them that they can close out the task by replying to the email and including CLOSE in their reply.

In the emails we sent out, we used tasks@example.com as the From address when sending, so replies should be sent to that address. So we have our mail processor only handle that email address. We'll also look for a matching subject, which we'll use to find the ID of the related task. We'll need to check that the task is active. If the task is active, and the reply includes the word CLOSE, we'll close the task.

class MailProcessor < Roda
  plugin 

  route do |r|
    r.rcpt "tasks@example.com" do
      r.subject /Task #(\d+) Updated/ do |task_id|
        unhandled_mail("no matching task") unless task = Task[task_id.to_i]
        unhandled_mail("task is not active") unless task.active?

        r.handle_text /\bCLOSE\b/i do
          task.update(false, from)
        end
      end
    end
  end
end

Getting mail for processing

There are a large variety of ways to get inbound email for processing. However the inbound email is retrieved, the mail_processor plugin operates on Mail instances (these come from the mail gem). To process an email, we call the process_mail method on the application with the Mail instance. This calls the routing tree in order to handle the email.

Here's an example for processing an email that is stored in a file in the file system. Mail.read will read the file and create a Mail instance, and that instance will be passed to process_mail in order to handle the mail.

MailProcessor.process_mail(
  Mail.read('/path/to/message.eml')
)

It is possible to receieve email bodies in web requests. For example, if the mail message was submitted as a parameter in a web request, we could have a route in our web routing tree that calls process_mail.

r.post "handle-email" do
  MailProcessor.process_mail(
    Mail.new(typecast_params.nonempty_str('mail'))
  )
  response.status = 204
  ''
end

We can also setup the mail_processor plugin to process all mail in a POP3 or IMAP mailbox, using process_mailbox.

MailProcessor.process_mailbox(
  Mail::POP3.new
)

Large mail processing routing trees

For large mail processing routing trees, it's best to split them up by recipient address. The mail_processor plugin has built-in support similar to the hash_branches plugin for this, using the rcpt class method. In addition to allowing us to split up mail processing into separate files, this also allows for faster processing, assuming the recipient addresses are strings.

class MailProcessor < Roda
  plugin 

  rcpt "tasks@example.com" do |r|
    r.subject /Task #(\d+) Updated/ do |task_id|
      unhandled_mail("no matching task") unless task = Task[task_id.to_i]
      unhandled_mail("task is not active") unless task.active?

      r.handle_text /\bCLOSE\b/i do
        task.update(false, from)
      end
    end
  end
end

SMTP senders and recipients

One general issue with email is that it can be trivially forged. The mail message headers (e.g. From and To) do not have to reflect the email addresses of the sender and recipient used when transferring the email. In order to get this information, we need to have the mail server record the SMTP envelope sender (MAIL FROM) and recipient (RCPT TO) into a separate header in the email. The rcpt method that the mail_processor plugin uses defaults to looking at the To and CC headers, but if we want to change it to look in a header set by mail server, we can use the mail_recipients method to do so. If we have the mail server store the actual recipients of the email in the X-SMTP-To header, we can change the rcpt method to respect that header.

class MailProcessor < Roda
  plugin 

  mail_recipients do
    Array(header['X-SMTP-To'].decoded)
  end
end

Detecting spoofed email

As anyone can spoof both the mail message From, To, and CC headers, as well as the SMTP envelope sender and recipients, we cannot trust any received email was sent by the person it claims to be sent from. If we want to have reasonable assurance that the email received was a reply to an email the system sent, it is best to include an HMAC when sending the email, and check that a valid HMAC exists when receiving the email.

Let's modify App::Mailer to include an HMAC when sending the task updated email. We'll assume we are storing the secret for the HMAC in an environment variable named APP_EMAIL_HMAC_SECRET. In the mail route for the task updated email, we take the ID of the task and use this to construct a string containing an HMAC specific to the task. We'll set that string to an instance variable named @ref (short for reference), and then we'll modify the task_updated.erb template to include that instance variable.

require 'openssl'
require 'securerandom'

class App::Mailer < Roda
  HMAC_SECRET = ENV.fetch('APP_EMAIL_HMAC_SECRET')

  plugin , 'mailer_views', nil
  plugin 
  plugin 

  def make_ref(type, id)
    hmac_data = "#{type}:#{id}:#{SecureRandom.hex(16)}"
    hmac = OpenSSL::HMAC.hexdigest(
      OpenSSL::Digest::SHA256.new,
      HMAC_SECRET,
      hmac_data
    )
    "ref:#{hmac_data}:#{hmac}:ref"
  end

  route do |r|
    r.on "tasks", Integer do |id|
      no_mail! unless @task = Task[id]

      from "tasks@example.com"
      to task.user.email

      r.mail "updated" do
        @ref = make_ref('task', id)
        subject "Task ##{id} Updated"
        
      end

      r.mail "finished" do
        subject "Task ##{id} Finished"
        
      end
    end
  end
end

After doing that, we need to modify MailProcessor to check that the correct reference is provided. We need to access the APP_EMAIL_HMAC_SECRET environment variable to get the HMAC secret, which should have the same value in both processes. We need to look in the body of the email to find the reference. We check that the reference includes a valid HMAC, and that the task ID in the reference matches the task ID in the subject.

class MailProcessor < Roda
  HMAC_SECRET = ENV.fetch('APP_EMAIL_HMAC_SECRET')

  plugin 

  def check_hmac(data, hmac)
    OpenSSL::HMAC.hexdigest(
      OpenSSL::Digest::SHA256.new,
      HMAC_SECRET,
      data
    ) == hmac
  end

  rcpt "tasks@example.com" do |r|
    r.subject /Task #(\d+) Updated/ do |task_id|
      unhandled_mail("no matching task") unless task = Task[task_id.to_i]
      unhandled_mail("task is not active") unless task.active?

      regexp = /ref:(task:(\d+):\h+):(\h+):ref/
      r.body(regexp) do |hmac_data, ref_task_id, hmac|
        unhandled_mail("bad HMAC") unless check_hmac(hmac_data, hmac)

        unless ref_task_id.to_i == task_id.to_i
          unhandled_mail("task ID mismatch")
        end

        r.handle_text /\bCLOSE\b/i do
          task.update(false, from)
        end
      end
    end
  end
end

Note that this approach provides reasonable security if the email is sent to only one person. For the task updated email, this is the case as it is sent to the user related to the task. However, if sending an email to more than one recipient, we'll want to include the email address or recipient ID as part of the reference, so we can more determine who received the email that the reply was generated from. In some cases, we may want to check that the recipient ID matches the sender of the email (from in the above code).

Additional features

The mail_processing plugin offers support for hooks that are called for all handled mail, all unhandled mail, as well as all mail regardless of whether it was handled. It also offers support for customizing the extraction of reply text. See the mail_processor plugin documentation for more details on these features.

Other plugins

In this section, we'll discuss plugins that don't fall neatly into one of the other categories.

sessions

HTTP is a stateless protocol, but most applications want to store state between requests. The common way to persist this state between requests is to store the state in a browser cookie. Roda ships with a sessions plugin that allows for persisting state between requests using an encrypted browser cookie. Because it is encrypted, the browser cannot see what the cookie contains.

We load the sessions plugin similar to other plugins. However, unlike most plugins, the sessions plugin requires that we provide a :secret option when loading it. The :secret option must be at least 64 bytes, and in most cases it should be randomly generated. As it is a secret, it should never be exposed to the user. It's common to store the secret in an environment variable. To reduce the chance of the secret leaking accidentally, we'll delete it from the environment when loading the plugin.

class App < Roda
  plugin , ENV.delete('APP_SESSION_SECRET')

  # ...
end

If we start the app without setting the APP_SESSION_SECRET environment variable, or when the APP_SESSION_SECRET environment variable is too short, we'll notice that it doesn't work, resulting in an exception. This is to prevent us from using an insecure configuration.

$ rackup
Traceback (most recent call last):
        4: from config.ru:3:in `<main>'
        3: from config.ru:4:in `<class:App>'
        2: from /some/path/lib/roda.rb:361:in `plugin'
        1: from /some/path/lib/roda/plugins/sessions.rb:192:in `configure'
/some/path/lib/roda/plugins/sessions.rb:166:in `split_secret': sessions plugin :secret option must be a String (Roda::RodaError)

Let's see how this plugin works. We'll add a route in our app called /intro/<name>, which captures the name and stores it in the session. The session is Ruby hash, and when creating the cookie, it is first serialized to JSON before being encrypted. As JSON does not support symbols, we shouldn't use symbols for session keys or in session values.

We'll also add a /hello route to our application, showing the name stored in the session if there is one, or World if the name was not stored in the session.

class App < Roda
  plugin , ENV.delete('APP_SESSION_SECRET')

  route do |r|
    r.get "intro", String do |name|
      r.session["name"] = name
      "<h1>It's nice to meet you, #{name}!</h1>"
    end

    r.get "hello" do
      "<h1>Hello #{r.session["name"] || 'World'}!</h1>"
    end
  end
end

Now let's navigate to http://localhost:9292/intro/Federico.

We see that we've been introduced.

Next, we'll navigate to /hello. Even though we didn't pass in a name this time, we're greeted by the name we provided a moment ago.

From this we can see that our user session has been saved from one request to the next. If we use a browser inspection tool to examine the cookies set by this site, we can even see that a roda.session cookie has been set.

Using an environment variable for the session secret makes it easy to configure our application without setting up any configuration files. This is important when using certain hosting services, such as Heroku. On the other hand, when we're developing locally, it's tedious to have to set the environment variable every time we start up the app.

$ APP_SESSION_SECRET=some_64_or_more_byte_secret rackup

To avoid having to specify the environment variable on the command line every time we launch the application, we'll use a simple approach for installation-specific application configuration. We'll have our config.ru file load a .env.rb file it if exists.

require './.env' if File.exist?(".env.rb")
require "./app"

run App

If the .env.rb file doesn't exist, there is no change. If the .env.rb does exist, Ruby will load it. This approach is more flexible than similar approaches that only handle setting environment variables, as they allow for other custom settings, such as using custom Ruby load paths for testing.

We can create the .env.rb file and set the environment variable if it doesn't already exist.

ENV["APP_SESSION_SECRET"] ||= "some_64_or_more_byte_secret"

In order to create the session secret, we can use Ruby's SecureRandom library to generate random data for us.

ruby -rsecurerandom -e 'puts SecureRandom.base64(64).inspect'

That will output a Ruby string that we can set as the value of the APP_SESSION_SECRET environment variable in the .env.rb file.

Remember, this .env.rb file is just for local development use, and should not be committed to the project's source code repository. We'll want to set the APP_SESSION_SECRET environment variable using another method or at least use a different .env.rb file when deploying the application to production.

Session middleware

The sessions plugin only offers session capability to the Roda app itself, but it does not offer session support for middleware that the app uses. If we want middleware to also be able to use the sessions support, we'll need to load a session middleware. Roda ships with a middleware that we can use, that is based on the sessions plugin, named Roda::RodaSessionMiddleware. We load this middleware using use, just like other middleware, and it takes the same options that the session plugin accepts.

require 'roda/session_middleware'

class App < Roda
  use Roda::RodaSessionMiddleware, ENV.delete('APP_SESSION_SECRET')

  # ...
end

It is also possible to use external session middleware, or session middleware that ships with rack, such as Rack::Session::Cookie.

flash

One common need in web applications that use standard HTML forms is to show a message after a successful form submission. A typical flow is to accept a form submission via POST, and if the submission is successful, after processing the form, the web application will redirect to another page, and that page will show a message that the form was successfully processed.

This type of functionality, where we show messages on the next requested page in the same session, is often called flash, and Roda has a flash plugin that implements support for it. The flash plugin depends on support for sessions, so we'll use the sessions plugin for sessions support.

We'll demonstrate this with a simple application. This application will respond to a GET request with a body of Default if there is no flash message. However, for a POST request, it will redirect to the same path, and the next GET request will have a response body of Success.

class App < Roda
  plugin , ENV.delete('APP_SESSION_SECRET')
  plugin 

  route do |r|
    r.get do
      flash['a'] || 'Default'
    end

    r.post do
      flash['a'] = 'Success'
      r.redirect
    end
  end
end

error_handler

We try our best to make sure our applications don't raise exceptions at runtime, but it's nice to have a plan for how to handle such exceptions. By default, Roda does not rescue exceptions that are raised, so if our routing tree raises an exception, Roda will raise an exception, and behavior at that point is dependent on the webserver. Most webservers will show a generic 500 error page in that case.

Many sites would prefer that even if their application raises an exception, that the user still sees a nicely formatted webpage explaining that an error occurred. To meet that need, Roda has an error_handler plugin. The error_handler plugin allows us to provide a match block, and the block is called with the exception instance. Just like any match block, the error_handler block should return a value that is used as the body of the response.

It can be tempting to just render a template in the error_handler block. In this example, we have our routing tree render the views/index.erb template, and have our error_handler render the views/500.erb template if there is an error doing that.

class App < Roda
  plugin , true

  plugin  do |e|
    view('500')
  end

  route do |r|
    view('index')
  end
end

This will work in most cases. However, this approach has an problem if the cause of the exception is due to code in the layout. This is because the error_handler code will attempt to render the layout, which will cause the error_handler to raise an exception. Roda does not attempt to handle exceptions raised in the error_handler blocks, so an error in the layout would result in an error being raised to the webserver.

Since unhandled exceptions in our applications should be fairly rare (we hope), it's best to use the simplest possible code in the error_handler. We can have a public/500.html file for the error page, and just return the contents of that file in the error_handler. This approach is less likely to cause us problems in the future if there is an error in the layout, but can also result in the error page becoming out of date with the layout.

class App < Roda
  plugin , true

  plugin  do |e|
    File.read('public/500.html')
  end

  route do |r|
    view('index')
  end
end

path

One common need in web applications is constructing paths and URLs. Roda includes a path plugin to help with path and URL construction.

Static paths

The path plugin easily handles static paths. We call the path class method with the name of the path, and a string for the path, and it adds an instance method that ends in _path for the path. With this application, all requests will use /tasks as the response body.

class App < Roda
  plugin 

  path , "/tasks"

  route do |r|
    tasks_path
  end
end

Dynamic paths

Static paths are simple, but not that interesting. Instead of calling the path method with a string for the path, we can pass a block that returns the path. This block is called with any additional arguments the path method is called with. With this application, all requests will return /tasks/1 as the response body.

class App < Roda
  plugin 

  path  do |task|
    "/tasks/#{task.id}"
  end

  route do |r|
    tasks_path(Task[1])
  end
end

Class-Based paths

In the section above describing dynamic paths, there is some duplication, since the method name and the argument both indicate that we are trying to get the path for a task. Wouldn't it be great if we could avoid this duplication? Thankfully, we can. Instead of passing a symbol for the name of the path, we can pass a class. In the routing tree, we call the path method, and it uses the class of the first argument to determine the type of path, and calls the appropriate block. So we can change our application to use this approach, which will still have all requests return /tasks/1 as the response body.

class App < Roda
  plugin 

  path Task do |task|
    "/tasks/#{task.id}"
  end

  route do |r|
    path(Task[1])
  end
end

URLs

The path method will only create a method to return a path by default. If we want to also create a method that returns the full URL, we can use a :url option to create such a method. Assuming our application is running at http://example.com, with this application, all requests will return http://example.com/tasks/1 as the response body.

class App < Roda
  plugin 

  path , true do |task|
    "/tasks/#{task.id}"
  end

  route do |r|
    tasks_url
  end
end

It's also possible to use the :url_only option, which will not create a method for the path, and will only create a method for the full URL.

When using class-based paths, we can also get the full URL by using the url instance method instead of the path instance method. Assuming our application is running at http://example.com, this application will also have all requests will return http://example.com/tasks/1 as the response body.

class App < Roda
  plugin 

  path Task do |task|
    "/tasks/#{task.id}"
  end

  route do |r|
    url(Task[1])
  end
end

environments

With core Roda, the typical way to check or set which environment the application is operating in is to operate on at ENV["RACK_ENV"]. However, some people may want a simpler way to deal with environments, and for that, Roda offers an environments plugin. The environments plugin offers an environment method to return the environment as a symbol, as well as a setter method to modify the environment. It also offers development?, test?, and production? methods for checking for the most common environments. Finally, it offers a configure method, which can be called with any environment symbols, which will yield to the block if the application is running in one of those environments. Here's an example showing all of the features of the environments plugin.

class App < Roda
  plugin 

  environment  # => :development
  development? # => true
  test?        # => false
  production?  # => false

  # Set the environment for the application
  self.environment =  
  test?        # => true

  configure do
    # called, as no environments given
  end

  configure ,  do
    # not called, as no environments match
  end

  configure  do
    # called, as environment given matches current environment
  end
end

All of these methods are class methods, so if we want to check the environment inside the routing tree, we need to use self.class.environment.

Conventions

By design, Roda doesn't ship with a project generator. However, it still has conventions for how Roda applications should be structured in terms of filesystem layout. Some of these conventions are implemented as defaults, such as the use of the views directory in the render plugin, the assets directory in the assets plugin, and the public directory in the public plugin. Other conventions are just recommendations and do not have associated defaults, such as the use of the routes directory in the hash_branches plugin.

Roda's website has a Conventions page describing Roda's conventions, and it is recommended that Roda applications follow these conventions.

Conventions For Small apps

In this section, we will explore the conventions for working with small applications. Now, what does the term small application mean in this context? There's no mention about this on the Conventions page, but a reasonable guess would be an application with few routes, maybe a maximum of 10. In other words, an app that can be easily understood in a simple scan. Let's see what a small To-Do app skeleton looks like if it follows these conventions.

First, we have the Rakefile. It should contain all the application-related tasks, and the default task should run all of the application's tests.

If we are using the assets plugin, we put all of our CSS and javascript files in the assets directory.

We use the config.ru file to let the webserver know which application to run.

The db.rb file should contain minimal code to setup the database connection, without loading any models. This will be used in cases where access to the database is needed, but we don't want to load the models, such as when we are running migrations.

All code for ORM models should be stored in separate files in the models directory, with one file per model class. The models.rb file should load the db.rb file, then configure the model library, then load all model files in the models.

If we decide to use an ORM that uses migrations to modify the database schema, the migration files should be placed in the migrate directory, with a separate file per migration.

In the public directory, we place all of our static files that will be served directly to users.

All of our testing code should go either in test or spec directory, depending on the type of tests we are using.

In the a todo.rb file, our Roda application code goes. The convention is to call the file the same as the application. So, if our app is called Todo, the file name should be todo.rb.

All template files including the layout should go in the views directory.

So the root of our Roda application directory for the Todo application would look something like this.

|-- Rakefile
|-- assets/
|-- config.ru
|-- db.rb
|-- models.rb
|-- models/
|-- migrate/
|-- public/
|-- test/
|-- todo.rb
+-- views/

Now let's dig into the todo.rb file. Reading from the top, we first require roda. We want to load the models.rb file in the same directory, so we use require_relative for that. We then define the Todo class, which subclasses from Roda. Then we define any constants we need to have in our application.

Next, we add any Rack middleware that we'll use in our application. In this example, we'll use the Rack::ETag middleware. It is best to limit the middleware the application uses to the minimum (ideally none), as each Rack middleware the application uses has a performance cost.

Then we add all the plugins we'll use in our application. I usually group them by functionality if there are too many of them. We follow the plugins by adding the route block. For small apps, we'll only use a single route block with all the routes. After the route block, we add all of the instance methods to be used in our route block or inside the any views.

require 'roda'
require_relative 'models'

class Todo < Roda
  # Constants
  SECRET = ENV.delete('TODO_SECRET')

  # Middleware
  use Rack::ETag

  # Plugins
  plugin , true, './layout'
  plugin 

  plugin , SECRET

  route do |r|
    r.on "something" do
      # ...
    end

    # ...
  end

  def panel
    "<div class='panel'>#{yield}</div>"
  end
end

In the config.ru file, we need to load the todo.rb file. Unless we are running in development mode, we freeze the application to avoid any thread-safety issues at runtime, and to make sure the application's configuration is not changed while the application is running. Then we let the webserver know the Rack application to run, which we can get by calling Todo.app.

require "./todo"
Todo.freeze unless ENV["RACK_ENV"] == "development"
run Todo.app

By using a config.ru file, we can load the application by running the rackup command without any arguments.

Conventions For Larger apps

In this section, we'll take a look at the conventions proposed for larger applications. Once an application grows, maintaining the entire application's routing tree in a single file becomes difficult. At that point, it is recommended to split the routing tree into separate routing files, using the hash_branches plugin.

If we know in advance that our application will become large, we could create it with this structure in mind. However, that's not necessary, we can start small and only split things up if we need to do so by moving code around.

In general, the convention for a directory structure for a large Roda application is similar to a small Roda application, with the addition of some directories.

If we have a large number of instance methods that we are calling in the routing tree or in any views, we can move those into files in the helpers directory, organized by the type of method. In general, these files would just reopen the Todo class and define methods, though we could define the methods in Ruby modules that are included in the Todo class.

We'll add a routes directory for handling the routing trees for separate branches in the application, with one file per branch. For very large routing tree branches, we can use subdirectories in the routes directory to handle smaller branches of that branch. We'll add subdirectories to the views directory, one for each branch of the routing tree. We'll then use the hash_branch_view_subdir plugin, which uses the hash_branches plugin to support separate routing trees per branch, and the view_options plugin to set an appropriate view subdirectory for each routing branch.

The test or spec directory now should have separate subdirectories for testing model code and web related code. Each subdirectory should have separate files for testing separate parts of the application. In the test/models or spec/models directory, there should be one file per model with tests for that model. In the test/web or spec/web directory, there should be one file per routing tree branch for testing routes in that branch.

|-- Rakefile
|-- assets/
|-- config.ru
|-- db.rb
|-- helpers/
|-- migrate/
|-- models.rb
|-- models/
|-- public/
|-- routes/
|   --- admin.rb
|   +-- auth.rb
|-- test/
|   --- models/
|   +-- web/
|-- todo.rb
+-- views/
    --- admin/
    +-- auth/

Starter Roda App: roda-sequel-stack

Roda is a toolkit, not a framework, and there are many different ways Roda can be used. This can be overwhelming for new users, who aren't sure how a basic Roda app should be structured. If you are looking for the equivalent of an application generator for Roda that sets up the initial structure, you can use roda-sequel-stack. To use roda-sequel-stack as the base of your application, clone the repository and run rake:

git clone https://github.com/jeremyevans/roda-sequel-stack.git my_app
cd my_app
rake "setup[MyApp]"

This uses a directory structure that follows Roda's conventions for large applications, and supports:

roda-sequel-stack uses the Sequel library for database access, but that and most other parts of roda-sequel-stack can be easily changed to handle application-specific needs.

Appendix: The lucid_http gem

Recently, I was in the process of writing this book and a course about Roda. From the very beginning I needed a way to show the interactions between the user and the server. I didn't like the idea of relying too heavily in the browser. I really like the idea of showing as much as I can in plain text, without leaving Emacs or printing browser images on the book.

Instead of going out looking for a library or application to help me with this, I decided to just write a quick dirty snippet using the http.rb library I learned in episode 428 of RubyTapas.

Unfortunately, I didn't save the snippet, but it went something like this:

require "http"

res = HTTP.get("http://localhost:9292/hello")
res.body.to_s                   # => "<h1>Hello World!</h1>"
res.status.to_s                 # => "200 OK"

Which was too verbose for what I needed.

The HTTP.rb library is excellent for making HTTP requests. It's powerful and flexible enough for my needs. It's aimed at performing the interactions, instead of showing them in a digestible way.

So I started adding a couple of methods to help me clean the code and developed a small DSL that evolved into a new library I called lucid_http.

The Mastering Roda repository contains a appendix_lucid_http_app.ru file that serves as an example application for this appendix. We can run this example using rackup.

$ cd mastering-roda
$ rackup appendix_lucid_http_app.ru

This will start a webserver, and we'll have the example app up and running to experiment. Once we have finished playing, we can just hit Control+c on the terminal and it will stop running.

Now let's start exploring the lucid_http gem. Start by opening an irb shell in a separate process on the same system, and loading the lucid_http library.

$ irb -r lucid_http

Say we want to make a GET request to the hello path.

We call the GET method, and pass the required path as an argument.

require "lucid_http"
GET "/hello"
# => "<h1>Hello World!</h1>"

We can find out that the base URL is for requests calling the base_url method on the LucidHttp module.

LucidHttp.base_url
# => "http://localhost:9292"

We can change it by assigning the new URL. and from now on, every new interaction will be using the new target URL.

LucidHttp.base_url
# => "http://localhost:9292"

LucidHttp.base_url = "https://fiachetti.gitlab.io/mastering-roda/"

LucidHttp.base_url
# => "https://fiachetti.gitlab.io/mastering-roda/"

Or, if we don't want to set the base_url manually, we can just set the TARGET_URL environment variable.

ENV["LUCID_HTTP_BASE_URL"] = "https://fiachetti.gitlab.io/mastering-roda/"

LucidHttp.base_url
# => "http://localhost:9292"

The other thing to notice is that we're preceding the path with a forward slash. This is intentional. If we remove it, we'd get an error. By design, lucid_http takes the target URL as is. It doesn't modify it at all. By not prepending a slash, we're appending the hello string to the 9292 port, which causes the error.

That's because the GET method doesn't automatically follow redirects. Well, it doesn't unless we tell it to, by passing the follower: :follow argument.

GET "/over_there", 
status           # => "200 OK"
body             # => "You have arrived here due to a redirection."

Up to here, we've been dealing with "successful" requests. However, what happens when we it doesn't succeed? For example, say we have a route with some kind of error. As expected, we get the correct status code. However, when we render the body, we see a long ugly string with the whole backtrace. This is far from ideal for presentation purposes.

GET "/500"
status   # => "500 Internal Server Error"
body     # => "ArgumentError: wrong number of arguments (given 0, expected 2+)\n\t/home/..."

For this cases, lucid_http provides an error method that will only return the first line. This is easier on the eyes.

Quick disclaimer
The backtrace response received here is produced by Rack::ShowExceptions, which is included in rackup's default development inenvironment. In a production environment, the output if a Rack application raises an exception will depend on the web server in use.

Now, if the request doesn't return a 500 code, lucid_code will be nice enough to let us know.

GET "/not_500"
status                          # => "200 OK"
error                           # => "No error found"

Now say we have a couple URLs, one of which returns a HTML response and one of which returns a JSON response. By default, lucid_http does not handle the JSON response specially, it shows the raw JSON data.

GET "/hello_world"
# => "You said: hello_world"

GET "/hello_world.json"
# => "{\"content\":\"You said: hello_world\"" + \
#    ",\"keyword\":\"hello_world\"" + \
#    ",\"timestamp\":\"2016-12-31 15:00:42 -0300\"" + \
#    ",\"method\":\"GET\"" + \
#    ",\"status\":200}"

Now, that doesn't look easy on the eyes. Let's format it as a hash, by passing the formatter: :json argument. That's better.

GET "/hello_world.json", 
# => {"content"=>"You said: hello_world",
#     "keyword"=>"hello_world",
#     "timestamp"=>"2016-12-31 15:00:42 -0300",
#     "method"=>"GET",
#     "status"=>200}

lucid_http also support a number of other HTTP verbs we can use.

GET     "/verb"                  # => "<GET>"
POST    "/verb"                  # => "<POST>"
PUT     "/verb"                  # => "<PUT>"
PATCH   "/verb"                  # => "<PATCH>"
DELETE  "/verb"                  # => "<DELETE>"
OPTIONS "/verb"                  # => "<OPTIONS>"

Finally, say we want to send some information to the server via a POST request.

We can do it by filling out the query string. Oops, that looks like JSON.

POST "/receive?item=book"
# => "{\"item\":\"book\"}"

However, what if we want to send it as a form? In that case, we just need to add the form as an argument, passing a Hash as the value.

POST "/receive", , {
       "book",
       1,
       50.0,
       "The complete guide to doing absolutely nothing at all."
     }
# => {"item"=>"book",
#     "quantity"=>"1",
#     "price"=>"50.0",
#     "title"=>"The complete guide to doing absolutely nothing at all."}

That was lucid_http. It's not a full featured HTTP library, but it provides the basic functionality we may need when showing off a web app in text only mode.