Rails’ routing infrastructure supports the concept of conditional routes: preconditions that must be satisfied before a particular route will trigger. Rails 2.1 supports one built-in condition, HTTP method checking, which is of some use but rather limited. What I needed was to be able to limit certain routes to only trigger when a particular host-name was used to access the application.
I thought I’d have to write messy additional logic until a little comment tucked away in ActionController::Routing::RouteSet and ActionController::Routing::Routing caught my eye. Here I briefly show you how to leverage this functionality for your own purposes.
The Goal — Conditional Routes in routes.rb
Let’s work backwards and see the result I was aiming for. I wanted to expand the existing capabilities of the routing engine and be able to restrict routes to specific hosts. The conditional routing option works by adding a parameter to your route specifications. Here are some examples:
map.with_options(:controller => 'feeds', :conditions => {:hosts => MY_HOSTS}) do |feed| feed.feeds_articles '/feeds/articles', :action => 'articles' feed.feeds_podcast '/feeds/podcast', :action => 'podcast' end
or
map.resources :podcasts, :conditions => {:hosts => MY_HOSTS}, :member => {:show_notes => :get, :transcript => :get}, :collection => {:admin => :get} do |podcast| podcast.resources :comments, :member => {:report_as_ham => :get, :report_as_spam => :get} end
or even
map.connect ':controller/:action/:id', :conditions => {:hosts => MY_HOSTS}
In Rails 2.1, however, no such option :hosts
exists, only an option to check the HTTP method via :method
.
The Implementation
I haven’t really ever needed to use the conditional routing support before, and didn’t really think about it due to it only supporting the HTTP method check. For that reason, I originally thought I’d have to write my own logic, either patching existing Routing routines (nearly right!) or by writing new stuff that could get messy (bad idea).
During a last scan through the code for the keyword “conditions”, I saw this comment:
# Plugins may override this method to add other conditions, like checks on # host, subdomain, and so forth. Note that changes here only affect route # recognition, not generation.
Good, a place to start afterall! The solution is elegant as it only requires overriding two simple routines. You can do this in your own app by writing code that gets loaded at startup. Here is one implementation in its entirety:
require 'action_controller' module ActionController module Routing class RouteSet def extract_request_environment(request) { :method => request.method, :host => request.host } end end class Route def recognition_conditions result = ["(match = #{Regexp.new(recognition_pattern).inspect}.match(path))"] result << "conditions[:method] === env[:method]" if conditions[:method] result << "conditions[:hosts].include?(env[:host])" if conditions[:hosts] result end end end end
My code is very simplistic and tuned for my needs, but gives you an example of where to patch in. Here, I simply supply a list of host names I care about, and check the incoming host against that list.
Use extract_request_environment
to parse out and store any data you will want to use in your conditional checks. This data will be available in the env
hash later on.
recognition_conditions
generates an Array of String objects that contain the Ruby code that will be used to build dynamic conditional test methods when the routing engine compiles the routes data in routes.rb
.
I drop the source file into my project’s pre-existing lib/plugins/action_controller_extensions/lib
directory as action_controller_extensions.rb
and include an init.rb
loader stub in my lib/plugins/action_controller_extensions
directory:
require 'action_controller_extensions'
My app deals with loading up such “plugins” at startup. You may have a different set-up. You can get the same effect by putting a require
for the main source file in your startup code.
It would be great to see other generally useful conditionals contributed by the community.