Hash Defaults and Nested OrderedHash in Rails
Thanks to ruby_switcher.sh I now have Ruby 1.9 running on my machine. Subsequently, I’ve just run into my first Ruby 1.9 induced bug: I relied upon 1.9’s Hash ordering. Fortunately, Rails’ ActiveSupport has it’s own undocumented OrderedHash implementation for Ruby 1.8. Unfortunately you can’t create an OrderdHash using the hash literal notation (i.e { :a => 1, :b => 2 }) so making a nested OrderedHash could be painful.
Consider the following code:
hash = ActiveSupport::OrderedHash.new
hash["2"] = ActiveSupport::OrderedHash.new
hash["2"]["b"] = "2.b"
hash["2"]["a"] = "2.a"
hash["1"] = ActiveSupport::OrderedHash.new
hash["1"]["b"] = "1.b"
hash["1"]["a"] = "1.a"This however seems unnecessarily verbose. Thankfully Ruby’s Hash defaults come to the rescue.
hash = ActiveSupport::OrderedHash.new{|h,k| h[k] = ActiveSupport::OrderedHash.new(&h.default_proc) }
hash["2"]["b"] = "2.b"
hash["2"]["a"] = "2.a"
hash["1"]["b"] = "1.b"
hash["1"]["a"] = "1.a"All the magic here happens in the somewhat arcane first line. Hash.new (and OrderedHash which is one of its subclasses) allows us to pass a block that defines the default behavior for undefined keys. The second bit of magic is the “h.default_proc” bit. This means that any automatically created OrderedHashes will have the same default behavior as their parent. As a result, we now have an OrderedHash that automatically creates OrderedHashes which in turn create OrderedHashes and so on.
The big benefit of this is that we don’t have to explicitly implement any of the child hashes. There is one gotcha however. You have to be careful with how you check for the existence of a key. While normally you could use “if hash[:key]” this will initialize an OrderedHash and will evaluate to true not to false. Instead, you should use “hash.has_key?(:key)”.
This is not a solution for all cases, but in some instances it can save you a few lines of clutter.
ActsAsNestedController with infinitely more documentation!
A while back I wrote a plugin to help with managing the logic of controllers with nested routes. Unfortunately, I was lazy and never documented it. I finally got around to refactoring it and adding proper documentation. You can find it at: http://github.com/rxcfc/acts_as_nested_controller
:use_route param for Rails URL helper
Imagine the following case. You have two landing pages, one generic one, and an account specific one. The urls are as follows:
map.landing 'landing', :controller => 'landing', :action => 'index'
map.account_landing 'accounts/:account_id/landing', :controller => 'landing', :action => 'index'Now imagine you want a path to the landing page, using the most specific route possible. If you have an account_id, use it, if not, skip it.
You could do:
url_for(:controller => 'landing', :action => 'index', :account_id => current_account)If current_account is set you’ll get “/accounts/:account_id/landing” if not, you’ll get “/landing”. However, that just looks ugly.
Enter :use_route => nil.
landing_path(:account_id => nil) # => '/landing'
landing_path(:account_id => 1) # => '/landing?account_id=1'
landing_path(:account_id => nil, :use_route => nil) # => '/landing'
landing_path(:account_id => 1, :use_route => nil) # => '/accounts/1/landing'Setting :use_route to nil, is equivalent to the earlier #url_for example.
Another use for :use_route is to force a route helper when using :url_for.
For instance, you can do:
url_for(:controller => 'landing', :action => 'index', :account_id => 1, :use_route => :landing)
# => '/landing?account_id=1'Though it’s not likely you would want that specific outcome, if you need to force #url_for to use a specific route form, it can be done.
This is likely to be most of use when working with a plugin, such as will_paginate, that calls #url_for internally.
Thanks to matbrown for the second tip.
Path Expander
UPDATE: This is not necessary. See this post for details.
From time to time, I’ve been working on a Rails app with nested routes and ran into the situation where I want to nest the url only if I have the necessary information. Consider the following example to see what I mean:
user_projects_path(current_user) #=> /users/:id/projectsNow this will work perfectly if current_user is defined, but what if current_user is nil? Unfortunately, I’ll end up with the less than desirable “/users//projects”. To avoid this I could use an if statement along the lines of:
current_user ? user_projects_path(current_user) : projects_path
#=> /users/:id/projects or /projectsThis will work of course, but it isn’t all that elegant. Another option would be something along the lines of:
projects_path(:user_id => current_user.id)This solution will also work and has the benefit of being concise. Unfortunately we lose the nice route formatting. Instead of “/users/:id/projects”, we get “/projects?user_id=:id”. Not the prettiest result.
So I started thinking, wouldn’t it be great if Rails would automatically expand that path into “/users/:id/projects” if possible and if not just fall back to “/projects”. I started hacking around a bit and this is what I came up with:
The biggest downside to this is the call to #recognize_path. From my understanding, this can be a bit computationally intensive, so if you’re worried about top notch performance, this solution may be a bit risky.
So have you dealt with this before? If so, have you been able to come up with a better solution than this?