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.
- checksums.yaml +4 -4
- data/lib/shape_of.rb +220 -52
- metadata +7 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7f00894a833612ace3d3197e5bc4ab0af9f34c3057420631a89ec611155bbef0
|
4
|
+
data.tar.gz: fe82328405f29cf631a7d292e939ded6ae5d6bb15463c7f50280b069e96feb65
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
166
|
+
validator = Validator.new(shape: shape, object: object)
|
93
167
|
elsif shape.instance_of? ::Array
|
94
|
-
|
168
|
+
validator = Validator.new(shape: Array[shape.first], object: object)
|
95
169
|
elsif shape.instance_of? ::Hash
|
96
|
-
|
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(
|
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
|
-
|
190
|
+
validator = Validator.new(shape: shape, object: object)
|
105
191
|
elsif shape.instance_of? ::Array
|
106
|
-
|
192
|
+
validator = Validator.new(shape: Array[shape.first], object: object)
|
107
193
|
elsif shape.instance_of? ::Hash
|
108
|
-
|
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[
|
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
|
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?
|
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
|
-
|
161
|
-
|
162
|
-
|
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?
|
261
|
+
Array[@shape.first].shape_of?(elem, validator: validator)
|
165
262
|
elsif @shape.is_a? ::Hash
|
166
|
-
Hash[@shape].shape_of?
|
263
|
+
Hash[@shape].shape_of?(elem, validator: validator)
|
167
264
|
elsif @shape.is_a? Class
|
168
|
-
elem.instance_of?
|
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:
|
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?
|
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
|
-
|
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
|
-
|
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.
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
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 ==
|
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.
|
378
|
+
rb_hash.transform_keys(&:to_s)
|
242
379
|
end
|
243
380
|
end
|
244
381
|
|
245
|
-
# Union[
|
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
|
-
|
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?
|
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?
|
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?
|
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?
|
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[
|
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
|
-
#
|
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
|
-
|
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:
|
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:
|
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.
|
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: []
|