# Nested hashes derail Rails’ url_for helpers

While working on the Sicirec PostgreSQL database front-end today, I had to pass a lot of nested parameters to a link_to helper in Rails. Software being what it is, this didn’t work.

There are a few patches awaiting acceptance. The most promising of these patches was part of an open Trac ticket. Because we use Rails as an svn external, applying the patch myself wouldn’t work when deploying unless I’d create a vendor branch for Rails in our own repository. Hoping that someone had forgotten to close the ticket, I first tried to upgrade to Rails 1.2.2, which was about time anyway because we were still in the 1.1 branch. The upgrade went fine but didn’t fix the problem.

Next, I tried to integrate the patch by redefining the methods changed by the patch in our lib/ directory. When this didn’t work, I decided to simply do some flattening of the hash myself for this one particular case.

A bit of googling around gave me many clues that the problem has cost a lot of people lots of time already.

Eventually, I settled with a derivate of some code by Peter Marklund to flatten my hashes:

class Hash
# Flatten a hash into a flat form suitable for an URL.
# Accepts as an optional parameter an array of names that pretend to be the ancestor key names.
#
# Example 1:
#
#   { 'animals' => {
#       'fish' => { 'legs' => 0, 'sound' => 'Blub' }
#       'cat' => { 'legs' => 4, 'sound' => 'Miaow' }
#   }.flatten_for_url
#
#   # => { 'animals[fish][legs]'  => 0,
#          'animals[fish][sound]' => 'Blub',
#          'animals[cat][legs]'   => 4,
#          'animals[cat][sound]'  => 'Miaow'
#        }
#
# Example 2:
#
#   {'color' => 'blue'}.flatten_for_url( %w(world things) )  # => {'world[things][color]' => 'blue'}
#
def flatten_for_url(ancestor_names = [])
flat_hash = Hash.new

each do |key, value|
names = Array.new(ancestor_names)
names << key

if value.is_a?(Hash)
flat_hash.merge!(value.flatten_for_url(names))
else
flat_key = names.shift.to_s.dup
names.each do |name|
flat_key << "[#{name}]"
end
flat_key << "[]" if value.is_a?(Array)
flat_hash[flat_key] = value
end
end

flat_hash
end
end

As you can see, I turned my code into a single method of the Hash class. It can be used simply in any url_for (based) call as in the following example:

url_for {
:controller => 'post',
:action => 'new',
'author' => {'name' => 'Rowan', 'gender' => 'm'}
}.flatten_for_url
# => /post/new?author[name]=Rowan&author[gender]=m 

Now if only some Rails developer would commit the patch already.