-
Notifications
You must be signed in to change notification settings - Fork 311
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #272 from msievers/deep_locate
Hashie::Extensions::DeepLocate
- Loading branch information
Showing
6 changed files
with
266 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |