[ActiveSupport] Hash#flatten_keys

Across numerous applications and gems, my team and I have frequently needed to be able to flatten a nested hash. The most common kind of task involves reading an arbitrarily deep, loosely structured hash (e.g. imagine an AST hash). Since the exact structure of the hash is unknown beforehand, flattening the hash allows for straightforward reading/parsing.

Here’s an example from one of our tools. We have a generic filter method that we can include into ActiveRecord models. You pass the filtering conditions as a structured hash:

{

attribute: 'value',

association: {

    field: 'value'

},

'computed_attribute(>)': 14

}

In order to build the appropriate ActiveRecord query, we need to parse this hash, but we can’t predict its shape. Here is a somewhat simplified example of the code that uses Hash#flatten_keys:

def filter(instructions_hash)

instructions_hash.flatten_keys.reduce(self) do |relation, (attribute_path, value)|

associations_array = attribute_path.slice(0, attribute_path.length - 1)

attribute_model = associations_array

                    .reduce(relation) do |obj, assoc|

                      obj.reflections[assoc.to_s]&.klass

                    end

associations_hash = associations_array

                      .reverse

                      .reduce({}) do |hash, association|

                        { association => hash }

                      end

instruction_operator = attribute_path.last[/\((.*?)\)/, 1] || '='

instruction_attribute = attribute_path.last.sub([/\((.*?)\)/, '')

relation.eager_load(associations_hash)

        .where(

          Arel::Nodes::InfixOperation.new(

            instruction_operator,

            Arel::Table.new(attribute_model.table_name)[instruction_attribute],

            Arel.sql(ActiveRecord::Base.connection.quote(value))

          )

        )

end

end

If you can implement a method like this without flattening the hash, I would genuinely be interested to see the code. Assuming for the time being, however, that situations like the above (needing to read/parse a hash of unknown depth or specific keypaths) are common enough and that no easily implementable viable alternative exists to flattening the hash, I propose adding Hash#flatten_keys to ActiveSupport.

I have a performant implementation ready for a PR if others think this is a good idea. I was imagining for the time being that a simple function would flatten the nested keys into an array key. However, it is possible to add other flatteners, like dot-separated or dash-separated or even rails-param style (e.g. a[b][c]). Also, the flattening can either flatten array values or not. If we flatten array values, the “key” put in the key path is simply the index of the item in the array.

I’m happy to get into any other specifics about implementation, but I thought it best to keep the original post fairly tightly focused on just proposing the basic idea.

Have you looked into Hash normalization like https://github.com/chrokh/normalizr? That’s just one of many, hope it helps.

Example structure:

  original_obj = {
posts: [
{
id: 11      ,
title: 'Relational normalization'      ,
author: {
id: 22        ,
name: 'Darkwing Duck'
      },
comments: {
id: 33        ,
body: 'Interesting...'
      }
}
]
}

  normalized_obj = {
posts: {
:11 => {
id: 11      ,
title: 'Relational normalization'      ,
author: 22      ,
comments: [33    ]
}
},
authors: {
:22 => {
id: 22      ,
name: 'Darkwing Duck'
    }
},
comments: {
:33 => {
body: 'Interesting...'
    }
}
}