Project

depth

0.0
No commit activity in last 3 years
No release in over 3 years
Depth is a utility gem for dealing with nested hashes and arrays
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 1.7
~> 10.0
>= 0
 Project Readme

Depth

Depth is a utility gem for deep manipulation of complex hashes, that is nested hash and array structures. As you have probably guessed it was originally created to deal with a JSON like document structure. Importantly it uses a non-recursive approach to its enumeration.

Installation

Add this line to your application's Gemfile:

gem 'depth'

And then execute:

$ bundle

Or install it yourself as:

$ gem install depth

Usage

The complex hash

  hash = { '$and' => [
    { '#weather' => { 'something' => [], 'thisguy' => 4 } },
    { '$or' => [
      { '#otherfeed' => {'thing' => [] } },
    ]}
  ]}

Nicked from a query engine we're using on Driftrock

The above is a sample complex hash, to use the gem to start manipulating it is pretty simple:

  complex_hash = Depth::ComplexHash.new(hash)

Not exactly rocket science (not even data science). You can retrieve the hash with either base or to_h.

Manipulation

Manipulation of the hash is done using routes. A route being a description of how to traverse the hash to get to this point.

The messages signatures relating to manipulation are:

  • set(route, value) = Set a value
  • find(route) = Find a value
  • alter(route, key:) = Alter a key (the last key in route)
  • alter(route, value:) = Alter a value, identical to set
  • alter(route, key: value:) = Alter a key and value, identical to a set and then delete
  • delete(route) = Delete a value

Routes can be defined as an array of keys or indeces:

  hash = { '$and' => [
    { '#weather' => { 'something' => [], 'thisguy' => 4 } },
    { '$or' => [
      { '#otherfeed' => {'thing' => [] } },
    ]}
  ]}
  route = ['$and', 1, '$or', 0, '#otherfeed', 'thing']
  Depth::ComplexHash.new(hash).find(route) # => []

  # Or with a default
  Depth::ComplexHash.new(hash).find(%w(not a route), default: 'hello') # => 'hello'

But there's something cool hidden in the set message, if part of the structure is missing, it'll fill it in as it goes, e.g.:

  hash = { '$and' => [
    { '#weather' => { 'something' => [], 'thisguy' => 4 } },
    { '$or' => [
      { '#otherfeed' => {'thing' => [] } },
    ]}
  ]}
  route = ['$and', 1, '$or', 0, '#sup', 'thisthing']
  Depth::ComplexHash.new(hash).set(route, 'hello')
  puts hash.inspect #=>
  # hash = { '$and' => [
  #   { '#weather' => { 'something' => [], 'thisguy' => 4 } },
  #   { '$or' => [
  #     { '#otherfeed' => {'thing' => [] } },
  #     { '#sup' => {'thisthing' => 'hello' } },
  #   ]}
  # ]}

Great if you want it to be a hash, but what if you want to add an array, no worries, just say so in the route:

  route = ['$and', 1, '$or', 0, ['#sup', :array], 0]
  # Routes can also be defined in other ways
  route = ['$and', 1, '$or', 0, { key: '#sup', type: :array }, 0]
  route = ['$and', 1, '$or', 0, RouteElement.new('#sup', type: :array), 0]

Find can also perform the same magic if you set the keyword argument create to be true. A default value can also be supplied:

  hash = { '$and' => [
    { '#weather' => { 'something' => [], 'thisguy' => 4 } },
    { '$or' => [
      { '#otherfeed' => {'thing' => [] } },
    ]}
  ]}
  route = ['$and', 1, '$or', 0, '#sup', 'thisthing']
  Depth::ComplexHash.new(hash).find(route, create: true)
  puts hash.inspect #=>
  # hash = { '$and' => [
  #   { '#weather' => { 'something' => [], 'thisguy' => 4 } },
  #   { '$or' => [
  #     { '#otherfeed' => {'thing' => [] } },
  #     { '#sup' => { } },
  #   ]}
  # ]}

  # Or if you supply a default value as well
  val = Depth::ComplexHash.new(hash).find(route, create: true, default: 'blargle')
  puts val #=> 'blargle'
  puts hash.inspect #=>
  # hash = { '$and' => [
  #   { '#weather' => { 'something' => [], 'thisguy' => 4 } },
  #   { '$or' => [
  #     { '#otherfeed' => {'thing' => [] } },
  #     { '#sup' => { 'thisthing' => 'blargle' } },
  #   ]}
  # ]}

