shape_of 2.0.1 → 3.0.2

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.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/lib/shape_of.rb +220 -52
  3. metadata +7 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 64fc24c8d36d335c6052f39457aac2f62dd4155538e57dda11c78e080748bac7
4
- data.tar.gz: ff28778300c41c9352a83da9d6ae2b95c75150dd5429abfd5e3a90c0dd1397e5
3
+ metadata.gz: 7f00894a833612ace3d3197e5bc4ab0af9f34c3057420631a89ec611155bbef0
4
+ data.tar.gz: fe82328405f29cf631a7d292e939ded6ae5d6bb15463c7f50280b069e96feb65
5
5
  SHA512:
6
- metadata.gz: 0725a89f09c62099a96287fdc2c14080d004de6f1682e9cf09cefc6ed7300e535cfb7d375f7e913d8ccd22b5f2b23b52da8072aa85dbfe2cca3dc7d6480672b5
7
- data.tar.gz: e285973c38a51f6fcbb662e82322ea9bcb291388617849e0b86bdfd0e44cf2c4007ae456f5259fffef63f80d178c3f94016b49fa4ef2096c4d39a2e7992ae84d
6
+ metadata.gz: 352ac07b96d72ee0214119b6afa6fe60d7729f44a5f630d8ceb9add63bf54697adaa0cd2b9cd00f3d702c7494ea7fee3e7ca7c0627f987a148678b4f54178121
7
+ data.tar.gz: 9e01690dc4c989c9661cf797f9939d226060c6e4ba4889c507cddd68a8cabe9db59d7eabc2b2c3617e8886bb6021d10f40290b7e65c0853f9e584b3f2dca453a
data/lib/shape_of.rb CHANGED
@@ -84,37 +84,125 @@
84
84
  # hash_shape.shape_of?({ value: [{}] }) # => false
85
85
  # ```
86
86
  #
87
+ require 'pp'
88
+
87
89
  module ShapeOf
90
+ # Used in Assertions, and can also be used separately. It is used to keep track of places where the shape of
91
+ # data does not match, so that you can have greater insight than just a simple true/false.
92
+ class Validator
93
+ attr_reader :shape, :object
94
+
95
+ def initialize(shape:, object:)
96
+ @current_error_key_nesting = [:base] # stack of the current error key.
97
+ @errors = {}
98
+ @object = object
99
+ @shape = shape
100
+
101
+ validate
102
+ end
103
+
104
+ def valid?
105
+ @errors.empty?
106
+ end
107
+
108
+ def errors
109
+ @errors[:base]
110
+ end
111
+
112
+ def error_message
113
+ errors.pretty_inspect
114
+ end
115
+
116
+ def add_error(message)
117
+ create_nested_error_structure
118
+
119
+ @errors.dig(*@current_error_key_nesting)[:errors] << message.dup
120
+ end
121
+
122
+ def push_key(key)
123
+ @current_error_key_nesting.push(key.dup)
124
+ end
125
+
126
+ def pop_key
127
+ @current_error_key_nesting.pop
128
+ end
129
+
130
+ private
131
+
132
+ def create_nested_error_structure
133
+ errors = @errors
134
+ @current_error_key_nesting.each do |key|
135
+ errors[key] ||= {}
136
+ errors = errors[key]
137
+ end
138
+ @errors.dig(*@current_error_key_nesting)[:errors] ||= []
139
+ end
140
+
141
+ def validate
142
+ shape.shape_of?(object, validator: self)
143
+ end
144
+ end
145
+
88
146
  # To be included in a MiniTest test class
89
147
  module Assertions
90
- def assert_shape_of(object, shape)
148
+ # For backward compatibility, this is "off" by default, and the order in the assertions are reverse.
149
+ @proper_expected_actual_order = false
150
+ def self.use_proper_expected_actual_order!
151
+ @proper_expected_actual_order = true
152
+ end
153
+
154
+ def assert_shape_of(arg1, arg2)
155
+ shape = object = nil
156
+ if @proper_expected_actual_order
157
+ shape = arg1
158
+ object = arg2
159
+ else
160
+ shape = arg2
161
+ object = arg1
162
+ end
163
+
164
+ validator = nil
91
165
  if shape.respond_to? :shape_of?
