hash_tools 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --markup markdown
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "http://rubygems.org"
2
+
3
+ group :development do
4
+ gem "rspec", "~> 3.2.0", '< 3.3'
5
+ gem "bundler", "~> 1.0"
6
+ gem "jeweler", "~> 2.0.1"
7
+ end
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
@@ -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
@@ -0,0 +1,4 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'hash_tools'
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: []