Roda is a routing tree web framework toolkit.
$ gem install roda
- Website
- Source
- Bugs
- Google Group
- IRC
-
chat.freenode.net/#roda
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.
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 block above:
After requiring the library and subclassing Roda, the use
method is called. This loads a Rack middleware into the current application.
The route
block is called whenever a new request comes in. 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
. Each of these “routing methods” takes a “match block”.
Each routing method takes each of the arguments (called matchers) that is given and tries to match it to the current request. If the method 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
matches anyGET
request when called without arguments. -
r.get
(when called with any arguments) matches only if the current request is aGET
request and there are no further entries in the path after matching. -
r.root
only matches aGET
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 (containing 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 be 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 r.redirect
is called without arguments and the current request method is not GET
, it redirects to the current path.
The .app
at the end is an (optional) optimization. It saves a few method calls for every response.
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. -
r.is
finalizes the routing path. -
r.get
andr.post
handle specific request methods.
So, a simple routing tree might 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, routing first by the path and last by the request method is likely to lead to simpler and DRYer code. This is because you can act on the request at any point during the routing. For example, if all requests in the /a
branch 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 routing is one of the major advantages of Roda, as compared to frameworks that do not use a routing tree.
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 preceded by a /
. So, a path such as /foo/bar//baz
has four segments: /foo
, /bar
, /
, and /baz
. The /
here is considered the empty segment.
If a string does not contain a colon or slash, it matches a single segment containing the text of the string, preceded by a slash.
"" # matches "/" "foo" # matches "/foo" "foo" # does not match "/food"
If a string contains any slashes, it matches one additional segment for each slash:
"foo/bar" # matches "/foo/bar" "foo/bar" # does not match "/foo/bard"
If a string contains a colon followed by any \w
characters, the colon and remaining \w
characters match 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 must be escaped before being used in a regular expression, so:
"\\d+(/\\w+)?" # matches "/\d+(/\w+)?" "\\d+(/\\w+)?" # does not match "/123/abc"
Regexps match one or more segments by looking for the pattern, preceded 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"
Symbols match any nonempty segment, yielding the segment except for the preceding slash:
:id # matches "/foo" yields "foo" :id # does not match "/"
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 do so if you add the captured text to r.captures
.
Arrays match when any of their elements match. If multiple matchers are given to r.on
, they all must match (an AND condition). 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
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
The :all
matcher matches if all of the entries in the given array match, 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
.
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 found before the period and extension (e.g., foo
).
The :method
matcher 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
The :param
matcher matches if the given parameter is present (even if it is empty).
{:param => "user"} # matches "/foo?user=bar", "/foo?user=" {:param => "user"} # does not matches "/foo"
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="
If false
or nil
is given directly as a matcher, it doesn’t match anything.
Everything else matches anything.
When it comes time to finalize a response, if a status code has not been set manually and anything has been written to the response, the response will use a 200 status code. Otherwise, it will use a 404 status code. This enables the principle of least surprise to work: 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
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 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 only a partial match on the request path, you need to use r.on
with the :method
hash matcher:
r.on "foo", :method=>:get do # Matches GET /foo(/.*)? end
As displayed above, you can also use r.root
as a match method. This method matches GET
requests where the current path is /
. 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
only matches GET
requests. So, to handle POST /
requests, use r.post ''
.
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. 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.
Roda tries very hard to avoid polluting the scope of the route
block. This should make it unlikely that Roda will cause namespace issues with your application code:
-
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
).
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:
-
Regexp captures:
/posts\/(\d+)-(.*)/
will yield two values, corresponding to each capture. -
String placeholders:
"users/:id"
will yield the value in the position of:id
. -
Symbols:
:foobar
will yield if a segment is available. -
File extensions:
:extension=>"css"
will yield the basename of the matched file. -
Parameters:
:param=>"user"
will yield the value of the parameteruser
, if present.
The first case is important because it shows the underlying effect of Regexp 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 parameter supplied is present in the request (via POST or QUERY_STRING) and it pushes the value as a capture.
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); whatever the Rack app returns will be returned as the response for the current application.
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.
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.
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 modifications to the parent class made after subclassing do not affect the subclass.
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. By default, view
will render the template inside the default layout template; 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:
class App < Roda plugin :render, :cache => false, # Disable template caching, useful in development :engine => 'erb', # Tilt engine/template file extension to use :escape => true, # Automatically escape output in erb templates :layout => "admin_layout", # Default layout template :layout_opts => {:ext=>'html.erb'}, # Default layout template options :opts => {:default_encoding=>'UTF-8'}, # Default template options :views => 'admin_views' # Default views directory end
By default, Roda doesn’t turn on sessions, but most users are going to want to turn on session support. The simplest way to do this 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
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.
If you are using sessions, you should also always set a session secret, using the :secret
option as shown above. Make sure that this secret is not disclosed, because if an attacker knows the :secret
value, they can inject arbitrary session values. In the worst case scenario, this 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 secret data in the session.
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.
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 <> <%== '<>' %> # outputs <>
This support requires Erubis.
For prevention of some other vulnerabilities, such as click-jacking, directory traversal, session hijacking, and IP spoofing, consider using Rack::Protection. This is a Rack middleware that can be added in the usual way:
require 'roda' require 'rack/protection' class App < Roda use Rack::Protection end
By design, Roda has a very small core, providing only the essentials. All nonessential features are added via plugins. This is why Roda is referred to as a routing tree web framework toolkit. Using a combination of Roda plugins, you can build the routing tree web framework that needs your needs.
Roda’s plugins can override any Roda method and call super
to get the default behavior, which makes Roda very extensible.
Roda ships with a large number of plugins, and some other libraries ship with support for Roda.
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.
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 that was 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
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:
class Roda module RodaPlugins module Markdown module InstanceMethods def markdown(str) BlueCloth.new(str).to_html end end end register_plugin :markdown, Markdown end end
To avoid namespace pollution, you should avoid creating your module directly in the Roda
namespace. Additionally, any instance variables created inside InstanceMethods
should be prefixed with an underscore (e.g., @_variable
) to avoid polluting the scope.
MIT
Jeremy Evans <[email protected]>