92
- assert_operator shape, :shape_of?, object
166
+ validator = Validator.new(shape: shape, object: object)
93
167
  elsif shape.instance_of? ::Array
94
- assert_operator Array[shape.first], :shape_of?, object
168
+ validator = Validator.new(shape: Array[shape.first], object: object)
95
169
  elsif shape.instance_of? ::Hash
96
- assert_operator Hash[shape], :shape_of?, object
170
+ validator = Validator.new(shape: Hash[shape], object: object)
97
171
  else
98
172
  raise TypeError, "Expected #{Shape.inspect}, an #{::Array.inspect}, or a #{::Hash.inspect} as the shape"
99
173
  end
174
+
175
+ assert validator.valid?, validator.error_message
100
176
  end
101
177
 
102
- def refute_shape_of(object, shape)
178
+ def refute_shape_of(arg1, arg2)
179
+ shape = object = nil
180
+ if @proper_expected_actual_order
181
+ shape = arg1
182
+ object = arg2
183
+ else
184
+ shape = arg2
185
+ object = arg1
186
+ end
187
+
188
+ validator = nil
103
189
  if shape.respond_to? :shape_of?
104
- refute_operator shape, :shape_of?, object
190
+ validator = Validator.new(shape: shape, object: object)
105
191
  elsif shape.instance_of? ::Array
106
- refute_operator Array[shape.first], :shape_of?, object
192
+ validator = Validator.new(shape: Array[shape.first], object: object)
107
193
  elsif shape.instance_of? ::Hash
108
- refute_operator Hash[shape], :shape_of?, object
194
+ validator = Validator.new(shape: Hash[shape], object: object)
109
195
  else
110
196
  raise TypeError, "Expected #{Shape.inspect}, an #{::Array.inspect}, or a #{::Hash.inspect} as the shape"
111
197
  end
198
+
199
+ refute validator.valid?, "#{shape} is shape_of? #{object}"
112
200
  end
113
201
  end
114
202
 
115
203
  # Generic shape which all shapes subclass from
116
204
  class Shape
117
- def self.shape_of?(*)
205
+ def self.shape_of?(object, validator: Validator.new(shape: self, object: object))
118
206
  raise NotImplementedError
119
207
  end
120
208
 
@@ -127,15 +215,18 @@ module ShapeOf
127
215
  end
128
216
  end
129
217
 
130
- # Array[Shape] denotes that it is an array of shapes.
218
+ # Array[shape] denotes that it is an array of shapes.
131
219
  # It checks every element in the array and verifies that the element is in the correct shape.
132
- # This, along with Array, are the core components of this module.
220
+ # This, along with Hash, are the core components of this module.
133
221
  # Note that a ShapeOf::Array[Integer].shape_of?([]) will pass because it is vacuously true for an empty array.
134
222
  class Array < Shape
135
223
  @internal_class = ::Array
136
224
 
137
- def self.shape_of?(object)
138
- object.instance_of? @internal_class
225
+ def self.shape_of?(object, validator: Validator.new(shape: self, object: object))
226
+ is_instance_of = object.instance_of?(@internal_class)
227
+ validator.add_error(object.inspect + " is not instance of " + @internal_class.inspect) unless is_instance_of
228
+
229
+ is_instance_of
139
230
  end
140
231
 
141
232
  def self.[](shape)
@@ -156,33 +247,54 @@ module ShapeOf
156
247
  @class_name
157
248
  end
158
249
 
159
- def self.shape_of?(array)
160
- super && array.all? do |elem|
161
- if @shape.respond_to? :shape_of?
162
- @shape.shape_of? elem
250
+ def self.shape_of?(array, validator: Validator.new(shape: self, object: array))
251
+ return false unless super
252
+
253
+ idx = 0
254
+ each_is_shape_of = true
255
+ array.each do |elem|
256
+ validator.push_key("idx_#{idx}".to_sym)
257
+
258
+ is_shape_of = if @shape.respond_to? :shape_of?
259
+ @shape.shape_of?(elem, validator: validator)
163
260
  elsif @shape.is_a? ::Array
