Dynamically set value in nested hash in ruby

von tobonaut

Yesterday I discovered a cool and elegant way to set a value dynamicly in a nested hash in ruby.

Normally this wouldn’t be a problem if you know the structure of the hash. But since I had to dynamically map a internal data structure to a given nested structure it was a bit more challenging.

For example I the following code (of course it’s simplified to visualize the problem) shows the internal hash source and a mapping table mapping that describes how to transform the structure:

source = {
  firstname: 'Homer',
  lastname:  'Simpson',
  street:    '742 Evergreen Terrace',
  city:      'Springfield'

mapping = {
  firstname: [:names, :firstname],
  lastname:  [:names, :lastname],
  street:    [:address, :street],
  city:      [:address, :city]

Following this example the target hash should look like the following one:

 target = {
   names: {
     firstname: 'Homer'
     lastname:  'Simpson'
   address: {
     street:    '742 Evergreen Terrace',
     city:      'Springfield'

To transform the source hash to the desired target one you could either write some long and complex code with a lot of conditons or just use a bit ruby magic like the following lines does:

1. source.each do |source_key, source_value|
2.   target_keys = [*mapping[source_key]]
3.   delta = target_keys.reverse
     .inject(source_value) do |value, key|
4.     { key => value }
5.   end
6.   target.deep_merge! delta
7. end

Ok to be honest – it will not win a „most beautiful code“ contest but it’s a short and in my opinion elegant way to set the nested hash with the values through the mapping.

To provide a better understanding I’ll try to explain what each line is doing and what the idea is behind the snippet:

  1. Iterate trough all key / value pairs in source hash. The variables source_key and source_key will contain e.g. :firstname and [:names, :firstname]
  2. Lookup the target keys in the mapping and ensure we’re getting an array even if it’s just a simple value. It uses the splat operator (*) to create a new array from the array or simple value.
    This could be also done by using Array.wrap from active-support.
  3. Use the inject method on the reversed array to wrap the value in hashes with keys from the array.
  4. Wrap the previous generated value (see next paragraph) into a simple hash.
  5. Nothing special „end“ of the block
  6. Merge the created delta hash into the target hash using active supports deep_merge! to not overwrite sub-hashes.
  7. Nothing special „end“ of the block

To visualize the important part of the snippet I’m going to write down the values for each iteration in line 3 for the first iteration on the outer loop:

# variables provided by the first iteration
# of the outer loop
source_key:   :firstname
source_value: 'Homer'

target_keys = [:names, :firstname]

# first iteration - creates inner hash 
# containting the actual value
value = 'Homer'
key   = :firstname
# => { firstname: 'Homer' }

# second iteration - wraps the previously 
# created hash into another hash
value = { firstname: 'Homer' }
key   = :names
# => { names: { firstname: 'Homer' }

# ...
# more iterations could follow and do
# the same thing as the second one

Disclaimer: I do not claim to have invented this. I’ve found this idea somewhere on the net during some research about how to solve the problem and wanted to write down a bit about it to keep it in my mind.