json_patterns 0.1.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.
- data/lib/json_patterns.rb +1148 -0
- data/test/test_json_patterns.rb +839 -0
- metadata +48 -0
|
@@ -0,0 +1,1148 @@
|
|
|
1
|
+
require 'set'
|
|
2
|
+
|
|
3
|
+
def set_union(sets)
|
|
4
|
+
sets.reduce { |u, s| u + s }
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
module HashInitialized
|
|
8
|
+
def initialize(opts={})
|
|
9
|
+
opts.each { |k, v|
|
|
10
|
+
|
|
11
|
+
# TODO: No guarantee that the method is really a reader for that attribute - this would be
|
|
12
|
+
# better handled with a specialized attribute maker.
|
|
13
|
+
|
|
14
|
+
raise "class #{self.class} has no attribute #{k.inspect}" unless self.respond_to?(k)
|
|
15
|
+
self.instance_variable_set("@#{k}", v)
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
module DeepEquality
|
|
21
|
+
def ==(other)
|
|
22
|
+
self.class == other.class and
|
|
23
|
+
self.instance_variables.map { |v| self.instance_variable_get(v) } ==
|
|
24
|
+
other.instance_variables.map { |v| other.instance_variable_get(v) }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module Inspectable # because overriding #to_s unfortunately wipes out #inspect
|
|
29
|
+
INSPECTING_KEY = ('Inspectable::' + '%016x' % rand(2**64)).to_sym
|
|
30
|
+
|
|
31
|
+
def inspect
|
|
32
|
+
Thread.current[INSPECTING_KEY] ||= {}
|
|
33
|
+
|
|
34
|
+
object_desc = "#{self.class}:0x#{(object_id << 1).to_s(16)}"
|
|
35
|
+
|
|
36
|
+
if Thread.current[INSPECTING_KEY][self]
|
|
37
|
+
attributes_desc = ' ...'
|
|
38
|
+
else
|
|
39
|
+
Thread.current[INSPECTING_KEY][self] = true
|
|
40
|
+
|
|
41
|
+
begin
|
|
42
|
+
attributes_desc = self.instance_variables.map { |v|
|
|
43
|
+
" #{v.to_s}=#{instance_variable_get(v).inspect}"
|
|
44
|
+
}.join
|
|
45
|
+
ensure
|
|
46
|
+
Thread.current[INSPECTING_KEY].delete(self)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
return "#<#{object_desc}#{attributes_desc}>"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Dummy classes for patterns
|
|
55
|
+
|
|
56
|
+
class Boolean; end
|
|
57
|
+
class Email; end
|
|
58
|
+
class URL; end
|
|
59
|
+
|
|
60
|
+
class DisjunctionPattern
|
|
61
|
+
include HashInitialized, Inspectable
|
|
62
|
+
|
|
63
|
+
attr_accessor :alternatives
|
|
64
|
+
|
|
65
|
+
def to_s
|
|
66
|
+
"one_of(#{alternatives.join(', ')})"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class DisjunctionKey
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def one_of(*patterns)
|
|
74
|
+
case patterns
|
|
75
|
+
when []
|
|
76
|
+
DisjunctionKey.new
|
|
77
|
+
else
|
|
78
|
+
DisjunctionPattern.new(alternatives: patterns)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
class UniformArrayPattern
|
|
83
|
+
include HashInitialized, Inspectable
|
|
84
|
+
|
|
85
|
+
attr_accessor :value
|
|
86
|
+
|
|
87
|
+
def to_s
|
|
88
|
+
"array_of(#{value})"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def array_of(value)
|
|
93
|
+
UniformArrayPattern.new(value: value)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
class AnythingPattern
|
|
97
|
+
include Inspectable
|
|
98
|
+
|
|
99
|
+
def to_s
|
|
100
|
+
'__'
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def __
|
|
105
|
+
AnythingPattern.new
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
class CyclicPattern
|
|
109
|
+
include Inspectable
|
|
110
|
+
|
|
111
|
+
attr_accessor :interior
|
|
112
|
+
|
|
113
|
+
# TODO: define to_s
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def cyclic(&proc)
|
|
117
|
+
p = CyclicPattern.new
|
|
118
|
+
p.interior = proc.call(p)
|
|
119
|
+
return p
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
class JsonType
|
|
123
|
+
include DeepEquality, Inspectable
|
|
124
|
+
|
|
125
|
+
attr_reader :type
|
|
126
|
+
|
|
127
|
+
def initialize(type)
|
|
128
|
+
@type = type
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def to_s
|
|
132
|
+
@type.to_s
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def self.new_from_value(value)
|
|
136
|
+
case value
|
|
137
|
+
when Hash
|
|
138
|
+
JsonType.new :object
|
|
139
|
+
when Array
|
|
140
|
+
JsonType.new :array
|
|
141
|
+
when String
|
|
142
|
+
JsonType.new :string
|
|
143
|
+
when Integer
|
|
144
|
+
JsonType.new :integer
|
|
145
|
+
when Float
|
|
146
|
+
JsonType.new :float
|
|
147
|
+
when TrueClass
|
|
148
|
+
JsonType.new :boolean
|
|
149
|
+
when FalseClass
|
|
150
|
+
JsonType.new :boolean
|
|
151
|
+
when NilClass
|
|
152
|
+
JsonType.new :null
|
|
153
|
+
else
|
|
154
|
+
raise "value has no JsonType: #{value.inspect}"
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def self.new_from_class(klass)
|
|
159
|
+
case klass.name
|
|
160
|
+
when 'Hash'
|
|
161
|
+
JsonType.new :object
|
|
162
|
+
when 'Array'
|
|
163
|
+
JsonType.new :array
|
|
164
|
+
when 'String'
|
|
165
|
+
JsonType.new :string
|
|
166
|
+
when 'Integer'
|
|
167
|
+
JsonType.new :integer
|
|
168
|
+
when 'Float'
|
|
169
|
+
JsonType.new :float
|
|
170
|
+
when 'Numeric'
|
|
171
|
+
JsonType.new :float
|
|
172
|
+
when 'Boolean'
|
|
173
|
+
JsonType.new :boolean
|
|
174
|
+
when 'NilClass'
|
|
175
|
+
JsonType.new :null
|
|
176
|
+
else
|
|
177
|
+
raise "class has no JsonType: #{klass}"
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def ===(value)
|
|
182
|
+
value_type = JsonType.new_from_value(value).type
|
|
183
|
+
value_type == @type or (value_type == 'number' and (@type == :integer or @type == :float))
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def json_type_name(value)
|
|
188
|
+
JsonType.new_from_value(value).type.to_s
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def shallow_value(value)
|
|
192
|
+
case value
|
|
193
|
+
when Hash
|
|
194
|
+
'object'
|
|
195
|
+
when Array
|
|
196
|
+
'array'
|
|
197
|
+
when NilClass
|
|
198
|
+
'null'
|
|
199
|
+
else
|
|
200
|
+
value.inspect
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
class ObjectMembersValidationResult
|
|
205
|
+
include HashInitialized
|
|
206
|
+
|
|
207
|
+
attr_reader :failures, :remainder
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
class ValidationFailure
|
|
211
|
+
include HashInitialized, DeepEquality, Inspectable
|
|
212
|
+
|
|
213
|
+
attr_reader :path
|
|
214
|
+
|
|
215
|
+
def to_json
|
|
216
|
+
Hash[*self.instance_variables.map { |var|
|
|
217
|
+
val = self.instance_variable_get(var)
|
|
218
|
+
[var.to_s.sub('@', ''), (val.is_a?(Set) ? val.to_a : val)]
|
|
219
|
+
}.reduce(:+)]
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def path_to_s
|
|
223
|
+
'$' + @path.map { |p| "[#{ p.is_a?(String) ? "'#{p.to_s}'" : p.to_s }]" }.join('')
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
class ValidationUnexpected < ValidationFailure
|
|
228
|
+
attr_reader :expected, :found
|
|
229
|
+
|
|
230
|
+
def to_s
|
|
231
|
+
expected = @expected.is_a?(Set) ?
|
|
232
|
+
(@expected.size == 1 ? @expected.to_a[0] : "one of: " + @expected.to_a.join(', ')) :
|
|
233
|
+
@expected
|
|
234
|
+
return "at #{path_to_s}; found #{@found}; expected #{expected}"
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
class ValidationAmbiguity < ValidationFailure
|
|
239
|
+
attr_reader :found, :overlapping_patterns
|
|
240
|
+
|
|
241
|
+
def to_s
|
|
242
|
+
overlapping_patterns = @overlapping_patterns.to_a.join(', ')
|
|
243
|
+
return "ambiguous patterns at #{path_to_s}; found #{@found}; overlapping patterns: #{overlapping_patterns}"
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
class Validation
|
|
248
|
+
include HashInitialized, DeepEquality, Inspectable
|
|
249
|
+
|
|
250
|
+
def shallow_match?(data)
|
|
251
|
+
validate([], data).empty?
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def shallow_describe
|
|
255
|
+
Set[to_s]
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def validate_from_root(data)
|
|
259
|
+
validate([], data)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def self.new_from_pattern(pattern)
|
|
263
|
+
self.memoized_new_from_pattern({}, pattern)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def self.memoized_new_from_pattern(translation, pattern)
|
|
267
|
+
return translation[pattern.object_id] if translation[pattern.object_id]
|
|
268
|
+
|
|
269
|
+
# The below could get stuck in an infinite loop, but only if the pattern
|
|
270
|
+
# writer is particularly mischievous, doing a deliberate assigment of the
|
|
271
|
+
# interior of a pattern back to itself.
|
|
272
|
+
|
|
273
|
+
if pattern.is_a?(CyclicPattern)
|
|
274
|
+
return memoized_new_from_pattern(translation, pattern.interior)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
case pattern
|
|
278
|
+
when {}
|
|
279
|
+
v = ObjectValidation.new(members: EmptyObjectMembersValidation.new)
|
|
280
|
+
when Hash
|
|
281
|
+
v = ObjectValidation.new(members: nil)
|
|
282
|
+
when UniformArrayPattern
|
|
283
|
+
v = UniformArrayValidation.new
|
|
284
|
+
when DisjunctionPattern
|
|
285
|
+
v = DisjunctionValidation.new
|
|
286
|
+
when Regexp
|
|
287
|
+
v = RegexpValidation.new(regexp: pattern.dup)
|
|
288
|
+
when JsonType
|
|
289
|
+
v = PrimitiveTypeValidation.new(type: pattern.dup)
|
|
290
|
+
when Symbol
|
|
291
|
+
v = PrimitiveTypeValidation.new(type: JsonType.new(pattern))
|
|
292
|
+
when Class
|
|
293
|
+
if pattern == Email
|
|
294
|
+
v = EmailValidation.new
|
|
295
|
+
elsif pattern == URL
|
|
296
|
+
v = URLValidation.new
|
|
297
|
+
else
|
|
298
|
+
v = PrimitiveTypeValidation.new(type: JsonType.new_from_class(pattern))
|
|
299
|
+
end
|
|
300
|
+
when String
|
|
301
|
+
v = PrimitiveValueValidation.new(value: pattern.dup)
|
|
302
|
+
when Numeric
|
|
303
|
+
v = PrimitiveValueValidation.new(value: pattern)
|
|
304
|
+
when TrueClass
|
|
305
|
+
v = PrimitiveValueValidation.new(value: pattern)
|
|
306
|
+
when FalseClass
|
|
307
|
+
v = PrimitiveValueValidation.new(value: pattern)
|
|
308
|
+
when NilClass
|
|
309
|
+
v = PrimitiveValueValidation.new(value: pattern)
|
|
310
|
+
when AnythingPattern
|
|
311
|
+
v = AnythingValidation.new
|
|
312
|
+
else
|
|
313
|
+
raise "Unrecognized type in pattern: #{pattern.class}"
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
translation[pattern.object_id] = v
|
|
317
|
+
|
|
318
|
+
case pattern
|
|
319
|
+
when {}
|
|
320
|
+
when Hash
|
|
321
|
+
v.members = ObjectMembersValidation.memoized_new_from_pattern(translation, pattern)
|
|
322
|
+
when UniformArrayPattern
|
|
323
|
+
v.value_validation = memoized_new_from_pattern(translation, pattern.value)
|
|
324
|
+
when DisjunctionPattern
|
|
325
|
+
v.alternatives = pattern.alternatives.map { |a| memoized_new_from_pattern(translation, a) }
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
return v
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def expects_an_object?
|
|
332
|
+
false
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def as_object_members
|
|
336
|
+
raise "attempted to treat non-object validation as object members: #{self}"
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
class DisjunctionValidation < Validation
|
|
341
|
+
# This is an abstract version that, at run time, becomes either:
|
|
342
|
+
# when all alternatives are objects -> An ObjectValidation containing an ObjectMembersDisjunctionValidation
|
|
343
|
+
# otherwise -> a ValueDisjunctionValidation
|
|
344
|
+
|
|
345
|
+
# TODO: Do this as a separate transformation step when compiling the validation
|
|
346
|
+
|
|
347
|
+
attr_accessor :alternatives
|
|
348
|
+
|
|
349
|
+
# TODO: Use some sort of delegator to handle these methods?
|
|
350
|
+
|
|
351
|
+
def validate(path, data)
|
|
352
|
+
concrete_validation.validate(path, data)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def shallow_match?(data)
|
|
356
|
+
concrete_validation.shallow_match?(data)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def shallow_describe
|
|
360
|
+
concrete_validation.shallow_describe
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def to_s
|
|
364
|
+
concrete_validation.to_s
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def concrete_validation
|
|
368
|
+
@concrete_validation ||= expects_an_object? ?
|
|
369
|
+
ObjectValidation.new(members: as_object_members) :
|
|
370
|
+
ValueDisjunctionValidation.new(alternatives: @alternatives)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def expects_an_object?
|
|
374
|
+
@alternatives.all? { |v| v.expects_an_object? }
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def as_object_members
|
|
378
|
+
ObjectMembersDisjunctionValidation.new(
|
|
379
|
+
alternatives: @alternatives.map { |v| v.as_object_members }
|
|
380
|
+
)
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
class ValueDisjunctionValidation < Validation
|
|
385
|
+
attr_accessor :alternatives
|
|
386
|
+
|
|
387
|
+
def validate(path, data)
|
|
388
|
+
matching = @alternatives.select { |v| v.shallow_match? data }
|
|
389
|
+
if matching.length == 0
|
|
390
|
+
return [ValidationUnexpected.new(
|
|
391
|
+
path: path,
|
|
392
|
+
found: shallow_value(data),
|
|
393
|
+
expected: shallow_describe,
|
|
394
|
+
)]
|
|
395
|
+
elsif matching.length == 1
|
|
396
|
+
return matching[0].validate(path, data)
|
|
397
|
+
else
|
|
398
|
+
return [ValidationAmbiguity.new(
|
|
399
|
+
path: path,
|
|
400
|
+
found: shallow_value(data),
|
|
401
|
+
overlapping_patterns: matching.flat_map { |v| v.shallow_describe.to_a },
|
|
402
|
+
)]
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def shallow_match?(data)
|
|
407
|
+
matching = @alternatives.select { |v| v.shallow_match? data }
|
|
408
|
+
return matching.length == 1
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def shallow_describe
|
|
412
|
+
set_union(@alternatives.map { |v| v.shallow_describe })
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def to_s
|
|
416
|
+
'(' + (@alternatives.map { |v| v.to_s }).join(' | ') + ')'
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
class DisjunctionKey
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def one_of(*patterns)
|
|
424
|
+
case patterns
|
|
425
|
+
when []
|
|
426
|
+
DisjunctionKey.new
|
|
427
|
+
else
|
|
428
|
+
DisjunctionPattern.new(alternatives: patterns)
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
class UniformArrayValidation < Validation
|
|
433
|
+
attr_accessor :value_validation
|
|
434
|
+
|
|
435
|
+
def validate(path, data)
|
|
436
|
+
if data.is_a? Array
|
|
437
|
+
return validate_members(path, data)
|
|
438
|
+
else
|
|
439
|
+
return [ValidationUnexpected.new(
|
|
440
|
+
path: path,
|
|
441
|
+
expected: 'array',
|
|
442
|
+
found: json_type_name(data),
|
|
443
|
+
)]
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def shallow_match?(data)
|
|
448
|
+
data.is_a? Array
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def shallow_describe
|
|
452
|
+
Set['array']
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def to_s
|
|
456
|
+
"[ #{@value_validation}, ... ]"
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
private
|
|
460
|
+
|
|
461
|
+
def validate_members(path, data)
|
|
462
|
+
failures = []
|
|
463
|
+
for i in 0..(data.length-1)
|
|
464
|
+
failures += @value_validation.validate(path + [i], data[i])
|
|
465
|
+
end
|
|
466
|
+
return failures
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
class ObjectValidation < Validation
|
|
471
|
+
attr_accessor :members
|
|
472
|
+
|
|
473
|
+
def validate(path, data)
|
|
474
|
+
if data.is_a? Hash
|
|
475
|
+
result = @members.validate_members(path, data)
|
|
476
|
+
failures = result.failures
|
|
477
|
+
if result.remainder.length > 0
|
|
478
|
+
failures << ValidationUnexpected.new(
|
|
479
|
+
path: path,
|
|
480
|
+
expected: 'end of object members',
|
|
481
|
+
found: "names: " + result.remainder.keys.map { |name| name.inspect }.join(', ')
|
|
482
|
+
)
|
|
483
|
+
end
|
|
484
|
+
return failures
|
|
485
|
+
else
|
|
486
|
+
return [ValidationUnexpected.new(
|
|
487
|
+
path: path,
|
|
488
|
+
expected: 'object',
|
|
489
|
+
found: json_type_name(data),
|
|
490
|
+
)]
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def shallow_match?(data)
|
|
495
|
+
data.is_a? Hash
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def shallow_describe
|
|
499
|
+
Set['object']
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def to_s
|
|
503
|
+
"{ #{@members} }"
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def expects_an_object?
|
|
507
|
+
true
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def as_object_members
|
|
511
|
+
@members
|
|
512
|
+
end
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
class ObjectMembersValidation
|
|
516
|
+
include HashInitialized, Inspectable
|
|
517
|
+
|
|
518
|
+
def self.memoized_new_from_pattern(translation, pattern)
|
|
519
|
+
case pattern
|
|
520
|
+
when Hash
|
|
521
|
+
validation = pattern.to_a.reverse.reduce(EmptyObjectMembersValidation.new) do |es, e|
|
|
522
|
+
(name, value) = e
|
|
523
|
+
name = name.to_s if name.is_a? Symbol
|
|
524
|
+
case name
|
|
525
|
+
when String
|
|
526
|
+
value_validation = Validation.memoized_new_from_pattern(translation, value)
|
|
527
|
+
v = SingleObjectMemberValidation.new(name: name, value_validation: value_validation)
|
|
528
|
+
when DisjunctionKey
|
|
529
|
+
unless value.is_a?(Array)
|
|
530
|
+
raise "one_of should only be used with an array"
|
|
531
|
+
end
|
|
532
|
+
vals = value.map { |pat|
|
|
533
|
+
ObjectMembersFromObjectValidation.new(object_validation:
|
|
534
|
+
Validation.memoized_new_from_pattern(translation, pat)
|
|
535
|
+
)
|
|
536
|
+
}
|
|
537
|
+
v = ObjectMembersDisjunctionValidation.new(alternatives: vals)
|
|
538
|
+
when OptionalKey
|
|
539
|
+
v = OptionalObjectMembersValidation.new(members:
|
|
540
|
+
ObjectMembersFromObjectValidation.new(object_validation:
|
|
541
|
+
Validation.memoized_new_from_pattern(translation, value)
|
|
542
|
+
)
|
|
543
|
+
)
|
|
544
|
+
when ManyKey
|
|
545
|
+
v = ManyObjectMembersValidation.new(
|
|
546
|
+
value_validation: Validation.memoized_new_from_pattern(translation, value)
|
|
547
|
+
)
|
|
548
|
+
when MembersKey
|
|
549
|
+
v = ObjectMembersFromObjectValidation.new(
|
|
550
|
+
object_validation: Validation.memoized_new_from_pattern(translation, value)
|
|
551
|
+
)
|
|
552
|
+
else
|
|
553
|
+
raise "unrecognized key type in pattern: #{name.class}"
|
|
554
|
+
end
|
|
555
|
+
SequencedObjectMembersValidation.new(left: v, right: es)
|
|
556
|
+
end
|
|
557
|
+
else
|
|
558
|
+
raise "cannot create object members validation from a #{pattern.class}"
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
class ObjectMembersFromObjectValidation < ObjectMembersValidation
|
|
564
|
+
# represents a type coercion from an ObjectValidation to and ObjectMembersValidation
|
|
565
|
+
|
|
566
|
+
# TODO: Do this as a separate transformation step when compiling the validation
|
|
567
|
+
|
|
568
|
+
attr_reader :object_validation
|
|
569
|
+
|
|
570
|
+
# TODO: Use some sort of delegator to handle these methods?
|
|
571
|
+
|
|
572
|
+
def possible_first_names
|
|
573
|
+
as_object_members.possible_first_names
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def first_value_validations(name)
|
|
577
|
+
as_object_members.first_value_validations(name)
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def matching_first_names(data)
|
|
581
|
+
as_object_members.matching_first_names(data)
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
def first_value_match?(name, value)
|
|
585
|
+
as_object_members.first_value_match?(name, value)
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
def validate_members(path, data)
|
|
589
|
+
as_object_members.validate_members(path, data)
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
def to_s
|
|
593
|
+
as_object_members.to_s
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
def as_object_members
|
|
597
|
+
@object_members_validation ||= @object_validation.as_object_members
|
|
598
|
+
end
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
class ObjectMembersDisjunctionValidation < ObjectMembersValidation
|
|
602
|
+
attr_accessor :alternatives
|
|
603
|
+
|
|
604
|
+
def possible_first_names
|
|
605
|
+
set_union(@alternatives.map { |v| v.possible_first_names })
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
def first_value_validations(name)
|
|
609
|
+
set_union(@alternatives.map { |v| v.first_value_validations(name) })
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
def matching_first_names(data)
|
|
613
|
+
set_union(@alternatives.map { |v| v.matching_first_names(data) })
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
def first_value_match?(name, value)
|
|
617
|
+
@alternatives.any? { |v| v.first_value_match?(name, value) }
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
def validate_members(path, data)
|
|
621
|
+
matching_first_names_by_validation =
|
|
622
|
+
Hash[*@alternatives.flat_map { |v| [v, v.matching_first_names(data)] }]
|
|
623
|
+
validations_with_matching_first_names =
|
|
624
|
+
matching_first_names_by_validation.select { |k, v| v.size > 0 }.keys
|
|
625
|
+
matching_names = set_union(matching_first_names_by_validation.values)
|
|
626
|
+
|
|
627
|
+
if matching_names.size == 0
|
|
628
|
+
found_names = data.empty? ?
|
|
629
|
+
'end of object members' :
|
|
630
|
+
"names: #{data.keys.map { |name| name.inspect }.join(', ')}"
|
|
631
|
+
return ObjectMembersValidationResult.new(
|
|
632
|
+
failures: [ValidationUnexpected.new(
|
|
633
|
+
path: path,
|
|
634
|
+
found: found_names,
|
|
635
|
+
expected: Set[*possible_first_names.map { |n| "name: #{n.inspect}" }],
|
|
636
|
+
)],
|
|
637
|
+
remainder: data,
|
|
638
|
+
)
|
|
639
|
+
elsif matching_names.size > 1
|
|
640
|
+
return ObjectMembersValidationResult.new(
|
|
641
|
+
failures: [ValidationAmbiguity.new(
|
|
642
|
+
path: path,
|
|
643
|
+
found: data.keys,
|
|
644
|
+
overlapping_patterns: validations_with_matching_first_names.flat_map { |v|
|
|
645
|
+
v.possible_first_names.to_a
|
|
646
|
+
},
|
|
647
|
+
)]
|
|
648
|
+
)
|
|
649
|
+
else
|
|
650
|
+
name = matching_names.to_a[0]
|
|
651
|
+
value = data[name]
|
|
652
|
+
remainder = data.dup
|
|
653
|
+
remainder.delete name
|
|
654
|
+
validations_matching_value =
|
|
655
|
+
validations_with_matching_first_names.select { |v| v.first_value_match?(name, value) }
|
|
656
|
+
if validations_matching_value.length == 0
|
|
657
|
+
return ObjectMembersValidationResult.new(
|
|
658
|
+
failures: [ValidationUnexpected.new(
|
|
659
|
+
path: path + [name],
|
|
660
|
+
found: shallow_value(data[name]),
|
|
661
|
+
expected: set_union(validations_with_matching_first_names.map { |v|
|
|
662
|
+
set_union(v.first_value_validations(name).map { |v| v.shallow_describe })
|
|
663
|
+
}),
|
|
664
|
+
)],
|
|
665
|
+
remainder: remainder,
|
|
666
|
+
)
|
|
667
|
+
elsif validations_matching_value.length == 1
|
|
668
|
+
return validations_matching_value[0].validate_members(path, data)
|
|
669
|
+
else
|
|
670
|
+
return ObjectMembersValidationResult.new(
|
|
671
|
+
failures: [ValidationAmbiguity.new(
|
|
672
|
+
path: path + [name],
|
|
673
|
+
found: shallow_value(data[name]),
|
|
674
|
+
overlapping_patterns: validations_matching_value.flat_map { |v|
|
|
675
|
+
v.first_value_validations(name).shallow_describe.to_a
|
|
676
|
+
},
|
|
677
|
+
)],
|
|
678
|
+
remainder: remainder,
|
|
679
|
+
)
|
|
680
|
+
end
|
|
681
|
+
end
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
def to_s
|
|
685
|
+
'(' + (@alternatives.map { |v| v.to_s }).join(' | ') + ')'
|
|
686
|
+
end
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
class SequencedObjectMembersValidation < ObjectMembersValidation
|
|
690
|
+
# TODO: Use an array instead of :left and :right. Easier to process, easier to inspect.
|
|
691
|
+
# Obviates the need for an EmptyObjectMembersValidation in some cases.
|
|
692
|
+
|
|
693
|
+
attr_accessor :left, :right
|
|
694
|
+
|
|
695
|
+
def possible_first_names
|
|
696
|
+
@left.possible_first_names
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
def first_value_validation
|
|
700
|
+
@left.first_value_validation
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
def matching_first_names(data)
|
|
704
|
+
@left.matching_first_names(data)
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
def first_value_match?(name, value)
|
|
708
|
+
@left.first_value_match?(name, value)
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
def first_value_validations(name)
|
|
712
|
+
@left.first_value_validations(name)
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
def validate_members(path, data)
|
|
716
|
+
result_left = @left.validate_members(path, data)
|
|
717
|
+
result_right = @right.validate_members(path, result_left.remainder)
|
|
718
|
+
return ObjectMembersValidationResult.new(
|
|
719
|
+
failures: result_left.failures + result_right.failures,
|
|
720
|
+
remainder: result_right.remainder,
|
|
721
|
+
)
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
def to_s
|
|
725
|
+
[@left.to_s, @right.to_s].select { |s| not s.empty? }.join(', ')
|
|
726
|
+
end
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
class EmptyObjectMembersValidation < ObjectMembersValidation
|
|
730
|
+
# TODO: Handle first_name/value methods?
|
|
731
|
+
|
|
732
|
+
def validate_members(path, data)
|
|
733
|
+
ObjectMembersValidationResult.new(
|
|
734
|
+
failures: [],
|
|
735
|
+
remainder: data,
|
|
736
|
+
)
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
def to_s
|
|
740
|
+
''
|
|
741
|
+
end
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
class SingleObjectMemberValidation < ObjectMembersValidation
|
|
745
|
+
attr_accessor :name, :value_validation
|
|
746
|
+
|
|
747
|
+
def possible_first_names
|
|
748
|
+
Set[@name]
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
def first_value_validation
|
|
752
|
+
@value_validation
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
def matching_first_names(data)
|
|
756
|
+
data.has_key?(@name) ? Set[@name] : Set[]
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
def first_value_match?(name, value)
|
|
760
|
+
return false unless name == @name
|
|
761
|
+
@value_validation.shallow_match?(value)
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
def first_value_validations(name)
|
|
765
|
+
Set[@value_validation]
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
def validate_members(path, data)
|
|
769
|
+
if data.has_key? @name
|
|
770
|
+
failures = @value_validation.validate(path + [@name], data[@name])
|
|
771
|
+
remainder = data.dup
|
|
772
|
+
remainder.delete @name
|
|
773
|
+
return ObjectMembersValidationResult.new(
|
|
774
|
+
failures: failures,
|
|
775
|
+
remainder: remainder,
|
|
776
|
+
)
|
|
777
|
+
else
|
|
778
|
+
found_names = data.empty? ?
|
|
779
|
+
'end of object members' :
|
|
780
|
+
"names: #{data.keys.map { |name| name.inspect }.join(', ')}"
|
|
781
|
+
return ObjectMembersValidationResult.new(
|
|
782
|
+
failures: [ValidationUnexpected.new(
|
|
783
|
+
path: path,
|
|
784
|
+
expected: "name: \"#@name\"",
|
|
785
|
+
found: found_names,
|
|
786
|
+
)],
|
|
787
|
+
remainder: data,
|
|
788
|
+
)
|
|
789
|
+
end
|
|
790
|
+
end
|
|
791
|
+
|
|
792
|
+
def to_s
|
|
793
|
+
"\"#{@name}\": #{@value_validation}"
|
|
794
|
+
end
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
class OptionalObjectMembersValidation < ObjectMembersValidation
|
|
798
|
+
attr_accessor :members
|
|
799
|
+
|
|
800
|
+
# TODO: If this is in a sequence, failure should trigger a check to the next member in the sequence
|
|
801
|
+
|
|
802
|
+
def possible_first_names
|
|
803
|
+
@members.possible_first_names
|
|
804
|
+
end
|
|
805
|
+
|
|
806
|
+
def matching_first_names(data)
|
|
807
|
+
@members.matching_first_names(data)
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
def first_value_match?(name, value)
|
|
811
|
+
@members.first_value_match?(name, value)
|
|
812
|
+
end
|
|
813
|
+
|
|
814
|
+
def first_value_validations(name)
|
|
815
|
+
@members.first_value_validations(name)
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
def validate_members(path, data)
|
|
819
|
+
if matching_first_names(data).size > 0
|
|
820
|
+
return @members.validate_members(path, data)
|
|
821
|
+
else
|
|
822
|
+
return ObjectMembersValidationResult.new(
|
|
823
|
+
failures: [],
|
|
824
|
+
remainder: data,
|
|
825
|
+
)
|
|
826
|
+
end
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
def to_s
|
|
830
|
+
"(#{@members})?"
|
|
831
|
+
end
|
|
832
|
+
end
|
|
833
|
+
|
|
834
|
+
class OptionalKey
|
|
835
|
+
end
|
|
836
|
+
|
|
837
|
+
def optional
|
|
838
|
+
OptionalKey.new
|
|
839
|
+
end
|
|
840
|
+
|
|
841
|
+
class ManyObjectMembersValidation < ObjectMembersValidation
|
|
842
|
+
attr_accessor :value_validation
|
|
843
|
+
|
|
844
|
+
def possible_first_names
|
|
845
|
+
# TODO: Need a way to indicate this can match any first names, rather than none
|
|
846
|
+
Set[]
|
|
847
|
+
end
|
|
848
|
+
|
|
849
|
+
def matching_first_names(data)
|
|
850
|
+
Set[*data.keys]
|
|
851
|
+
end
|
|
852
|
+
|
|
853
|
+
def first_value_match?(name, value)
|
|
854
|
+
@value_validation.validate([], value).empty?
|
|
855
|
+
end
|
|
856
|
+
|
|
857
|
+
def first_value_validations(name)
|
|
858
|
+
Set[@value_validation]
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
def to_s
|
|
862
|
+
"__: #{@value_validation}, ..."
|
|
863
|
+
end
|
|
864
|
+
|
|
865
|
+
def validate_members(path, data)
|
|
866
|
+
failures = []
|
|
867
|
+
data = data.to_a
|
|
868
|
+
for i in 0..(data.length-1)
|
|
869
|
+
failures += @value_validation.validate(path + [data[i][0]], data[i][1])
|
|
870
|
+
end
|
|
871
|
+
return ObjectMembersValidationResult.new(
|
|
872
|
+
failures: failures,
|
|
873
|
+
remainder: {},
|
|
874
|
+
)
|
|
875
|
+
end
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
class ManyKey
|
|
879
|
+
end
|
|
880
|
+
|
|
881
|
+
def many
|
|
882
|
+
ManyKey.new
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
class CyclicValueValidation < Validation
|
|
886
|
+
attr_accessor :interior
|
|
887
|
+
@@references = {}
|
|
888
|
+
@@count = 0
|
|
889
|
+
|
|
890
|
+
def initialize(opts)
|
|
891
|
+
super(opts)
|
|
892
|
+
@@count += 1
|
|
893
|
+
@@references[self] = @@count
|
|
894
|
+
end
|
|
895
|
+
|
|
896
|
+
def validate(path, data)
|
|
897
|
+
interior.validate(path, data)
|
|
898
|
+
end
|
|
899
|
+
|
|
900
|
+
def shallow_match?(data)
|
|
901
|
+
interior.shallow_match?(data)
|
|
902
|
+
end
|
|
903
|
+
|
|
904
|
+
def shallow_describe
|
|
905
|
+
interior.shallow_describe
|
|
906
|
+
end
|
|
907
|
+
|
|
908
|
+
def to_s
|
|
909
|
+
# TODO: Use a dynamically scoped variable to distinguish the first printing of this value
|
|
910
|
+
|
|
911
|
+
"&#{@@references[self]}"
|
|
912
|
+
end
|
|
913
|
+
end
|
|
914
|
+
|
|
915
|
+
class MembersKey
|
|
916
|
+
end
|
|
917
|
+
|
|
918
|
+
def members
|
|
919
|
+
MembersKey.new
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
class CyclicObjectMembersValidation < ObjectMembersValidation
|
|
923
|
+
attr_accessor :members
|
|
924
|
+
@@references = {}
|
|
925
|
+
@@count = 0
|
|
926
|
+
|
|
927
|
+
def initialize(opts)
|
|
928
|
+
super(opts)
|
|
929
|
+
@@count += 1
|
|
930
|
+
@@references[self] = @@count
|
|
931
|
+
end
|
|
932
|
+
|
|
933
|
+
def possible_first_names
|
|
934
|
+
@members.possible_first_names
|
|
935
|
+
end
|
|
936
|
+
|
|
937
|
+
def matching_first_names(data)
|
|
938
|
+
@members.matching_first_names(data)
|
|
939
|
+
end
|
|
940
|
+
|
|
941
|
+
def first_value_match?(name, value)
|
|
942
|
+
@members.first_value_match?(name, value)
|
|
943
|
+
end
|
|
944
|
+
|
|
945
|
+
def first_value_validations(name)
|
|
946
|
+
@members.first_value_validations(name)
|
|
947
|
+
end
|
|
948
|
+
|
|
949
|
+
def validate_members(path, data)
|
|
950
|
+
@members.validate_members(path, data)
|
|
951
|
+
end
|
|
952
|
+
|
|
953
|
+
def to_s
|
|
954
|
+
"&:#{@@references[self]}"
|
|
955
|
+
end
|
|
956
|
+
end
|
|
957
|
+
|
|
958
|
+
class RegexpValidation < Validation
|
|
959
|
+
attr_accessor :regexp
|
|
960
|
+
|
|
961
|
+
def validate(path, data)
|
|
962
|
+
if data.is_a? String
|
|
963
|
+
if data =~ regexp
|
|
964
|
+
return []
|
|
965
|
+
else
|
|
966
|
+
return [ValidationUnexpected.new(
|
|
967
|
+
path: path,
|
|
968
|
+
expected: "string matching #{@regexp.inspect}",
|
|
969
|
+
found: data.inspect,
|
|
970
|
+
)]
|
|
971
|
+
end
|
|
972
|
+
else
|
|
973
|
+
return [ValidationUnexpected.new(path: path, expected: 'string', found: json_type_name(data))]
|
|
974
|
+
end
|
|
975
|
+
end
|
|
976
|
+
|
|
977
|
+
def to_s
|
|
978
|
+
@regexp.inspect
|
|
979
|
+
end
|
|
980
|
+
end
|
|
981
|
+
|
|
982
|
+
class EmailValidation < Validation
|
|
983
|
+
# TODO: Replace this with a conjunction of RegexpValidations with customized errors?
|
|
984
|
+
|
|
985
|
+
@@email_regexp = Regexp::new("^" +
|
|
986
|
+
# local name
|
|
987
|
+
"(?:" +
|
|
988
|
+
"(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)" +
|
|
989
|
+
"|" +
|
|
990
|
+
"(?:\"[^\"]+\")" +
|
|
991
|
+
")" +
|
|
992
|
+
"@" +
|
|
993
|
+
# host name
|
|
994
|
+
"(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)" +
|
|
995
|
+
# domain name
|
|
996
|
+
"(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*" +
|
|
997
|
+
# TLD identifier
|
|
998
|
+
"(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))" +
|
|
999
|
+
"$", true
|
|
1000
|
+
)
|
|
1001
|
+
|
|
1002
|
+
def validate(path, data)
|
|
1003
|
+
if data.is_a? String
|
|
1004
|
+
if data =~ @@email_regexp
|
|
1005
|
+
return []
|
|
1006
|
+
else
|
|
1007
|
+
return [ValidationUnexpected.new(
|
|
1008
|
+
path: path,
|
|
1009
|
+
expected: "email",
|
|
1010
|
+
found: data.inspect,
|
|
1011
|
+
)]
|
|
1012
|
+
end
|
|
1013
|
+
else
|
|
1014
|
+
return [ValidationUnexpected.new(
|
|
1015
|
+
path: path,
|
|
1016
|
+
expected: 'string',
|
|
1017
|
+
found: json_type_name(data),
|
|
1018
|
+
)]
|
|
1019
|
+
end
|
|
1020
|
+
end
|
|
1021
|
+
|
|
1022
|
+
def to_s
|
|
1023
|
+
'email'
|
|
1024
|
+
end
|
|
1025
|
+
end
|
|
1026
|
+
|
|
1027
|
+
class URLValidation < Validation
|
|
1028
|
+
# TODO: Replace this with a conjunction of RegexpValidations with customized errors?
|
|
1029
|
+
|
|
1030
|
+
@@url_regexp = Regexp::new("^" +
|
|
1031
|
+
# protocol identifier
|
|
1032
|
+
"(?:(?:https?|ftp)://)" +
|
|
1033
|
+
# user:pass authentication
|
|
1034
|
+
"(?:\\S+(?::\\S*)?@)?" +
|
|
1035
|
+
"(?:" +
|
|
1036
|
+
# IP address exclusion
|
|
1037
|
+
# private & local networks
|
|
1038
|
+
"(?!10(?:\\.\\d{1,3}){3})" +
|
|
1039
|
+
"(?!127(?:\\.\\d{1,3}){3})" +
|
|
1040
|
+
"(?!169\\.254(?:\\.\\d{1,3}){2})" +
|
|
1041
|
+
"(?!192\\.168(?:\\.\\d{1,3}){2})" +
|
|
1042
|
+
"(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})" +
|
|
1043
|
+
# IP address dotted notation octets
|
|
1044
|
+
# excludes loopback network 0.0.0.0
|
|
1045
|
+
# excludes reserved space >= 224.0.0.0
|
|
1046
|
+
# excludes network & broadcast addresses
|
|
1047
|
+
# (first & last IP address of each class)
|
|
1048
|
+
"(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" +
|
|
1049
|
+
"(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" +
|
|
1050
|
+
"(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +
|
|
1051
|
+
"|" +
|
|
1052
|
+
# host name
|
|
1053
|
+
"(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)" +
|
|
1054
|
+
# domain name
|
|
1055
|
+
"(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*" +
|
|
1056
|
+
# TLD identifier
|
|
1057
|
+
"(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))" +
|
|
1058
|
+
")" +
|
|
1059
|
+
# port number
|
|
1060
|
+
"(?::\\d{2,5})?" +
|
|
1061
|
+
# resource path
|
|
1062
|
+
"(?:/[^\\s]*)?" +
|
|
1063
|
+
"$", true
|
|
1064
|
+
)
|
|
1065
|
+
|
|
1066
|
+
def validate(path, data)
|
|
1067
|
+
if data.is_a? String
|
|
1068
|
+
if data =~ @@url_regexp
|
|
1069
|
+
return []
|
|
1070
|
+
else
|
|
1071
|
+
return [ValidationUnexpected.new(
|
|
1072
|
+
path: path,
|
|
1073
|
+
expected: "URL",
|
|
1074
|
+
found: data.inspect,
|
|
1075
|
+
)]
|
|
1076
|
+
end
|
|
1077
|
+
else
|
|
1078
|
+
return [ValidationUnexpected.new(
|
|
1079
|
+
path: path,
|
|
1080
|
+
expected: 'string',
|
|
1081
|
+
found: json_type_name(data),
|
|
1082
|
+
)]
|
|
1083
|
+
end
|
|
1084
|
+
end
|
|
1085
|
+
|
|
1086
|
+
def to_s
|
|
1087
|
+
'URL'
|
|
1088
|
+
end
|
|
1089
|
+
end
|
|
1090
|
+
|
|
1091
|
+
class PrimitiveTypeValidation < Validation
|
|
1092
|
+
attr_reader :type
|
|
1093
|
+
|
|
1094
|
+
def validate(path, data)
|
|
1095
|
+
if @type === data
|
|
1096
|
+
return []
|
|
1097
|
+
else
|
|
1098
|
+
return [ValidationUnexpected.new(
|
|
1099
|
+
path: path,
|
|
1100
|
+
expected: @type.to_s,
|
|
1101
|
+
found: JsonType.new_from_value(data).to_s,
|
|
1102
|
+
)]
|
|
1103
|
+
end
|
|
1104
|
+
end
|
|
1105
|
+
|
|
1106
|
+
def to_s
|
|
1107
|
+
@type.to_s
|
|
1108
|
+
end
|
|
1109
|
+
end
|
|
1110
|
+
|
|
1111
|
+
class PrimitiveValueValidation < Validation
|
|
1112
|
+
attr_reader :value
|
|
1113
|
+
|
|
1114
|
+
def validate(path, data)
|
|
1115
|
+
if JsonType.new_from_value(@value) === data
|
|
1116
|
+
if data == @value
|
|
1117
|
+
return []
|
|
1118
|
+
else
|
|
1119
|
+
return [ValidationUnexpected.new(path: path, expected: to_s, found: data.inspect)]
|
|
1120
|
+
end
|
|
1121
|
+
else
|
|
1122
|
+
return [ValidationUnexpected.new(
|
|
1123
|
+
path: path,
|
|
1124
|
+
expected: json_type_name(@value),
|
|
1125
|
+
found: JsonType.new_from_value(data).to_s,
|
|
1126
|
+
)]
|
|
1127
|
+
end
|
|
1128
|
+
end
|
|
1129
|
+
|
|
1130
|
+
def to_s
|
|
1131
|
+
case @value
|
|
1132
|
+
when nil
|
|
1133
|
+
'null'
|
|
1134
|
+
else
|
|
1135
|
+
@value.inspect
|
|
1136
|
+
end
|
|
1137
|
+
end
|
|
1138
|
+
end
|
|
1139
|
+
|
|
1140
|
+
class AnythingValidation < Validation
|
|
1141
|
+
def validate(path, data)
|
|
1142
|
+
return []
|
|
1143
|
+
end
|
|
1144
|
+
|
|
1145
|
+
def to_s
|
|
1146
|
+
'__'
|
|
1147
|
+
end
|
|
1148
|
+
end
|