shape_of 2.0.0 → 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/lib/shape_of.rb +218 -52
  3. metadata +7 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9934786c810465f0edc75a35faa47a305da950a63c337ec1c33b7184c1dd103e
4
- data.tar.gz: bf6c05010803284ccb2a6be09d410d903d3693e53ec006be85089a1c0522257c
3
+ metadata.gz: 9a8e5e89c8b1a515b043085f63fd764221ece2ecafd1c91f471250e9d62a3b36
4
+ data.tar.gz: d676fa033eeb7da482e60789042c080ef95d317e4aa05d4452547166126191ef
5
5
  SHA512:
6
- metadata.gz: 19c075c5e72d649267aab15fcdf3872a104ac8ac2534e95159f121ca15cdd2add334f0010f53bd82b64c5ce09b1910717e04e7a6374febb86b143936d05afa4d
7
- data.tar.gz: 4166c74b3c7df3ae858b2edeb1239c306e05479c96a0536b08815b279c82209f563f2f469a7ab6d1254c12f249d6014d484f12077e4a6471aa223ef0a15d2578
6
+ metadata.gz: c1d4be16b5e9b2fa80b08999860ea45ac44ee11fb17c7092ccdf4d1c82b1e17f2c40064aae3d7b8cdf7f222ecc14d5785b2a8aa1bdc411a8f1765fbc8fa1faf2
7
+ data.tar.gz: 5fbd3d63b7a00d38dac0e7b9521ebd44ec7f24eaa25b4786e40dd7e549d34b6ec066f8e5f64ac031586c33f24f2060643acc5d64e0992189298b592b0c6758e1
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].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]].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,43 @@ 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 { |shape| shape.respond_to?(:shape_of?) }
419
+ class_shapes = @shapes.select do |shape|
420
+ !shape.respond_to?(:shape_of?) && !shape.is_a?(::Hash) && !shape.is_a?(::Array) && shape.is_a?(Class)
421
+ end
422
+ object_shapes = @shapes.select do |shape|
423
+ !shape.respond_to?(:shape_of?) && !shape.is_a?(::Hash) && !shape.is_a?(::Array) && !shape.is_a?(Class)
424
+ end
425
+ validator.add_error(object.inspect + " is not shape of any of (" + shape_shapes.map(&:inspect).join(", ") +
426
+ ") or is not instance of any of (" + class_shapes.map(&:inspect).join(", ") +
427
+ ") or is not equal to (==) any of (" + object_shapes.map(&:inspect).join(", ") + ")")
428
+ end
429
+
430
+ is_any_shape_of
278
431
  end
279
432
  end
280
433
  end
281
434
  end
282
435
 
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)
436
+ # Optional[shape] denotes that the usual type is a shape, but is optional
437
+ # (meaning if it is nil or the key is not present in the Hash, it's still true)
284
438
  class Optional < Shape
285
439
  def self.[](shape)
286
440
  raise TypeError, "Shape cannot be nil" if shape.nil? || shape == NilClass
@@ -299,15 +453,17 @@ module ShapeOf
299
453
  end
300
454
  end
301
455
 
456
+ # Anything matches unless key does not exist in the Hash.
302
457
  class Any < Shape
303
- def self.shape_of?(object)
458
+ def self.shape_of?(object, validator: Validator.new(shape: self, object: object))
304
459
  true
305
460
  end
306
461
  end
307
462
 
308
- # Nothing only passes when the key does not exist in the Hash.
463
+ # Only passes when the key does not exist in the Hash.
309
464
  class Nothing < Shape
310
- def self.shape_of?(object)
465
+ def self.shape_of?(object, validator: Validator.new(shape: self, object: object))
466
+ validator.add_error("key present when not allowed")
311
467
  false
312
468
  end
313
469
 
@@ -316,6 +472,9 @@ module ShapeOf
316
472
  end
317
473
  end
318
474
 
475
+ # Matches a Regexp against a String using Regexp#match?.
476
+ # Pretty much a wrapper around Regexp because a Regexp instance will be tested for equality
477
+ # in the ShapeOf::Hash and ShapeOf::Array since it's not a class.
319
478
  class Pattern < Shape
320
479
  def self.[](shape)
321
480
  raise TypeError, "Shape must be #{Regexp.inspect}, was #{shape.inspect}" unless shape.instance_of? Regexp
@@ -336,19 +495,26 @@ module ShapeOf
336
495
  @class_name
337
496
  end
338
497
 
339
- def self.shape_of?(object)
340
- raise TypeError, "expected #{String.inspect}, was instead #{object.inspect}" unless object.instance_of?(String)
498
+ def self.shape_of?(object, validator: Validator.new(shape: self, object: object))
499
+ unless object.instance_of?(String)
500
+ validator.add_error(object.inspect + " is not instance of " + String.inspect)
501
+ return false
502
+ end
341
503
 
342
- @shape.match?(object)
504
+ does_regexp_match = @shape.match?(object)
505
+ validator.add_error(object.inspect + " does not match " + @shape.inspect) unless does_regexp_match
506
+ does_regexp_match
343
507
  end
344
508
  end
345
509
  end
346
510
  end
347
511
 
512
+ # Union[Integer, Float, Rational, Complex]
348
513
  Numeric = Union[Integer, Float, Rational, Complex].tap do |this|
349
514
  this.instance_variable_set(:@class_name, this.name.sub(/Union.*/, 'Numeric'))
350
515
  end
351
516
 
517
+ # Union[TrueClass, FalseClass]
352
518
  Boolean = Union[TrueClass, FalseClass].tap do |this|
353
519
  this.instance_variable_set(:@class_name, this.name.sub(/Union.*/, 'Boolean'))
354
520
  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.0
4
+ version: 3.0.1
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-17 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: []