Added Hashie::Extensions::DeepLocate
This commit is contained in:
parent
7f63f34352
commit
b76d087140
|
@ -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)
|
||||
|
|
42
README.md
42
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.
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue