A routing tree web toolkit, designed for building fast and maintainable web applications in Ruby.
Table of contents¶ ↑
-
Installation
-
Resources
-
Goals
-
Usage
-
Running the application
-
The routing tree
-
Matchers
-
Optional segments
-
Match/Route Block Return Values
-
Status codes
-
Verb methods
-
Root method
-
Request and Response
-
Pollution
-
Composition
-
Testing
-
Settings
-
Rendering
-
Security
-
Code Reloading
-
Plugins
-
No introspection
-
Inspiration
-
Ruby Support Policy
Installation¶ ↑
$ gem install roda
Resources¶ ↑
- Website
- Source
- Bugs
- Discussion Forum (GitHub Discussions)
- Alternate Discussion Forum (Google Group)
Goals¶ ↑
-
Simplicity
-
Reliability
-
Extensibility
-
Performance
Simplicity¶ ↑
Roda is designed to be simple, both internally and externally. It uses a routing tree to enable you to write simpler and DRYer code.
Reliability¶ ↑
Roda supports and encourages immutability. Roda apps are designed to be frozen in production, which eliminates possible thread safety issues. Additionally, Roda limits the instance variables, constants, and methods that it uses, so that they do not conflict with the ones you use for your application.
Extensibility¶ ↑
Roda is built completely out of plugins, which makes it very extensible. You can override any part of Roda and call super to get the default behavior.
Performance¶ ↑
Roda has low per-request overhead, and the use of a routing tree and intelligent caching of internal datastructures makes it significantly faster than other popular ruby web frameworks.
Usage¶ ↑
Here’s a simple application, showing how the routing tree works:
# cat config.ru require "roda" class App < Roda route do |r| # GET / request r.root do r.redirect "/hello" end # /hello branch r.on "hello" do # Set variable for all routes in /hello branch @greeting = 'Hello' # GET /hello/world request r.get "world" do "#{@greeting} world!" end # /hello request r.is do # GET /hello request r.get do "#{@greeting}!" end # POST /hello request r.post do puts "Someone said #{@greeting}!" r.redirect end end end end end run App.freeze.app
Here’s a breakdown of what is going on in the block above:
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 are 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 .freeze.app
at the end is optional. Freezing the app makes modifying app-level settings raise an error, alerting you to possible thread-safety issues in your application. It is recommended to freeze the app in production and during testing. The .app
is an optimization, which saves a few method calls for every request.
Running the Application¶ ↑
Running a Roda application is similar to running any other rack-based application that uses a config.ru
file. You can start a basic server using rackup
, puma
, unicorn
, passenger
, or any other webserver that can handle config.ru
files:
$ rackup
The Routing Tree¶ ↑
Roda is called a routing tree web toolkit 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.
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| # GET / r.root do "Home" end # GET /about r.get "about" do "About" end # GET /post/2011/02/16/hello r.get "post", Integer, Integer, Integer, String do |year, month, day, slug| "#{year}-#{month}-#{day} #{slug}" #=> "2011-02-16 hello" end # GET /username/foobar branch r.on "username", String, method: :get do |username| user = User.find_by_username(username) # GET /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 # GET /username/foobar/following r.is "following" do user.following.size.to_s #=> "1301" end end # /search?q=barbaz r.get "search" do "Searched for #{r.params['q']}" #=> "Searched for barbaz" end r.is "login" do # GET /login r.get do "Login" end # POST /login?user=foo&password=baz r.post do "#{r.params['user']}:#{r.params['password']}" #=> "foo:baz" 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.
String¶ ↑
If a string does not contain a 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"
Regexp¶ ↑
Regexps match one or more segments by looking for the pattern, preceded by a slash, and followed by a slash or the end of the path:
/foo\w+/ # matches "/foobar" /foo\w+/ # does not match "/foo/bar" /foo/i # matches "/foo", "/Foo/" /foo/i # does not match "/food"
If any patterns are captured by the Regexp, they are yielded:
/foo\w+/ # matches "/foobar", yields nothing /foo(\w+)/ # matches "/foobar", yields "bar"
Class¶ ↑
There are two classes that are supported as matchers, String and Integer.
- String
-
matches any non-empty segment, yielding the segment except for the preceding slash
- Integer
-
matches any segment of 0-9, returns matched values as integers
Using String and Integer is the recommended way to handle arbitrary segments
String # matches "/foo", yields "foo" String # matches "/1", yields "1" String # does not match "/" Integer # does not match "/foo" Integer # matches "/1", yields 1 Integer # does not match "/"
Symbol¶ ↑
Symbols match any nonempty segment, yielding the segment except for the preceding slash:
:id # matches "/foo" yields "foo" :id # does not match "/"
Symbol matchers operate the same as the class String matcher, and is the historical way to do arbitrary segment matching. It is recommended to use the class String matcher in new code as it is a bit more intuitive.
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 do so if you add the captured text to r.captures
.
Arrays¶ ↑
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
Hash¶ ↑
Hashes allow easily calling specialized match methods on the request. The default registered matchers included with Roda are documented below. Some plugins add additional hash matchers, and the hash_matcher plugin allows for easily defining your own:
class App < Roda plugin :hash_matcher 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 match, so
r.on all: [String, String] do # ... end
is the same as:
r.on String, String 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', Integer]}] do end
would match /foo
and /foos/10
, but not /foos
.
:method¶ ↑
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
true¶ ↑
If true
is given directly as a matcher, it always matches.
false, nil¶ ↑
If false
or nil
is given directly as a matcher, it doesn’t match anything.
Everything else¶ ↑
Everything else raises an error, unless support is specifically added for it (some plugins add support for additional matcher types).
Optional segments¶ ↑
There are multiple ways you can handle optional segments in Roda. For example, let’s say you want to accept both /items/123
and /items/123/456
, with 123 being the item’s id, and 456 being some optional data.
The simplest way to handle this is by treating this as two separate routes with a shared branch:
r.on "items", Integer do |item_id| # Shared code for branch here # /items/123/456 r.is Integer do |optional_data| end # /items/123 r.is do end end
This works well for many cases, but there are also cases where you really want to treat it as one route with an optional segment. One simple way to do that is to use a parameter instead of an optional segment (e.g. /items/123?opt=456
).
r.is "items", Integer do |item_id| optional_data = r.params['opt'].to_s end
However, if you really do want to use a optional segment, there are a couple different ways to use matchers to do so. One is using an array matcher where the last element is true:
r.is "items", Integer, [String, true] do |item_id, optional_data| end
Note that this technically yields only one argument instead of two arguments if the optional segment isn’t provided.
An alternative way to implement this is via a regexp:
r.is "items", /(\d+)(?:\/(\d+))?/ do |item_id, optional_data| end
Match/Route Block Return Values¶ ↑
If the response body has already been written to by calling response.write
directly, then any return value of a match block or route block is ignored.
If the response body has not already been written to, then the match block or route block return value is inspected:
- String
-
used as the response body
- nil, false
-
ignored
- everything else
-
raises an error
Plugins can add support for additional match block and route block return values. One example of this is the json plugin, which allows returning arrays and hashes in match and route blocks and converts those directly to JSON and uses the JSON as the response body.
Status Codes¶ ↑
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
When redirecting, the response will use a 302 status code by default. You can change this by passing a second argument to r.redirect
:
route do |r| r.get "hello" do r.redirect "/other", 301 # use 301 Moved Permanently end end
Verb Methods¶ ↑
As displayed above, Roda has r.get
and r.post
methods for matching based on the HTTP request method. If you want to match on other HTTP request methods, use the all_verbs plugin.
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 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
Root Method¶ ↑
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 empty path or /
, you can use r.get ["", true]
, or use the slash_path_empty plugin.
Note that r.root
only matches 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.
If you want to extend the request and response objects with additional modules, you can use the module_include plugin.
Pollution¶ ↑
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. Some of the things Roda does:
-
The only instance variables defined by default in the scope of the
route
block are@_request
and@_response
. All instance variables in the scope of theroute
block used by plugins that ship with Roda are prefixed with an underscore. -
The main methods defined, beyond the default methods for
Object
, areenv
,opts
,request
,response
, andsession
.call
and_call
are also defined, but are deprecated. All other methods defined are prefixed withroda
-
Constants inside the Roda namespace are all prefixed with
Roda
(e.g.,Roda::RodaRequest
).
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 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 have a lot of rack applications that you want to dispatch to, and which one to dispatch to is based on the request path prefix, look into the multi_run
plugin.
hash_branches plugin¶ ↑
If you are just looking to split up the main route block up by branches, you should use the hash_branches
plugin, which keeps the current scope of the route
block:
class App < Roda plugin :hash_branches hash_branch "api" do |r| r.is do # ... end end route do |r| r.hash_branches 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 minitest/spec. The default Rake task will run the specs for Roda.
Settings¶ ↑
Each Roda app can store settings in the opts
hash. The settings are inherited by subclasses.
Roda.opts[:layout] = "guest" class Users < Roda; end class Admin < Roda opts[:layout] = "admin" end 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
). This should be is done so that modifications to the parent class made after subclassing do not affect the subclass, and vice-versa.
The plugins that ship with Roda freeze their settings and only allow modification to their settings by reloading the plugin, and external plugins are encouraged to follow this approach.
The following options are respected by the default library or multiple plugins:
- :add_script_name
-
Prepend the SCRIPT_NAME for the request to paths. This is useful if you mount the app as a path under another app.
- :check_arity
-
Whether arity for blocks passed to Roda should be checked to determine if they can be used directly to define methods or need to be wrapped. By default, for backwards compatibility, this is true, so Roda will check blocks and handle cases where the arity of the block does not match the expected arity. This can be set to
:warn
to issue warnings whenever Roda detects an arity mismatch. If set tofalse
, Roda does not check the arity of blocks, which can result in failures at runtime if the arity of the block does not match what Roda expects. Note that Roda does not check the arity for lambda blocks, as those are strict by default. - :check_dynamic_arity
-
Similar to :check_arity, but used for checking blocks where the number of arguments Roda will call the blocks with is not possible to determine when defining the method. By default, Roda checks arity for such methods, but doing so actually slows the method down even if the number of arguments matches the expected number of arguments.
- :freeze_middleware
-
Whether to freeze all middleware when building the rack app.
- :json_parser
-
A callable for parsing JSON (
JSON.parse
in general used by default). - :json_serializer
-
A callable for serializing JSON (
to_json
in general used by default). - :root
-
Set the root path for the app. This defaults to the current working directory of the process.
- :sessions_convert_symbols
-
This should be set to
true
if the sessions in use do not support roundtripping of symbols (for example, when sessions are serialized via JSON).
There may be other options supported by individual plugins, if so it will be mentioned in the documentation for the plugin.
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. 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.get "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.get "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, escape: true, # Automatically escape output in erb templates using Erubi's escaping support views: 'admin_views', # Default views directory layout_opts: {template: 'admin_layout', engine: 'html.erb'}, # Default layout options template_opts: {default_encoding: 'UTF-8'} # Default template options 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¶ ↑
By default, Roda doesn’t turn on sessions, and if you don’t need sessions, you can skip this section. If you do need sessions, Roda offers two recommended ways to implement cookie-based sessions.
If you do not need any session support in middleware, and only need session support in the Roda application, then use the sessions plugin:
require 'roda' class App < Roda plugin :sessions, secret: ENV['SESSION_SECRET'] end
The :secret
option should be a randomly generated string of at least 64 bytes.
If you have middleware that need access to sessions, then use the RodaSessionMiddleware
that ships with Roda:
require 'roda' require 'roda/session_middleware' class App < Roda use RodaSessionMiddleware, secret: ENV['SESSION_SECRET'] end
If you need non-cookie based sessions (such as sessions stored in a database), you should use an appropriate external middleware.
It is possible to use other session cookie middleware such as Rack::Session::Cookie
, but other middleware may not have the same security features that Roda’s session support does. For example, the session cookies used by the Rack::Session::Cookie
middleware provided by Rack before Rack 3 are not encrypted, just signed to prevent tampering.
For any cookie-based sessions, make sure that the necessary secrets (:secret
option) are not disclosed to an attacker. Knowledge of the secret(s) can allow an attacker to inject arbitrary session values. In the case of Rack::Session::Cookie
, that can also lead remote code execution.
Cross Site Request Forgery (CSRF)¶ ↑
CSRF can be prevented by using the route_csrf
plugin that ships with Roda. The route_csrf
plugin uses modern security practices to create CSRF tokens, requires request-specific tokens by default, and offers control to the user over where in the routing tree that CSRF tokens are checked. For example, if you are using the public
plugin to serve static files and the assets
plugin to serve assets, you wouldn’t need to check for CSRF tokens for either of those, so you could put the CSRF check after those in the routing tree, but before handling other requests:
route do |r| r.public r.assets check_csrf! # Must call this to check for valid CSRF tokens # ... end
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 <> <%== '<>' %> # outputs <>
When using the :escape
option, you will need to ensure that your layouts are not escaping the output of the content template:
<%== yield %> # not <%= yield %>
This support requires Erubi.
Unexpected Parameter Types¶ ↑
Rack converts submitted parameters into a hash of strings, arrays, and nested hashes. Since the user controls the submission of parameters, you should treat any submission of parameters with caution, and should be explicitly checking and/or converting types before using any submitted parameters. One way to do this is explicitly after accessing them:
# Convert foo_id parameter to an integer request.params['foo_id'].to_i
However, it is easy to forget to convert the type, and if the user submits foo_id
as a hash or array, a NoMethodError will be raised. Worse is if you do:
some_method(request.params['bar'])
Where some_method
supports both a string argument and a hash argument, and you expect the parameter will be submitted as a string, and some_method
‘s handling of a hash argument performs an unauthorized action.
Roda ships with a typecast_params
plugin that can easily handle the typecasting of submitted parameters, and it is recommended that all Roda applications that deal with parameters use it or another tool to explicitly convert submitted parameters to the expected types.
Content Security Policy¶ ↑
The Content-Security-Policy HTTP header can be used to instruct the browser on what types of content to allow and where content can be loaded from. Roda ships with a content_security_policy
plugin that allows for the easy configuration of the content security policy. Here’s an example of a fairly restrictive content security policy configuration:
class App < Roda plugin :content_security_policy do |csp| csp.default_src :none # deny everything by default csp.style_src :self csp.script_src :self csp.connect_src :self csp.img_src :self csp.font_src :self csp.form_action :self csp.base_uri :none csp.frame_ancestors :none csp.block_all_mixed_content csp.report_uri 'CSP_REPORT_URI' end end
Other Security Related HTTP Headers¶ ↑
You may want to look into setting the following HTTP headers, which can be done at the web server level, but can also be done at the application level using using the default_headers
plugin:
- Strict-Transport-Security
-
Enforces SSL/TLS Connections to the application.
- X-Content-Type-Options
-
Forces some browsers to respect a declared Content-Type header.
- X-Frame-Options
-
Provides click-jacking protection by not allowing usage inside a frame. Only include this if you want to support and protect old browsers that do not support Content-Security-Policy.
Example:
class App < Roda plugin :default_headers, 'Content-Type'=>'text/html', 'Strict-Transport-Security'=>'max-age=63072000; includeSubDomains', 'X-Content-Type-Options'=>'nosniff', 'X-Frame-Options'=>'deny' end
Rendering Templates Derived From User Input¶ ↑
Roda’s rendering plugin by default checks that rendered templates are inside the views directory. This is because rendering templates outside the views directory is not commonly needed, and it prevents a common attack (which is especially severe if there is any location on the file system that users can write files to).
You can specify which directories are allowed using the :allowed_paths
render plugin option. If you really want to turn path checking off, you can do so via the check_paths: false
render plugin option.
Code Reloading¶ ↑
Roda does not ship with integrated support for code reloading, but there are rack-based reloaders that will work with Roda apps.
Zeitwerk (which Rails now uses for reloading) can be used with Roda. It requires minimal setup and handles most cases. It overrides require
when activated. If it can meet the needs of your application, it’s probably the best approach.
rack-unreloader uses a fast approach to reloading while still being fairly safe, as it only reloads files that have been modified, and unloads constants defined in the files before reloading them. It can handle advanced cases that Zeitwerk does not support, such as classes defined in multiple files (common when using separate route files for different routing branches in the same application). However, rack-unreloader does not modify core classes and using it requires modifying your application code to use rack-unreloader specific APIs, which may not be simple.
AutoReloader provides transparent reloading for all files reached from one of the reloadable_paths
option entries, by detecting new top-level constants and removing them when any of the reloadable loaded files changes. It overrides require
and require_relative
when activated (usually in the development environment). No configurations other than reloadable_paths
are required.
rerun uses a fork/exec approach for loading new versions of your app. It work without any changes to application code, but may be slower as they have to reload the entire application on every change. However, for small apps that load quickly, it may be a good approach.
There is no one reloading solution that is the best for all applications and development approaches. Consider your needs and the tradeoffs of each of the reloading approaches, and pick the one you think will work best. If you are unsure where to start, it may be best to start with Zeitwerk, and only consider other options if it does not work well for you.
Plugins¶ ↑
By design, Roda has a very small core, providing only the essentials. All nonessential features are added via plugins.
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.
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 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
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:
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. Finally, do not add any constants inside the InstanceMethods module, add constants to the plugin module itself (Markdown
in the above example).
If you are planning on shipping your plugin in an external gem, it is recommended that you follow standard gem naming conventions for extensions. So if your plugin module is named FooBar
, your gem name should be roda-foo_bar
.
No Introspection¶ ↑
Because a routing tree does not store the routes in a data structure, but directly executes the routing tree block, you cannot introspect the routes when using a routing tree.
If you would like to introspect your routes when using Roda, there is an external plugin named roda-route_list, which allows you to add appropriate comments to your routing files, and has a parser that will parse those comments into routing metadata that you can then introspect.
Inspiration¶ ↑
Roda was inspired by Sinatra and Cuba. 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. Roda’s plugin system is based on the plugin system used by Sequel.
Ruby Support Policy¶ ↑
Roda fully supports the currently supported versions of Ruby (MRI) and JRuby. It may support unsupported versions of Ruby or JRuby, but such support may be dropped in any minor version if keeping it becomes a support issue. The minimum Ruby version required to run the current version of Roda is 1.9.2, and the minimum JRuby version is 9.0.0.0.
License¶ ↑
MIT
Maintainer¶ ↑
Jeremy Evans <code@jeremyevans.net>