diff options
author | Daniel Doubrovkine (dB.) @dblockdotorg <dblock@dblock.org> | 2015-02-03 06:35:21 -0500 |
---|---|---|
committer | Daniel Doubrovkine (dB.) @dblockdotorg <dblock@dblock.org> | 2015-02-03 06:35:21 -0500 |
commit | 118eeaf6a62b3847048106be8ba84fd3976171de (patch) | |
tree | a1020bda8ae74634a342412bb401450b1cc7503f | |
parent | dc3bc1aa698324ad91b071b76f58fd6ce9423497 (diff) | |
parent | b3c123cc95df28e2b240a4ab2b3c7620139aa90e (diff) | |
download | hashie-118eeaf6a62b3847048106be8ba84fd3976171de.tar.gz |
Merge pull request #272 from msievers/deep_locate
Hashie::Extensions::DeepLocate
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | README.md | 42 | ||||
-rw-r--r-- | lib/hashie.rb | 1 | ||||
-rw-r--r-- | lib/hashie/extensions/deep_find.rb | 25 | ||||
-rw-r--r-- | lib/hashie/extensions/deep_locate.rb | 94 | ||||
-rw-r--r-- | spec/hashie/extensions/deep_locate_spec.rb | 124 |
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) @@ -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 |