hash_miner 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 47cc3f1132fb53760a709194961033a327634cda3d5b5f32583cb8ab8af1cc61
4
+ data.tar.gz: 192a1594d31a7565aaa0d1bfc0835a29b2cdf57326c131bd6756aa58901fc5d8
5
+ SHA512:
6
+ metadata.gz: 2dde32d4292f9f66374cac7c048ec5373537bfc4b988a563202aa824a394a6b7e3b0b6af057eda8e9c481aac93910b9a188714e6e2e77760c57debdd1fd98335
7
+ data.tar.gz: 05354d5f27da7d4d0a6f0de3c58315929193596f9a08d651e83f352df6b9f3573070016bb8e84fb64eaa2ebf8395e97c4689fbf69d38476044ed991b17297233
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,36 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.7
3
+ NewCops: enable
4
+ SuggestExtensions: false
5
+
6
+ Style/StringLiterals:
7
+ Enabled: true
8
+ EnforcedStyle: single_quotes
9
+
10
+ Style/StringLiteralsInInterpolation:
11
+ Enabled: true
12
+ EnforcedStyle: single_quotes
13
+
14
+ Layout/LineLength:
15
+ Max: 130
16
+
17
+ Metrics/AbcSize:
18
+ Enabled: false
19
+
20
+ Metrics/BlockLength:
21
+ Enabled: false
22
+
23
+ Metrics/ClassLength:
24
+ Max: 120
25
+
26
+ Metrics/MethodLength:
27
+ Max: 40
28
+
29
+ Metrics/CyclomaticComplexity:
30
+ Max: 20
31
+
32
+ Metrics/PerceivedComplexity:
33
+ Max: 20
34
+
35
+ Style/MultilineBlockChain:
36
+ Enabled: false
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in hash_miner.gemspec
6
+ gemspec
7
+
8
+ gem 'rake', '~> 13.0'
9
+
10
+ gem 'rspec', '~> 3.0'
11
+
12
+ gem 'rubocop', '~> 1.21'
13
+
14
+ gem 'pry', '~> 0.14'
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 alexo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # HashMiner
2
+
3
+ Ever experienced the pain of working with deeply nested hashes in Ruby?
4
+
5
+ HashMiner expands the base Hash class in Ruby to provide helpful methods to traverse and manipulate your Hash Object regardless of complexity.
6
+
7
+ The following gives a flavour of how HashMiner can help solve headaches when working with complex hashes:
8
+
9
+ ```ruby
10
+ require 'hash_miner'
11
+
12
+ hash = { my: { super: { duper: { deeply: 'nested hash' } } } }
13
+
14
+ # Deleting a key #
15
+ # Without HashMiner
16
+ hash[:my][:super].delete(:duper) # => {:deeply=>"nested hash"}
17
+ # With HashMiner
18
+ hash.deep_remove(key: :duper) # => {:my=>{:super=>{}}}
19
+
20
+ # Updating a key #
21
+ # Without HashMiner
22
+ hash[:my][:super][:duper][:deeply] = 'modified nested hash' # => "modified nested hash"
23
+ # With HashMiner
24
+ hash.deep_update(key: :deeply, value: 'modified nested hash') # => {:my=>{:super=>{:duper=>{:deeply=>"modified nested hash"}}}}
25
+
26
+ # Checking a key exists #
27
+ # Without HashMiner
28
+ hash[:my][:super][:duper][:deeply] # => "nested hash"
29
+ # With HashMiner
30
+ hash.deep_find(key: :deeply) # => ["nested hash"]
31
+ ```
32
+
33
+ ## Installation
34
+
35
+ Install the gem and add to the application's Gemfile by executing:
36
+
37
+ $ bundle add hash_miner
38
+
39
+ If bundler is not being used to manage dependencies, install the gem by executing:
40
+
41
+ $ gem install hash_miner
42
+
43
+ ## Usage
44
+
45
+ - HashMiner methods have no side-effects
46
+
47
+ ### Methods available
48
+ #### deep_update
49
+
50
+ - Updates all values that match the given key
51
+
52
+ ```ruby
53
+ require 'hash_miner'
54
+
55
+ nasty_hash = { my: { pretty: [{ nasty: :hash }, nil], is: { pretty: :nasty } } }
56
+
57
+ nasty_hash.deep_update(key: :nasty, value: :complicated_hash) # => {:my=>{:pretty=>[{:nasty=>:complicated_hash}, nil], :is=>{:pretty=>:nasty}}}
58
+
59
+ # Errors on uniqueness
60
+ nasty_hash.deep_update(key: :pretty, value: :super_duper) # => throws KeyNotUniqueError
61
+ nasty_hash.deep_update(key: :pretty, value: :super_duper, error_on_uniqueness: false) # => {:my=>{:pretty=>:super_duper, :is=>{:pretty=>:super_duper}}}
62
+
63
+ # Errors on missing
64
+ nasty_hash.deep_update(key: :huh, value: :where_am_i) # => throws KeyNotFoundError
65
+ nasty_hash.deep_update(key: :pretty, value: :super_duper, error_on_missing: false) # => {:my=>{:pretty=>[{:nasty=>:hash}, nil], :is=>{:pretty=>:nasty}}, :huh=>:where_am_i}
66
+
67
+ ```
68
+ ---
69
+ #### deep_remove
70
+
71
+ - Removes all values that match the given key
72
+
73
+ ```ruby
74
+ require 'hash_miner'
75
+
76
+ nasty_hash = { my: { pretty: [{ nasty: :hash }, nil], is: { pretty: :nasty } } }
77
+
78
+ nasty_hash.deep_remove(key: :nasty) # => {:my=>{:pretty=>[{}, nil], :is=>{:pretty=>:nasty}}}
79
+
80
+ # Errors on uniqueness
81
+ nasty_hash.deep_remove(key: :pretty) # => throws KeyNotUniqueError
82
+ nasty_hash.deep_remove(key: :pretty, error_on_uniqueness: false) # => {:my=>{:is=>{}}}
83
+ ```
84
+ ---
85
+ #### deep_find
86
+
87
+ - Returns all values that match the given key
88
+
89
+ ```ruby
90
+ require 'hash_miner'
91
+
92
+ nasty_hash = { my: { pretty: [{ nasty: :hash }, nil], is: { pretty: :nasty } } }
93
+
94
+ nasty_hash.deep_find(key: :nasty) # => [:hash]
95
+ nasty_hash.deep_find(key: :pretty) # => [{:nasty=>:hash}, nil, :nasty]
96
+ ```
97
+ ---
98
+ #### deep_compact
99
+
100
+ - Removes nil and empty values from Hash
101
+ - Will not remove values within an Array i.e `[nil, {}]` will remain
102
+ ```ruby
103
+ require 'hash_miner'
104
+
105
+ nasty_hash = { my: { pretty: [{ nasty: :hash }, nil], is: { pretty: {}, nasty: nil } } }
106
+
107
+ nasty_hash.deep_compact # => {:my=>{:pretty=>[{:nasty=>:hash}, nil]}}
108
+ ```
109
+ ---
110
+ #### deep_count
111
+
112
+ - Returns a count for the given key
113
+ ```ruby
114
+ require 'hash_miner'
115
+
116
+ nasty_hash = { my: { pretty: [{ nasty: :hash }, nil], is: { pretty: :nasty } } }
117
+
118
+ nasty_hash.deep_count(key: :pretty) # => 2
119
+ nasty_hash.deep_count(key: :nasty) # => 1
120
+ ```
121
+ ---
122
+ #### deep_contains?
123
+
124
+ - Returns `true|false` depending on if given is found
125
+ ```ruby
126
+ require 'hash_miner'
127
+
128
+ nasty_hash = { my: { pretty: [{ nasty: :hash }, nil], is: { pretty: :nasty } } }
129
+
130
+ nasty_hash.deep_contains?(key: :nasty) # => true
131
+ nasty_hash.deep_contains?(key: :super_nasty) # => false
132
+ ```
133
+
134
+ ## Development
135
+
136
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
137
+
138
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
139
+
140
+ ## Contributing
141
+
142
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/hash_miner.
143
+
144
+ ## License
145
+
146
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require 'rubocop/rake_task'
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec]
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class KeyNotUniqueError < StandardError
4
+ end
5
+
6
+ class KeyNotFoundError < StandardError
7
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Adds additional methods to the Hash class
4
+ class Hash
5
+ include HashMiner
6
+
7
+ # Checks if the key exists within a Hash
8
+ #
9
+ # @param key [String, Symbol] the key you want to check
10
+ # @param hash [Hash] the hash obj - used for recursive calling
11
+ #
12
+ # @return [Boolean] whether the key was found
13
+ def deep_contains?(key:, hash: self)
14
+ return nil unless hash.is_a? Hash
15
+ return true if hash.include? key
16
+
17
+ hash.filter_map do |_k, v|
18
+ case v
19
+ when Hash
20
+ [v.deep_contains?(key: key)]
21
+ when Array
22
+ [v.map { |i| deep_contains?(key: key, hash: i) }]
23
+ else
24
+ false
25
+ end
26
+ end.flatten.include? true
27
+ end
28
+
29
+ # Count the number of occurrences a key has in a Hash
30
+ #
31
+ # @param key [String, Symbol]
32
+ # @param hash [Hash]
33
+ #
34
+ # @return [Integer] the number of occurrences for a given key
35
+ def deep_count(key:, hash: self)
36
+ found = 0
37
+ return found unless hash.is_a? Hash
38
+ return found unless hash.deep_contains?(key: key)
39
+
40
+ hash.each do |k, v|
41
+ found += 1 if k.eql? key
42
+ found += deep_count(hash: v, key: key) if v.is_a?(Hash)
43
+ v.each { |i| found += deep_count(hash: i, key: key) } if v.is_a?(Array)
44
+ end
45
+
46
+ found
47
+ end
48
+
49
+ # Finds the value for a given key
50
+ #
51
+ # @param key [String, Symbol]
52
+ # @param hash [Hash]
53
+ #
54
+ # @return [Array] with all values that match the key
55
+ def deep_find(key:, hash: self)
56
+ return nil unless hash.is_a? Hash
57
+ return nil unless hash.deep_contains?(key: key)
58
+
59
+ hash.filter_map do |k, v|
60
+ if k.eql? key
61
+ v
62
+ elsif v.is_a?(Hash)
63
+ deep_find(hash: v, key: key)
64
+ elsif v.is_a?(Array)
65
+ [v.filter_map { |i| deep_find(hash: i, key: key) }]
66
+ end
67
+ end.flatten
68
+ end
69
+
70
+ # Removed nil/empty from Hash
71
+ #
72
+ # @return [Hash] with no nil/empty values
73
+ def deep_compact
74
+ if is_a?(Hash)
75
+ to_h do |k, v|
76
+ case v
77
+ when Hash
78
+ [k, v.deep_compact]
79
+ when Array
80
+ [k, v.map do |i|
81
+ i.is_a?(Hash) ? i.deep_compact : i
82
+ end]
83
+ else
84
+ [k, v]
85
+ end
86
+ end.delete_if { |_, v| v.nil? || (v.respond_to?(:empty?) && v.empty?) }
87
+ else
88
+ self
89
+ end
90
+ end
91
+
92
+ # Updates specified key in nested hash.
93
+ #
94
+ # @param key [String, Symbol] the key to update
95
+ # @param value [String] the value to be set
96
+ # @param error_on_missing [Boolean] error if key missing
97
+ # @param error_on_uniqueness [Boolean] error if key not unique
98
+ #
99
+ # @return [Hash] the object with specified key updated.
100
+ def deep_update(key:, value:, error_on_missing: true, error_on_uniqueness: true)
101
+ return self unless is_a? Hash
102
+
103
+ if error_on_uniqueness && (deep_count(key: key) > 1)
104
+ raise KeyNotUniqueError, "Key: '#{key}' not unique | Pass 'error_on_uniqueness: false' if you do not care"
105
+ end
106
+
107
+ unless deep_contains?(key: key)
108
+ if error_on_missing
109
+ raise KeyNotFoundError,
110
+ "Key: '#{key}' not found in hash | Pass 'error_on_missing: false' if you do not care"
111
+ end
112
+
113
+ LOG.warn('Key not found in hash, adding to the top level')
114
+
115
+ hash = dup
116
+ hash[key] = value
117
+ return hash
118
+ end
119
+
120
+ to_h do |k, v|
121
+ if k.eql?(key)
122
+ [k, value]
123
+ elsif v.is_a?(Hash) && v.deep_contains?(key: key)
124
+ [k, v.deep_update(key: key, value: value, error_on_missing: error_on_missing, error_on_uniqueness: error_on_uniqueness)]
125
+ elsif v.is_a?(Array)
126
+ [k, v.map do |item|
127
+ if item.is_a?(Hash) && item.deep_contains?(key: key)
128
+ item.deep_update(key: key, value: value, error_on_missing: error_on_missing, error_on_uniqueness: error_on_uniqueness)
129
+ else
130
+ item
131
+ end
132
+ end]
133
+ else
134
+ [k, v]
135
+ end
136
+ end
137
+ end
138
+
139
+ # Removes specified key from nested hash.
140
+ #
141
+ # @param key [String] the key to remove.
142
+ #
143
+ # @return [Hash] the object with specified key removed.
144
+ def deep_remove(key:, error_on_uniqueness: true)
145
+ return self unless is_a? Hash
146
+
147
+ if error_on_uniqueness && (deep_count(key: key) > 1)
148
+ raise KeyNotUniqueError, "Key: '#{key}' not unique | Pass 'error_on_uniqueness: false' if you do not care"
149
+ end
150
+
151
+ # filter_map removes nil from Array
152
+ filter_map do |k, v|
153
+ if k.eql? key
154
+ nil
155
+ elsif v.is_a?(Hash)
156
+ [k, v.deep_remove(key: key, error_on_uniqueness: error_on_uniqueness)]
157
+ elsif v.is_a?(Array)
158
+ [k, v.map do |item|
159
+ if item.is_a?(Hash) && item.deep_contains?(key: key)
160
+ item.deep_remove(key: key, error_on_uniqueness: error_on_uniqueness)
161
+ else
162
+ item
163
+ end
164
+ end]
165
+ else
166
+ [k, v]
167
+ end
168
+ end.to_h
169
+ end
170
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HashMiner
4
+ VERSION = '1.0.0'
5
+ end
data/lib/hash_miner.rb ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'hash_miner/version'
4
+ require_relative 'hash_miner/hash'
5
+ require_relative 'hash_miner/errors'
6
+
7
+ require 'pry'
8
+ require 'logger'
9
+
10
+ module HashMiner
11
+ class Error < StandardError; end
12
+
13
+ LOG = Logger.new($stdout)
14
+ end
@@ -0,0 +1,4 @@
1
+ module HashMiner
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hash_miner
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - alexo
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-07-05 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Additional Hash methods to help navigate the tricky world of nested Hashes.
14
+ email:
15
+ - alexander.omahoney@dvla.gov.uk
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".rspec"
21
+ - ".rubocop.yml"
22
+ - Gemfile
23
+ - LICENSE.txt
24
+ - README.md
25
+ - Rakefile
26
+ - lib/hash_miner.rb
27
+ - lib/hash_miner/errors.rb
28
+ - lib/hash_miner/hash.rb
29
+ - lib/hash_miner/version.rb
30
+ - sig/hash_miner.rbs
31
+ homepage: https://github.com/dvla/hash_miner
32
+ licenses:
33
+ - MIT
34
+ metadata:
35
+ homepage_uri: https://github.com/dvla/hash_miner
36
+ source_code_uri: https://github.com/dvla/hash_miner
37
+ changelog_uri: https://github.com/dvla/hash_miner
38
+ rubygems_mfa_required: 'true'
39
+ post_install_message:
40
+ rdoc_options: []
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 2.7.0
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ requirements: []
54
+ rubygems_version: 3.3.7
55
+ signing_key:
56
+ specification_version: 4
57
+ summary: Additional Hash methods to help navigate the tricky world of nested Hashes.
58
+ test_files: []