Roda

Roda is a routing tree web framework.

Installation

$ gem install roda

Resources

Website

roda.jeremyevans.net

Source

github.com/jeremyevans/roda

Bugs

github.com/jeremyevans/roda/issues

Google Group

groups.google.com/group/ruby-roda

IRC

chat.freenode.net/#roda

Inspiration

Roda was inspired by Sinatra and Cuba, two other Ruby web frameworks. It started out as a fork of Cuba, from which it borrows the idea of using a routing tree (which Cuba in turn took from Rum). From Sinatra it takes the ideas that route blocks should return the request bodies and that routes should be canonical. It pilfers the idea for an extensible plugin system from the Ruby database library Sequel.

Usage

Here’s a simple application, showing how the routing tree works:

# cat config.ru
require "roda"

class App < Roda
  use Rack::Session::Cookie, :secret => ENV['SECRET']

  route do |r|
    # GET / request
    r.root do
      r.redirect "/hello"
    end

    # /hello branch
    r.on "hello" do

      # GET /hello/world request
      r.get "world" do
        "Hello world!"
      end

      # /hello request
      r.is do
        # GET /hello request
        r.get do
          "Hello!"
        end

        # POST /hello request
        r.post do
          puts "Someone said hello!"
          r.redirect
        end
      end
    end
  end
end

run App.app

Here’s a breakdown of what is going on in the above block:

After requiring the library and subclassing Roda, the use method is called, which loads a rack middleware into the current application.

The route block is called whenever a new request comes in, and it is yielded an instance of a subclass of Rack::Request with some additional methods for matching routes. By convention, this argument should be named r.

The primary way routes are matched in Roda is by calling r.on, r.is, r.root, r.get, or r.post. These methods are calling the routing methods, and each of them takes a block. The block is referred to as a match block.

Each routing method takes each of the arguments (called matchers) given and tries to match them to the current request. If it is able to match all of the arguments, it yields to the match block, otherwise the block is skipped and execution continues.

r.on matches if all of the arguments match. r.is matches if all of the arguments match, and there are no further entries in the path after matching. r.get when called without arguments matches any GET request. r.get when called with any arguments matches only if the current request is a GET request and there are no further entries in the path after matching. r.root only matches a GET request where the current path is /.

If a routing method matches and control is yielded to the match block, whenever the match block returns, Roda will return the rack response array of status, headers, and body, to the caller.

If the match block returns a string and the response body hasn’t already been written to, the block return value will interpreted as the body for the response. If none of the routing methods match and the route block returns a string, it will be interpreted as the body for the response.

r.redirect immediately returns the response, allowing for code such as r.redirect(path) if some_condition. If called without arguments, it redirects to the current path if the current request method is not GET.

The .app at the end is an optimization, which you can leave off, but which saves a few methods call for every response.

The Routing Tree

Roda is called a routing tree web framework because the way most sites are structured, routing takes the form of a tree based on the URL structure of the site. In general, r.on is used to split the tree into different branches, and r.is is finalizes the routing, where the request is actually handled.

So a simple routing tree may look something like this:

r.on "a" do           # /a branch
  r.on "b" do         # /a/b branch
    r.is "c" do       # /a/b/c request
      r.get do end    # GET /a/b/c request
      r.post do end   # POST /a/b/c request
    end
    r.get "d" do end  # GET /a/b/d request
    r.post "e" do end # POST /a/b/e request
  end
end

It’s also possible to handle the same requests, but structure the routing tree by first branching on the request method:

r.get do              # GET 
  r.on "a" do         # GET /a branch
    r.on "b" do       # GET /a/b branch
      r.is "c" do end # GET /a/b/c request
      r.is "d" do end # GET /a/b/d request
    end
  end
end

r.post do             # POST
  r.on "a" do         # POST /a branch
    r.on "b" do       # POST /a/b branch
      r.is "c" do end # POST /a/b/c request
      r.is "e" do end # POST /a/b/e request
    end
  end