164
- Array[@shape.first].shape_of? elem
261
+ Array[@shape.first].shape_of?(elem, validator: validator)
165
262
  elsif @shape.is_a? ::Hash
166
- Hash[@shape].shape_of? elem
263
+ Hash[@shape].shape_of?(elem, validator: validator)
167
264
  elsif @shape.is_a? Class
168
- elem.instance_of? @shape
265
+ is_instance_of = elem.instance_of?(@shape)
266
+ validator.add_error(elem.inspect + " is not instance of " + @shape.inspect) unless is_instance_of
267
+
268
+ is_instance_of
169
269
  else
170
- elem == @shape
270
+ is_equal_to = elem == @shape
271
+ validator.add_error(elem.inspect + " is not equal to (==) " + @shape.inspect) unless is_equal_to
272
+
273
+ is_equal_to
171
274
  end
275
+
276
+ validator.pop_key
277
+ idx += 1
278
+ each_is_shape_of &&= is_shape_of
172
279
  end
280
+ each_is_shape_of
173
281
  end
174
282
  end
175
283
  end
176
284
  end
177
285
 
178
- # Hash[key: Shape, ...] denotes it is a hash of shapes with a very specific structure. Hash (without square brackets) is just a hash with any shape.
286
+ # Hash[key: shape, ...] denotes it is a hash of shapes with a very specific structure.
287
+ # Hash (without square brackets) is just a hash with any shape.
179
288
  # This, along with Array, are the core components of this module.
180
289
  # Note that the keys are converted to strings for comparison for both the shape and object provided.
181
290
  class Hash < Shape
182
291
  @internal_class = ::Hash
183
292
 
184
- def self.shape_of?(object)
185
- object.instance_of? @internal_class
293
+ def self.shape_of?(object, validator: Validator.new(shape: self, object: object))
294
+ is_instance_of = object.instance_of?(@internal_class)
295
+ validator.add_error(object.inspect + " is not instance of " + @internal_class.inspect) unless is_instance_of
296
+
297
+ is_instance_of
186
298
  end
187
299
 
188
300
  def self.[](shape = {})
@@ -205,32 +317,57 @@ module ShapeOf
205
317
  @class_name
206
318
  end
207
319
 
208
- def self.shape_of?(hash)
320
+ def self.shape_of?(hash, validator: Validator.new(shape: self, object: hash))
209
321
  return false unless super
210
322
 
323
+ each_is_shape_of = true
211
324
  rb_hash = stringify_rb_hash_keys(hash)
212
325
 
213
326
  rb_hash.keys.each do |key|
214
- return false unless @shape.key?(key)
327
+ has_key = @shape.key?(key)
328
+ unless has_key
329
+ validator.push_key(key)
330
+ validator.add_error("unexpected key")
331
+ validator.pop_key
332
+ each_is_shape_of = false
333
+ end
215
334
  end
216
335
 
217
336
  @shape.each do |key, shape|
218
- return false unless rb_hash.key?(key) || shape.respond_to?(:required?) && !shape.required?
337
+ unless rb_hash.key?(key) || shape.respond_to?(:required?) && !shape.required?
338
+ validator.push_key(key)
339
+ validator.add_error("required key not present")
340
+ validator.pop_key
341
+ each_is_shape_of = false
342
+ end
219
343
  end
220
344
 
221
- rb_hash.all? do |key, elem|
222
- if @shape[key].respond_to? :shape_of?
223
- @shape[key].shape_of? elem
224
- elsif @shape[key].is_a? ::Array
225
- Array[@shape[key].first].shape_of? elem
226
- elsif @shape[key].is_a? ::Hash
227
- Hash[@shape[key]].shape_of? elem
228
- elsif @shape[key].is_a? Class
229
- elem.instance_of? @shape[key]
345
+ rb_hash.each do |key, elem|
346
+ shape_elem = @shape[key]
347
+ validator.push_key(key)
348
+
349
+ is_shape_of = if shape_elem.respond_to? :shape_of?
350
+ shape_elem.shape_of?(elem, validator: validator)
351
+ elsif shape_elem.is_a? ::Array
352
+ Array[shape_elem.first].shape_of?(elem, validator: validator)
353
+ elsif shape_elem.is_a? ::Hash
354
+ Hash[shape_elem].shape_of?(elem, validator: validator)
355
+ elsif shape_elem.is_a? Class
356
+ is_instance_of = elem.instance_of?(shape_elem)
357
+ validator.add_error(elem.inspect + " is not instance of " + shape_elem.inspect) unless is_instance_of
358
+
359
+ is_instance_of
230
360
  else
