shape_of 2.0.1 → 3.0.2

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