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.
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