231
- elem == @shape[key]
361
+ is_equal_to = elem == shape_elem
362
+ validator.add_error(elem.inspect + " is not equal to (==) " + shape_elem.inspect) unless is_equal_to
363
+
364
+ is_equal_to
232
365
  end
366
+
367
+ validator.pop_key
368
+ each_is_shape_of &&= is_shape_of
233
369
  end
370
+ each_is_shape_of
234
371
  end
235
372
  end
236
373
  end
@@ -238,11 +375,11 @@ module ShapeOf
238
375
  private
239
376
 
240
377
  def self.stringify_rb_hash_keys(rb_hash)
241
- rb_hash.to_a.map { |k, v| [k.to_s, v] }.to_h
378
+ rb_hash.transform_keys(&:to_s)
242
379
  end
243
380
  end
244
381
 
245
- # Union[Shape1, Shape2, ...] denotes that it can be of one the provided shapes
382
+ # Union[shape1, shape2, ...] denotes that it can be of one the provided shapes.
246
383
  class Union < Shape
247
384
  def self.[](*shapes)
248
385
  Class.new(self) do
@@ -261,26 +398,45 @@ module ShapeOf
261
398
  @class_name
262
399
  end
263
400
 
264
- def self.shape_of?(object)
265
- @shapes.any? do |shape|
401
+ def self.shape_of?(object, validator: Validator.new(shape: self, object: object))
402
+ is_any_shape_of_shape_or_hash_or_array = false
403
+ is_any_shape_of = @shapes.any? do |shape|
266
404
  if shape.respond_to? :shape_of?
267
- shape.shape_of? object
405
+ is_any_shape_of_shape_or_hash_or_array ||= shape.shape_of?(object)
268
406
  elsif shape.is_a? ::Hash
269
- Hash[shape].shape_of? object
407
+ is_any_shape_of_shape_or_hash_or_array ||= Hash[shape].shape_of?(object)
270
408
  elsif shape.is_a? ::Array
271
- Array[shape].shape_of? object
409
+ is_any_shape_of_shape_or_hash_or_array ||= Array[shape].shape_of?(object)
272
410
  elsif shape.is_a? Class
273
- object.instance_of? shape
411
+ object.instance_of?(shape)
274
412
  else
275
413
  object == shape
276
414
  end
277
415
  end
416
+
417
+ if !is_any_shape_of && !is_any_shape_of_shape_or_hash_or_array
418
+ shape_shapes = @shapes.select do |shape|
419
+ shape.respond_to?(:shape_of?) || shape.is_a?(::Hash) || shape.is_a?(::Array)
420
+ end
421
+ class_shapes = @shapes.select do |shape|
422
+ !shape.respond_to?(:shape_of?) && !shape.is_a?(::Hash) && !shape.is_a?(::Array) && shape.is_a?(Class)
423
+ end
424
+ object_shapes = @shapes.select do |shape|
425
+ !shape.respond_to?(:shape_of?) && !shape.is_a?(::Hash) && !shape.is_a?(::Array) && !shape.is_a?(Class)
426
+ end
427
+ validator.add_error(object.inspect + " is not shape of any of (" + shape_shapes.map(&:inspect).join(", ") +
428
+ ") or is not instance of any of (" + class_shapes.map(&:inspect).join(", ") +
429
+ ") or is not equal to (==) any of (" + object_shapes.map(&:inspect).join(", ") + ")")
430
+ end
431
+
432
+ is_any_shape_of
278
433
  end
279
434
  end
280
435
  end
281
436
  end
282
437
 
