Monday, July 20, 2009

Enhancing Conditional Routing in 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 Id 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 Lets 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 havent really ever needed to use the conditional routing support before, and didnt really think about it due to it only supporting the HTTP method check. For that reason, I originally thought Id 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 projects 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.

No comments:

Post a Comment