end

This allows you to easily separate your GET request handling from your POST request handling. If you only have a small number of POST request URLs and a large number of GET request URLs, this may make things easier.

However, in general routing first by the path and last by the request method is likely to lead to simpler and DRYer code. This is because at any point during the routing, you can act on the request. For example, if all requests in the /a branch need need access permission A and all requests in the /a/b branch need access permission B, you can easily handle this in the routing tree:

r.on "a" do           # /a branch
  check_perm(:A)  
  r.on "b" do         # /a/b branch
    check_perm(:B)  
    r.is "c" do       # /a/b/c request
      r.get do end    # GET /a/b/c request
      r.post do end   # POST /a/b/c request
    end
    r.get "d" do end  # GET /a/b/d request
    r.post "e" do end # POST /a/b/e request
  end
end

Being able to operate on the request at any point during the the routing is one of the major advantages of Roda compared to other web frameworks that do not use a routing tree.

Matchers

Other than r.root, the routing methods all take arguments called matchers. If all of the matchers match, the routing method yields to the match block. Here’s an example showcasing how different matchers work:

class App < Roda
  route do |r|
    # only GET requests
    r.get do

      # /
      r.root do
        "Home"
      end

      # /about
      r.is "about" do
        "About"
      end

      # /styles/basic.css
      r.is "styles", :extension => "css" do |file|
        "Filename: #{file}" #=> "Filename: basic"
      end

      # /post/2011/02/16/hello
      r.is "post/:y/:m/:d/:slug" do |y, m, d, slug|
        "#{y}-#{m}-#{d} #{slug}" #=> "2011-02-16 hello"
      end

      # /username/foobar
      r.on "username/:username" do |username|
        user = User.find_by_username(username) # username == "foobar"

        # /username/foobar/posts
        r.is "posts" do
          # You can access user here, because the blocks are closures.
          "Total Posts: #{user.posts.size}" #=> "Total Posts: 6"
        end

        # /username/foobar/following
        r.is "following" do
          user.following.size.to_s #=> "1301"
        end
      end

      # /search?q=barbaz
      r.is "search", :param=>"q" do |query|
        "Searched for #{query}" #=> "Searched for barbaz"
      end
    end

    # only POST requests
    r.post do
      r.is "login" do

        # POST /login, user: foo, pass: baz
        r.on({:param=>"user"}, {:param=>"pass"}) do |user, pass|
          "#{user}:#{pass}" #=> "foo:baz"
        end

        # If the params user and pass are not provided, this
        # will get executed.
        "You need to provide user and pass!"
      end
    end
  end
end

Here’s a description of the matchers. Note that segment as used here means one part of the path preceeded by a /. So a path such as /foo/bar//baz has 4 segments, /foo, /bar, / and /baz. The / here is considered the empty segment.

String

If it does not contain a colon or slash, it matches single segment with the text of the string, preceeded by a slash.

""    # matches "/"
"foo" # matches "/foo"
"foo" # does not match "/food"

If it contains any slashes, it matches one additional segment for each slash:

"foo/bar" # matches "/foo/bar"
"foo/bar" # does not match "/foo/bard"

If it contains a colon followed by any \w characters, the colon and remaing \w characters matches any nonempty segment that contains at least one character:

"foo/:id" # matches "/foo/bar", "/foo/baz", etc.
"foo/:id" # does not match "/fo/bar"

You can use multiple colons in a string:

":x/:y" # matches "/foo/bar", "/bar/foo" etc.
":x/:y" # does not match "/foo", "/bar/"

You can prefix colons:

"foo:x/bar:y" # matches "/food/bard", "/fool/bart", etc.
"foo:x/bar:y" # does not match "/foo/bart", "/fool/bar", etc.

If any colons are used, the block will yield one argument for each segment matched containing the matched text. So:

"foo:x/:y" # matching "/fool/bar" yields "l", "bar"

