hash_miner 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
+ 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: []