More About Request Lambdas

Update: A small matter of nomenclature: to better capture the idea that we are mapping a request to a block, I am now calling this feature “request lambdas.”

I am going to try and write a little bit every day (or a few times a week at least) to kind of elaborate on various aspects of Waves. One of the most common questions after the initial launch was “how is Waves different than [framework X]?” Although I now have a link on the front-page What makes Waves unique?, you can only get so far in a quick tour. Sometimes, you can only really understand what makes something different by getting into details.

(As an aside, I think Ruby is a lot like this. I see attempts all the time where people try to explain to the resident skeptic why Ruby is so cool. There is always someone there who lists off a bunch of features, and the skeptic always says, “well, my language has those, too.” And they’re right. In the end, it isn’t about the features, it’s about how they’re implemented. I mean, C++ has iterators and a fantastic collection library. But it still ain’t Enumerable.)

I got into a bit of discussion about the concept of request mappings on the mailing list, so I thought I’d recap that here as my first attempt to look at the nuts and bolts of Waves. Request mappings are akin to routes in Rails or Merb. But there are some profound differences. Request mappings are exactly that – they map a request to a block of code.

It’s called a request mapping because we are talking here about the entire request, not just the path. You can match against the URL, the path, the HTTP method, the accept header, etc. The entire request can be matched against, not just the path. Further, the block of code you are mapping into can do anything you want. For example, in Waves, the “hello world” application is just the one line in the mapping file, no controller or view.

path('/') { 'Hello World!' }

On the other hand, you can also do things like this:

# add / update the given resource for the given model
path %r{^/#{model}/#{name}/?$}, :method => :post do | model, name |
  use( model ) | controller { update( name ) }; redirect( url )
end

We’re matching a binding regular expression here and constraining it to match only an HTTP POST. We pass the regular expression bindings into the block as paremeters. The methods or variables model and name have been defined elsewhere in the mapping module to help make the rule more readable. Within the block, we set the resource to be used based on the URL and then call the appropriate controller’s update method. Finally, we redirect when we’re done with the update to the GET version of the same URL.

A couple of crucial differences from routes in a framework like Rails:

This is a departure from classic MVC, in that it completely decouples the controller and view. In reality, most Web application frameworks already partially decouple them anyway; Waves just finishes the job, as it were.

All a controller is supposed to do in Waves is process a request in terms of a model. Models don’t know about requests. They don’t know about request parameters or 404s. And well they shouldn’t. So controllers act as a sort of receptionist for models, keeping them insulated from the request context. The request mappings become a sort of meta-controller.

Along the same lines, filters are handled within the request mapping, not the controller. This makes it much easier to apply filters consistently across a variety of controllers. For example, suppose we want to ensure that anyone accessing an admin screen first logs in. We might create a request filter that looks like this:

before %r{^/admin/} do | model, name |
  redirect('/login') unless session[:user]
end

Now, there is no way to accidentally expose a secure controller method, because any path that starts with /admin/ is going to require a log in anyway. (Well, I shouldn’t say no way … but you can’t do it just by adding a new method to a controller. You’d have to have your mappings messed up.) And everything is in one place in terms of how a request is actually processed.

Of course, you can (and probably should) refactor your mappings into modular components. Waves provides an example of how to do this in the Waves::Mapping::PrettyUrls module. This packages up common mapping pattern for use with “pretty URLs” (resources accessed via name instead of id). So now you can add a whole variety of powerful mappings to your application just by using include. This is the way the default application is set up:

module Blog
  module Configurations
    module Mapping
      extend Waves::Mapping
      # your custom rules go here
      include Waves::Mapping::PrettyUrls::RestRules
      include Waves::Mapping::PrettyUrls::GetRules
    end
  end
end

As you develop common mapping patterns, you can simply put them in a module like this and then include them. Waves does not dictate to you what these patterns are (although I expect to introduce a number of common ones in addition to PrettyUrls.) And this is also a great example of the Waves philosophy. Ruby already provides the ability to define DSLs and package reusable code up into modules. There isn’t really a need to for a special method to define particular sets of URLs that only Waves “gurus” understand: we just use what Ruby already provides.

So that’s a hopefully helpful glimpse into what makes Waves unique and powerful. In my next post, I’ll start to get into the intracacies of the MVC wiring within the blocks themselves. Until then, don’t hesitate to comment or post questions.