hash_tools 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.rspec +1 -0
- data/.yardopts +1 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +20 -0
- data/README.md +52 -0
- data/Rakefile +53 -0
- data/hash_tools.gemspec +59 -0
- data/lib/hash_tools/indifferent.rb +103 -0
- data/lib/hash_tools.rb +135 -0
- data/spec/hash_tools/indifferent_spec.rb +51 -0
- data/spec/hash_tools_spec.rb +171 -0
- data/spec/spec_helper.rb +4 -0
- metadata +106 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 7c49be4b67ede742b42848405b4ac0226266a345
|
4
|
+
data.tar.gz: e4d2509e181d2466bd4a2f42bbdd328c2f9bad4e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5e96a94e34f64344db9236a10bd12af7e8e726a00b9ba0b597be36c2ed7cdb15d1e7e62669eadf49ab91a250382fa2ac5d03981faa8a04f4a5bbc467dbc32046
|
7
|
+
data.tar.gz: 2a1e88310651ce839db9e14c116b9df405917a347f2a3ceb8c2ace9ac3309c26ce35224625fa6bc1ea4a3fd8e56d5debcb4f2e89346d22d57f024096b7deb865
|
data/.document
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--markup markdown
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2015 Julik Tarkhanov
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# hash_tools
|
2
|
+
|
3
|
+
Do things to Hashes, without injecting methods into them or extending core classes of the language.
|
4
|
+
And mostly without being too smart. Does not require ActiveSupport.
|
5
|
+
|
6
|
+
Transforming the keys of a hash:
|
7
|
+
|
8
|
+
HashTools::Transform.transform_keys_of({'foo' => 1}, &:upcase) #=> {'FOO' => 1}, works recursively
|
9
|
+
|
10
|
+
Fetching multiple values from a Hash:
|
11
|
+
|
12
|
+
h = {
|
13
|
+
'foo' => {
|
14
|
+
'bar' => 2
|
15
|
+
'baz' => 1
|
16
|
+
}
|
17
|
+
}
|
18
|
+
HashTools.deep_fetch(h, 'foo/bar') #=> 2
|
19
|
+
HashTools.deep_fetch_multi(h, 'foo/bar', 'foo/baz') #=> [2, 1]
|
20
|
+
|
21
|
+
Fetching multiple values from arrays of Hashes
|
22
|
+
|
23
|
+
records = [
|
24
|
+
{'name': 'Jake'},
|
25
|
+
{'name': 'Barbara'},
|
26
|
+
]
|
27
|
+
|
28
|
+
HashTools.deep_map_value(records, 'name') #=> ['Jake', 'Barbara']
|
29
|
+
|
30
|
+
A simple indifferent access proxy:
|
31
|
+
|
32
|
+
h = {'foo'=>{bar: 2}}
|
33
|
+
w = HashTools.indifferent(h)
|
34
|
+
w[:foo][:bar] #=> 2
|
35
|
+
|
36
|
+
Check the documentation for the separate modules for more.
|
37
|
+
|
38
|
+
## Contributing to hash_tools
|
39
|
+
|
40
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
|
41
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
|
42
|
+
* Fork the project.
|
43
|
+
* Start a feature/bugfix branch.
|
44
|
+
* Commit and push until you are happy with your contribution.
|
45
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
46
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
47
|
+
|
48
|
+
## Copyright
|
49
|
+
|
50
|
+
Copyright (c) 2015 Julik Tarkhanov. See LICENSE.txt for
|
51
|
+
further details.
|
52
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative 'lib/hash_tools'
|
4
|
+
|
5
|
+
require 'rubygems'
|
6
|
+
require 'bundler'
|
7
|
+
begin
|
8
|
+
Bundler.setup(:default, :development)
|
9
|
+
rescue Bundler::BundlerError => e
|
10
|
+
$stderr.puts e.message
|
11
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
12
|
+
exit e.status_code
|
13
|
+
end
|
14
|
+
require 'rake'
|
15
|
+
|
16
|
+
require 'jeweler'
|
17
|
+
Jeweler::Tasks.new do |gem|
|
18
|
+
gem.version = HashTools::VERSION
|
19
|
+
# gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options
|
20
|
+
gem.name = "hash_tools"
|
21
|
+
gem.homepage = "http://github.com/julik/hash_tools"
|
22
|
+
gem.license = "MIT"
|
23
|
+
gem.description = %Q{Do useful things to Ruby Hashes}
|
24
|
+
gem.summary = %Q{Do useful things to Ruby Hashes}
|
25
|
+
gem.email = "me@julik.nl"
|
26
|
+
gem.authors = ["Julik Tarkhanov"]
|
27
|
+
# dependencies defined in Gemfile
|
28
|
+
end
|
29
|
+
Jeweler::RubygemsDotOrgTasks.new
|
30
|
+
|
31
|
+
require 'rspec/core'
|
32
|
+
require 'rspec/core/rake_task'
|
33
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
34
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
35
|
+
end
|
36
|
+
|
37
|
+
desc "Code coverage detail"
|
38
|
+
task :simplecov do
|
39
|
+
ENV['COVERAGE'] = "true"
|
40
|
+
Rake::Task['spec'].execute
|
41
|
+
end
|
42
|
+
|
43
|
+
task :default => :spec
|
44
|
+
|
45
|
+
require 'rdoc/task'
|
46
|
+
Rake::RDocTask.new do |rdoc|
|
47
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
48
|
+
|
49
|
+
rdoc.rdoc_dir = 'rdoc'
|
50
|
+
rdoc.title = "hash_tools #{version}"
|
51
|
+
rdoc.rdoc_files.include('README*')
|
52
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
53
|
+
end
|
data/hash_tools.gemspec
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
# stub: hash_tools 1.0.0 ruby lib
|
6
|
+
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.name = "hash_tools"
|
9
|
+
s.version = "1.0.0"
|
10
|
+
|
11
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
12
|
+
s.require_paths = ["lib"]
|
13
|
+
s.authors = ["Julik Tarkhanov"]
|
14
|
+
s.date = "2015-10-17"
|
15
|
+
s.description = "Do useful things to Ruby Hashes"
|
16
|
+
s.email = "me@julik.nl"
|
17
|
+
s.extra_rdoc_files = [
|
18
|
+
"LICENSE.txt",
|
19
|
+
"README.md"
|
20
|
+
]
|
21
|
+
s.files = [
|
22
|
+
".document",
|
23
|
+
".rspec",
|
24
|
+
".yardopts",
|
25
|
+
"Gemfile",
|
26
|
+
"LICENSE.txt",
|
27
|
+
"README.md",
|
28
|
+
"Rakefile",
|
29
|
+
"hash_tools.gemspec",
|
30
|
+
"lib/hash_tools.rb",
|
31
|
+
"lib/hash_tools/indifferent.rb",
|
32
|
+
"spec/hash_tools/indifferent_spec.rb",
|
33
|
+
"spec/hash_tools_spec.rb",
|
34
|
+
"spec/spec_helper.rb"
|
35
|
+
]
|
36
|
+
s.homepage = "http://github.com/julik/hash_tools"
|
37
|
+
s.licenses = ["MIT"]
|
38
|
+
s.rubygems_version = "2.2.2"
|
39
|
+
s.summary = "Do useful things to Ruby Hashes"
|
40
|
+
|
41
|
+
if s.respond_to? :specification_version then
|
42
|
+
s.specification_version = 4
|
43
|
+
|
44
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
45
|
+
s.add_development_dependency(%q<rspec>, ["< 3.3", "~> 3.2.0"])
|
46
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.0"])
|
47
|
+
s.add_development_dependency(%q<jeweler>, ["~> 2.0.1"])
|
48
|
+
else
|
49
|
+
s.add_dependency(%q<rspec>, ["< 3.3", "~> 3.2.0"])
|
50
|
+
s.add_dependency(%q<bundler>, ["~> 1.0"])
|
51
|
+
s.add_dependency(%q<jeweler>, ["~> 2.0.1"])
|
52
|
+
end
|
53
|
+
else
|
54
|
+
s.add_dependency(%q<rspec>, ["< 3.3", "~> 3.2.0"])
|
55
|
+
s.add_dependency(%q<bundler>, ["~> 1.0"])
|
56
|
+
s.add_dependency(%q<jeweler>, ["~> 2.0.1"])
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
# A tiny version of HashWithIndifferentAccess. Works like a wrapper proxy around a Ruby Hash.
|
4
|
+
# Does not support all of the methods for a Ruby Hash object, but nevertheless can be useful
|
5
|
+
# for checking params and for working with parsed JSON.
|
6
|
+
class HashTools::Indifferent < SimpleDelegator
|
7
|
+
# Create a new Indifferent by supplying a Ruby Hash object to wrap. The Hash being
|
8
|
+
# wrapped is not going to be altered or copied.
|
9
|
+
#
|
10
|
+
# @param naked_hash [Hash] the Hash object to wrap with an Indifferent
|
11
|
+
def initialize(naked_hash)
|
12
|
+
__setobj__(naked_hash)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Get a value from the Hash, bu supplying a Symbol or a String
|
16
|
+
# Key presence is verified by first trying a Symbol, and then a String.
|
17
|
+
#
|
18
|
+
# @param k the key to fetch
|
19
|
+
# @return the value, wrapped in {Indifferent} if it is a Hash
|
20
|
+
def [](k)
|
21
|
+
v = __getobj__[__transform_key__(k)]
|
22
|
+
__rewrap__(v)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Set a value in the Hash, bu supplying a Symbol or a String as a key.
|
26
|
+
# Key presence is verified by first trying a Symbol, and then a String.
|
27
|
+
#
|
28
|
+
# @param k the key to set
|
29
|
+
# @param v the value to set
|
30
|
+
# @return v
|
31
|
+
def []=(k, v)
|
32
|
+
__getobj__[ __transform_key__(k) ] = v
|
33
|
+
end
|
34
|
+
|
35
|
+
# Fetch a value, by supplying a Symbol or a String as a key.
|
36
|
+
# Key presence is verified by first trying a Symbol, and then a String.
|
37
|
+
#
|
38
|
+
# @param k the key to set
|
39
|
+
# @param blk the block for no value
|
40
|
+
# @return v
|
41
|
+
def fetch(k, &blk)
|
42
|
+
v = __getobj__.fetch( __transform_key__(k) , &blk)
|
43
|
+
__rewrap__(v)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Get the keys of the Hash. The keys are returned as-is (both Symbols and Strings).
|
47
|
+
#
|
48
|
+
# @return [Array] an array of keys
|
49
|
+
def keys
|
50
|
+
__getobj__.keys.map{|k| __transform_key__(k) }
|
51
|
+
end
|
52
|
+
|
53
|
+
# Checks for key presence whether the key is a String or a Symbol
|
54
|
+
#
|
55
|
+
# @param k[String,Symbol] the key to check
|
56
|
+
def key?(k)
|
57
|
+
__getobj__.has_key?( __transform_key__(k))
|
58
|
+
end
|
59
|
+
|
60
|
+
# Yields each key - value pair of the indifferent.
|
61
|
+
# If the value is a Hash as well, that hash will be wrapped in an Indifferent before returning
|
62
|
+
def each(&blk)
|
63
|
+
__getobj__.each do |k, v|
|
64
|
+
blk.call([__transform_key__(k), __rewrap__(v)])
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Yields each key - value pair of the indifferent.
|
69
|
+
# If the value is a Hash as well, that hash will be wrapped in an Indifferent before returning
|
70
|
+
def each_pair
|
71
|
+
o = __getobj__
|
72
|
+
keys.each do | k |
|
73
|
+
value = o[__transform_key__(k)]
|
74
|
+
yield(k, __rewrap__(value))
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def map(&blk)
|
79
|
+
keys.map do |k|
|
80
|
+
tk = __transform_key__(k)
|
81
|
+
yield [tk, __rewrap__(__getobj__[tk])]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
alias_method :has_key?, :key?
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def __transform_key__(k)
|
90
|
+
if __getobj__.has_key?(k.to_sym)
|
91
|
+
k.to_sym
|
92
|
+
else
|
93
|
+
k.to_s
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def __rewrap__(v)
|
98
|
+
return v if v.is_a?(self.class)
|
99
|
+
return self.class.new(v) if v.is_a?(Hash)
|
100
|
+
return v.map{|e| __rewrap__(e)} if v.is_a?(Array)
|
101
|
+
v
|
102
|
+
end
|
103
|
+
end
|
data/lib/hash_tools.rb
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
module HashTools
|
2
|
+
VERSION = '1.0.0'
|
3
|
+
|
4
|
+
require_relative 'hash_tools/indifferent'
|
5
|
+
|
6
|
+
FWD_SLASH = '/' # Used as the default separator for deep_fetch
|
7
|
+
INT_KEY_RE = /^\-?\d+$/ # Regular expression to detect array indices in the path ("phones/0/code")
|
8
|
+
|
9
|
+
# Fetch a deeply-nested hash key from a hash, using a String representing a path
|
10
|
+
#
|
11
|
+
# deep_fetch({
|
12
|
+
# 'a' => {
|
13
|
+
# 'b' => {
|
14
|
+
# 'c' => value}}
|
15
|
+
# }, 'a/b/c') #=> value
|
16
|
+
#
|
17
|
+
# @param hash [Hash] the (potentially deep) string-keyed Hash to fetch the value from
|
18
|
+
# @param path [String] the path to the item in `hash`. The path may contain numbers for deeply nested arrays ('foo/0/bar')
|
19
|
+
# @param separator [String] the path separator, defaults to '/'
|
20
|
+
# @param default_blk The default value block for when there is no value.
|
21
|
+
# @return the fetched value or the value of the default_block
|
22
|
+
def deep_fetch(hash, path, separator: FWD_SLASH, &default_blk)
|
23
|
+
keys = path.split(separator)
|
24
|
+
keys.inject(hash) do |hash_or_array, k|
|
25
|
+
if !hash_or_array.respond_to?(:fetch)
|
26
|
+
raise "#{hash_or_array.inspect} does not respond to #fetch"
|
27
|
+
elsif hash_or_array.is_a?(Array) && k =~ INT_KEY_RE
|
28
|
+
hash_or_array.fetch(k.to_i, &default_blk)
|
29
|
+
else
|
30
|
+
hash_or_array.fetch(k, &default_blk)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Fetches multiple keys from a deep hash, using a Strings representing paths
|
36
|
+
#
|
37
|
+
# deep_fetch({
|
38
|
+
# 'z' => 1,
|
39
|
+
# 'a' => {
|
40
|
+
# 'b' => {
|
41
|
+
# 'c' => value}}
|
42
|
+
# }, 'a/b', 'z') #=> [value, 1]
|
43
|
+
#
|
44
|
+
# @param hash [Hash] the (potentially deep) string-keyed Hash to fetch the value from
|
45
|
+
# @param key_paths [String] the paths to the items in `hash`. The paths may contain numbers for deeply nested arrays ('foo/0/bar')
|
46
|
+
# @param separator [String] the path separator, defaults to '/'
|
47
|
+
# @return [Array] the fetched values
|
48
|
+
def deep_fetch_multi(hash, *key_paths, separator: FWD_SLASH)
|
49
|
+
key_paths.map{|k| deep_fetch(hash, k, separator: separator) }
|
50
|
+
end
|
51
|
+
|
52
|
+
# Fetches a deeply nested key from each of the Hashes in a given Array.
|
53
|
+
#
|
54
|
+
# arr = [
|
55
|
+
# {'age' => 12, 'name' => 'Jack'},
|
56
|
+
# {'age' => 25, 'name' => 'Joe'},
|
57
|
+
# ]
|
58
|
+
# deep_map_value(arr, 'age') => [12, 25]
|
59
|
+
#
|
60
|
+
# @param enum_of_hashes [Enumerable] a list of Hash objects to fetch the values from
|
61
|
+
# @param path [String] the paths to the value. Paths may contain numbers for deeply nested arrays ('foo/0/bar')
|
62
|
+
# @param separator [String] the path separator, defaults to '/'
|
63
|
+
# @return [Array] the fetched values
|
64
|
+
def deep_map_value(enum_of_hashes, path, separator: FWD_SLASH)
|
65
|
+
enum_of_hashes.map{|h| deep_fetch(h, path, separator: separator)}
|
66
|
+
end
|
67
|
+
|
68
|
+
# Recursively transform string keys and values of a passed
|
69
|
+
# Hash or Array using the passed transformer
|
70
|
+
#
|
71
|
+
# @param any [Hash,String,Array] the value to transform the contained items in
|
72
|
+
# @param transformer The block applied to each string key and value, recursively
|
73
|
+
# @return the transformed value
|
74
|
+
def transform_string_keys_and_values_of(any, &transformer)
|
75
|
+
transform_string_values_of(transform_keys_of(any, &transformer), &transformer)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Recursively convert string values in nested hashes and
|
79
|
+
# arrays using a passed block. The block will receive the String
|
80
|
+
# to transform and should return a transformed string.
|
81
|
+
#
|
82
|
+
# @param any [Hash,String,Array] the value to transform the contained items in
|
83
|
+
# @param transformer The block applied to each string value, recursively
|
84
|
+
# @return the transformed value
|
85
|
+
def transform_string_values_of(any, &transformer)
|
86
|
+
if any.is_a?(String)
|
87
|
+
transformer.call(any)
|
88
|
+
elsif any.is_a?(Array)
|
89
|
+
any.map{|e| transform_string_values_of(e, &transformer) }
|
90
|
+
elsif any.is_a?(Hash)
|
91
|
+
h = {}
|
92
|
+
any.each_pair do |k, v|
|
93
|
+
h[k] = transform_string_values_of(v, &transformer)
|
94
|
+
end
|
95
|
+
h
|
96
|
+
else
|
97
|
+
any
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Recursively convert hash keys using a block.
|
102
|
+
# using a passed block. The block will receive a hash key
|
103
|
+
# to be transformed and should return a transformed key
|
104
|
+
# For example, to go from uderscored notation to camelized:
|
105
|
+
#
|
106
|
+
# h = {'foo_bar' => 1}
|
107
|
+
# transform_keys_of(h) {|k| k.to_s.camelize(:lower) } # => {'fooBar' => 1}
|
108
|
+
#
|
109
|
+
# @param any [Hash] the Hash to transform
|
110
|
+
# @param transformer the block to apply to each key, recursively
|
111
|
+
# @return [Hash] the transformed Hash
|
112
|
+
def transform_keys_of(any, &transformer)
|
113
|
+
if any.is_a?(Array)
|
114
|
+
return any.map{|e| transform_keys_of(e, &transformer) }
|
115
|
+
elsif any.is_a?(Hash)
|
116
|
+
h = {}
|
117
|
+
any.each_pair do |k, v|
|
118
|
+
h[transformer.call(k.to_s)] = transform_keys_of(v, &transformer)
|
119
|
+
end
|
120
|
+
h
|
121
|
+
else
|
122
|
+
any
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Returns an {HashTools::Indifferent} wrapper for the given Hash.
|
127
|
+
#
|
128
|
+
# @param hash [Hash] the Hash to wrap
|
129
|
+
# @return [Indifferent] the wrapper for the hash
|
130
|
+
def indifferent(hash)
|
131
|
+
Indifferent.new(hash)
|
132
|
+
end
|
133
|
+
|
134
|
+
extend self
|
135
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
describe HashTools::Indifferent do
|
4
|
+
it 'supports indifferent access' do
|
5
|
+
h_syms = {a: 1, 'b' => 2}
|
6
|
+
wrapper = described_class.new(h_syms)
|
7
|
+
|
8
|
+
expect(wrapper['a']).to eq(1)
|
9
|
+
expect(wrapper[:a]).to eq(1)
|
10
|
+
expect(wrapper.fetch('a')).to eq(1)
|
11
|
+
expect(wrapper.fetch(:a)).to eq(1)
|
12
|
+
|
13
|
+
expect(wrapper['b']).to eq(2)
|
14
|
+
expect(wrapper[:b]).to eq(2)
|
15
|
+
expect(wrapper.fetch('b')).to eq(2)
|
16
|
+
expect(wrapper.fetch(:b)).to eq(2)
|
17
|
+
|
18
|
+
expect(wrapper.keys).to eq(h_syms.keys)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'supports indifferent access to deeply nested hashes' do
|
22
|
+
h_deep = {a: {:b => 1, 'c' => 2}}
|
23
|
+
|
24
|
+
wrapper = described_class.new(h_deep)
|
25
|
+
expect(wrapper[:a][:b]).to eq(1)
|
26
|
+
expect(wrapper['a']['b']).to eq(1)
|
27
|
+
|
28
|
+
expect(wrapper[:a][:c]).to eq(2)
|
29
|
+
expect(wrapper['a']['c']).to eq(2)
|
30
|
+
|
31
|
+
expect(wrapper.keys).to eq(h_deep.keys)
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'supports map' do
|
35
|
+
h_deep = {:a => {:b => 1}, 'b' => {'b' => 2}}
|
36
|
+
wrapper = described_class.new(h_deep)
|
37
|
+
|
38
|
+
wrapper.map do |(k, v)|
|
39
|
+
expect(v['b']).not_to be_nil
|
40
|
+
expect(v[:b]).not_to be_nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'supports indifferent access to inner arrays' do
|
45
|
+
h_deep = {:a => [{:b => 1}]}
|
46
|
+
wrapper = described_class.new(h_deep)
|
47
|
+
|
48
|
+
expect(wrapper[:a][0][:b]).to eq(1)
|
49
|
+
expect(wrapper['a'][0]['b']).to eq(1)
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
describe HashTools do
|
4
|
+
let(:t) do
|
5
|
+
Class.new do
|
6
|
+
include HashTools
|
7
|
+
end.new
|
8
|
+
end
|
9
|
+
let(:uppercase) { ->(k){ k.upcase } }
|
10
|
+
|
11
|
+
describe '.indifferent' do
|
12
|
+
it 'returns an indifferent wrapper' do
|
13
|
+
h = {'foo' => {bar: 1}}
|
14
|
+
ind = t.indifferent(h)
|
15
|
+
expect(ind[:foo][:bar]).to eq(1)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'transforms hash keys' do
|
20
|
+
s = {"set" => 10}
|
21
|
+
ref = {"SET" => 10}
|
22
|
+
expect(t.transform_keys_of(s, &uppercase)).to eq(ref)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'transforms nested hash keys' do
|
26
|
+
s = {"set" => {"two"=>123}}
|
27
|
+
ref = {"SET"=>{"TWO"=>123}}
|
28
|
+
expect(t.transform_keys_of(s, &uppercase)).to eq(ref)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'does nothing to the array' do
|
32
|
+
a = %w( a b c d)
|
33
|
+
expect(t.transform_keys_of(a, &uppercase)).to eq(a)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'transforms hashes embedded in arrays' do
|
37
|
+
a = [{"me"=>"Julik"}]
|
38
|
+
ref = [{"ME"=>"Julik"}]
|
39
|
+
expect(t.transform_keys_of(a, &uppercase)).to eq(ref)
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'transforms nested hashes in an array' do
|
43
|
+
a = {"foo" => [{"me"=>"Julik"}]}
|
44
|
+
ref = {"FOO"=>[{"ME"=>"Julik"}]}
|
45
|
+
expect(t.transform_keys_of(a, &uppercase)).to eq(ref)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'exposes methods on the module itself' do
|
49
|
+
expect(HashTools).to respond_to(:transform_keys_of)
|
50
|
+
end
|
51
|
+
|
52
|
+
describe '.transform_string_values_of' do
|
53
|
+
it 'transforms the string value' do
|
54
|
+
x = "foo"
|
55
|
+
expect(t.transform_string_values_of(x, &uppercase)).to eq('FOO')
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'transforms strings in array' do
|
59
|
+
x = %w( foo bar baz)
|
60
|
+
ref = %w( FOO BAR BAZ )
|
61
|
+
expect(t.transform_string_values_of(x, &uppercase)).to eq(ref)
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'transforms string values in a Hash' do
|
65
|
+
x = {"foo" => "bar"}
|
66
|
+
ref = {"foo" => "BAR"}
|
67
|
+
expect(t.transform_string_values_of(x, &uppercase)).to eq(ref)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe '.transform_string_keys_and_values_of' do
|
72
|
+
it 'transforms both keys and values' do
|
73
|
+
x = {"foo" => "bar"}
|
74
|
+
ref = {"FOO" => "BAR"}
|
75
|
+
expect(t.transform_string_keys_and_values_of(x, &uppercase)).to eq(ref)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe '.deep_fetch' do
|
80
|
+
let(:deep) {
|
81
|
+
{
|
82
|
+
'foo' =>1,
|
83
|
+
'bar' => {
|
84
|
+
'baz' => 2
|
85
|
+
},
|
86
|
+
'array' => [1,2,3],
|
87
|
+
'array-with-hashes' => [{'name' => 'Joe'}, {'name' => 'Jane'}]
|
88
|
+
}
|
89
|
+
}
|
90
|
+
|
91
|
+
it 'accepts a block for a default value' do
|
92
|
+
v = described_class.deep_fetch(deep, 'bar/nonexistent') { :default}
|
93
|
+
expect(v).to eq(:default)
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'fetches deep keys from a hash keyed by strings' do
|
97
|
+
expect(described_class.deep_fetch(deep, 'foo')).to eq(deep.fetch('foo'))
|
98
|
+
expect(described_class.deep_fetch(deep, 'bar/baz')).to eq(deep.fetch('bar').fetch('baz'))
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'fetches deep keys with a custom separator' do
|
102
|
+
expect(described_class.deep_fetch(deep, 'bar.baz', separator: '.')).to eq(deep.fetch('bar').fetch('baz'))
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'causes a KeyError to be raised for missing keys' do
|
106
|
+
expect {
|
107
|
+
described_class.deep_fetch(deep, 'bar/nonexistent')
|
108
|
+
}.to raise_error(KeyError, 'key not found: "nonexistent"')
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'allows fetches from arrays' do
|
112
|
+
expect(described_class.deep_fetch(deep, 'array/0')).to eq(1)
|
113
|
+
expect(described_class.deep_fetch(deep, 'array/-1')).to eq(3)
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'allows fetches from hashes within arrays' do
|
117
|
+
expect(described_class.deep_fetch(deep, 'array-with-hashes/0/name')).to eq('Joe')
|
118
|
+
expect {
|
119
|
+
described_class.deep_fetch(deep, 'array-with-hashes/10/name')
|
120
|
+
}.to raise_error(IndexError, /index 10 outside of array bounds/)
|
121
|
+
|
122
|
+
default_value = described_class.deep_fetch(deep, 'array-with-hashes/0/jake') { :default }
|
123
|
+
expect(default_value).to eq(:default)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
describe '.deep_fetch_multi' do
|
128
|
+
let(:deep) {
|
129
|
+
{
|
130
|
+
'foo' =>1,
|
131
|
+
'bar' => {
|
132
|
+
'baz' => 2
|
133
|
+
},
|
134
|
+
'array' => [1,2,3],
|
135
|
+
'array-with-hashes' => [{'name' => 'Joe'}, {'name' => 'Jane'}]
|
136
|
+
}
|
137
|
+
}
|
138
|
+
|
139
|
+
it 'fetches mutiple keys' do
|
140
|
+
expect(described_class.deep_fetch_multi(deep, 'foo', 'bar/baz')).to eq([1,2])
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'fetches deep keys with a custom separator' do
|
144
|
+
expect(described_class.deep_fetch_multi(deep, 'foo', 'bar.baz', separator: '.')).to eq([1,2])
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'causes a KeyError to be raised for missing keys' do
|
148
|
+
expect {
|
149
|
+
described_class.deep_fetch_multi(deep, 'foo', 'nonexistent')
|
150
|
+
}.to raise_error(KeyError)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
describe '.deep_map_value' do
|
155
|
+
it 'deep maps the values' do
|
156
|
+
v = [
|
157
|
+
{'foo' => 5},
|
158
|
+
{'foo' => 6},
|
159
|
+
]
|
160
|
+
expect(described_class.deep_map_value(v, "foo")).to eq([5,6])
|
161
|
+
end
|
162
|
+
|
163
|
+
it 'deep maps the values with a custom separator' do
|
164
|
+
v = [
|
165
|
+
{'foo' => {'bar' => 1}},
|
166
|
+
{'foo' => {'bar' => 2}},
|
167
|
+
]
|
168
|
+
expect(described_class.deep_map_value(v, "foo-bar", separator: '-')).to eq([1,2])
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hash_tools
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Julik Tarkhanov
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-10-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "<"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.3'
|
20
|
+
- - "~>"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 3.2.0
|
23
|
+
type: :development
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "<"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '3.3'
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 3.2.0
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: bundler
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '1.0'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '1.0'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: jeweler
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 2.0.1
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 2.0.1
|
61
|
+
description: Do useful things to Ruby Hashes
|
62
|
+
email: me@julik.nl
|
63
|
+
executables: []
|
64
|
+
extensions: []
|
65
|
+
extra_rdoc_files:
|
66
|
+
- LICENSE.txt
|
67
|
+
- README.md
|
68
|
+
files:
|
69
|
+
- ".document"
|
70
|
+
- ".rspec"
|
71
|
+
- ".yardopts"
|
72
|
+
- Gemfile
|
73
|
+
- LICENSE.txt
|
74
|
+
- README.md
|
75
|
+
- Rakefile
|
76
|
+
- hash_tools.gemspec
|
77
|
+
- lib/hash_tools.rb
|
78
|
+
- lib/hash_tools/indifferent.rb
|
79
|
+
- spec/hash_tools/indifferent_spec.rb
|
80
|
+
- spec/hash_tools_spec.rb
|
81
|
+
- spec/spec_helper.rb
|
82
|
+
homepage: http://github.com/julik/hash_tools
|
83
|
+
licenses:
|
84
|
+
- MIT
|
85
|
+
metadata: {}
|
86
|
+
post_install_message:
|
87
|
+
rdoc_options: []
|
88
|
+
require_paths:
|
89
|
+
- lib
|
90
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '0'
|
100
|
+
requirements: []
|
101
|
+
rubyforge_project:
|
102
|
+
rubygems_version: 2.2.2
|
103
|
+
signing_key:
|
104
|
+
specification_version: 4
|
105
|
+
summary: Do useful things to Ruby Hashes
|
106
|
+
test_files: []
|