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,839 @@
1
+ require 'test/unit'
2
+ require 'json_patterns'
3
+ require 'set'
4
+
5
+ class JsonValidationTest < Test::Unit::TestCase
6
+ def test_validation_failure_string_and_json_representation
7
+ failure = ValidationUnexpected.new(
8
+ path: ['santa', 'came', 4, 'you'],
9
+ found: "a box of chocolates",
10
+ expected: "two lumps of coal",
11
+ )
12
+ assert_equal(
13
+ "at $['santa']['came'][4]['you']; found a box of chocolates; expected two lumps of coal",
14
+ failure.to_s,
15
+ )
16
+ assert_equal(
17
+ {
18
+ "path" => ['santa', 'came', 4, 'you'],
19
+ "found" => "a box of chocolates",
20
+ "expected" => "two lumps of coal",
21
+ },
22
+ failure.to_json,
23
+ )
24
+
25
+ failure = ValidationUnexpected.new(
26
+ path: ['some', 'where'],
27
+ found: 'names: "foo"',
28
+ expected: Set['name: ape'],
29
+ )
30
+ assert_equal(
31
+ "at $['some']['where']; found names: \"foo\"; expected name: ape",
32
+ failure.to_s,
33
+ )
34
+ assert_equal(
35
+ {
36
+ "path" => ['some', 'where'],
37
+ "found" => "names: \"foo\"",
38
+ "expected" => ["name: ape"],
39
+ },
40
+ failure.to_json,
41
+ )
42
+
43
+ failure = ValidationUnexpected.new(
44
+ path: ['some', 'where'],
45
+ found: 'names: "foo"',
46
+ expected: Set['name: ape', 'name: boar', 'name: cat'],
47
+ )
48
+ assert_equal(
49
+ "at $['some']['where']; found names: \"foo\"; expected one of: name: ape, name: boar, name: cat",
50
+ failure.to_s,
51
+ )
52
+ assert_equal(
53
+ {
54
+ "path" => ['some', 'where'],
55
+ "found" => "names: \"foo\"",
56
+ "expected" => ["name: ape", "name: boar", "name: cat"],
57
+ },
58
+ failure.to_json,
59
+ )
60
+
61
+ failure = ValidationAmbiguity.new(
62
+ path: ['some', 'where'],
63
+ found: '"frob"',
64
+ overlapping_patterns: Set['/f.ob/', '/fr.b/'],
65
+ )
66
+ assert_equal(
67
+ "ambiguous patterns at $['some']['where']; found \"frob\"; overlapping patterns: /f.ob/, /fr.b/",
68
+ failure.to_s,
69
+ )
70
+ assert_equal(
71
+ {
72
+ "path" => ["some", "where"],
73
+ "found" => "\"frob\"",
74
+ "overlapping_patterns" => ["/f.ob/", "/fr.b/"],
75
+ },
76
+ failure.to_json,
77
+ )
78
+ end
79
+
80
+ def test_literal_string_validation
81
+ pattern = 'xyz'
82
+ validation = Validation.new_from_pattern(pattern)
83
+
84
+ value = 'xyz'
85
+ failures = validation.validate_from_root(value)
86
+ assert_equal [], failures
87
+
88
+ value = 'zyx'
89
+ failures = validation.validate_from_root(value)
90
+ assert_equal [ValidationUnexpected.new(
91
+ path: [],
92
+ found: '"zyx"',
93
+ expected: '"xyz"',
94
+ )], failures
95
+
96
+ value = 4
97
+ failures = validation.validate_from_root(value)
98
+ assert_equal [ValidationUnexpected.new(
99
+ path: [],
100
+ found: 'integer',
101
+ expected: 'string',
102
+ )], failures
103
+ end
104
+
105
+ def test_literal_integer_validation
106
+ pattern = 54
107
+ validation = Validation.new_from_pattern(pattern)
108
+
109
+ value = 54
110
+ failures = validation.validate_from_root(value)
111
+ assert_equal [], failures
112
+
113
+ value = 45
114
+ failures = validation.validate_from_root(value)
115
+ assert_equal [ValidationUnexpected.new(
116
+ path: [],
117
+ found: '45',
118
+ expected: '54',
119
+ )], failures
120
+
121
+ value = 'xyz'
122
+ failures = validation.validate_from_root(value)
123
+ assert_equal [ValidationUnexpected.new(
124
+ path: [],
125
+ found: 'string',
126
+ expected: 'integer',
127
+ )], failures
128
+ end
129
+
130
+ def test_literal_float_validation
131
+ pattern = 54.32
132
+ validation = Validation.new_from_pattern(pattern)
133
+
134
+ value = 54.32
135
+ failures = validation.validate_from_root(value)
136
+ assert_equal [], failures
137
+
138
+ value = 23.45
139
+ failures = validation.validate_from_root(value)
140
+ assert_equal [ValidationUnexpected.new(
141
+ path: [],
142
+ found: '23.45',
143
+ expected: '54.32',
144
+ )], failures
145
+
146
+ value = 54
147
+ failures = validation.validate_from_root(value)
148
+ assert_equal [ValidationUnexpected.new(
149
+ path: [],
150
+ found: 'integer',
151
+ expected: 'float',
152
+ )], failures
153
+ end
154
+
155
+ def test_regex_match
156
+ pattern = /who+ah/
157
+ validation = Validation.new_from_pattern(pattern)
158
+
159
+ value = 'whoooooah'
160
+ failures = validation.validate_from_root(value)
161
+ assert_equal [], failures
162
+
163
+ value = 4
164
+ failures = validation.validate_from_root(value)
165
+ assert_equal [ValidationUnexpected.new(
166
+ path: [],
167
+ found: 'integer',
168
+ expected: 'string',
169
+ )], failures
170
+
171
+ value = 'whah'
172
+ failures = validation.validate_from_root(value)
173
+ assert_equal [ValidationUnexpected.new(
174
+ path: [],
175
+ found: '"whah"',
176
+ expected: 'string matching /who+ah/',
177
+ )], failures
178
+ end
179
+
180
+ def test_email_match
181
+ pattern = Email
182
+ validation = Validation.new_from_pattern(pattern)
183
+
184
+ value = 1234
185
+ failures = validation.validate_from_root(value)
186
+ assert_equal [ValidationUnexpected.new(
187
+ path: [],
188
+ found: 'integer',
189
+ expected: 'string',
190
+ )], failures
191
+
192
+ value = 1234
193
+ failures = validation.validate_from_root(value)
194
+ assert_equal [ValidationUnexpected.new(
195
+ path: [],
196
+ found: 'integer',
197
+ expected: 'string',
198
+ )], failures
199
+
200
+ value = 'larry@janrain.com'
201
+ failures = validation.validate_from_root(value)
202
+ assert_equal [], failures
203
+
204
+ value = '@.'
205
+ failures = validation.validate_from_root(value)
206
+ assert_equal [ValidationUnexpected.new(
207
+ path: [],
208
+ found: '"@."',
209
+ expected: 'email',
210
+ )], failures
211
+
212
+ value = '"Larry Drebes"@janrain.com'
213
+ failures = validation.validate_from_root(value)
214
+ assert_equal [], failures
215
+ end
216
+
217
+ def test_URL_match
218
+ pattern = URL
219
+ validation = Validation.new_from_pattern(pattern)
220
+
221
+ value = 1234
222
+ failures = validation.validate_from_root(value)
223
+ assert_equal [ValidationUnexpected.new(
224
+ path: [],
225
+ found: 'integer',
226
+ expected: 'string',
227
+ )], failures
228
+
229
+ value = 'http://janrain.com'
230
+ failures = validation.validate_from_root(value)
231
+ assert_equal [], failures
232
+
233
+ value = 'janrain.com'
234
+ failures = validation.validate_from_root(value)
235
+ assert_equal [ValidationUnexpected.new(
236
+ path: [],
237
+ found: '"janrain.com"',
238
+ expected: 'URL',
239
+ )], failures
240
+
241
+ value = 'http://warbler.janrain.mobi/products/zumba?238234;3512'
242
+ failures = validation.validate_from_root(value)
243
+ assert_equal [], failures
244
+
245
+ value = 'ftp://larry:coorslight@ftp.janrain.com'
246
+ failures = validation.validate_from_root(value)
247
+ assert_equal [], failures
248
+ end
249
+
250
+ def test_literal_nil_match
251
+ pattern = nil
252
+ validation = Validation.new_from_pattern(pattern)
253
+
254
+ value = nil
255
+ failures = validation.validate_from_root(value)
256
+ assert_equal [], failures
257
+
258
+ value = ''
259
+ failures = validation.validate_from_root(value)
260
+ assert_equal [ValidationUnexpected.new(
261
+ path: [],
262
+ found: 'string',
263
+ expected: 'null',
264
+ )], failures
265
+ end
266
+
267
+ def test_uniform_arrays
268
+ pattern = array_of(String)
269
+ validation = Validation.new_from_pattern(pattern)
270
+
271
+ value = { "a" => 'b' }
272
+ failures = validation.validate_from_root(value)
273
+ assert_equal [ValidationUnexpected.new(
274
+ path: [],
275
+ found: 'object',
276
+ expected: 'array',
277
+ )], failures
278
+
279
+ value = []
280
+ failures = validation.validate_from_root(value)
281
+ assert_equal [], failures
282
+
283
+ value = ['2', 'plus', 3, 'is', 5]
284
+ failures = validation.validate_from_root(value)
285
+ assert_equal [
286
+ ValidationUnexpected.new(
287
+ path: [2],
288
+ found: 'integer',
289
+ expected: 'string',
290
+ ),
291
+ ValidationUnexpected.new(
292
+ path: [4],
293
+ found: 'integer',
294
+ expected: 'string',
295
+ ),
296
+ ], failures
297
+
298
+ value = ['hello', 'to', 'five', 'people']
299
+ failures = validation.validate_from_root(value)
300
+ assert_equal [], failures
301
+ end
302
+
303
+ def test_uniform_objects
304
+ pattern = { many => String }
305
+ validation = Validation.new_from_pattern(pattern)
306
+
307
+ value = ['a', 'b']
308
+ failures = validation.validate_from_root(value)
309
+ assert_equal [ValidationUnexpected.new(
310
+ path: [],
311
+ found: 'array',
312
+ expected: 'object',
313
+ )], failures
314
+
315
+ value = {}
316
+ failures = validation.validate_from_root(value)
317
+ assert_equal [], failures
318
+
319
+ value = { "movie" => 'Up', "book" => 12345, "avg" => 46.2 }
320
+ failures = validation.validate_from_root(value)
321
+ assert_equal [
322
+ ValidationUnexpected.new(
323
+ path: ['book'],
324
+ found: 'integer',
325
+ expected: 'string',
326
+ ),
327
+ ValidationUnexpected.new(
328
+ path: ['avg'],
329
+ found: 'float',
330
+ expected: 'string',
331
+ ),
332
+ ], failures
333
+
334
+ value = { "movie" => 'Up', "book" => 'Twilight' }
335
+ failures = validation.validate_from_root(value)
336
+ assert_equal [], failures
337
+ end
338
+
339
+ def test_object_with_fixed_names
340
+ pattern = {
341
+ name: String,
342
+ age: Integer,
343
+ registered: Boolean,
344
+ }
345
+ validation = Validation.new_from_pattern(pattern)
346
+
347
+ value = {
348
+ "name" => 'Bob',
349
+ "age" => 22,
350
+ "registered" => true,
351
+ }
352
+ failures = validation.validate_from_root(value)
353
+ assert_equal [], failures
354
+
355
+ value = {
356
+ "name" => 909,
357
+ "age" => 22,
358
+ }
359
+ failures = validation.validate_from_root(value)
360
+ assert_equal [
361
+ ValidationUnexpected.new(
362
+ path: ['name'],
363
+ found: 'integer',
364
+ expected: 'string',
365
+ ),
366
+ ValidationUnexpected.new(
367
+ path: [],
368
+ found: "end of object members",
369
+ expected: 'name: "registered"',
370
+ ),
371
+ ], failures
372
+ end
373
+
374
+ def test_dispatch_of_case_by_name
375
+ pattern = { one_of => [
376
+ { car: String, numDoors: Integer },
377
+ { bike: String, numSpokes: Integer },
378
+ ]}
379
+ validation = Validation.new_from_pattern(pattern)
380
+ value = { "car" => 'Studebaker', "numDoors" => 4 }
381
+ failures = validation.validate_from_root(value)
382
+ assert_equal [], failures
383
+
384
+ value = { "bike" => 'Specialized', "numSpokes" => 30 }
385
+ failures = validation.validate_from_root(value)
386
+ assert_equal [], failures
387
+
388
+ value = { "car" => 'Studebaker', "numSpokes" => 4 }
389
+ failures = validation.validate_from_root(value)
390
+ assert_equal [
391
+ ValidationUnexpected.new(
392
+ path: [],
393
+ found: 'names: "numSpokes"',
394
+ expected: 'name: "numDoors"',
395
+ ),
396
+ ValidationUnexpected.new(
397
+ path: [],
398
+ found: 'names: "numSpokes"',
399
+ expected: 'end of object members',
400
+ ),
401
+ ], failures
402
+ end
403
+
404
+ def test_dispatch_of_case_by_value
405
+ pattern = { one_of => [
406
+ { model: 'Subaru', numDoors: Integer },
407
+ { model: 'Yamaha', numCylinders: Integer },
408
+ ]}
409
+ validation = Validation.new_from_pattern(pattern)
410
+ value = { "model" => 'Subaru', "numDoors" => 4 }
411
+ failures = validation.validate_from_root(value)
412
+ assert_equal [], failures
413
+
414
+ value = { "model" => 'Yamaha', "numCylinders" => 4 }
415
+ failures = validation.validate_from_root(value)
416
+ assert_equal [], failures
417
+
418
+ value = { "model" => 'Yamaha', "numDoors" => 4 }
419
+ failures = validation.validate_from_root(value)
420
+ assert_equal [
421
+ ValidationUnexpected.new(
422
+ path: [],
423
+ found: 'names: "numDoors"',
424
+ expected: 'name: "numCylinders"',
425
+ ),
426
+ ValidationUnexpected.new(
427
+ path: [],
428
+ found: 'names: "numDoors"',
429
+ expected: 'end of object members',
430
+ ),
431
+ ], failures
432
+ end
433
+
434
+ def test_validation_with_good_name_but_unrecognized_value
435
+ pattern = { life: 'good' }
436
+ validation = Validation.new_from_pattern(pattern)
437
+
438
+ value = { "life" => 'bad' }
439
+ failures = validation.validate_from_root(value)
440
+ assert_equal [ValidationUnexpected.new(
441
+ path: ['life'],
442
+ found: '"bad"',
443
+ expected: '"good"',
444
+ )], failures
445
+ end
446
+
447
+ def test_validation_with_complex_incorrect_value
448
+ pattern = { person: { age: Integer, name: String } }
449
+ validation = Validation.new_from_pattern(pattern)
450
+
451
+ value = { "person" => ['Ralph', 'Amy'] }
452
+ failures = validation.validate_from_root(value)
453
+ assert_equal [ValidationUnexpected.new(
454
+ path: ['person'],
455
+ found: 'array',
456
+ expected: 'object',
457
+ )], failures
458
+ end
459
+
460
+ def test_validation_with_good_name_and_value_not_matching_pattern
461
+ pattern = one_of({ life: 'good' }, { death: 'bad' })
462
+ validation = Validation.new_from_pattern(pattern)
463
+
464
+ value = { "life" => 'bad' }
465
+ failures = validation.validate_from_root(value)
466
+ assert_equal [ValidationUnexpected.new(
467
+ path: ['life'],
468
+ found: '"bad"',
469
+ expected: Set['"good"'],
470
+ )], failures
471
+ end
472
+
473
+ def test_validation_with_alternate_patterns_where_value_may_also_contain_alternate_patterns
474
+ pattern = one_of({ name: one_of('a', 'c') }, { name: one_of('b', 'd') })
475
+ validation = Validation.new_from_pattern(pattern)
476
+
477
+ value = { "name" => 'd' }
478
+ failures = validation.validate_from_root(value)
479
+ assert_equal [], failures
480
+
481
+ value = { "name" => 'e' }
482
+ failures = validation.validate_from_root(value)
483
+ assert_equal [ValidationUnexpected.new(
484
+ path: ['name'],
485
+ found: '"e"',
486
+ expected: Set['"a"', '"c"', '"b"', '"d"'],
487
+ )], failures
488
+ end
489
+
490
+ def test_validation_with_nested_alternate_patterns
491
+ pattern = one_of(
492
+ one_of('a', 'b'),
493
+ one_of('c', 'd'),
494
+ )
495
+ validation = Validation.new_from_pattern(pattern)
496
+
497
+ ['a', 'b', 'c', 'd'].each do |value|
498
+ failures = validation.validate_from_root(value)
499
+ assert_equal [], failures
500
+ end
501
+
502
+ value = "e"
503
+ failures = validation.validate_from_root(value)
504
+ assert_equal [ValidationUnexpected.new(
505
+ path: [],
506
+ found: '"e"',
507
+ expected: Set['"a"', '"b"', '"c"', '"d"'],
508
+ )], failures
509
+
510
+ pattern = {
511
+ one_of => [
512
+ { one_of => [{ a: __ }, { b: __ }] },
513
+ { one_of => [{ c: __ }, { d: __ }] },
514
+ ]
515
+ }
516
+ validation = Validation.new_from_pattern(pattern)
517
+
518
+ ['a', 'b', 'c', 'd'].each do |name|
519
+ failures = validation.validate_from_root({ name => 'x' })
520
+ assert_equal [], failures
521
+ end
522
+
523
+ value = { 'e' => 'x' }
524
+ failures = validation.validate_from_root(value)
525
+ assert_equal [
526
+ ValidationUnexpected.new(
527
+ path: [],
528
+ found: 'names: "e"',
529
+ # TODO: Factor out the 'names' (names: a, b, c, d)
530
+ expected: Set['name: "a"', 'name: "b"', 'name: "c"', 'name: "d"'],
531
+ ),
532
+ ValidationUnexpected.new(
533
+ path: [],
534
+ found: 'names: "e"',
535
+ expected: 'end of object members',
536
+ ),
537
+ ], failures
538
+ end
539
+
540
+ def test_validation_with_alternate_object_patterns_inside_alternate_value_patterns
541
+ pattern = one_of('x', one_of({ y: Integer }, { z: String }))
542
+ validation = Validation.new_from_pattern(pattern)
543
+
544
+ value = 'x'
545
+ failures = validation.validate_from_root(value)
546
+ assert_equal [], failures
547
+
548
+ value = { "y" => 1 }
549
+ failures = validation.validate_from_root(value)
550
+ assert_equal [], failures
551
+
552
+ value = { "z" => "amazing" }
553
+ failures = validation.validate_from_root(value)
554
+ assert_equal [], failures
555
+
556
+ value = {}
557
+ failures = validation.validate_from_root(value)
558
+ assert_equal [ValidationUnexpected.new(
559
+ path: [],
560
+ found: 'end of object members',
561
+ expected: Set['name: "y"', 'name: "z"'],
562
+ )], failures
563
+ end
564
+
565
+ def test_validation_of_optional_members
566
+ pattern = { optional => { count: Integer } }
567
+ validation = Validation.new_from_pattern(pattern)
568
+
569
+ value = {}
570
+ failures = validation.validate_from_root(value)
571
+ assert_equal [], failures
572
+
573
+ value = { "count" => 3 }
574
+ failures = validation.validate_from_root(value)
575
+ assert_equal [], failures
576
+
577
+ value = { "blink" => 3 }
578
+ failures = validation.validate_from_root(value)
579
+ assert_equal [ValidationUnexpected.new(
580
+ path: [],
581
+ found: 'names: "blink"',
582
+ expected: 'end of object members',
583
+ )], failures
584
+
585
+ value = { "count" => 'x' }
586
+ failures = validation.validate_from_root(value)
587
+ assert_equal [ValidationUnexpected.new(
588
+ path: ['count'],
589
+ found: 'string',
590
+ expected: 'integer',
591
+ )], failures
592
+ end
593
+
594
+ def test_optional_disjunction
595
+ patterns = [
596
+ {
597
+ foo: Integer,
598
+ optional => {
599
+ one_of => [
600
+ { bar: Integer },
601
+ { baz: Integer },
602
+ ]
603
+ }
604
+ },
605
+ {
606
+ foo: Integer,
607
+ optional => one_of(
608
+ { bar: Integer },
609
+ { baz: Integer },
610
+ ),
611
+ },
612
+ ]
613
+
614
+ patterns.each_with_index do |pattern, pattern_index|
615
+ validation = Validation.new_from_pattern(pattern)
616
+
617
+ value = { "foo" => 1 }
618
+ failures = validation.validate_from_root(value)
619
+ assert_equal [], failures, "pattern ##{pattern_index}"
620
+
621
+ value = { "foo" => 1, "bar" => 2 }
622
+ failures = validation.validate_from_root(value)
623
+ assert_equal [], failures, "pattern ##{pattern_index}"
624
+
625
+ value = { "foo" => 1, "baz" => 2 }
626
+ failures = validation.validate_from_root(value)
627
+ assert_equal [], failures, "pattern ##{pattern_index}"
628
+
629
+ value = { "foo" => 1, "quux" => 2 }
630
+ failures = validation.validate_from_root(value)
631
+ assert_equal [ValidationUnexpected.new({
632
+ path: [],
633
+ found: 'names: "quux"',
634
+ expected: 'end of object members',
635
+ })], failures, "pattern ##{pattern_index}"
636
+ end
637
+ end
638
+
639
+ def test_disjunction_in_member_context
640
+ patterns = [
641
+ {
642
+ foo: Integer,
643
+ members => {
644
+ one_of => [
645
+ { bar: Integer },
646
+ { baz: Integer },
647
+ ]
648
+ }
649
+ },
650
+ {
651
+ foo: Integer,
652
+ members => one_of(
653
+ { bar: Integer },
654
+ { baz: Integer },
655
+ ),
656
+ },
657
+ ]
658
+
659
+ patterns.each_with_index do |pattern, pattern_index|
660
+ validation = Validation.new_from_pattern(pattern)
661
+
662
+ value = { "foo" => 1 }
663
+ failures = validation.validate_from_root(value)
664
+ assert_equal [ValidationUnexpected.new({
665
+ path: [],
666
+ found: 'end of object members',
667
+ expected: Set['name: "bar"', 'name: "baz"'],
668
+ })], failures, "pattern ##{pattern_index}"
669
+
670
+ value = { "foo" => 1, "bar" => 2 }
671
+ failures = validation.validate_from_root(value)
672
+ assert_equal [], failures, "pattern ##{pattern_index}"
673
+
674
+ value = { "foo" => 1, "baz" => 2 }
675
+ failures = validation.validate_from_root(value)
676
+ assert_equal [], failures, "pattern ##{pattern_index}"
677
+
678
+ value = { "foo" => 1, "quux" => 2 }
679
+ failures = validation.validate_from_root(value)
680
+ assert_equal [
681
+ ValidationUnexpected.new({
682
+ path: [],
683
+ found: 'names: "quux"',
684
+ expected: Set['name: "bar"', 'name: "baz"'],
685
+ }),
686
+ ValidationUnexpected.new({
687
+ path: [],
688
+ found: 'names: "quux"',
689
+ expected: 'end of object members',
690
+ }),
691
+ ], failures, "pattern ##{pattern_index}"
692
+ end
693
+ end
694
+
695
+ def test_cyclic_value_expression
696
+ pattern = cyclic { |pattern|
697
+ one_of(nil, { left: pattern, right: pattern })
698
+ }
699
+ validation = Validation.new_from_pattern(pattern)
700
+
701
+ value = nil
702
+ failures = validation.validate_from_root(value)
703
+ assert_equal [], failures
704
+
705
+ value = { "left" => nil, "right" => nil }
706
+ failures = validation.validate_from_root(value)
707
+ assert_equal [], failures
708
+
709
+ value = { "left" => { "left" => nil, "right" => nil }, "right" => nil }
710
+ failures = validation.validate_from_root(value)
711
+ assert_equal [], failures
712
+
713
+ value = { "left" => { "left" => nil, "right" => 1 }, "right" => nil }
714
+ failures = validation.validate_from_root(value)
715
+ assert_equal [ValidationUnexpected.new(
716
+ path: ['left', 'right'],
717
+ found: '1',
718
+ expected: Set['null', 'object'],
719
+ )], failures
720
+ end
721
+
722
+ def test_cyclic_object_members_expression
723
+ deets = cyclic { |deets| {
724
+ address: String,
725
+ phone: String,
726
+ optional => {
727
+ friend: {
728
+ friendName: String,
729
+ members => deets,
730
+ }
731
+ }
732
+ }}
733
+ pattern = {
734
+ name: String,
735
+ members => deets,
736
+ }
737
+ validation = Validation.new_from_pattern(pattern)
738
+
739
+ value = {
740
+ "name" => "Tim Cook",
741
+ "address" => '1 Infinite Loop',
742
+ "phone" => '971-555-1212',
743
+ }
744
+ failures = validation.validate_from_root(value)
745
+ assert_equal [], failures
746
+
747
+ value = {
748
+ "name" => "Tim Cook",
749
+ "address" => '1 Infinite Loop',
750
+ "phone" => '971-555-1212',
751
+ "friend" => {
752
+ "friendName" => "Bill Gates",
753
+ "address" => '2 Blue Screen Way',
754
+ "phone" => '241-555-1212',
755
+ }
756
+ }
757
+ failures = validation.validate_from_root(value)
758
+ assert_equal [], failures
759
+ end
760
+
761
+ def test_using_object_member_patterns_in_value_contexts
762
+ pattern = {
763
+ members => cyclic { |es| {
764
+ foo: Integer,
765
+ optional => { bar: es, baz: String }
766
+ }
767
+ }
768
+ }
769
+ validation = Validation.new_from_pattern(pattern)
770
+
771
+ value = {
772
+ "foo" => 1,
773
+ "bar" => {
774
+ "foo" => 2,
775
+ "bar" => { "foo" => 3 },
776
+ "baz" => 'rope',
777
+ },
778
+ "baz" => 'hope',
779
+ }
780
+ failures = validation.validate_from_root(value)
781
+ assert_equal [], failures
782
+
783
+ pattern = {
784
+ members => cyclic { |es| {
785
+ foo: Integer,
786
+ bar: one_of(es, nil),
787
+ }
788
+ }
789
+ }
790
+ validation = Validation.new_from_pattern(pattern)
791
+
792
+ value = {
793
+ "foo" => 1,
794
+ "bar" => {
795
+ "foo" => 2,
796
+ "bar" => nil,
797
+ }
798
+ }
799
+ failures = validation.validate_from_root(value)
800
+ assert_equal [], failures
801
+ end
802
+
803
+ def test_using_object_patterns_in_object_member_contexts
804
+ object = {
805
+ foo: Integer,
806
+ bar: Integer,
807
+ }
808
+ pattern = {
809
+ baz: object,
810
+ members => object,
811
+ }
812
+ validation = Validation.new_from_pattern(pattern)
813
+
814
+ value = {
815
+ "foo" => 1,
816
+ "bar" => 2,
817
+ "baz" => { "foo" => 3, "bar" => 4 },
818
+ }
819
+ failures = validation.validate_from_root(value)
820
+ assert_equal [], failures
821
+ end
822
+
823
+ def test_ambiguous_patterns
824
+ pattern = one_of(/f.ob/, /fr.b/)
825
+ validation = Validation.new_from_pattern(pattern)
826
+
827
+ value = "frob"
828
+ failures = validation.validate_from_root(value)
829
+ assert_equal [ValidationAmbiguity.new(
830
+ path: [],
831
+ found: '"frob"',
832
+ overlapping_patterns: ['/f.ob/', '/fr.b/'],
833
+ )], failures
834
+ end
835
+
836
+ def test_array_with_a_fixed_number_of_values
837
+ # TODO
838
+ end
839
+ end