shape_of 1.2.2 → 3.0.0

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