Colons that are not followed by a \w character are matched literally:

":/a" # matches "/:/a"

Note that strings are regexp escaped before being used in a regular expression, so:

"\\d+(/\\w+)?" # matches "/\d+(/\w+)?"
"\\d+(/\\w+)?" # does not match "/123/abc"

Regexp

Regexps match one or more segments by looking for the pattern preceeded by a slash:

/foo\w+/ # matches "/foobar"
/foo\w+/ # does not match "/foo/bar"

If any patterns are captured by the regexp, they are yielded:

/foo\w+/   # matches "/foobar", yields nothing
/foo(\w+)/ # matches "/foobar", yields "bar"

Symbol

Symbols match any nonempty segment, yielding the segment except for the preceeding slash:

:id # matches "/foo" yields "foo"
:id # does not match "/"

Proc

Procs match unless they return false or nil:

proc{true}  # matches anything
proc{false} # does not match anything

Procs don’t capture anything by default, but they can if you add the captured text to r.captures.

Arrays

Arrays match when any of their elements matches. If multiple matchers are given to r.on, they all must match (an AND condition), while if an array of matchers is given, only one needs to match (an OR condition). Evaluation stops at the first matcher that matches.

Additionally, if the matched object is a String, the string is yielded. This makes it easy to handle multiple strings without a Regexp:

['page1', 'page2'] # matches "/page1", "/page2"
[]                 # does not match anything

Hash

Hashes allow easily calling specialized match methods on the request. The default registered matchers included with Roda are documented below. You can add your own hash matchers using the hash_matcher class method, which creates an appropriate request match method. The hash_matcher block will be called with the value of the hash.

class App < Roda
  hash_matcher(:foo) do |v|
    # ...
  end

  route do |r|
    r.on :foo=>'bar' do
      # ...
    end
  end
end

:all

The :all matcher matches if all of the entries in the given array matches. So

r.on :all=>[:a, :b] do
  # ...
end

is the same as:

r.on :a, :b do
  # ...
end

The reason it also exists as a separate hash matcher is so you can use it inside an array matcher. so:

r.on ['foo', {:all=>['foos', :id]}] do
end

Would match /foo and /foos/10, but not /foos.

:extension

The :extension matcher matches any nonempty path ending with the given extension:

{:extension => "css"} # matches "/foo.css", "/bar.css"
{:extension => "css"} # does not match "/foo.css/x", "/foo.bar", "/.css"

This matcher yields the part before the extension.

:method

This matches the method of the request. You can provide an array to specify multiple request methods and match on any of them:

{:method => :post}             # matches POST
{:method => ['post', 'patch']} # matches POST and PATCH

:param

The :param matcher matches if the given parameter is present, even if empty.

{:param => "user"} # matches "/foo?user=bar", "/foo?user="
{:param => "user"} # does not matches "/foo"

:param!

The :param! matcher matches if the given parameter is present and not empty.

{:param! => "user"} # matches "/foo?user=bar"
{:param! => "user"} # does not matches "/foo", "/foo?user="

false, nil

If false or nil is given directly as a matcher, it doesn’t match anything.

Everything else

Everything else matches anything.

Status codes

When it comes time to finalize a response, if a status code has not been set manually, it will use a 200 status code if anything has been written to the response, otherwise it will use a 404 status code. This enables the principle of least surprise to work, where if you don’t handle an action, a 404 response is assumed.

You can always set the status code manually via the status attribute for the response.

route do |r|
  r.get "hello" do
    response.status = 200
  end
end

Verb Methods

The main match method is r.on, but as displayed above, you can also use r.get or r.post. When called without any arguments, these match as long as the request has the appropriate method, so:

r.get do end

matches any GET request, and

r.post do end

matches any POST request

If any arguments are given to the method, these match only if the request method matches, all arguments match, and only the path has been fully matched by the arguments. So:

r.post "" do end

matches only POST requests where the current path is /.

r.get "a/b" do end

