Enhancing Conditional Routing in Rails

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.


Add Your Comments

(not published)

There is a way!

From: AJ ONeal, 04/22/10 10:08 PM

http://api.rubyonrails.org/classes/ActiveRecord/Base.html Protected attributes won‘t be set unless they are given in a block. For example: # Now 'Bob' exist and is an 'admin' User.find_or_create_by_name(:name => 'Bob', :age => 40) { |u| u.admin = true }

RE: Metaprogramming

From: Christopher Haupt, 09/03/08 09:46 AM

Michael: Good point about alias_method_chain. I'll rev the article when I can to refactor to use it. Before using alias_method_chain (and friends), I'd like to explain how it works since that sometimes trips people up. Thanks!

Metaprogramming

From: Michael Bleigh, 09/03/08 08:58 AM

It might be better if you used alias_method_chain instead of directly overriding the methods. That way if the core implementation changes, your code isn't brittle. I've coded up a quick example here: http://gist.github.com/8615 Also just thought I'd mention that my plugin SubdomainFu adds a :subdomain conditional using this same method. You can see the code for that here: http://github.com/mbleigh/subdomain-fu/tree/master%2Flib%2Fsubdomain_fu%2Frouting_extensions.rb?raw=true

What is Old is New Again

From: Christopher Haupt, 08/27/08 01:31 PM

I'm always amazed how much you can learn from reading the code (over and over again) with a specific goal in mind. While I've been through the routing code a number of times, it was never with this need in mind. It is great to "rediscover" features you can tap like this. Going through our archives, only after I did all of the leg work did I know the right way to search for other implementations. We have some links you can check out for other write-ups. Have fun!

 

Sponsored By

New Relic Rails Performance Monitoring