283
- # Optional[Shape] denotes that the usual type is a Shape, but is optional (meaning if it is nil or the key is not present in the Hash, it's still true)
438
+ # Optional[shape] denotes that the usual type is a shape, but is optional
439
+ # (meaning if it is nil or the key is not present in the Hash, it's still true)
284
440
  class Optional < Shape
285
441
  def self.[](shape)
286
442
  raise TypeError, "Shape cannot be nil" if shape.nil? || shape == NilClass
@@ -299,15 +455,17 @@ module ShapeOf
299
455
  end
300
456
  end
301
457
 
458
+ # Anything matches unless key does not exist in the Hash.
302
459
  class Any < Shape
303
- def self.shape_of?(object)
460
+ def self.shape_of?(object, validator: Validator.new(shape: self, object: object))
304
461
  true
305
462
  end
306
463
  end
307
464
 
308
- # Nothing only passes when the key does not exist in the Hash.
465
+ # Only passes when the key does not exist in the Hash.
309
466
  class Nothing < Shape
310
- def self.shape_of?(object)
467
+ def self.shape_of?(object, validator: Validator.new(shape: self, object: object))
468
+ validator.add_error("key present when not allowed")
311
469
  false
312
470
  end
313
471
 
@@ -316,6 +474,9 @@ module ShapeOf
316
474
  end
317
475
  end
318
476
 
477
+ # Matches a Regexp against a String using Regexp#match?.
478
+ # Pretty much a wrapper around Regexp because a Regexp instance will be tested for equality
479
+ # in the ShapeOf::Hash and ShapeOf::Array since it's not a class.
319
480
  class Pattern < Shape
320
481
  def self.[](shape)
321
482
  raise TypeError, "Shape must be #{Regexp.inspect}, was #{shape.inspect}" unless shape.instance_of? Regexp
@@ -336,19 +497,26 @@ module ShapeOf
336
497
  @class_name
337
498
  end
338
499
 
339
- def self.shape_of?(object)
340
- raise TypeError, "expected #{String.inspect}, was instead #{object.inspect}" unless object.instance_of?(String)
500
+ def self.shape_of?(object, validator: Validator.new(shape: self, object: object))
501
+ unless object.instance_of?(String)
502
+ validator.add_error(object.inspect + " is not instance of " + String.inspect)
503
+ return false
504
+ end
341
505
 
342
- @shape.match?(object)
506
+ does_regexp_match = @shape.match?(object)
507
+ validator.add_error(object.inspect + " does not match " + @shape.inspect) unless does_regexp_match
508
+ does_regexp_match
343
509
  end
344
510
  end
345
511
  end
346
512
  end
347
513
 
514
+ # Union[Integer, Float, Rational, Complex]
348
515
  Numeric = Union[Integer, Float, Rational, Complex].tap do |this|
349
516
  this.instance_variable_set(:@class_name, this.name.sub(/Union.*/, 'Numeric'))
350
517
  end
351
518
 
519
+ # Union[TrueClass, FalseClass]
352
520
  Boolean = Union[TrueClass, FalseClass].tap do |this|
353
521
  this.instance_variable_set(:@class_name, this.name.sub(/Union.*/, 'Boolean'))
354
522
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shape_of
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.1
4
+ version: 3.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Isom
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-20 00:00:00.000000000 Z
11
+ date: 2022-01-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -38,7 +38,7 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1'
41
- description:
41
+ description:
42
42
  email: john@johnisom.dev
43
43
  executables: []
44
44
  extensions: []
@@ -49,7 +49,7 @@ homepage: https://github.com/johnisom/shape_of
49
49
  licenses:
50
50
  - MIT
51
51
  metadata: {}
52
- post_install_message:
52
+ post_install_message:
53
53
  rdoc_options: []
54
54
  require_paths:
55
55
  - lib
@@ -64,8 +64,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
64
64
  - !ruby/object:Gem::Version
65
65
  version: '0'
66
66
  requirements: []
67
- rubygems_version: 3.2.17
68
- signing_key:
67
+ rubygems_version: 3.2.22
68
+ signing_key:
69
69
  specification_version: 4
70
70
  summary: A shape/type checker for Ruby objects.
71
71
  test_files: []