matches only GET requests where the current path is /a/b.

The reason for this difference in behavior is that if you are not providing any arguments, you probably don’t want to to also test for an exact match with the current path. If that is something you do want, you can provide true as an argument:

r.on "foo" do
  r.get true do # Matches GET /foo, not GET /foo/.*
  end
end

If you want to match the request method and do a partial match on the request path instead of a full match, you need to use r.on with the :method hash matcher:

r.on "foo", :method=>:get do # Matches GET /foo(/.*)?
end

Root Method

As displayed above, you can also use r.root as a match method. This method matches GET requests where the current path /. r.root is similar to r.get "", except that it does not consume the / from the path.

Unlike the other matching methods, r.root takes no arguments.

Note that r.root does not match if the path is empty, you should use r.get true for that. If you want to match either the the empty path or /, you can use r.get ["", true].

Note that r.root does not match non-GET requests, so to handle POST / requests, use r.post ''.

Request and Response

While the request object is yielded to the route block, it is also available via the request method. Likewise, the response object is available via the response method.

The request object is an instance of a subclass of Rack::Request with some additional methods, and the response object is an instance of a subclass of Rack::Response with some additional methods.

If you want to extend the request and response objects with additional modules, you can do so via the request_module or response_module methods, or via plugins.

Pollution

Roda tries very hard to avoid polluting the scope of the route block. The only instance variables defined by default in the scope of the route block are @_request and @_response. The only methods defined (beyond the default methods for Object) are: env, opts, request, response, call, session, and _route (private). Constants inside the Roda namespace are all prefixed with Roda (e.g. Roda::RodaRequest). This should make it unlikely that Roda will cause a namespace issue with your application code.

Captures

You may have noticed that some matchers yield a value to the block. The rules for determining if a matcher will yield a value are simple:

  1. Regexp captures: /posts\/(\d+)-(.*)/ will yield two values, corresponding to each capture.

  2. String placeholders: "users/:id" will yield the value in the position of :id.

  3. Symbols: :foobar will yield if a segment is available.

  4. File extensions: :extension=>"css" will yield the basename of the matched file.

  5. Parameters: :param=>"user" will yield the value of the parameter user, if present.

The first case is important because it shows the underlying effect of regex captures.

In the second case, the substring :id gets replaced by ([^\/]+) and the regexp becomes /users\/([^\/]+)/ before performing the match, thus it reverts to the first form we saw.

In the third case, the symbol, no matter what it says, gets replaced by /([^\/]+)/, and again we are in presence of case 1.

The fourth case, again, reverts to the basic matcher: it generates the string /([^\/]+?)\.#{ext}\z/ before performing the match.

The fifth case is different: it checks if the the parameter supplied is present in the request (via POST or QUERY_STRING) and it pushes the value as a capture.

Composition

You can mount any Rack app (including another Roda app), with its own middlewares, inside a Roda app, using r.run:

class API < Roda
  use SomeMiddleware

  route do |r|
    r.is do
      # ...
    end
  end
end

class App < Roda
  route do |r|
    r.on "api" do
      r.run API
    end
  end
end

run App.app

This will take any path starting with /api and send it to API. In this example, API is a Roda app, but it could easily be a Sinatra, Rails, or other Rack app.

When you use r.run, Roda calls the given Rack app (API in this case), and whatever the Rack app returns will be returned as the response for the current application.

multi_route plugin

If you are just looking to split up the main route block up by branches, you should use the multi_route plugin, which keeps the current scope of the route block:

class App < Roda
  plugin :multi_route

  route "api" do |r|
    r.is do
      # ...
    end
  end

  route do |r|
    r.on "api" do
      r.route "api"
    end
  end
end

run App.app

This allows you to set instance variables in the main route block, and still have access to them inside the api route block.

Testing

It is very easy to test Roda with Rack::Test or Capybara. Roda’s own tests use RSpec. The default rake task will run the specs for Roda, if RSpec is installed.

