summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Doubrovkine (dB.) @dblockdotorg <dblock@dblock.org>2015-02-03 06:35:21 -0500
committerDaniel Doubrovkine (dB.) @dblockdotorg <dblock@dblock.org>2015-02-03 06:35:21 -0500
commit118eeaf6a62b3847048106be8ba84fd3976171de (patch)
treea1020bda8ae74634a342412bb401450b1cc7503f
parentdc3bc1aa698324ad91b071b76f58fd6ce9423497 (diff)
parentb3c123cc95df28e2b240a4ab2b3c7620139aa90e (diff)
downloadhashie-118eeaf6a62b3847048106be8ba84fd3976171de.tar.gz
Merge pull request #272 from msievers/deep_locate
Hashie::Extensions::DeepLocate
-rw-r--r--CHANGELOG.md1
-rw-r--r--README.md42
-rw-r--r--lib/hashie.rb1
-rw-r--r--lib/hashie/extensions/deep_find.rb25
-rw-r--r--lib/hashie/extensions/deep_locate.rb94
-rw-r--r--spec/hashie/extensions/deep_locate_spec.rb124
6 files changed, 266 insertions, 21 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1b76eaa..de34803 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@
* [#261](https://github.com/intridea/hashie/pull/261): Fixed bug where Dash.property modifies argument object - [@d_tw](https://github.com/d_tw).
* [#264](https://github.com/intridea/hashie/pull/264): Methods such as abc? return true/false with Hashie::Extensions::MethodReader - [@Zloy](https://github.com/Zloy).
* [#269](https://github.com/intridea/hashie/pull/269): Add #extractable_options? so ActiveSupport Array#extract_options! can extract it - [@ridiculous](https://github.com/ridiculous).
+* [#269](https://github.com/intridea/hashie/pull/272): Added Hashie::Extensions::DeepLocate - [@msievers](https://github.com/msievers).
* Your contribution here.
## 3.3.2 (11/26/2014)
diff --git a/README.md b/README.md
index b53156c..8f13b23 100644
--- a/README.md
+++ b/README.md
@@ -313,6 +313,48 @@ user.deep_find_all(:name) #=> [{ first: 'Bob', last: 'Boberts' }, 'Rubyists', 'O
user.deep_select(:name) #=> [{ first: 'Bob', last: 'Boberts' }, 'Rubyists', 'Open source enthusiasts']
```
+### DeepLocate
+
+This extension can be mixed in to provide a depth first search based search for enumerables matching a given comparator callable.
+
+It returns all enumerables which contain at least one element, for which the given comparator returns ```true```.
+
+Because the container objects are returned, the result elements can be modified in place. This way, one can perform modifications on deeply nested hashes without the need to know the exact paths.
+
+```ruby
+
+books = [
+ {
+ title: "Ruby for beginners",
+ pages: 120
+ },
+ {
+ title: "CSS for intermediates",
+ pages: 80
+ },
+ {
+ title: "Collection of ruby books",
+ books: [
+ {
+ title: "Ruby for the rest of us",
+ pages: 576
+ }
+ ]
+ }
+]
+
+books.extend(Hashie::Extensions::DeepLocate)
+
+# for ruby 1.9 leave *no* space between the lambda rocket and the braces
+# http://ruby-journal.com/becareful-with-space-in-lambda-hash-rocket-syntax-between-ruby-1-dot-9-and-2-dot-0/
+
+books.deep_locate -> (key, value, object) { key == :title && value.include?("Ruby") }
+# => [{:title=>"Ruby for beginners", :pages=>120}, {:title=>"Ruby for the rest of us", :pages=>576}]
+
+books.deep_locate -> (key, value, object) { key == :pages && value <= 120 }
+# => [{:title=>"Ruby for beginners", :pages=>120}, {:title=>"CSS for intermediates", :pages=>80}]
+```
+
## 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.
diff --git a/lib/hashie.rb b/lib/hashie.rb
index 383e59f..51a9c5f 100644
--- a/lib/hashie.rb
+++ b/lib/hashie.rb
@@ -22,6 +22,7 @@ module Hashie
autoload :SymbolizeKeys, 'hashie/extensions/symbolize_keys'
autoload :DeepFetch, 'hashie/extensions/deep_fetch'
autoload :DeepFind, 'hashie/extensions/deep_find'
+ autoload :DeepLocate, 'hashie/extensions/deep_locate'
autoload :PrettyInspect, 'hashie/extensions/pretty_inspect'
autoload :KeyConversion, 'hashie/extensions/key_conversion'
autoload :MethodAccessWithOverride, 'hashie/extensions/method_access'
diff --git a/lib/hashie/extensions/deep_find.rb b/lib/hashie/extensions/deep_find.rb
index 5e9f725..d22476a 100644
--- a/lib/hashie/extensions/deep_find.rb
+++ b/lib/hashie/extensions/deep_find.rb
@@ -27,32 +27,15 @@ module Hashie
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
+ _deep_find_all(key, object).first
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) }
+ deep_locate_result = Hashie::Extensions::DeepLocate.deep_locate(key, object).tap do |result|
+ result.map! { |element| element[key] }
end
- matches
- end
-
- def reduce_to_match(key, enumerable)
- enumerable.reduce(nil) do |found, value|
- return found if found
-
- _deep_find(key, value)
- end
+ matches.concat(deep_locate_result)
end
end
end
diff --git a/lib/hashie/extensions/deep_locate.rb b/lib/hashie/extensions/deep_locate.rb
new file mode 100644
index 0000000..bf17c83
--- /dev/null
+++ b/lib/hashie/extensions/deep_locate.rb
@@ -0,0 +1,94 @@
+module Hashie
+ module Extensions
+ module DeepLocate
+ # The module level implementation of #deep_locate, incase you do not want
+ # to include/extend the base datastructure. For further examples please
+ # see #deep_locate.
+ #
+ # @example
+ # books = [
+ # {
+ # title: "Ruby for beginners",
+ # pages: 120
+ # },
+ # ...
+ # ]
+ #
+ # Hashie::Extensions::DeepLocate.deep_locate -> (key, value, object) { key == :title }, books
+ # # => [{:title=>"Ruby for beginners", :pages=>120}, ...]
+ def self.deep_locate(comparator, object)
+ # ensure comparator is a callable
+ unless comparator.respond_to?(:call)
+ comparator = lambda do |non_callable_object|
+ ->(key, _, _) { key == non_callable_object }
+ end.call(comparator)
+ end
+
+ _deep_locate(comparator, object)
+ end
+
+ # Performs a depth-first search on deeply nested data structures for a
+ # given comparator callable and returns each Enumerable, for which the
+ # callable returns true for at least one the its elements.
+ #
+ # @example
+ # books = [
+ # {
+ # title: "Ruby for beginners",
+ # pages: 120
+ # },
+ # {
+ # title: "CSS for intermediates",
+ # pages: 80
+ # },
+ # {
+ # title: "Collection of ruby books",
+ # books: [
+ # {
+ # title: "Ruby for the rest of us",
+ # pages: 576
+ # }
+ # ]
+ # }
+ # ]
+ #
+ # books.extend(Hashie::Extensions::DeepLocate)
+ #
+ # # for ruby 1.9 leave *no* space between the lambda rocket and the braces
+ # # http://ruby-journal.com/becareful-with-space-in-lambda-hash-rocket-syntax-between-ruby-1-dot-9-and-2-dot-0/
+ #
+ # books.deep_locate -> (key, value, object) { key == :title && value.include?("Ruby") }
+ # # => [{:title=>"Ruby for beginners", :pages=>120}, {:title=>"Ruby for the rest of us", :pages=>576}]
+ #
+ # books.deep_locate -> (key, value, object) { key == :pages && value <= 120 }
+ # # => [{:title=>"Ruby for beginners", :pages=>120}, {:title=>"CSS for intermediates", :pages=>80}]
+ def deep_locate(comparator)
+ Hashie::Extensions::DeepLocate.deep_locate(comparator, self)
+ end
+
+ private
+
+ def self._deep_locate(comparator, object, result = [])
+ if object.is_a?(::Enumerable)
+ if object.any? do |value|
+ if object.is_a?(::Hash)
+ key, value = value
+ else
+ key = nil
+ end
+
+ comparator.call(key, value, object)
+ end
+ result.push object
+ else
+ (object.respond_to?(:values) ? object.values : object.entries).each do |value|
+ _deep_locate(comparator, value, result)
+ end
+ end
+ end
+
+ result
+ end
+ end
+ end
+end
diff --git a/spec/hashie/extensions/deep_locate_spec.rb b/spec/hashie/extensions/deep_locate_spec.rb
new file mode 100644
index 0000000..3391b27
--- /dev/null
+++ b/spec/hashie/extensions/deep_locate_spec.rb
@@ -0,0 +1,124 @@
+require 'spec_helper'
+
+describe Hashie::Extensions::DeepLocate do
+ let(:hash) do
+ {
+ from: 0,
+ size: 25,
+ query: {
+ bool: {
+ must: [
+ {
+ query_string: {
+ query: 'foobar',
+ default_operator: 'AND',
+ fields: [
+ 'title^2',
+ '_all'
+ ]
+ }
+ },
+ {
+ match: {
+ field_1: 'value_1'
+ }
+ },
+ {
+ range: {
+ lsr09: {
+ gte: 2014
+ }
+ }
+ }
+ ],
+ should: [
+ {
+ match: {
+ field_2: 'value_2'
+ }
+ }
+ ],
+ must_not: [
+ {
+ range: {
+ lsr10: {
+ gte: 2014
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ end
+
+ describe '.deep_locate' do
+ context 'if called with a non-callable comparator' do
+ it 'creates a key comparator on-th-fly' do
+ expect(described_class.deep_locate(:lsr10, hash)).to eq([hash[:query][:bool][:must_not][0][:range]])
+ end
+ end
+
+ it 'locates enumerables for which the given comparator returns true for at least one element' do
+ examples = [
+ [
+ ->(key, _value, _object) { key == :fields },
+ [
+ hash[:query][:bool][:must].first[:query_string]
+ ]
+ ],
+ [
+ ->(_key, value, _object) { value.is_a?(String) && value.include?('value') },
+ [
+ hash[:query][:bool][:must][1][:match],
+ hash[:query][:bool][:should][0][:match]
+ ]
+ ],
+ [
+ lambda do |_key, _value, object|
+ object.is_a?(Array) &&
+ !object.extend(described_class).deep_locate(:match).empty?
+ end,
+ [
+ hash[:query][:bool][:must],
+ hash[:query][:bool][:should]
+ ]
+ ]
+ ]
+
+ examples.each do |comparator, expected_result|
+ expect(described_class.deep_locate(comparator, hash)).to eq(expected_result)
+ end
+ end
+
+ it 'returns an empty array if nothing was found' do
+ expect(described_class.deep_locate(:muff, foo: 'bar')).to eq([])
+ end
+ end
+
+ context 'if extending an existing object' do
+ let(:extended_hash) do
+ hash.extend(described_class)
+ end
+
+ it 'adds #deep_locate' do
+ expect(extended_hash.deep_locate(:bool)).to eq([hash[:query]])
+ end
+ end
+
+ context 'if included in a hash' do
+ let(:derived_hash_with_extension_included) do
+ Class.new(Hash) do
+ include Hashie::Extensions::DeepLocate
+ end
+ end
+
+ let(:instance) do
+ derived_hash_with_extension_included.new.update(hash)
+ end
+
+ it 'adds #deep_locate' do
+ expect(instance.deep_locate(:bool)).to eq([hash[:query]])
+ end
+ end
+end