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 great to be here at RubyConf. This talk is entitled “Roda: The Routing Tree Web Framework”.
My name is Jeremy Evans, and I am the lead developer of the Roda web framework. I’m also the lead developer of the Sequel ruby database library.
I think I should start off explaining why I created Roda. After all, there are a lot of ruby web frameworks, so there should be a good reason for creating another one, especially if you want to recommend that other people use it. To explain why I created Roda, I need to go back about 10 years, to when I started using Ruby.
I first got into Ruby in late 2004, about 6 months after Rails was first released. I think 0.8.5 was the first Rails version I used. I found that Rails made web development much easier than the frameworkless PHP I was generally using for web development at the time.|I used Rails for pretty much all of my web development for a few years, but I gradually got disillusioned by the complexity of Rails.
When I found out about Sinatra in late 2007, I was amazed with the simplicity. The fact that you just specified the routes you wanted to handle and they just yielded to a block to handle the action just made developing web applications so much simpler.|In April 2008, I starting using Sinatra for all of my new web applications. Over the years, I found that as the applications I was working on got more complex, I ended up with a lot of duplication in my Sinatra routes. Since my only real basis of comparison was Rails, and the Sinatra code was still a lot simpler than what I would have written in Rails, I didn’t really consider this too much of an issue.
In July of this year, I was looking at a comparison of a bunch of lesser known ruby web frameworks, and read about Cuba. Cuba has been around since 2010, but this was my first exposure to it. Anyway, as I read about Cuba, I saw how it addressed the duplication issues that I was seeing in my more complex Sinatra applications. From reviewing some benchmarks, I also saw that Cuba was much faster than Sinatra.|After trying to convert one of my simpler Sinatra applications to Cuba, I found there were quite a few things I didn’t like about Cuba, that were easier in Sinatra. I came to the conclusion I should create a new web framework based on Cuba that borrowed many features from Sinatra, along with some ideas I had about extensibility and nonpollution, trying to get the best web framework for the types of web applications that I develop.
And that’s the story of Roda’s creation. When I originally decided to fork Cuba, I hadn’t decided on a name, so the temporary name I used was Sinuba, since it borrowed features from Sinatra and Cuba. As I was laying in bed one night thinking of a name, I thought about what made the framework special. In my mind, the main difference between Roda and Sinatra was that in Sinatra you are iterating over an array of possible routes, while in Roda, the routing process is broken down and generally takes the form of a tree.|So Roda is named after the Roda Trees, which appear in the Ys video game series and help the main characters accomplish their goals. This picture is from Ys Origins, and shows a Roda Tree prominently.
As I said, the main feature that separates Roda from most other ruby web frameworks is that it is designed around the idea of a routing tree. Since most programmers are not familiar with the routing tree concept, I’m first going to talk about what a routing tree is and how it works.
Before I can talk about a routing tree, I need to talk about routing in general.
In general, routing a request is the process of taking the request and finding the code that should handle that request. While routing can consider any aspect of the request, in most cases only two parts of the request will be used during routing.
These two parts are both contained in the HTTP request line, which is the first data transmitted from the client to the server during an HTTP request.
An HTTP request line looks like this. Here, GET is the request method, and /albums/1/tracks is the request path. It is possible to use other parts of the request during routing, such as routing using information from the request headers or the request body, but in most cases only the request method and request path are used.
GET /albums/1/tracks HTTP/1.1
Sinatra and other similar ruby web frameworks look at the full path of the request when deciding how to route a request. The iterate over their array of possible routes, checking each route to see if it matches the current request. A routing tree handles things differently, routing by segments of the path.
GET /albums/1/tracks HTTP/1.1
When routing a request for /albums/1/tracks, a routing tree is going to look at the first segment, albums. If the /albums segment doesn’t match, it skips the albums branch, so that other routes under /albums are not considered.
GET /albums/1/tracks HTTP/1.1
If the /albums segment matches, it looks inside the /albums branch of the tree for a route for /1/tracks, ignoring other branches.|Incidentally, this is more or less how a file system works. When you ask a file system to open a file, it doesn’t compare every path in the file system and see if it matches the requested path. It looks at the first segment in the path, sees if it is a directory, and if so, it looks for the rest of the path inside that directory.
GET /albums/1/tracks HTTP/1.1
If the use of a routing tree was purely a performance issue, it wouldn’t be all that interesting. What makes routing trees interesting is that at any point during the routing, you can operate on the current request.
If any of you have used the Ragel state machine compiler, this is similar to Ragel’s ability to execute arbitrary code during parsing.
So now that I’ve briefly gone over what a routing tree is, let me go over how Roda implements the routing tree concept.
Let’s start with Hello World, since that’s the simplest case. This Roda app will return Hello World! as the response body for every request. While this doesn’t show off the routing tree aspects of Roda, it does show that Roda gets the response body from the value the block returns, similar to Sinatra.
Roda.route do |r|
"Hello World!"
end
In almost all Roda apps, you are going to want to actually use Roda’s routing tree methods. The first routing tree method I’m going to discuss is called r.on, which creates branches in the routing tree. Here, you call r.on with the string albums, which will match the current request if the request path starts with albums. Let me break down what is going on here.
Roda.route do |r|
r.on "albums" do
"Hello Albums"
end
end
In the first line, we are calling the Roda.route method, which is the start of the routing tree. All requests that come to the web application are yielded to the block you pass to Roda.route. The block is yielded a Rack::Request instance with some additional methods. By convention, the block argument is named r.
Roda.route do |r|
r.on "albums" do
"Hello Albums"
end
end
The additional methods added to the Rack::Request instance are related to routing the request. As mentioned earlier, the request’s on method is used to create branches in the routing tree. Any arguments you pass to this method are called matchers, and are used to match the current request.|In this case, a single matcher is given, which is the string albums. In Roda, string matchers match the first segment in the request path. So if the request path starts with /albums, this will match, and the request will be routed to the block passed to on. That block returns a Hello Albums string, which Roda will use as the response body.
Roda.route do |r|
r.on "albums" do
"Hello Albums"
end
end
If the request path is /artists, this will not match, so r.on will return nil without yielding to the block, and execution will continue after the call. In this case, there is nothing after the call to r.on, so the return value of the Roda.route block will be nil. Since the block didn’t return a string, Roda will use a 404 response status code with an empty response body.|This provides the principal of least surprise, where if you don’t specifically handle a request, an empty 404 response is used.
Roda.route do |r|
r.on "albums" do
"Hello Albums"
end
end
While it works, this code has issues, because it returns the same response for all paths under /albums, including /albums/should/not/exist. In general, that isn’t what you want. What you probably want to do is return a 404 response for any path that you don’t specifically handle.
Roda.route do |r|
r.on "albums" do
# /albums
# /albums/should/not/exist
"Hello Albums"
end
end
So if you only want to handle /albums and not any paths below /albums, you can use the r.is method. r.is is similar to r.on, but it does a terminal match, so it only matches if the request path is empty after processing the matchers. So this code will not match the /albums/should/not/exist path.|The reason for the method naming here is that r.on matches ON the route’s prefix, while r.is matches only if the match IS complete.
Roda.route do |r|
r.is "albums" do
# /albums
"Hello Albums"
end
end
So routing trees are built in Roda using a combination of the r.on and r.is methods. r.on does prefix matching on the request path, and r.is does full matching on the request path.|If you were routing purely based on the request path, then you could say that r.on creates branches in the routing tree, and r.is creates leaf nodes.|Here, r.on albums creates a branch, handling all paths under /albums.
Roda.route do |r|
r.on "albums" do
r.is "list" do
"Hello Albums"
end
end
end
Here you are calling r.is list, which will match only if the current request path is /list. It may seem odd that this works, but the reason it does is that the request path is being modified as the request is being routed.
Roda.route do |r|
r.on "albums" do
r.is "list" do
"Hello Albums"
end
end
end
When a request for /albums/list comes in, the routing tree uses the initial request path.
Roda.route do |r|
# path: "/albums/list"
r.on "albums" do
r.is "list" do
"Hello Albums"
end
end
end
When the r.on albums method matches, it consumes /albums from the front of the request path.
Roda.route do |r|
# path: "/albums/list"
r.on "albums" do
r.is "list" do
"Hello Albums"
end
end
end
So inside of the r.on block, the request path is /list.
Roda.route do |r|
# path: "/albums/list"
r.on "albums" do
# path: "/list"
r.is "list" do
"Hello Albums"
end
end
end
r.is matches only if all of its matchers match and the request path is completely consumed by the matchers. Since the request path is /list and that matches the string given to r.is, this request matches and Hello Albums will be returned.
Roda.route do |r|
# path: "/albums/list"
r.on "albums" do
# path: "/list"
r.is "list" do
"Hello Albums"
end
end
end
If you get a request for /albums/list/all, the r.on call still matches, but the path inside the r.on block is /list/all. Since r.is list does not completely consume the request path, this request will not be matched, and Roda will return an empty 404 response.
Roda.route do |r|
# path: "/albums/list/all"
r.on "albums" do
# path: "/list/all"
r.is "list" do
"Hello Albums"
end
end
end
So far I’ve just been focusing on routing using the request path. As I mentioned earlier, routing usually takes into account the request method as well.|Consider this routing tree, which will handle requests for /albums/new.
Roda.route do |r|
r.on "albums" do
r.is "new" do
r.get do
"Hello Albums"
end
r.post do
"Album Added"
end
end
end
end
In order to handle the GET and POST request methods, Roda has r.get and r.post routing methods. If you call these methods with no arguments, they match against the request method, so r.get matches the GET request method, and r.post matches the POST request method.
Roda.route do |r|
r.on "albums" do
r.is "new" do
r.get do
"Hello Albums"
end
r.post do
"Album Added"
end
end
end
end
Here, a GET request for /albums/new will return Hello Albums.
Roda.route do |r|
# GET /albums/new
r.on "albums" do
r.is "new" do
r.get do
"Hello Albums"
end
r.post do
"Album Added"
end
end
end
end
And a POST request for /albums/new will return Album Added.
Roda.route do |r|
# POST /albums/new
r.on "albums" do
r.is "new" do
r.get do
"Hello Albums"
end
r.post do
"Album Added"
end
end
end
end
So the way a routing tree is usually built in Roda is by combining the r.on, r.is, r.get, and r.post methods. You use the r.on method to branch based on the request path prefix. You use the r.is method for doing a complete match on the path, and you use the r.get or r.post methods to handle different request methods for the same request path.
Roda.route do |r|
# POST /albums/new
r.on "albums" do
r.is "new" do
r.get do
"Hello Albums"
end
r.post do
"Album Added"
end
end
end
end
As I mentioned, if you don’t provide any matchers to the r.get or r.post methods, they just do a simple check against the request method. If you provide any matchers to the methods, they also do a terminal match on the request path, allowing for an API similar to Sinatra.|So a GET request for /albums will be matched by a r.get albums,
Roda.route do |r|
# GET /albums
r.get "albums" do
"Hello Albums"
end
r.post "artists" do
"Album Added"
end
end
and a POST request for /artists will be matched by a r.post artists,
Roda.route do |r|
# POST /artists
r.get "albums" do
"Hello Albums"
end
r.post "artists" do
"Album Added"
end
end
but a POST request for /artists/1 will not be matched by either. It won’t be matched by r.get because it isn’t a GET request, and it won’t be matched by r.post because the request path will not be completely consumed by the matchers.
Roda.route do |r|
# POST /artists/1
r.get "albums" do
"Hello Albums"
end
r.post "artists" do
"Album Added"
end
end
So when you are building a routing tree, if you only want to handle GET requests for /albums/list, and not other request methods, instead of calling r.is list and calling r.get inside of that,
Roda.route do |r|
# GET /albums/list
r.on "albums" do
r.is "list" do
r.get do
"Hello Albums"
end
end
end
end
You just call r.get list, which is a more succinct way of expressing the same routing tree.
Roda.route do |r|
# GET /albums/list
r.on "albums" do
r.get "list" do
"Hello Albums"
end
end
end
Now that we’ve covered the the four basic routing methods, let’s talk about the arguments that you give to these methods, which are called matchers.
We’ve already covered one type of matcher, the string matcher. This matches the verbatim string in the first segment of the request path, so it will match /albums, but not /artists.
Roda.route do |r|
# /albums
r.get "albums" do
"Hello Albums"
end
end
Strings can contain slashes if you want to match multiple segments in the request path. So this matches /albums/list, but not /albums/1.
Roda.route do |r|
# /albums/list
r.get "albums/list" do
"Hello Albums"
end
end
You can use embedded colons in your strings, which will match arbitrary segments in the request path. So this matches both /albums/1 and /albums/2.
Roda.route do |r|
# /albums/1, /albums/2
r.get "albums/:id" do |album_id|
"Hello Album #{album_id}"
end
end
Note that when you use an embedded colon in a string, the text that is matched by that colon is yielded to the block. In Roda, this is the primary way that data from the request path is extracted for use inside the route handling code.
Roda.route do |r|
# /albums/1, /albums/2
r.get "albums/:id" do |album_id|
"Hello Album #{album_id}"
end
end
Another way of specifying the previous matcher is to use a separate symbol matcher, like this. Just as with the embedded colon in the string, it yields the matched segment to the block.
Roda.route do |r|
# /albums/1, /albums/2
r.get "albums", :id do |album_id|
"Hello Albums #{album_id}"
end
end
Yet another way of doing it is using a regular expression. With a regular expression matcher, any regular expression captures are yielded to the block.
Roda.route do |r|
# /albums/1, /albums/2
r.get /albums\/(\d+)/ do |album_id|
"Hello Album #{album_id}"
end
end
There are other types of matchers that allow for more advanced matching, but in the interest of time I won’t be going over them.
I mentioned earlier that one of the main advantages of routing trees is that they have the ability to execute arbitrary code during the routing process. This may not sound important, but it is the main reason that Roda allows for simpler and DRYer code compared to most other ruby web frameworks.
If you want to make sure someone is logged in before accessing these routes, you can just put the code that checks for a login as the first line in the Roda.route block.|This provides a similar feature to the capabilities of a global before filter in Rails or Sinatra, so it doesn’t sound like anything special.
Roda.route do |r|
require_login!
r.on "albums" do
# ...
end
end
Because you can execute code at any point in the routing tree, you can put this check after the routes for logging in. I think this is more elegant than a global before filter that does a login check, which must check that the current path isn’t the login action, or else nobody would be able to login.
Roda.route do |r|
r.post 'login' do
session[:logged_in] = true
end
require_login!
r.on "albums" do
# ...
end
end
This principle of executing code during routing applies at all points of the routing tree. If only certain logged in users should have access to view or update albums, you can have that check near the top of the r.on albums block.
Roda.route do |r|
require_login!
r.on "albums" do
check_albums_access!
# ...
end
end
The most common place this is useful is when dealing with separate request methods for the same request path. Assuming that a GET request for /albums/1 will display a form for editing the album with id 1, and a POST request for /albums/1 will process the form’s input, the routes can share code for retrieving the album, as shown here.|Now, this isn’t revolutionary, and you can accomplish pretty much the same thing using before filters in Rails and Sinatra. In both Rails and Sinatra, the before filters are going to be separated from the code being executed, making it harder to reason about.|Also, if you use this pattern a lot, then you need to add a separate before filter for every set of get and post routes you have, which is cumbersome, and on Sinatra negatively affects performance.
Roda.route do |r|
require_login!
r.on "albums" do
check_albums_access!
r.is :id do |album_id|
@album = Album[album_id]
r.get do
view('album')
end
r.post do
@album.update(params[:album])
end
end
end
end
Speaking of performance, Roda is one of the best performing Ruby web frameworks. With Ruby’s reputation for performance, or lack thereof, you may think that’s similar to saying Roda is one of the fastest turtles, but it is a much faster turtle than Rails or Sinatra.
For a simple hello world app with a single route, Roda is about 2 and a half times faster than Sinatra. The reason for this is that Roda has much lower overhead than Sinatra.
Now, a hello world benchmark doesn’t really tell you how well Roda will perform in real world applications.
My production applications were always faster using Roda than Rails or Sinatra. How much faster depends on the specific action, with simpler actions performing significantly faster, and more complex actions performing about the same, as most of the time for those was spent inside the action, not during routing.
One pleasent surprise was my integration test suites sped up significantly. The exact same rack/test-based integration tests run 50% faster with Roda than Sinatra, and twice as fast after converting from Rails to Roda.
In terms of memory usage, Roda uses about 10MB memory less than Sinatra in terms of just requiring the library, but in my real world apps I’ve only noticed about a 1MB or 2MB decrease in the amount of memory used.|When compared to Rails, it’s a different story. On the largest app I converted from Rails to Roda, I saw memory decrease from 150MB to 80MB per unicorn worker process, and on the second largest, I saw memory decrease from 100MB to 60MB per unicorn worker process.
My largest app is probably about the size of the average Rails app, with about 200 routes, so this doesn’t really tell you if Roda’s approach scales to large applications. Since I thought this would be useful information to know if I was going to advocate that other people use Roda, I decided to see how well Roda would scale to applications with a large number of routes.
You may be familiar with the c10k problem, which was stated by Dan Kegel in 2001, and said that it was time that web servers should be able to serve 10,000 clients simultaneously.
I’m going to state the r10k problem, which is that web frameworks should be able to handle 10,000 routes efficiently. So I wrote a code generator that generates web applications with 10, 100, 1000, and 10,000 routes for Roda, Sinatra, and Rails, and I benchmarked them.
For 10 routes, r10k generates routes from /a to /j, using a single segment per request path.
/a |
/b |
/c |
/d |
/e |
/f |
/g |
/h |
/i |
/j |
For 100 routes, r10k generates routes /a/a to /j/j, using 2 segments per request path.|This approach extends to 1000 routes, which uses 3 segments per request path, and 10,000 routes, which uses 4 segments per request path.|Let’s first look at the results of a comparison of Roda, Rails, and Sinatra at 10, 100, and 1000 routes.
/a/a | /a/b | /a/c | /a/d | /a/e | /a/f | /a/g | /a/h | /a/i | /a/j |
/b/a | /b/b | /b/c | /b/d | /b/e | /b/f | /b/g | /b/h | /b/i | /b/j |
/c/a | /c/b | /c/c | /c/d | /c/e | /c/f | /c/g | /c/h | /c/i | /c/j |
/d/a | /d/b | /d/c | /d/d | /d/e | /d/f | /d/g | /d/h | /d/i | /d/j |
/e/a | /e/b | /e/c | /e/d | /e/e | /e/f | /e/g | /e/h | /e/i | /e/j |
/f/a | /f/b | /f/c | /f/d | /f/e | /f/f | /f/g | /f/h | /f/i | /f/j |
/g/a | /g/b | /g/c | /g/d | /g/e | /g/f | /g/g | /g/h | /g/i | /g/j |
/h/a | /h/b | /h/c | /h/d | /h/e | /h/f | /h/g | /h/h | /h/i | /h/j |
/i/a | /i/b | /i/c | /i/d | /i/e | /i/f | /i/g | /i/h | /i/i | /i/j |
/j/a | /j/b | /j/c | /j/d | /j/e | /j/f | /j/g | /j/h | /j/i | /j/j |
Here are the runtime results for 20,000 requests. Note that these are using the Rack API directly, so this does not include the web server overhead.|As you can see from the Roda and Rails lines, there isn’t a significant performance decrease in runtime performance for either as the number of routes increases. This is because Roda uses a routing tree and Rails uses finite automata for routing requests.|Since Sinatra just iterates over an array of routes, performance decreases linearly as the number of routes increases. At 1000 routes Sinatra is almost as slow as Rails. Regardless of the number of routes, Roda is much faster than either Rails or Sinatra. You could probably tell where this is going, so let’s look at the results with 10,000 routes.
By the time you get to 10,000 routes, Sinatra’s performance is much worse than Rails.|Because of the scale of the graph, you can’t really tell how long Roda is taking at 10,000 routes, but I can tell you it’s less than 5 seconds even for 10,000 routes. Note that these numbers do not include startup time.
Adding startup time doesn’t change the picture in general, except when using 10,000 routes in Rails, where it dramatically increases the time, with Rails taking about as long to startup as it takes to serve 20,000 requests. When trying to use 10,000 routes in Rails, almost all of the startup time spent inside routes.draw. If I had to guess, this is due to building the finite automata for the router.
Part of performance is also the amount of memory used. As mentioned earlier and as you can see here, Roda uses less memory than Sinatra, and significantly less memory than Rails, regardless of the number of routes.
This trend gets even more dramatic when you go from 1000 routes to 10,000 routes. With 10,000 routes, Roda uses less than half the memory of Sinatra, and about a fifth of the memory of Rails. Roda uses less memory with 10,000 routes than Rails uses with 10 routes.
Because benchmarks are worthless unless the source is available for reproduction and criticism, you can find the source code for these benchmarks in my r10k GitHub repository. I encourage anyone to double check my work and make sure I’m not doing anything stupid or unfair towards Sinatra or Rails.
http://github.com/jeremyevans/r10k
So I’ve gone over what I think are some design and performance advantages to using Roda. However, the benefits Roda offers come at a cost.
And that cost is the loss of route introspection. Because all routing in Roda is done at the instance level, you can’t introspect your routes like you can in Sinatra or Rails. In most cases, this doesn’t matter, but there are some applications that rely on introspection of the routes, and those would need to be handled differently with Roda.
Now that I’ve talked about the advantages and disadvantages of using Roda, I’m going to talk a little about the the history of routing tree web frameworks in ruby.
The first routing tree web framework for ruby was Rum, which was written in Christian Neukirchen, the author of Rack, in January 2009. Rum was never released as a gem, and was basically just a proof of concept, showing how you could use a routing tree to route requests.
Cuba was developed by Michel Martens starting in April 2010, and started off as a simple wrapper around Rum with support for haml templates. It’s now expanded to include support for any template library supported by tilt. Cuba was the first routing tree web framework for ruby that was released as a gem, and has enjoyed some modest success, with about 40,000 downloads.
As I mentioned at the start of the presention, Roda was forked from Cuba, because while I liked the idea of using a routing tree, there were various aspects of Cuba’s design and implementation that I didn’t like. I think that forking someone else’s software should really be a last resort, as it’s better to collaborate and work together to achieve a common goal.|Some of these things I didn’t like about Cuba were fundemental to Cuba’s design or philosophy, so it wouldn’t really have been possible to use Cuba and still accomplish the goals I was looking to accomplish with Roda.|In the case where the goals of the the fork are different than the goals of the original software, and you can’t accomplish the goals of the fork without compromising the goals of the original software, I think forking is appropriate.
When I first started using Cuba, in version 3.1.1, status code handling violated the principle of least surprise.
If you had this routing tree in Cuba, a request for /albums with no id would return an empty 200 response, instead of an empty 404 response, because the status code was set to 200 as soon as the on albums method matched.|This was a well known issue with Cuba, with a whole section in the README about how to work around the problem, and there was a pull request to fix the issue that had been open in Cuba’s issue tracker for over 6 months. This was eventually fixed in Cuba about a week after Roda’s initial release, using basically the same approach that Roda uses.
on "albums" do
on :id do
res.write "Hello Album #{album_id}"
end
end
The second signficant issue I had with Cuba was that it didn’t have built-in support for terminal routes.
Cuba has no built in equivalent to Roda’s r.is method, which makes sure there is a terminal match of the request path. When you use Cuba’s on method, it only makes sure that the route has a prefix that matches. So this code will match /albums, but also /albums/1 and /albums/should/not/exist.
on 'albums' do
res.write '1'
end
Cuba’s recommendation if you want a terminal match is to use \z at the end of a regular expression. Let’s just say I didn’t think this was very friendly to the user.
on /albums\z/ do
res.write '1'
end
Another thing I didn’t like about Cuba was that it required explicit writing of response bodies, instead of using the route block return value as the response body, which is how Sinatra operates.
Everytime you want to write a response body in Cuba, which is most requests, you have to call the res.write method explicitly. I just think the Sinatra approach is simpler, and don’t think there is a net benefit by making the response body writing explicit. This is one of the philosphical differences between Roda and Cuba.|I was originally thinking I could just work around this issue using a Cuba plugin that made response body writing implicit.
on 'albums' do
res.write 'Hello albums'
end
But I found that Cuba’s plugin system was unable to override methods defined by Cuba.
I wanted to write a plugin that just overrode on and called super, and if the on block returned a String, write it to the response body before returning from the block.|Unfortunately, in Cuba this does not work, at least not if you add the plugin directly to the Cuba class, and after speaking with the author, I found that this was by design.|I was used to Sequel’s plugin system, where you can override any method and call super to get the default behavior. It was at this point that I decided to fork, and make sure the fork used a more extensible plugin system, based on Sequel’s plugin system.
def on
super do |*a|
ret = yield(*a)
res.write ret if ret.is_a?(String)
ret
end
end
After I decided to fork, I wanted to fix an issue that I had with every ruby web framework I’ve used, which is that instance variables, methods, and constants pollute the namespace, which can cause problems for the user of the framework.|One of the things you can see in this picture is that Roda trees naturally resist the pollution that is corrupting the rest of the land.
Can anyone see the problem with this Sinatra code?
get '/response/:id' do
@response = Response[params[:id]]
erb(:response)
end
The problem is that the @response instance variable is already used by Sinatra internally, so this clobbers the response variable, and completely breaks Sinatra.|It just so happens that one of the apps I work on deals with a domain object called a response, so using Response as the class name and @response as the instance variable name was the most natural way to express the code.
get '/response/:id' do
@response = Response[params[:id]]
erb(:response)
end
Because I wanted it to work easily in Sinatra, I had to rename the instance variable to @agency_response. I didn’t like this, because a name like @agency_response indicates to me that there are other types of responses that the system deals with, and that’s not true in this case.
get '/response/:id' do
@agency_response = Response[params[:id]]
erb(:response)
end
In Roda, all instance variables used internally in the scope of the Roda.route block are prefixed with an underscore, so they won’t conflict with instance variables the user wants to use.
@_variable
Namespace pollution is not just an issue with instance variables, but with constants as well.
Can anyone see the problem with this Cuba code?
require 'response'
class App < Cuba
define do
on do
response = Response[1]
res.write response.title
end
end
end
The problem is that even though you are requiring a top level Response class in the first line, the reference to the Response class inside the Cuba app references Cuba::Response, which is a subclass of Rack::Response, instead of the top level Response class that you required.
require 'response'
class App < Cuba
define do
on do
response = Response[1]
res.write response.title
end
end
end
To avoid pollution of the constant namespace, Roda prefixes all constants inside the Roda namespace with Roda, so the internal classes are named Roda::RodaRequest, Roda::RodaResponse, and so on. This makes it unlikely that Roda’s constants will conflict with your application’s constants.
Roda::RodaResponse
Finally, Roda avoids polluting the method namespace. With Cuba, all of the routing methods such as on, get, and root, are instance methods in the scope of the route block, so you can’t define view methods that conflict with them.|I don’t want to imply that this is a problem specific to Cuba, since Cuba is actually much better than most other ruby web frameworks in this regard, with only 23 additional methods over what is defined in Object. With Sinatra, there are 68 additional methods. In Roda there are only 6 additional methods.
The reason that Roda is able to have only 6 additional methods is by moving the routing methods to the Request class, which is why the Roda.route method yields the request instance to the block, and routing methods are methods called on the request instance.|Incidentally, if you look at Cuba’s routing methods, they all have feature envy, since pretty much all they do is call methods on the request instance, so even if this wasn’t being done to avoid pollution, it’s still a good idea from an object oriented design perspective.
Roda.route do |r|
r.on "albums" do
r.is do
"Hello Albums"
end
end
end
This brings us to the final section of this talk, which is going to talk about Roda’s plugins. Roda’s philosophy is to have a very small core, with only the essentials. All nonessential features are added via plugins. This way you get to choose to load only the features that you need.
Loading plugins in Roda is similar to loading plugins in Sequel and Cuba, just calling the Roda.plugin method.|Like Sequel but unlike Cuba, Roda ships with a set of official plugins, but external plugins can be loaded via the same method, so whether the plugin ships with Roda or is external is transparent to the user. In this case, this loads the render plugin, which supports rendering templates using the tilt library.
Roda.plugin :render
Usage is similar to Sinatra, except instead of having separate methods per template engine, it just has a single view method. In my experience with Sinatra, I rarely use more than one template engine in the same application, so with Roda’s render plugin, you set the default template engine when loading the plugin. If you want to use a non-default template engine, you can specify an option to override the default when calling view.|Similar to Sinatra, the view method just returns a string, so if you call view as the last expression in your route block, the result of the view is used as the response body.
r.is "albums" do
view('albums')
end
Of special note in the render plugin is the :escape option, which switches the default template engine to an Erubis subclass that automatically escapes output, preventing common cross site scripting vulnerabilties.
Roda.plugin :render, escape: true
When using the :escape option, the usual percent equal tag will escape the output.
<%= '<a>' %>
<a>
To get unescaped output, you use the percent double equal tag. The use of percent equal for escaping and percent double equal for not escaping provides most of the security benefits of the default escaping that Rails uses, without the complexity of ActiveSupport::SafeBuffer.
<%== '<a>' %>
<a>
Sinatra users may think there is nothing special about this, since Sinatra supports the :escape_html erb option, which will do roughly the same thing. This is what Sinatra recommends in its FAQ when asked about how to automatically escape HTML.
set :erb, escape_html: true
What they don’t tell you is that this is at least partially broken, in that it doesn’t support postfix conditionals inside percent equal tags. This is odd, as postfix conditionals inside percent equal tags are supported when not using the :escape_html option.
<%= '<a>' if true %>
The reason that percent equal doesn’t work with postfix conditionals is because it generates this ruby code, just using the percent equal tag content as the argument to escape_xml.|Unfortunately this is not valid ruby syntax, because you can’t use a postfix conditional expression directly as a method argument.
escape_xml('<a>' if true)
In order to make it valid ruby syntax, you need to wrap the expression in parentheses, which is what Roda does.
escape_xml(('<a>' if true))
Roda ships with another security related plugin, csrf, which adds protection against cross site request forgery. This loads rack_csrf as one of the middleware for the application.
Roda.plugin :csrf
It also adds some view helper methods, allowing you to easily create the necessary CSRF token tags in your views.
<%= crsf_tag %>
If you are doing server-side rendering using the render plugin, and you want to improve performance, in addition to thinking about application performance, you also need to think about rendering performance on the client. One of the best ways to improve rendering performance on the client is to flush the head tag for the response while still preparing the reminder of the response.|The chunked plugin lets you do that, using Transfer-Encoding chunked.
Roda.plugin :chunked
With the chunked plugin, you can use chunked instead of view as the method name, and it will stream the response to the client in chunks. By default, it will flush the top part of the layout template before rendering the content template.
Roda.route do |r|
r.on "albums" do
chunked('albums')
end
end
You can a pass block to the chunked method, which will be yielded to after the top part of the layout is flushed to the client, but before rendering the content template. This allows you to execute code necessary to render the content while the client is making requests to load the assets needed to display the page. This can significantly decrease the total time the client takes to fully render the page.
Roda.route do |r|
r.on "albums" do
chunked('albums') do
# retrieve albums
end
end
end
Additionally, at any point inside the content or layout templates, you can call flush to send the partially rendered response to the client. This is mostly useful when loading large pages, where you want the client to see visible content before the all of the content is finished rendering.
<% flush %>
The chunked plugin also has a chunk_by_default option, which makes view default to using chunked encoding if the client supports it, allowing you to speed up client rendering performance for all of your views.
Roda.plugin :chunked,
:chunk_by_default=>true
One of the issues with Roda’s approach of having a single routing block that all requests are yielded to, is that you can’t split a block across multiple files. For larger sites, it’s not practical to have all routes in a single file, so Roda comes with a multi_route plugin.
Roda.plugin :multi_route
With the multi_route plugin, in your main Roda application file, let’s say you have a large routing subtree for paths under albums, which you want to move to a separate file.
# file: app.rb
Roda.route do |r|
r.on "albums" do
# ...
end
end
You replace the subtree with a call to r.route with the name of the route you want to use. In most cases this will be the same as the matched part of the request path.
# file: app.rb
Roda.route do |r|
r.on "albums" do
r.route("albums")
end
end
You take the routing subtree you removed from the main source file, and you place it in a separate file. By convention these files are stored in a routes subdirectory, and you just require all files from the routes subdirectory in your main application file.|Inside these routing files, you call Roda.route, but you pass it the name for the route, which is the same as the name that you used in the r.route call in the main Roda application file.
# file: routes/albums.rb
Roda.route('albums') do |r|
# ...
end
For larger apps, it’s common to move all of your routing code to routing files. Assuming you named all of your routes with the request path prefix, you can dispatch to all of your named routes with a single r.multi_route call. In addition to DRYing up your code, this is also faster as it matches against all route prefixes simultaneously using a single regular expression.
# file: app.rb
Roda.route do |r|
r.multi_route
end
If you are writing an app that uses the multi_route plugin, it’s probably large enough that you may want to not use a single directory for all of your views. While you could just specify the subdirectory each time you want to render a template, that’s not very DRY.|Roda ships with a view_subdirs plugin that allows for setting a view subdirectory to use for a given branch.
Roda.plugin :view_subdirs
With the view_subdirs plugin, you can call set_view_subdir anywhere in your routing code, and when you call view, it will automatically prefix the template with that subdirectory. That’s more DRY, but some people might not think it’s DRY enough.
Roda.route('albums') do |r|
set_view_subdir('albums')
r.get 'list' do
view 'index' # views/albums/list.erb
end
end
For the DRYest code, you can use the symbol views plugin.
Roda.plugin :symbol_views
With the symbol views plugin, you can just have your route block return a symbol, which is interpreted as a view template name, and will render the view and use the result as the response body.|Now, some people may consider this too DRY. But the great thing about Roda is this behavior is loaded via a plugin, so if you don’t like it, you just don’t have to load the plugin.
r.get 'list' do
:list
end
Similar to the symbol_views plugin, Roda ships with a json plugin.
Roda.plugin :json
The json plugin makes it easy to create JSON API sites. You just have your route block return either an array or a hash, and Roda will automatically convert it to JSON and use the JSON as the response body.
r.get 'list' do
DB[:albums].to_a
end
A common issue with most ruby web frameworks is that by default, they tend to be too liberal in terms of what they match. The symbol_matchers plugin exists to make it easier to use more exact matching.
Roda.plugin :symbol_matchers
Take this simple route, which matches albums followed by a segment for the album’s id. While this works, in general it is too liberal.
r.get 'albums/:id' do |album_id|
@album = Album[album_id.to_i]
view('album')
end
It’s kind of obvious it is too liberal, since you are converting the matched album_id to an integer. So what you really want to do is only match segments consisting solely of decimal digits.
r.get 'albums/:id' do |album_id|
@album = Album[album_id.to_i]
view('album')
end
You could switch to using a regular expression as your matcher. But then you have to specify the grouping manually, and it takes more cognitive overhead to use a regular expression.
r.get /albums\/(\d+)/ do |album_id|
@album = Album[album_id.to_i]
view('album')
end
With the symbol_matchers plugin, Roda automatically treats some symbols specially, using specific regular expressions for them. One of these symbols is :d, which matches only decimal digits. So this makes sure that only albums followed by a segment with only decimal digits will be matched.
r.get 'albums/:d' do |album_id|
@album = Album[album_id.to_i]
view('album')
end
The symbol_matchers plugin also makes it easy to define your own symbol matchers. Let’s say you have lots of routes in your application that include a user’s username. But you don’t allow arbitrary characters in your usernames, you have a strict requirement of only lowercase ASCII letters, and usernames must be between 6 and 20 characters.
r.get :username do |username|
@user = User.first(:username=>username)
view('user')
end
With the symbol_matchers plugin, you can use the symbol_matcher method, passing it the symbol you want to match, and the regular expression to use for that symbol. Then, everywhere in your app where you use that symbol, it will use that regular expression instead of the default of matching any sequence of characters. As shown in the previous examples, this works both for symbols and for embedded colons in strings.
Roda.symbol_matcher :username,
/([a-z]{6,20})/
Roda ships with a flash plugin that adds support for flash handling, similar to Rails flash handling or the flash handling you get from sinatra-flash.
Roda.plugin :flash
If you want to handle request methods other than GET and POST, Roda ships with an all_verbs plugin that adds routing methods for all of the HTTP request methods.
Roda.plugin :all_verbs
Roda ships with a not_found plugin, for Sinatra-like support of specifying a block to call when no route matches.
Roda.plugin :not_found
Roda ships with a error_handler plugin, for Sinatra-like support of specifying a block to call when an error is raised while handling a request.
Roda.plugin :error_handler
Roda ships with a pass plugin, for a Sinatra-like pass method. The pass method jumps out of the current routing block as if it did not match, allowing it to continue the routing process.
Roda.plugin :pass
Roda ships with an indifferent_params plugin, for support for a Sinatra-like params hash that works with both symbols and strings. You could call this stealing from Sinatra, if you didn’t know that I’m the one who introduced indifferent params to Sinatra in the first place.|To be fair to me, Sinatra originally symbolized all params, which opened it up to denial of service attacks, so introducing indifferent access was done to keep backwards compatibility, not because indifferent params are a good idea in general.
Roda.plugin :indifferent_params
If you want some actual stealing from Sinatra, Roda ships with a streaming plugin with an implementation mostly borrowed from Sinatra, though it does have some significant improvements that make it easier to use safely.
Roda.plugin :streaming
Likewise, Roda ships with a caching plugin with an implementation mostly borrowed from Sinatra, adding support for cache control, last modified, and etags.
Roda.plugin :caching
Roda also ships with an assets plugin that allows for easily compiling and rendering your CSS and javascript assets on the fly in development, and compiling them into a single, compressed file in production for maximum performance.
Roda.plugin :assets
Roda ships with a middleware plugin that turns your Roda app into a Rack middleware. If you have a Rails or Sinatra application that you want to speed up, but don’t have the time to fully replace with Roda, you can use the middleware plugin to turn the Roda app into middleware, then use that middleware in your Rails or Sinatra application.|Any route that the Roda app doesn’t handle will be forwarded to your Rails or Sinatra application, allowing you to gradually upgrade.
Roda.plugin :middleware
Roda also ships with over 10 additional plugins, but as I’m running out of time, I can’t cover all of them.
Roda.plugin :*
So let me review what I think are the main advantages of using Roda over other ruby web frameworks.
First, I think for simple web applications Roda is about as simple to use as Sinatra, and much simpler than Rails, and the Roda codebase itself is significantly smaller and simpler than either Sinatra or Rails.
As your application gets more complex, I think Roda does a better job than Sinatra of managing the complexity, while always remaining a lot less complex than Rails.
Roda performs significantly better than Sinatra and Rails for simple sites, and scales much better than either Rails or Sinatra for truly large sites.
Roda doesn’t pollute your namespaces, so you don’t need to worry about Roda conflicting with your application code.
Finally, because of the Sequel-based plugin system it uses, Roda is very extensible, so if you need functionality that isn’t included in one of the plugins that ship with Roda, it’s easy to add yourself.
So that’s why I think you should consider Roda for ruby web application development.
That concludes the presentation. Thank you all for attending and listening to me present Roda.
If you have any questions about Roda, I’ll be happy to answer them now.