Settings

Each Roda app can store settings in the opts hash. The settings are inherited if you happen to subclass Roda.

Roda.opts[:layout] = "guest"

class Users < Roda; end
class Admin < Roda; end

Admin.opts[:layout] = "admin"

Users.opts[:layout] # => 'guest'
Admin.opts[:layout] # => 'admin'

Feel free to store whatever you find convenient. Note that when subclassing, Roda only does a shallow clone of the settings. If you store nested structures and plan to mutate them in subclasses, it is your responsibility to dup the nested structures inside Roda.inherited (making sure to call super). The plugins that ship with Roda all handle this. Also, note that this means that future modifications to the parent class after subclassing do not affect the subclass.

Rendering

Roda ships with a render plugin that provides helpers for rendering templates. It uses Tilt, a gem that interfaces with many template engines. The erb engine is used by default.

Note that in order to use this plugin you need to have Tilt installed, along with the templating engines you want to use.

This plugin adds the render and view methods, for rendering templates. The difference between render and view is that view will by default attempt to render the template inside the default layout template, where render will just render the template.

class App < Roda
  plugin :render

  route do |r|
    @var = '1'

    r.is "render" do
      # Renders the views/home.erb template, which will have access to the
      # instance variable @var, as well as local variable content
      render("home", :locals=>{:content => "hello, world"})
    end

    r.is "view" do
      @var2 = '1'

      # Renders the views/home.erb template, which will have access to the
      # instance variables @var and @var2, and takes the output of that and
      # renders it inside views/layout.erb (which should yield where the
      # content should be inserted).
      view("home")
    end
  end
end

You can override the default rendering options by passing a hash to the plugin, or modifying the render_opts hash after loading the plugin:

class App < Roda
  plugin :render, :escape => true # Automatically escape output in erb templates
  render_opts[:views] = 'admin_views' # Default views directory
  render_opts[:layout] = "admin_layout" # Default layout template
  render_opts[:layout_opts] = {:engine=>'haml'} # Default layout template options
  render_opts[:opts] = {:default_encoding=>'UTF-8'} # Default template options
  render_opts[:cache] = false # Disable template caching
  render_opts[:engine] = 'slim' # Tilt engine/template file extension to use
end

Sessions

By default, Roda doesn’t turn on sessions, but most users are going to want to turn on session support, and the simplest way to do that is to use the Rack::Session::Cookie middleware that comes with rack:

require "roda"

class App < Roda
  use Rack::Session::Cookie, :secret => ENV['SECRET']
end

Security

Web application security is a very large topic, but here are some things you can do with Roda to prevent some common web application vulnerabilities.

Session Security

If you are using sessions, you should also always set a session secret using the :secret option as shown above. Make sure this secret is not disclosed, because if an attacker knows the :secret value, they can inject arbitrary session values, which in the worst case scenario can lead to remote code execution.

Keep in mind that with Rack::Session::Cookie, the content in the session cookie is not encrypted, just signed to prevent tampering. This means you should not store any data in the session that itself is secret.

Cross Site Request Forgery (CSRF)

CSRF can be prevented by using the csrf plugin that ships with Roda, which uses the rack_csrf library. Just make sure that you include the CSRF token tags in your html as appropriate.

It’s also possible to use the Rack::Csrf middleware directly, you don’t have to use the csrf plugin.

Cross Site Scripting (XSS)

The easiest way to prevent XSS with Roda is to use a template library that automatically escapes output by default. The :escape option to the render plugin sets the ERB template processor to escape by default, so that in your templates:

<%= '<>' %>  # outputs &lt;&gt; 
<%== '<>' %> # outputs <>

Note that unlike most other render options, the :escape option must be passed to the plugin :render call, it won’t be respected if added later.

This support requires Erubis.

Other

For prevention of some other vulnerabilities, such as click-jacking, directory traversal, session hijacking, and IP spoofing, consider using Rack::Protection, which is a rack middleware that can be added the usual way:

