Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Hashie::Extensions::DeepFind #209

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* [#204](https://github.com/intridea/hashie/pull/204): Added Hashie::Extensions::MethodOverridingWriter and Hashie::Extensions::MethodAccessWithOverride - [@michaelherold](https://github.com/michaelherold).
* [#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).
* [#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)
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions lib/hashie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ module Extensions
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'

Expand Down
59 changes: 59 additions & 0 deletions lib/hashie/extensions/deep_find.rb
Original file line number Diff line number Diff line change
@@ -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
45 changes: 45 additions & 0 deletions spec/hashie/extensions/deep_find_spec.rb
Original file line number Diff line number Diff line change
@@ -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