summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Sievers <michael_sievers@web.de>2015-02-02 12:04:30 +0100
committerMichael Sievers <michael_sievers@web.de>2015-02-03 10:34:52 +0100
commitb76d0871403fba0e9bf8e02c74125b030352e357 (patch)
tree1c79e8d936c34f489dc9113b3c8412cb2b50b0e7
parent7f63f343524d40d409e184004b809af382b0002d (diff)
downloadhashie-b76d0871403fba0e9bf8e02c74125b030352e357.tar.gz
Added 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_locate.rb94
-rw-r--r--spec/hashie/extensions/deep_locate_spec.rb124
5 files changed, 262 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4333585..81b369d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,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_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