shape_of 1.2.2 → 3.0.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.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/lib/shape_of.rb +204 -46
  3. metadata +7 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ee7f353196efa5fe84c5b077228696920b0cfecc74c0f7da62ef18eaa15e4cd5
4
- data.tar.gz: 4ae20130b4b1f2307755015f0bffcc6a82138ab6a75e81e8c71012e5c7926383
3
+ metadata.gz: 302fcc2359111f2a5ea57aa97fa6b686499fe2e93048ca204655989711157ee1
4
+ data.tar.gz: 8481cc4a6c1c709b680c6babbe2b5aa3335f3a689ec45c8bf24551b80c776f3e
5
5
  SHA512:
6
- metadata.gz: a51dd81d6468c8cee82ad6a633eea7510950a475f8bd7d3bed251f88a4e029d7c5f6a557dc26e084706b17b4904a50485814814748c20aff4c1eb5de4fb3be66
7
- data.tar.gz: ebdd687b4046fbd6bce79024f927aee2ef0e625776c4bfde98a4b12d12513b66d5a64c7df4a7871c75e249358ae1142f4c787c10d1ee4e23584951fd45b1e372
6
+ metadata.gz: 5319dce493532fccf310d7b2c52c53fad2699dab8c23d4a914a7a9754f8db22c336874005eca5a317ab60f185af6ac7057334388e7498760d70e93382a791ad8
7
+ data.tar.gz: dfaaedf3abcf1dd77c0830215a787a9e2a29d0f06b2db35fc592a28add9aa495344ed7e4c22f46370d840da490fc57b240225a72092d28fccf758df87467ef09
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
 
@@ -134,8 +222,11 @@ module ShapeOf
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,20 +247,37 @@ 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
@@ -182,8 +290,11 @@ module ShapeOf
182
290
  class Hash < Shape
183
291
  @internal_class = ::Hash
184
292
 
185
- def self.shape_of?(object)
186
- 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
187
298
  end
188
299
 
189
300
  def self.[](shape = {})
@@ -206,32 +317,57 @@ module ShapeOf
206
317
  @class_name
207
318
  end
208
319
 
209
- def self.shape_of?(hash)
320
+ def self.shape_of?(hash, validator: Validator.new(shape: self, object: hash))
210
321
  return false unless super
211
322
 
323
+ each_is_shape_of = true
212
324
  rb_hash = stringify_rb_hash_keys(hash)
213
325
 
214
326
  rb_hash.keys.each do |key|
215
- 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
216
334
  end
217
335
 
218
336
  @shape.each do |key, shape|
219
- 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
220
343
  end
221
344
 
222
- rb_hash.all? do |key, elem|
223
- if @shape[key].respond_to? :shape_of?
224
- @shape[key].shape_of? elem
225
- elsif @shape[key].is_a? ::Array
226
- Array[@shape[key].first].shape_of? elem
227
- elsif @shape[key].is_a? ::Hash
228
- Hash[@shape[key]].shape_of? elem
229
- elsif @shape[key].is_a? Class
230
- 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
231
360
  else
232
- 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
233
365
  end
366
+
367
+ validator.pop_key
368
+ each_is_shape_of &&= is_shape_of
234
369
  end
370
+ each_is_shape_of
235
371
  end
236
372
  end
237
373
  end
@@ -239,7 +375,7 @@ module ShapeOf
239
375
  private
240
376
 
241
377
  def self.stringify_rb_hash_keys(rb_hash)
242
- rb_hash.to_a.map { |k, v| [k.to_s, v] }.to_h
378
+ rb_hash.transform_keys(&:to_s)
243
379
  end
244
380
  end
245
381
 
@@ -262,20 +398,36 @@ module ShapeOf
262
398
  @class_name
263
399
  end
264
400
 
265
- def self.shape_of?(object)
266
- @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|
267
404
  if shape.respond_to? :shape_of?
268
- shape.shape_of? object
405
+ is_any_shape_of_shape_or_hash_or_array ||= shape.shape_of?(object)
269
406
  elsif shape.is_a? ::Hash
270
- Hash[shape].shape_of? object
407
+ is_any_shape_of_shape_or_hash_or_array ||= Hash[shape].shape_of?(object)
271
408
  elsif shape.is_a? ::Array
272
- Array[shape].shape_of? object
409
+ is_any_shape_of_shape_or_hash_or_array ||= Array[shape].shape_of?(object)
273
410
  elsif shape.is_a? Class
274
- object.instance_of? shape
411
+ object.instance_of?(shape)
275
412
  else
276
413
  object == shape
277
414
  end
278
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
279
431
  end
280
432
  end
281
433
  end
@@ -303,14 +455,15 @@ module ShapeOf
303
455
 
304
456
  # Anything matches unless key does not exist in the Hash.
305
457
  class Any < Shape
306
- def self.shape_of?(object)
458
+ def self.shape_of?(object, validator: Validator.new(shape: self, object: object))
307
459
  true
308
460
  end
309
461
  end
310
462
 
311
463
  # Only passes when the key does not exist in the Hash.
312
464
  class Nothing < Shape
313
- 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")
314
467
  false
315
468
  end
316
469
 
@@ -342,10 +495,15 @@ module ShapeOf
342
495
  @class_name
343
496
  end
344
497
 
345
- def self.shape_of?(object)
346
- 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
347
503
 
348
- @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
349
507
  end
350
508
  end
351
509
  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: 1.2.2
4
+ version: 3.0.0
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-28 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: []