Add Hashie::Extensions::DeepFind

This extension adds a `#deep_find` (aliased as `#deep_detect`) and
a `#deep_find_all` (aliased as `#deep_select`) method to any Hash-like
object. These methods perform a depth-first search on the object and its
values and return either the first occurrence (for `#deep_find`) or an
array of all occurrences (for `#deep_find_all`) within the nested
structure of the hash. They work for nested Hash-like objects and nested
Enumerables.

[Closes #156]
This commit is contained in:
Michael Herold 2014-08-22 09:27:46 -05:00 committed by dblock
parent 05aee4d5b8
commit 36ece58665
5 changed files with 132 additions and 0 deletions

View File

@ -10,6 +10,7 @@
* [#205](http://github.com/intridea/hashie/pull/205): Added Hashie::Extensions::Mash::SafeAssignment - [@michaelherold](https://github.com/michaelherold).
* [#206](http://github.com/intridea/hashie/pull/206): Fixed stack overflow from repetitively including coercion in subclasses - [@michaelherold](https://github.com/michaelherold).
* [#207](http://github.com/intridea/hashie/pull/207): Fixed inheritance of transformations in Trash - [@fobocaster](https://github.com/fobocaster).
* [#209](http://github.com/intridea/hashie/pull/209): Added Hashie::Extensions::DeepFind - [@michaelherold](https://github.com/michaelherold).
* Your contribution here.
## 3.2.0 (7/10/2014)

View File

@ -260,6 +260,32 @@ user.deep_fetch :name, :middle { |key| 'default' } # => 'default'
user.deep_fetch :groups, 1, :name # => 'Open source enthusiasts'
```
### DeepFind
This extension can be mixed in to provide for concise searching for keys within a deeply nested hash.
It can also search through any Enumerable contained within the hash for objects with the specified key.
Note: The searches are depth-first, so it is not guaranteed that a shallowly nested value will be found before a deeply nested value.
```ruby
user = {
name: { first: 'Bob', last: 'Boberts' },
groups: [
{ name: 'Rubyists' },
{ name: 'Open source enthusiasts' }
]
}
user.extend Hashie::Extensions::DeepFind
user.deep_find(:name) #=> { first: 'Bob', last: 'Boberts' }
user.deep_detect(:name) #=> { first: 'Bob', last: 'Boberts' }
user.deep_find_all(:name) #=> [{ first: 'Bob', last: 'Boberts' }, 'Rubyists', 'Open source enthusiasts']
user.deep_select(:name) #=> [{ first: 'Bob', last: 'Boberts' }, 'Rubyists', 'Open source enthusiasts']
```
## Mash
Mash is an extended Hash that gives simple pseudo-object functionality that can be built from hashes and easily extended. It is intended to give the user easier access to the objects within the Mash through a property-like syntax, while still retaining all Hash functionality.

View File

@ -21,6 +21,7 @@ module Hashie
autoload :StringifyKeys, 'hashie/extensions/stringify_keys'
autoload :SymbolizeKeys, 'hashie/extensions/symbolize_keys'
autoload :DeepFetch, 'hashie/extensions/deep_fetch'
autoload :DeepFind, 'hashie/extensions/deep_find'
autoload :PrettyInspect, 'hashie/extensions/pretty_inspect'
autoload :KeyConversion, 'hashie/extensions/key_conversion'

View File

@ -0,0 +1,59 @@
module Hashie
module Extensions
module DeepFind
# Performs a depth-first search on deeply nested data structures for
# a key and returns the first occurrence of the key.
#
# options = {user: {location: {address: '123 Street'}}}
# options.deep_find(:address) # => '123 Street'
def deep_find(key)
_deep_find(key)
end
alias_method :deep_detect, :deep_find
# Performs a depth-first search on deeply nested data structures for
# a key and returns all occurrences of the key.
#
# options = {users: [{location: {address: '123 Street'}}, {location: {address: '234 Street'}}]}
# options.deep_find_all(:address) # => ['123 Street', '234 Street']
def deep_find_all(key)
matches = _deep_find_all(key)
matches.empty? ? nil : matches
end
alias_method :deep_select, :deep_find_all
private
def _deep_find(key, object = self)
if object.respond_to?(:key?)
return object[key] if object.key?(key)
reduce_to_match(key, object.values)
elsif object.is_a?(Enumerable)
reduce_to_match(key, object)
end
end
def _deep_find_all(key, object = self, matches = [])
if object.respond_to?(:key?)
matches << object[key] if object.key?(key)
object.values.each { |v| _deep_find_all(key, v, matches) }
elsif object.is_a?(Enumerable)
object.each { |v| _deep_find_all(key, v, matches) }
end
matches
end
def reduce_to_match(key, enumerable)
enumerable.reduce(nil) do |found, value|
return found if found
_deep_find(key, value)
end
end
end
end
end

View File

@ -0,0 +1,45 @@
require 'spec_helper'
describe Hashie::Extensions::DeepFind do
subject { Class.new(Hash) { include Hashie::Extensions::DeepFind } }
let(:hash) do
{
library: {
books: [
{ title: 'Call of the Wild' },
{ title: 'Moby Dick' }
],
shelves: nil,
location: {
address: '123 Library St.',
title: 'Main Library'
}
}
}
end
let(:instance) { subject.new.update(hash) }
describe '#deep_find' do
it 'detects a value from a nested hash' do
expect(instance.deep_find(:address)).to eq('123 Library St.')
end
it 'detects a value from a nested array' do
expect(instance.deep_find(:title)).to eq('Call of the Wild')
end
it 'returns nil if it does not find a match' do
expect(instance.deep_find(:wahoo)).to be_nil
end
end
describe '#deep_find_all' do
it 'detects all values from a nested hash' do
expect(instance.deep_find_all(:title)).to eq(['Call of the Wild', 'Moby Dick', 'Main Library'])
end
it 'returns nil if it does not find any matches' do
expect(instance.deep_find_all(:wahoo)).to be_nil
end
end
end