Enumeration

The messages signatures relating to enumeration are:

  • each = yields key_or_index and fragment, returns the complex hash
  • select = yields key_or_index, fragment, returns a new complex hash
  • reject = yields key_or_index, fragment, returns a new complex hash
  • map = yields key_or_index, fragment and parent_type, returns a new complex hash
  • map_values = yields fragment, returns a new complex hash
  • map_keys = yields key_or_index, returns a new complex hash
  • map!, map_keys! and map_keys_and_values!, returns the base complex hash
  • reduce(memo) = yields memo, key and fragment, returns memo
  • each_with_object(obj) = yields key, fragment and object, returns object

Fragment refers to a chunk of the original hash

These, perhaps, require a bit more explanation:

NB All of these methods yield an argument at the end which is the route taken to get to this fragment of the hash, I've not detailed the use of it here because it's rarely necessary but it's available in case you have some complex map rules that change based on where you are in the hash.

each

The staple, and arguably the most important, of all the enumeration methods,

  hash = { ... }
  Depth::ComplexHash.new(hash).each { |key, fragment| }

Each yields keys and associated fragments from the leaf nodes backwards. For example, the hash:

  { '$and' => [{ 'something' => { 'x' => 4 } }] }

would yield:

  1. x, 4
  2. something, { "x" => 4 }
  3. 0, { "something" => { "x" => 4 } }
  4. $and, [{ "something" => { "x" => 4 } }]

select

  hash = { 'x' => 1, '$c' => 2, 'v' => { '$x' => :a }, '$f' => { 'a' => 3, '$l' => 4 }}

  Depth::ComplexHash.new(hash).select do |key, fragment|
    key =~ /^\$/
  end

The above would yield:

  1. x, 1
  2. $c, 2
  3. $x, a
  4. v, {"$x"=>:a}
  5. a, 3
  6. $l, 4
  7. $f, {"$l"=>4}

and with the boolean only selecting keys with a dollar sign it would return a new complex hash

{ "$c" => 2, "$f" => { '$l' => 4 } }

reject

Unsurprisingly this is the inverse of select.

map

Map yields both the current key/index and the current fragment, expecting both returned in an array. I've yet to decide if there should be a third argument that tells you whether or not the key/index is for an array or a hash. I've not needed it but I suspect it might be useful. If it comes up I'll add it.

  hash = { '$and' => [{ 'something' => { 'x' => 4 } }] }
  Depth::ComplexHash.new(hash).map do |key, fragment|
    [key, fragment]
  end

like each the above would yield:

  1. x, 4
  2. something, { "x" => 4 }
  3. 0, { "something" => { "x" => 4 } }
  4. $and, [{ "something" => { "x" => 4 } }]

and with the contents being unchanged it would return a new complex hash with equal contents to the current one.

map_values

  hash = { '$and' => [{ 'something' => { 'x' => 4 } }] }
  Depth::ComplexHash.new(hash).map_values do |fragment|
    fragment
  end

This will yield only the fragments from map, useful if you only wish to alter the value parts of the hash.

map_keys

  hash = { '$and' => [{ 'something' => { 'x' => 4 } }] }
  Depth::ComplexHash.new(hash).map_keys do |key|
    key
  end

This will yield only the keys from map, useful if you only wish to alter the keys.

map!, map_keys!, map_values!

The same as their non-exclamation marked siblings save that they will cause the complex hash on which they operate to change.

reduce and each_with_object

Operate as you would expect. Can I take a moment to point out how irritating it is that each_with_object yields the object you pass in as its last argument while reduce yields it as its first O_o?

  hash = { '$and' => [{ 'something' => { 'x' => 4 } }] }
  Depth::ComplexHash.new(hash).reduce(0) do |memo, key, fragment|
    memo += 1
  end

  Depth::ComplexHash.new(hash).each_with_object([]) do |key, fragment, obj|
    obj << key
  end

Why?

Alright, we needed to be able to find certain keys from all the keys contained within the complex hash as said keys were the instructions as to what data the hash would be able to match against. This peice of code was originally recursive. We were adding a feature that required us to also be able to edit these keys, mark them with a unique identifier. As I was writing this I decided I wasn't happy with the recursive nature of the key search as we have no guarantees about how nested the hash could be. As I refactored the find and built the edit it became obvious that the code wasn't tied to the project at hand so I refactored it out to here.

Contributing

  1. Fork it ( https://github.com/[my-github-username]/depth/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request