json_patterns 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/lib/json_patterns.rb +1148 -0
  2. data/test/test_json_patterns.rb +839 -0
  3. 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