Added Hashie::Extensions::DeepLocate

This commit is contained in:
Michael Sievers 2015-02-02 12:04:30 +01:00
parent 7f63f34352
commit b76d087140
5 changed files with 262 additions and 0 deletions

View File

@ -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)

View File

@ -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.

View File

@ -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'

View File

@ -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

View File

@ -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