z, ? | toggle help (this) |
space, → | next slide |
shift-space, ← | previous slide |
d | toggle debug mode |
## <ret> | go to slide # |
r | reload slides |
n | toggle notes |
Hello everyone. It is an honor to be able to present to RubyConf Thailand. My presentation today is on Roda, Ruby’s fourth most popular web framework, after Rails, Sinatra, and Grape. Roda was released back in 2014, so it is over 8 years old now.|Roda is focused on 4 goals: simplicity, reliability, extensibility, and performance. In this presentation, I will discuss how Roda achieves these goals, and why you may want to use it in your applications.
My name is Jeremy Evans. I am a Ruby committer who focuses on fixing bugs in Ruby.
I am also the author of Polished Ruby Programming, which was published last year. This book is aimed at intermediate Ruby programmers and focuses on teaching principles of Ruby programming, as well as trade-offs to consider when making implementation decisions.
What differentiates Roda from most other Ruby web frameworks,
is that Roda is based on the concept of a routing tree built out of Ruby blocks.
Here’s what that looks like. The routing tree integrates routing with request handling, which has multiple advantages compared to routing approaches used by other Ruby web frameworks.
route do |r|
r.on "album", Integer do |album_id| # /albums/:id branch
next unless @album = Album[album_id]
r.is do # /albums/:id route
r.get do # GET /albums/:id route
end
r.post do # POST /albums/:id route
end
end
r.get "tracks" do # GET /albums/:id/tracks route
end
end
end
You use Roda’s route method to set the routing tree for the application.
route do |r|
r.on "album", Integer do |album_id| # /albums/:id branch
next unless @album = Album[album_id]
r.is do # /albums/:id route
r.get do # GET /albums/:id route
end
r.post do # POST /albums/:id route
end
end
r.get "tracks" do # GET /albums/:id/tracks route
end
end
end
All requests to the web application are yielded to the routing tree block. Roda’s convention is to use r as the name for the route block variable. Unlike most other Ruby web frameworks, where you do not have control over the details of the routing process, with Roda, you fully control how routing happens.
route do |r|
r.on "album", Integer do |album_id| # /albums/:id branch
next unless @album = Album[album_id]
r.is do # /albums/:id route
r.get do # GET /albums/:id route
end
r.post do # POST /albums/:id route
end
end
r.get "tracks" do # GET /albums/:id/tracks route
end
end
end
You control routing by calling methods on the request object. Here, r.on will yield to the block if all of the arguments passed to the method match the request. So if the request path starts with /foo, this will not match, and routing will continue after the method. However, if the request path starts with /album/ followed by some number, this will match, that part of the path will be consumed, and the block passed to the method will be called.
route do |r|
r.on "album", Integer do |album_id| # /albums/:id branch
next unless @album = Album[album_id]
r.is do # /albums/:id route
r.get do # GET /albums/:id route
end
r.post do # POST /albums/:id route
end
end
r.get "tracks" do # GET /albums/:id/tracks route
end
end
end
Because the Integer class was used, if this matches, the number will be yielded to the block, as an integer. This makes it simple to extract data from the request path, instead of having to reference into a hash of parameters.
route do |r|
r.on "album", Integer do |album_id| # /albums/:id branch
next unless @album = Album[album_id]
r.is do # /albums/:id route
r.get do # GET /albums/:id route
end
r.post do # POST /albums/:id route
end
end
r.get "tracks" do # GET /albums/:id/tracks route
end
end
end
This is the line that shows the true power of Roda. At any point during routing, since you are writing the routing code, you can implement your own behavior. In this case, we are using the integer taken from the request path, and trying to find a matching album. If we find the album, we set the album instance variable, which all routes inside this branch can use.|This ability to share logic and perform arbitrary actions at any point during routing is what makes Roda applications significantly simpler than web applications written in other frameworks.
route do |r|
r.on "album", Integer do |album_id| # /albums/:id branch
next unless @album = Album[album_id]
r.is do # /albums/:id route
r.get do # GET /albums/:id route
end
r.post do # POST /albums/:id route
end
end
r.get "tracks" do # GET /albums/:id/tracks route
end
end
end
If we find a matching album, routing continues. The next method called is r.is with no arguments, which will only match if the request path has already been completely consumed. So this will match for requests such as /albums/1, but not /albums/1/tracks.
route do |r|
r.on "album", Integer do |album_id| # /albums/:id branch
next unless @album = Album[album_id]
r.is do # /albums/:id route
r.get do # GET /albums/:id route
end
r.post do # POST /albums/:id route
end
end
r.get "tracks" do # GET /albums/:id/tracks route
end
end
end
Assuming the request path was fully consumed, the block passed to r.is will be called. Inside that block, we have calls to r.get and r.post. r.get will yield if the request method is GET, and r.post will yield if the request method is POST. Inside the r.get and r.post blocks are where you would put the code to handle the related routes, which can both use the album instance variable.
route do |r|
r.on "album", Integer do |album_id| # /albums/:id branch
next unless @album = Album[album_id]
r.is do # /albums/:id route
r.get do # GET /albums/:id route
end
r.post do # POST /albums/:id route
end
end
r.get "tracks" do # GET /albums/:id/tracks route
end
end
end
Assume the request path is /albums/1/tracks. In that case, the r.is method call will not match, since the request path was not fully consumed by the time r.is was called. In that case, r.is returns without yielding to the block, and routing will continue to the next statement,
route do |r|
r.on "album", Integer do |album_id| # /albums/:id branch
next unless @album = Album[album_id]
r.is do # /albums/:id route
r.get do # GET /albums/:id route
end
r.post do # POST /albums/:id route
end
end
r.get "tracks" do # GET /albums/:id/tracks route
end
end
end
r.get with an argument of tracks. This will match only if the request method is GET, and the remaining part of the request path not yet consumed is /tracks.|So if the request path is /albums/1/tracks, and the request method is GET, this will match, because the /albums/1 part of the path had already been consumed by the r.on call, and the r.get method call will match the remaining /tracks.
route do |r|
r.on "album", Integer do |album_id| # /albums/:id branch
next unless @album = Album[album_id]
r.is do # /albums/:id route
r.get do # GET /albums/:id route
end
r.post do # POST /albums/:id route
end
end
r.get "tracks" do # GET /albums/:id/tracks route
end
end
end
Hopefully that gives you a flavor for how routing works in Roda.
route do |r|
r.on "album", Integer do |album_id| # /albums/:id branch
next unless @album = Album[album_id]
r.is do # /albums/:id route
r.get do # GET /albums/:id route
end
r.post do # POST /albums/:id route
end
end
r.get "tracks" do # GET /albums/:id/tracks route
end
end
end
The most important part to remember is that Roda gives you the ability to run arbitrary code at any point during the routing process.
route do |r|
r.on "album", Integer do |album_id| # /albums/:id branch
next unless @album = Album[album_id]
r.is do # /albums/:id route
r.get do # GET /albums/:id route
end
r.post do # POST /albums/:id route
end
end
r.get "tracks" do # GET /albums/:id/tracks route
end
end
end
I mentioned that Roda focuses on simplicity, reliability, extensibility, and performance. Of these, performance is the most objective advantage, in that you can directly compare the performance difference between Roda and other Ruby web frameworks.
Tech Empower has a fairly well known set of web framework benchmarks. These are the results of the benchmarks when using the Puma web server. As shown here, the combination of Roda with the Sequel database library is the fastest. It’s about 60% faster than Sinatra with Sequel, and over 5 times faster than Rails. One thing to be aware of is that Tech Empower’s benchmarks only test applications with a small number of routes.
It’s also useful to benchmark an application with a large number of routes. The r10k benchmark uses applications with 10, 100, 1000, and 10,000 routes to check for routing scalability. To avoid web server overhead, it tests using the rack API directly.
Here are the runtime results for Roda, Sinatra, Rails, and Hanami. Pay no attention to the absolute numbers, as it is only the relative performance differences that matter.|While the graph makes it obvious Roda is much faster, it’s hard to see how much faster. In the benchmark, Roda is about 13-670 times faster than Sinatra, 40-75 times faster than Rails, and 5-8 faster than Hanami, depending on the number of routes.
In terms of memory usage, Roda always uses the least amount memory, with about 15 to 65% less memory at 10 route, and 55 to 60% less memory at 10,000 routes.
I think most of us know that performance differences in benchmarks are often not a good indication of performance differences in production applications. I have converted multiple production Rails applications to Roda. My experience is that Roda is about twice as fast as Rails for the same production application, while using a third less memory.
Roda easily wins on performance, but to me, the larger advantage is that Roda allows you to write simpler code to implement your web application.|When you can write simpler code, you are likely to decrease the number of bugs in your application, and make it easier to fix those bugs and add features.
The simplicity advantage that Roda offers over most other Ruby web frameworks is due to its integration of routing and request handling. Roda recognizes that routing a request is not an end in itself, it is purely a means to make sure the request is handled correctly.|With a routing tree, routing is not separate from request handling, the two are integrated. So as you are routing a request, you can also be handling the request. In most other Ruby web frameworks, routing is separate from request handling.
The advantages of the integration of routing and request handling may not be obvious. I gave a brief example earlier, but I am going to discuss the integration in more detail now, and then discuss what web frameworks that lack this integration offer in terms of similar functionality.
Let me first start with some example Sinatra code. Here we have two routes, both related to a specific album, one for GET and one for POST. When I was using Sinatra, this was pretty typical in many of my applications.
get '/albums/:id' do
@album = Album[params[:id].to_i]
erb(:album)
end
post '/albums/:id' do
@album = Album[params[:id].to_i]
@album.update(params[:album])
redirect "/albums/#{@album.id}"
end
Sinatra’s approach leads to duplication. Here you see the path is duplicated in both of the routes.
get '/albums/:id' do
@album = Album[params[:id].to_i]
erb(:album)
end
post '/albums/:id' do
@album = Album[params[:id].to_i]
@album.update(params[:album])
redirect "/albums/#{@album.id}"
end
The conversion of the parameter from a string to an integer, and the retrieval of the album from the database, is also duplicated in both of the routes.
get '/albums/:id' do
@album = Album[params[:id].to_i]
erb(:album)
end
post '/albums/:id' do
@album = Album[params[:id].to_i]
@album.update(params[:album])
redirect "/albums/#{@album.id}"
end
Using a routing tree, you can simplify things.
r.is 'albums', Integer do |id|
@album = Album[id]
r.get do
view(:album)
end
r.post do
@album.update(params[:album])
redirect "/albums/#{@album.id}"
end
end
Instead of duplicating the path in both cases, it is specified once in the branch. Additionally, by using the Integer class argument, the conversion of the parameter to integer happens automatically. Another advantage of using the Integer class argument is that this route will only match if the id is provided as an integer, it will not match in other cases.
r.is 'albums', Integer do |id|
@album = Album[id]
r.get do
view(:album)
end
r.post do
@album.update(params[:album])
redirect "/albums/#{@album.id}"
end
end
As soon as the branch is taken, the album is retreived from the database.
r.is 'albums', Integer do |id|
@album = Album[id]
r.get do
view(:album)
end
r.post do
@album.update(params[:album])
redirect "/albums/#{@album.id}"
end
end
In both the get and post routes, the album instance variable is available for use.
r.is 'albums', Integer do |id|
@album = Album[id]
r.get do
view(:album)
end
r.post do
@album.update(params[:album])
redirect "/albums/#{@album.id}"
end
end
So one of the primary advantages of a routing tree is that it allows you to easily eliminate redundant code, by moving it to the highest branch where it is shared by all routes under that branch.
r.is 'albums', Integer do |id|
@album = Album[id]
r.get do
view(:album)
end
r.post do
@album.update(params[:album])
redirect "/albums/#{@album.id}"
end
end
Now, it is possible to do something similar in Sinatra. You can use before blocks in Sinatra and provide a path to the before block, and Sinatra will iterate over all of the before blocks before routing the request, checking each to see if the request path prefix matches the before block. If so, it will yield to the before block. So using a before block, you can still convert the parameter to integer and retrieve the album from the database in a single place.
before '/albums/:id' do
@album = Album[params[:id].to_i]
end
get '/albums/:id' do
erb(:album)
end
post '/albums/:id' do
@album.update(params[:album])
redirect "/albums/#{@album.id}"
end
However, now you need to specify the path itself three times, instead of just once.
before '/albums/:id' do
@album = Album[params[:id].to_i]
end
get '/albums/:id' do
erb(:album)
end
post '/albums/:id' do
@album.update(params[:album])
redirect "/albums/#{@album.id}"
end
Unlike when using a routing tree, the shared behavior is in a separate lexical scope, which makes it more difficult to understand the connection between the shared behavior and the two routes. The two routes are also in separate lexical scopes, which makes it more difficult to understand how they are connected.|Additionally, using before blocks like this in Sinatra has significant negative effect on performance.
before '/albums/:id' do
@album = Album[params[:id].to_i]
end
get '/albums/:id' do
erb(:album)
end
post '/albums/:id' do
@album.update(params[:album])
redirect "/albums/#{@album.id}"
end
In Rails, you specify the routes in the config/routes.rb file, and the code to handle the routes goes in a controller class in a separate controller file, using a separate method per route.|This separation of routing code and controller code is one of the things I dislike about Rails, as it adds significant conceptual overhead, since it takes more work to figure out where the code that handles a route will be located.
def show
@album = Album.find(params[:id])
render(action: 'edit')
end
def update
@album = Album.find(params[:id])
@album.update_attributes(params[:product])
redirect_to @album
end
As in the initial Sinatra example, this approach duplicates the parameter conversion and retrieval of the album from the database in both methods.
def show
@album = Album.find(params[:id].to_i)
render(action: 'edit')
end
def update
@album = Album.find(params[:id].to_i)
@album.update_attributes(params[:product])
redirect_to @album
end
Rails also offers a way to eliminate the redundant code, using a before filter to specify a method to call before the action, for a given set of actions.
before_filter :find_album, only: [:show, :update]
def show
render(action: 'edit')
end
def update
@album.update_attributes(params[:product])
redirect_to @album
end
private
def find_album
@album = Album.find(params[:id].to_i)
end
The main issue with this approach is that if you add more routes where you want to retrieve the album, you need to remember to update the only option to the before filter.|Also, just like when you try to share behavior in Sinatra, the shared behavior is in a separate lexical scope, which makes it more difficult to understand how it is connected to the route handling methods.
before_filter :find_album, only: [:show, :update]
def show
render(action: 'edit')
end
def update
@album.update_attributes(params[:product])
redirect_to @album
end
private
def find_album
@album = Album.find(params[:id].to_i)
end
So Sinatra and Rails and most other Ruby web frameworks can use before filters to emulate code placed at the top of a routing tree block. However, how can you handle more complex cases? Assume you want to run code only for some of the routes in a branch, but not run code for other routes in a branch. As Roda’s routing tree is directly executed for each request, you can run arbitrary code at any point during routing, not just at the top of the blocks.
route do |r|
r.post 'login' do
session[:logged_in] = true
end
require_login!
r.on 'albums' do
# ...
end
end
One of the common places where this is useful is when doing access control. For example, if part of your site allows anonymous access, and part of your site does not, you can place the part that allows anonymous access first, and then run the check for a login, and then have the rest of the routes where anonymous access is not allowed.
route do |r|
r.post 'login' do
session[:logged_in] = true
end
require_login!
r.on 'albums' do
# ...
end
end
Note that this is an issue with most sites that support authentication, since the login action must be available to users who are not already authenticated.
route do |r|
r.post 'login' do
session[:logged_in] = true
end
require_login!
r.on 'albums' do
# ...
end
end
This type of access control is kind of a pain to handle in Sinatra. When I was using Sinatra, the usual way I would handle this would be to specifically whitelist each path or prefix that allowed anonymous access. This works OK if you only have a small number of paths that allow anonymous access, but quickly becomes difficult if you have a large number of separate paths that allow anonymous access.
before do
unless env['PATH_INFO'] =~ \
%r{\A(/login|/foo|/bar/baz)}
require_login!
end
end
Implementing this type of access control is also more complex in Rails. Usually in Rails, this would be handled by using a before filter in ApplicationController that required a login.
ApplicationController.before_filter :require_login!
Then, in each controller where you want to allow anonymous access, you need to skip the before filter. This spreads the access control handling to multiple places in the application, and again requires you to specifically whitelist all of the allowed actions.
ApplicationController.before_filter :require_login!
LoginController.skip_before_filter :require_login!
FooController.skip_before_filter :require_login!, \
except: [:index]
BarController.skip_before_filter :require_login!, \
except: [:baz]
For individual routes, these improvements may seem small. However, in my experience, the majority of routes in an application benefit from the ability to share logic using a routing tree. For a large application with many routes, all of the improvements add up and result in a much simpler application.|I analyzed a small application I originally developed in Sinatra and later converted to Roda.
The application has 72 total routes.
To get to those 72 routes, there are a total of 35 branches in the routing tree.
Of the 35 branches, 29 contained code that was shared by all routes under the branch.
That means that 83% of the time there are branches in the routing tree, Roda’s integration of routing and request handling resulted in the elimination of redundant code.
It also shows that Roda’s use of a routing tree eliminates 29 separate before filters that would be needed if the application used Sinatra or Rails and wanted to avoid redundancy.
Using a routing tree makes it natural to share code for all routes under a branch, so web applications that use a routing tree naturally tend to avoid redundant code. Using before filters to eliminate redundant code is not natural in most other web frameworks, so even though it is possible, it often is not done, and the natural approach leads to redundant code.
One big problem I have seen related to redundant code in other web frameworks is that the redundant code is not always consistent. It is common to have two similar routes where you want to share some behavior. However, over time, you make a change in only one route instead of in both routes.
It is especially bad when the inconsistency is related to access control, because when that happens, it often results in a security vulnerability in the application. Avoiding redundancy and inconsistency does not eliminate security issues, but it does help to reduce them.
Another aspect of simplicity is how simple it is to handle upgrades to the framework.
Some web frameworks radically change their API between versions, making upgrading to a new version difficult. This is an example of revolutionary change. Roda rejects revolutionary change,
and chooses evolutionary change instead. To make upgrading simple for the user, Roda ships a new minor release every month, usually adding a new plugin, feature, or optimization. When Roda does break compatibility in major version upgrades, it includes backwards compatibility plugins. Almost all Roda 1 applications built in 2014 could run on the current version of Roda with the addition of a plugin or two.
Now that I’ve discussed Roda’s simplicity advantages, I am going to talk about reliability.
One way to look at reliability is in terms of the framework itself being reliable. You could call this internal reliability. Part of Roda’s reliability comes from the fact that it has 100% line and branch coverage of all code.
While internal reliability is important, it’s probably more important to you that the web framework you use allows you to write more reliable applications. Roda has two features that result in your applications being more reliable.
One way that Roda can make your applications more reliable is allowing them to be frozen at runtime. By freezing your Roda application after it is configured but before accepting requests, you can eliminate most issues caused by the application being modified at runtime. Roda pioneered the approach of freezing web applications at runtime years ago, and as far as I know, it is still the only Ruby web framework to support and encourage being frozen at runtime.
One unexpected advantage of freezing Roda applications is that Roda can perform additional optimizations for frozen applications, by inlining methods where it knows the implementation has not been modified. This type of optimization is only safe when freezing an application, because no further changes can be made after the application is frozen.
Another way Roda increases reliability is to avoid polluting the scope of your application with unnecessary instance variables and methods. Roda believes you should be able to use the instance variables and methods you want in your application.
One of my production applications deals with many different types of requests, such as requests for time off. Another application deals with responses received from other companies.
r.get '/time_off', Integer do |request_id|
@request = TimeOffRequest[request_id]
view(:time_off)
end
r.get '/response', Integer do |response_id|
@response = CompanyResponse[response_id]
view(:response)
end
It is natural in my application to store a time off request in an instance variable named request,
r.get '/time_off', Integer do |request_id|
@request = TimeOffRequest[request_id]
view(:time_off)
end
r.get '/response', Integer do |response_id|
@response = CompanyResponse[response_id]
view(:response)
end
and a company response in an instance variable named response, since these will be the instance variables used in the related templates. And this approach works just fine in Roda.
r.get '/time_off', Integer do |request_id|
@request = TimeOffRequest[request_id]
view(:time_off)
end
r.get '/response', Integer do |response_id|
@response = CompanyResponse[response_id]
view(:response)
end
Unfortunately, if you are using Sinatra, this approach does not work. Here’s the equivalent Sinatra code.
get '/time_off/:request_id' do
@request = TimeOffRequest[params[:request_id].to_i]
erb(:time_off)
end
get '/response/:response_id' do
@response = CompanyResponse[params[:response_id].to_i]
erb(:response)
end
This does not work because Sinatra uses the request instance variable internally to store information related to the HTTP request. So if you do this, Sinatra will raise an exception later.
get '/time_off/:request_id' do
@request = TimeOffRequest[params[:request_id].to_i]
erb(:time_off)
end
get '/response/:response_id' do
@response = CompanyResponse[params[:response_id].to_i]
erb(:response)
end
Similarly, Sinatra stores the HTTP response in the response instance variable. So if you set the response instance variable, Sinatra will raise an exception later.
get '/time_off/:request_id' do
@request = TimeOffRequest[params[:request_id].to_i]
erb(:time_off)
end
get '/response/:response_id' do
@response = CompanyResponse[params[:response_id].to_i]
erb(:response)
end
Roda avoids the problems Sinatra has by prefixing all instance variables used internally with an underscore. Rails uses a similar approach.
r.get '/time_off', Integer do |request_id|
@request = TimeOffRequest[request_id]
view(:time_off)
end
r.get '/response', Integer do |response_id|
@response = CompanyResponse[response_id]
view(:response)
end
Unfortunately, when it comes to method pollution, Rails does not fair nearly as well.
As of Rails 7, inside a Rails controller action, there are over 300 additional methods not prefixed by an underscore, beyond the methods defined by default by Ruby in Object. If you override any of these methods, you can potentially cause problems with the framework and break things.
Inside the Roda routing tree, there are only 6 additional methods defined by default. This reduces the chance that you’ll want to define a method that the framework uses.
So those are the three approaches Roda uses to achieve high reliability. First, maintain 100% line and branch coverage. Second, run frozen in production to avoid possible thread-safety issues. Third, avoid polluting the execution environment with instance variables and methods the user may want to use.
Now that I have discussed how Roda achieves its goals of performance, simplicity, and reliability, we can discuss the final goal, which is extensibility. Roda’s goal of extensibility means that Roda has a very small core, which is focused on routing requests via the request method and path.
All non-core features are added via plugins. Roda ships with over 100 plugins, and there are many plugins that are shipped in external gems. Plugins can add methods to the scope of the route block, as well as methods to the request and response classes.
Each Roda plugin is similar to a tool, and Roda ships with a large toolkit. Some web applications may require HTML template rendering, and other web applications may require the ability to return data in JSON format.|When using Roda, you choose the appropriate tools from Roda’s toolkit, and build your application using those plugins. You do not have to pay the cost for any tools that you are not using. One of Roda’s core tenets is that you only pay for what you use.
I’m now going to highlight some of the plugins that ship with Roda, and the features they add.
Roda ships with support for a complete view layer, using the render plugin. The render plugin likely supports any template engine you would like to use. Like Rails, Roda’s default template engine is ERB.|The render plugin has extensive support for compiled templates, ensuring that template rendering is as fast as possible. It even uses compiled template support in development mode to increase development velocity.
If you want to handle automatic generation of JavaScript and/or CSS during development, and compile them into single files in production, Roda ships with an assets plugin for that. Roda’s assets plugin is simple to configure and does not require any alternative language runtimes such as node installed, unless such a runtime is required by the asset template engine you choose.
If you want to serve static files from a directory, Roda has a public plugin that supports that. Because Roda exposes the static file serving using a routing tree, it is possible that have static files served only if the request has passed access control checks.
In some cases, you may have multiple directories used to serve static files, such as to support different file types or to serve to different classes of users. Roda has a multi_public plugin that supports that, so you can serve separate static directories at different points in the routing tree.
As an example of how minimal Roda is, Roda does not have a method for HTML escaping by default. Roda ships with an h plugin that adds an h method for HTML escaping.
For API applications that are designed to return JSON instead of HTML, Roda ships with a json plugin. The json plugin allows your routing tree blocks to return hashes or arrays, which will be automatically converted to JSON and used as the response.
If your application needs to accept JSON input, Roda comes with a json_parser plugin, which will parse request bodies submitted in JSON format and treat them as submitted parameters.
Roda uses a single route block by default. Since you cannot have a Ruby block that spans multiple files, this means that all routing must happen in a single Ruby file, which is only appropriate for small web applications.|Roda has multiple ways of splitting the routing tree into multiple Ruby blocks stored in separate files, in order to support larger applications. The most common plugin for this is hash_branches, which allows using a separate block and file for each top-level branch of the routing tree. For very large web applications, hash_branches can be used in a nested format to support arbitrarily complex routing trees.
Roda ships with a content_security_policy plugin, to allow you to easily configure an appropriate security policy for your application, which can be customized as needed in each routing branch.
One of the largest security issues in Ruby web applications comes from incorrectly handling submitted parameters. Due to the fact that Ruby uses dynamic typing, it is easy for attackers to submit unexpected types in parameters.|Roda ships with a typecast_params plugin that handles almost all parameter typecasting needs, allowing you to convert submitted parameters to the expected types before the parameters are used.
Roda ships with a route_csrf plugin that implements strong cross site request forgery protection, so that all forms need to be submitted with a token valid for the current session, request method, and request path. The CSRF checks can be performed at any point in the routing tree, making it easy to check CSRF tokens for some requests and not others.|The protection offered by the route_csrf plugin is significantly stronger that the CSRF protection Rails uses even if you configure Rails to use per form CSRF tokens, since Rails will still accept generic CSRF tokens in that case.
Finally, Roda includes a sessions plugin for encrypted cookie sessions. This checks for a valid HMAC before attempting to decrypt a submitted cookie, avoiding timing and other cryptographic attacks on sessions.
To sum up, Roda has a small core, where almost all features are supported via plugins, so you only have to pay the cost for the plugins you use in your application. This is in contrast to Rails, which ships with all features enabled by default, where you have to choose which high-level features you want to disable. This is also in contrast to Sinatra, which does not include support for many features that are shipped with Roda.
Some Ruby programmers believe that if you use something that is not Rails, you end up having to rebuild most of what Rails gives you. That may be true with Sinatra, but definitely is not true with Roda. In some cases, Roda ships with an equivalent to features that Rails offers. In other cases, there are superior third-party libraries that work with both Rails and Roda. I will briefly go over the different parts of Rails, and what the equivalent could be for Roda.
ActionPack is the heart of Rails, implementing the routing and handling of requests.
Core Roda and the many of the routing plugins that ship with Roda are a direct replacement for ActionPack.
ActionView is what Rails uses for template rendering.
Roda’s render plugin offers equivalent functionality.
ActionMailer is what Rails uses to send email.
Roda has a mailer plugin for that. The mailer plugin uses the routing tree to route requests to send email, allowing similar emails to share code. This has the same benefits that the routing tree offers for web requests, resulting in simpler email generation code.
ActionMailbox is what Rails uses to process received email.
Roda has a mailbox_processor plugin for that. The mailbox_processor plugin uses a modified routing tree approach to share logic during the processing of received emails, which results in simpler email processing code.
So those four parts of Rails have direct equivalents in Roda. Now let’s look at some other parts of Rails that do not have direct equivalents in Roda, but can be handled by superior third party libraries.
Rails uses ActiveRecord for database access.
With Roda, you would probably want to use the Sequel database library, as it is significantly faster and has more features than ActiveRecord. Sequel also uses a similar plugin system design where you only pay for what you use.
Rails uses ActiveModel as an abstraction layer for model objects, handling things like validations.
Sequel supports many of the same features as ActiveModel, and can comply with the ActiveModel API using the Sequel active_model plugin.
ActionCable is what Rails uses to implement Websockets support.
In general, you can replace ActionCable with AnyCable, which offers much better performance.
Rails uses ActiveStorage to handle and process uploaded files.
Shrine is a superior third party library for handling uploaded files, and it supports both Rails and Roda.
Rails uses ActiveJob as an abstraction layer for various job libraries.
Unless you really need such an abstraction layer, you can avoid the overhead and use the native API for the job library you are using, such as Sidekiq.
Rails uses ActionText to handle rich text content editing, which uses the Trix JavaScript library to implement the editor.
You can replace ActionText with CKEditor on the JavaScript side, and storing data using Sequel instead of ActiveRecord.
The final piece of Rails is ActiveSupport, which modifies many of Ruby’s core classes, and often ends up breaking things in libraries not designed around usage with Rails.
In general, you can replace ActiveSupport with Ruby’s core classes and standard library. In my opinion, one of the best parts about using any web framework other than Rails is that you are not forced into using ActiveSupport.
So if you are familiar with Rails,
Hopefully you now have a better idea about how you can handle all of the same needs using Roda.
If you want to get starting using Roda, there is a free online book named Mastering Roda. This book was originally written by Federico Iachetti, and I now keep it up to date with changes in Roda.
If you enjoyed this presentation, and want to read more of my thoughts on Ruby programming, consider picking up a copy of Polished Ruby Programming.
That concludes my presentation. I want to thank you all for listening to me talk about Roda. I’m out of time, so if you have questions, please ask me during the next break.