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 toset
-
alter(route, key: value:)
= Alter a key and value, identical to aset
and thendelete
-
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
= yieldskey_or_index
andfragment
, returns the complex hash -
select
= yieldskey_or_index
,fragment
, returns a new complex hash -
reject
= yieldskey_or_index
,fragment
, returns a new complex hash -
map
= yieldskey_or_index
,fragment
andparent_type
, returns a new complex hash -
map_values
= yieldsfragment
, returns a new complex hash -
map_keys
= yieldskey_or_index
, returns a new complex hash -
map!
,map_keys!
andmap_keys_and_values!
, returns the base complex hash -
reduce(memo)
= yieldsmemo
,key
andfragment
, returns memo -
each_with_object(obj)
= yieldskey
,fragment
andobject
, 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:
x, 4
something, { "x" => 4 }
0, { "something" => { "x" => 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:
x, 1
$c, 2
$x, a
v, {"$x"=>:a}
a, 3
$l, 4
$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:
x, 4
something, { "x" => 4 }
0, { "something" => { "x" => 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
- Fork it ( https://github.com/[my-github-username]/depth/fork )
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request