require 'roda'
require 'rack/protection'

class App < Roda
  use Rack::Protection
end

Plugins

Roda provides a way to extend its functionality with plugins. Plugins can override any Roda method and call super to get the default behavior.

Included Plugins

These plugins ship with roda:

all_verbs

Adds routing methods to the request for all http verbs.

backtracking_array

Allows array matchers to backtrack if later matchers do not match.

content_for

Allows storage of content in one template and retrieval of that content in a different template.

csrf

Adds CSRF protection and helper methods using rack_csrf.

default_headers

Override the default response headers used.

error_handler

Adds a error block that is called for all responses that raise exceptions.

flash

Adds a flash handler.

h

Adds h method for html escaping.

halt

Augments request#halt method to take status and/or body or status, headers, and body.

head

Treat HEAD requests like GET requests with an empty response body.

header_matchers

Adds host, header, and accept hash matchers.

hooks

Adds before and after methods to run code before and after requests.

indifferent_params

Adds params method with indifferent access to params, allowing use of symbol keys for accessing params.

json

Allows match blocks to return arrays and hashes, using a json representation as the response body.

middleware

Allows the Roda app to be used as a rack middleware, calling the next middleware if no route matches.

multi_route

Adds the ability for multiple named route blocks, with the ability to dispatch to them add any point in the main route block.

not_allowed

Adds support for automatically returning 405 Method Not Allowed responses.

not_found

Adds a not_found block that is called for all 404 responses without bodies.

pass

Adds a pass method allowing you to skip the current r.on block as if it did not match.

per_thread_caching

Switches the thread-safe cache from a shared cache to a per-thread cache.

render

Adds support for rendering templates via tilt, as described above.

render_each

Render a template for each value in an enumerable.

streaming

Adds support for streaming responses.

symbol_matchers

Adds support for symbol-specific matching regexps.

symbol_views

Allows match blocks to return template name symbols, uses the template view as the response body.

view_subdirs

Allows for setting a view subdirectory to use on a per-request basis.

External Plugins

The following libraries include Roda plugins:

forme

Adds support for easy HTML form creation in erb templates.

autoforme

Adds support for easily creating a simple administrative front end for Sequel models.

How to create plugins

Authoring your own plugins is pretty straightforward. Plugins are just modules, which may contain any of the following modules:

InstanceMethods

module included in the Roda class

ClassMethods

module that extends the Roda class

RequestMethods

module included in the class of the request

RequestClassMethods

module extending the class of the request

ResponseMethods

module included in the class of the response

ResponseClassMethods

module extending the class of the response

If the plugin responds to load_dependencies, it will be called first, and should be used if the plugin depends on another plugin.

If the plugin responds to configure, it will be called last, and should be used to configure the plugin.

Both load_dependencies and configure are called with the additional arguments and block given to the plugin call.

So a simple plugin to add an instance method would be:

module MarkdownHelper
  module InstanceMethods
    def markdown(str)
      BlueCloth.new(str).to_html
    end
  end
end

Roda.plugin MarkdownHelper

Registering plugins

If you want to ship a Roda plugin in a gem, but still have Roda load it automatically via Roda.plugin :plugin_name, you should place it where it can be required via roda/plugins/plugin_name, and then have the file register it as a plugin via Roda::RodaPlugins.register_plugin. It’s recommended but not required that you store your plugin module in the Roda::RodaPlugins namespace:

module Roda
  module RodaPlugins
    module Markdown
      module InstanceMethods
        def markdown(str)
          BlueCloth.new(str).to_html
        end
      end
    end

    register_plugin :markdown, Markdown
  end
end

You should avoid creating your module directly in the Roda namespace to avoid polluting the namespace. Additionally, any instance variables created inside InstanceMethods should be prefixed with an underscore (e.g. @_variable) to avoid polluting the scope.

License

MIT

Maintainer

Jeremy Evans <[email protected]>