hash_delegator 0.1.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: 62b082b39fe733c614750de7ae37885d846872e0867b34846bc9329db6b5f743
4
+ data.tar.gz: 7892a0389be4d989747fc2a330c86196d8744c90fb1dbbecedabaf009fa19171
5
+ SHA512:
6
+ metadata.gz: 3138e6b948c4828b6bad9d3811e5cfe4eb18d83059f866a321166a93fbf766ca59701d959d835a3c807592b21d3af6294273bc647a3ccdb8871684e0f24a47c5
7
+ data.tar.gz: 4d5859d57253c4fd6dd7ad275d8ca785a38a907ede3c94712455069249649a9aa5b4543d2b6b7204d8cc61911dbf15152d22807232da276491df7ce80e446dd5
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+ vendor/bundle/
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --no-color
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.6.7
6
+ - 2.7.3
7
+ - 3.0.1
8
+ before_install: gem install bundler -v 2.1.4
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in hash_delegator.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 12.0"
7
+ gem "rspec", "~> 3.0"
8
+
9
+ gem "rubocop"
10
+ gem "rubocop-rspec"
data/Gemfile.lock ADDED
@@ -0,0 +1,59 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ hash_delegator (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ ast (2.4.2)
10
+ diff-lcs (1.4.4)
11
+ parallel (1.20.1)
12
+ parser (3.0.1.1)
13
+ ast (~> 2.4.1)
14
+ rainbow (3.0.0)
15
+ rake (12.3.3)
16
+ regexp_parser (2.1.1)
17
+ rexml (3.2.5)
18
+ rspec (3.10.0)
19
+ rspec-core (~> 3.10.0)
20
+ rspec-expectations (~> 3.10.0)
21
+ rspec-mocks (~> 3.10.0)
22
+ rspec-core (3.10.1)
23
+ rspec-support (~> 3.10.0)
24
+ rspec-expectations (3.10.1)
25
+ diff-lcs (>= 1.2.0, < 2.0)
26
+ rspec-support (~> 3.10.0)
27
+ rspec-mocks (3.10.2)
28
+ diff-lcs (>= 1.2.0, < 2.0)
29
+ rspec-support (~> 3.10.0)
30
+ rspec-support (3.10.2)
31
+ rubocop (1.16.1)
32
+ parallel (~> 1.10)
33
+ parser (>= 3.0.0.0)
34
+ rainbow (>= 2.2.2, < 4.0)
35
+ regexp_parser (>= 1.8, < 3.0)
36
+ rexml
37
+ rubocop-ast (>= 1.7.0, < 2.0)
38
+ ruby-progressbar (~> 1.7)
39
+ unicode-display_width (>= 1.4.0, < 3.0)
40
+ rubocop-ast (1.7.0)
41
+ parser (>= 3.0.1.1)
42
+ rubocop-rspec (2.4.0)
43
+ rubocop (~> 1.0)
44
+ rubocop-ast (>= 1.1.0)
45
+ ruby-progressbar (1.11.0)
46
+ unicode-display_width (2.0.0)
47
+
48
+ PLATFORMS
49
+ ruby
50
+
51
+ DEPENDENCIES
52
+ hash_delegator!
53
+ rake (~> 12.0)
54
+ rspec (~> 3.0)
55
+ rubocop
56
+ rubocop-rspec
57
+
58
+ BUNDLED WITH
59
+ 2.2.16
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Delon Newman
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,60 @@
1
+ [![Build Status](https://travis-ci.com/delonnewman/hash_delegator.svg?branch=master)](https://travis-ci.com/delonnewman/hash_delegator)
2
+
3
+ # HashDelegator
4
+
5
+ Thread-safe immutable objects that provide delegation and basic validation to hashes.
6
+
7
+ ## Synopsis
8
+
9
+ ```ruby
10
+ class Person < HashDelegator
11
+ require :first_name, :last_name
12
+ transform_keys(&:to_sym)
13
+
14
+ def name
15
+ "#{first_name} #{last_name}"
16
+ end
17
+ end
18
+
19
+ person = Person.new(first_name: "Mary", last_name: "Lamb", age: 32)
20
+ person.age # => 32
21
+ person.name # => "Mary Lamb"
22
+
23
+ # it supports all non-mutating methods of Hash
24
+ person.merge!(favorite_food: "Thai") # => NoMethodError
25
+ person.merge(favorite_food: "Thai") # => #<Person { first_name: "Mary", last_name: "Lamb", age: 32 }>
26
+
27
+ # respects inheritance
28
+ class Employee < Person
29
+ require :employee_id
30
+ end
31
+
32
+ Employee.new(age: 32, employee_id: 1234) # => Error, first_name attribute is required
33
+ Employee.new(first_name: "John", last_name: "Smith", age: 23, employee_id: 3456) # => #<Employee ...>
34
+ ```
35
+
36
+ ## Installation
37
+
38
+ Add this line to your application's Gemfile:
39
+
40
+ ```ruby
41
+ gem 'hash_delegator'
42
+ ```
43
+
44
+ And then execute:
45
+
46
+ $ bundle install
47
+
48
+ Or install it yourself as:
49
+
50
+ $ gem install hash_delegator
51
+
52
+ ## See Also
53
+
54
+ - [Dry Struct](https://dry-rb.org/gems/dry-struct)
55
+ - [Clojure Records](https://clojure.org/reference/datatypes#_deftype_and_defrecord)
56
+ - [Delegator](https://rubyapi.org/3.0/o/delegator)
57
+
58
+ ## License
59
+
60
+ 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,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,29 @@
1
+ require_relative 'lib/hash_delegator/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "hash_delegator"
5
+ spec.version = HashDelegator::VERSION
6
+ spec.authors = ["Delon Newman"]
7
+ spec.email = ["contact@delonnewman.name"]
8
+
9
+ spec.summary = %q{Method delegation to a hash}
10
+ spec.description = spec.summary
11
+ spec.homepage = "https://github.com/delonnewman/hash_delegator"
12
+ spec.license = "MIT"
13
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
14
+
15
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+ spec.metadata["changelog_uri"] = "#{spec.homepage}#changelog"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
+ end
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+ end
data/lib/core_ext.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ if RUBY_VERSION.split('.').take(2).join('.').to_f < 3
4
+ class Hash
5
+ def except(*keys)
6
+ h = dup
7
+ keys.each do |key|
8
+ h.delete(key)
9
+ end
10
+ h
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,301 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+ require 'core_ext'
5
+ require 'hash_delegator/version'
6
+
7
+ # Provides delegation and basic validation for Hashes
8
+ class HashDelegator
9
+ class << self
10
+ # Return required attributes or nil
11
+ #
12
+ # @return [Array, nil]
13
+ def required_attributes
14
+ return @required_attributes if @required_attributes
15
+
16
+ superclass.required_attributes if superclass.respond_to?(:required_attributes)
17
+ end
18
+
19
+ # Specifiy required attributes
20
+ #
21
+ # @param attributes [Array]
22
+ # @return [HashDelegator]
23
+ def required(*attributes)
24
+ @required_attributes =
25
+ if superclass.respond_to?(:required_attributes) && !superclass.required_attributes.nil?
26
+ superclass.required_attributes + attributes
27
+ else
28
+ attributes
29
+ end
30
+
31
+ self
32
+ end
33
+
34
+ # @deprecated
35
+ def require(*attributes)
36
+ warn 'HashDelegator.require is deprecated'
37
+ required(*attrbutes)
38
+ end
39
+
40
+ # Specify the default value if the value is a Proc or a block is passed
41
+ # each hash's default_proc attribute will be set.
42
+ #
43
+ # @param value [Object] default value
44
+ # @param block [Proc] default proc
45
+ # @return [HashDelegator]
46
+ def default(value = nil, &block)
47
+ if block
48
+ @default_value = block
49
+ return self
50
+ end
51
+
52
+ if value.is_a?(Proc) && value.lambda? && value.arity != 2
53
+ lambda = value
54
+ value = ->(*args) { lambda.call(*args.slice(0, lambda.arity)) }
55
+ end
56
+
57
+ @default_value = value
58
+
59
+ self
60
+ end
61
+
62
+ # Return the default value
63
+ def default_value
64
+ return @default_value if @default_value
65
+
66
+ superclass.default_value if superclass.respond_to?(:default_value)
67
+ end
68
+
69
+ # Specify the key transformer
70
+ def transform_keys(&block)
71
+ @key_transformer = block
72
+ end
73
+
74
+ # Return the key transformer
75
+ def key_transformer
76
+ return @key_transformer if @key_transformer
77
+
78
+ superclass.key_transformer if superclass.respond_to?(:key_transformer)
79
+ end
80
+ end
81
+
82
+ # Methods that mutate the internal hash, these cannot be called publicly.
83
+ MUTATING_METHODS = Set[
84
+ :clear,
85
+ :delete,
86
+ :update,
87
+ :delete_if,
88
+ :keep_if,
89
+ :compact!,
90
+ :filter!,
91
+ :merge!,
92
+ :reject!,
93
+ :select!,
94
+ :transform_keys!,
95
+ :transform_values!,
96
+ :default=,
97
+ :default_proc=,
98
+ :compare_by_identity,
99
+ :rehash,
100
+ :replace,
101
+ :initialize_copy,
102
+ :shift,
103
+ :store
104
+ ].freeze
105
+
106
+ # Methods that are closed (in the algebraic sense) meaning that
107
+ # they will not remove required keys.
108
+ CLOSED_METHODS = Set[
109
+ :compact,
110
+ :merge
111
+ ].freeze
112
+
113
+ EMPTY_HASH = {}.freeze
114
+ private_constant :EMPTY_HASH
115
+
116
+ # Initialize the HashDelegator with the given hash.
117
+ # If the hash is not frozen it will be duplicated. If a key transformer
118
+ # is specified the hashes keys will be processed with it (duplicating the original hash).
119
+ # The hash will be validated for the existance of the required attributes (note
120
+ # that a key with a nil value still exists in the hash).
121
+ #
122
+ #
123
+ # @param hash [Hash]
124
+ def initialize(hash = EMPTY_HASH)
125
+ raise 'HashDelegator should not be initialized' if instance_of?(HashDelegator)
126
+
127
+ @hash =
128
+ if self.class.key_transformer
129
+ hash.transform_keys(&self.class.key_transformer)
130
+ elsif hash.frozen?
131
+ hash
132
+ else
133
+ hash.dup
134
+ end
135
+
136
+ if self.class.default_value.is_a?(Proc)
137
+ @hash.default_proc = self.class.default_value
138
+ else
139
+ @hash.default = self.class.default_value
140
+ end
141
+
142
+ if self.class.required_attributes
143
+ self.class.required_attributes.each do |attribute|
144
+ attribute = self.class.key_transformer.call(attribute) if self.class.key_transformer
145
+ raise "#{attribute.inspect} is required, but is missing" unless key?(attribute)
146
+ end
147
+ end
148
+ end
149
+
150
+ # If the given keys include any required attributes
151
+ # the hash will be duplicated and except will be called
152
+ # on the duplicated hash. Otherwise a new instance of
153
+ # the HashDelegator will be return without the specified keys.
154
+ #
155
+ # @param keys [Array]
156
+ # @return [Hash, HashDelegator]
157
+ def except(*keys)
158
+ common = keys & self.class.required_attributes
159
+
160
+ if common.empty?
161
+ self.class.new(@hash.except(*keys))
162
+ else
163
+ to_hash.except(*keys)
164
+ end
165
+ end
166
+
167
+ # If the given keys include all of the required attributes
168
+ # a new HashDelegator will be returned with only the specified keys.
169
+ # Otherwise a internal hash will be duplicated and slice will
170
+ # be called on the duplicated hash.
171
+ #
172
+ # @param keys [Array]
173
+ # @return [Hash, HashDelegator]
174
+ def slice(*keys)
175
+ required = self.class.required_attributes
176
+ common = keys & required
177
+
178
+ if keys.size == common.size && common.size == required.size
179
+ self.class.new(@hash.slice(*keys))
180
+ else
181
+ to_hash.slice(*keys)
182
+ end
183
+ end
184
+
185
+ # Return a duplicate of the delegated hash.
186
+ #
187
+ # @return [Hash]
188
+ def to_hash
189
+ @hash.dup
190
+ end
191
+ alias to_h to_hash
192
+
193
+ def to_s
194
+ "#<#{self.class} #{@hash.inspect}>"
195
+ end
196
+ alias inspect to_s
197
+
198
+ # Return the value associated with the given key. If a key transformer
199
+ # is special the key will be transformed first. If the key is missing
200
+ # the default value will be return (nil unless specified).
201
+ #
202
+ # @param key
203
+ def [](key)
204
+ if self.class.key_transformer
205
+ @hash[self.class.key_transformer.call(key)]
206
+ else
207
+ @hash[key]
208
+ end
209
+ end
210
+
211
+ # Return the numerical hash of the decorated hash.
212
+ #
213
+ # @return [Integer]
214
+ def hash
215
+ @hash.hash
216
+ end
217
+
218
+ # Return true if the other object has the same numerical hash
219
+ # as this object.
220
+ #
221
+ # @return [Boolean]
222
+ def eql?(other)
223
+ @hash.hash == other.hash
224
+ end
225
+
226
+ # Return true if the other object has all of this objects required attributes.
227
+ #
228
+ # @param other
229
+ def ===(other)
230
+ required = self.class.required_attributes
231
+
232
+ other.respond_to?(:keys) && (common = other.keys & required) &&
233
+ common.size == other.keys.size && common.size == required.size
234
+ end
235
+
236
+ # Return true if the other object is of the same class and the
237
+ # numerical hash of the other object and this object are equal.
238
+ #
239
+ # @param other
240
+ #
241
+ # @return [Boolean]
242
+ def ==(other)
243
+ other.instance_of?(self.class) && eql?(other)
244
+ end
245
+
246
+ # Return true if the superclass responds to the method
247
+ # or if the method is a key of the internal hash or
248
+ # if the hash responds to this method. Otherwise return false.
249
+ #
250
+ # @note DO NOT USE DIRECTLY
251
+ #
252
+ # @see Object#respond_to?
253
+ # @see Object#respond_to_missing?
254
+ #
255
+ # @param method [Symbol]
256
+ # @param include_all [Boolean]
257
+ def respond_to_missing?(method, include_all)
258
+ super || key?(method) || hash_respond_to?(method)
259
+ end
260
+
261
+ # If the method is a key of the internal hash return it's value.
262
+ # If the internal hash responds to the method forward the method
263
+ # to the hash. If the method is 'closed' retrun a new HashDelegator
264
+ # otherwise return the raw result. If none of these conditions hold
265
+ # call the superclass' method_missing.
266
+ #
267
+ # @see CLOSED_METHODS
268
+ # @see Object#method_missing
269
+ #
270
+ # @param method [Symbol]
271
+ # @param args [Array]
272
+ # @param block [Proc]
273
+ def method_missing(method, *args, &block)
274
+ return @hash[method] if @hash.key?(method)
275
+
276
+ if hash_respond_to?(method)
277
+ result = @hash.public_send(method, *args, &block)
278
+ return result unless CLOSED_METHODS.include?(method)
279
+
280
+ return self.class.new(result)
281
+ end
282
+
283
+ super
284
+ end
285
+
286
+ private
287
+
288
+ def hash_respond_to?(method)
289
+ !MUTATING_METHODS.include?(method) && @hash.respond_to?(method)
290
+ end
291
+
292
+ protected
293
+
294
+ # Set the key of the internal hash to the given value.
295
+ #
296
+ # @param key
297
+ # @param value
298
+ def []=(key, value)
299
+ @hash[key] = value
300
+ end
301
+ end
@@ -0,0 +1,3 @@
1
+ class HashDelegator
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hash_delegator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Delon Newman
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-06-14 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Method delegation to a hash
14
+ email:
15
+ - contact@delonnewman.name
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".gitignore"
21
+ - ".rspec"
22
+ - ".travis.yml"
23
+ - Gemfile
24
+ - Gemfile.lock
25
+ - LICENSE.txt
26
+ - README.md
27
+ - Rakefile
28
+ - hash_delegator.gemspec
29
+ - lib/core_ext.rb
30
+ - lib/hash_delegator.rb
31
+ - lib/hash_delegator/version.rb
32
+ homepage: https://github.com/delonnewman/hash_delegator
33
+ licenses:
34
+ - MIT
35
+ metadata:
36
+ allowed_push_host: https://rubygems.org
37
+ homepage_uri: https://github.com/delonnewman/hash_delegator
38
+ source_code_uri: https://github.com/delonnewman/hash_delegator
39
+ changelog_uri: https://github.com/delonnewman/hash_delegator#changelog
40
+ post_install_message:
41
+ rdoc_options: []
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: 2.3.0
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ requirements: []
55
+ rubygems_version: 3.1.6
56
+ signing_key:
57
+ specification_version: 4
58
+ summary: Method delegation to a hash
59
+ test_files: []