schemurai 1.0.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.
@@ -0,0 +1,857 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "base64"
5
+ require_relative "evaluation"
6
+
7
+ module Schemurai
8
+ module Internal
9
+ class Evaluator
10
+ MISSING_SEGMENT = Object.new.freeze
11
+
12
+ def initialize(graph, root, content: false, format: false)
13
+ @validate_content = content
14
+ @validate_format = format
15
+ @graph = graph
16
+ @root = root
17
+ @regexps = nil
18
+ @active = nil
19
+ end
20
+
21
+ def validate(instance)
22
+ errors = []
23
+ each_error(instance) { |error| errors << error }
24
+ Result.new(errors)
25
+ end
26
+
27
+ def valid?(instance)
28
+ @error_callback = nil
29
+ @error_count = 0
30
+ @track_dynamic_scope = @graph.dynamic_scope?
31
+ @instance_path = nil
32
+ @schema_path = nil
33
+ evaluate_valid(@root, instance)
34
+ end
35
+
36
+ private def each_error(instance, &callback)
37
+ @error_callback = callback
38
+ @error_count = 0
39
+ @track_dynamic_scope = @graph.dynamic_scope?
40
+ @instance_path = []
41
+ @schema_path = []
42
+ evaluate(@root, instance)
43
+ ensure
44
+ @error_callback = nil
45
+ end
46
+
47
+ private def evaluate_valid(node, instance)
48
+ schema = node.schema
49
+ return schema if schema == true || schema == false
50
+ return true unless schema.is_a?(Hash)
51
+
52
+ # These applicators depend on annotations collected from sibling
53
+ # applicators. Keep them on the full evaluation path; schemas without
54
+ # them can avoid allocating Evaluation objects and JSON Pointer paths.
55
+ if schema.key?("unevaluatedProperties") || schema.key?("unevaluatedItems")
56
+ @instance_path ||= []
57
+ @schema_path ||= []
58
+ return evaluate(node, instance).valid?
59
+ end
60
+
61
+ if @track_dynamic_scope
62
+ entered_scope = @dynamic_scope.nil? || !@dynamic_scope.last.equal?(node.resource)
63
+ (@dynamic_scope ||= []) << node.resource if entered_scope
64
+ end
65
+
66
+ if schema.key?("$ref")
67
+ return false unless valid_reference?(node, @graph.resolve(node, schema["$ref"]), instance)
68
+ return true unless node.dialect.ref_siblings?
69
+ end
70
+
71
+ if schema.key?("$recursiveRef")
72
+ return false unless valid_reference?(node, recursive_target(node, schema["$recursiveRef"]), instance)
73
+ end
74
+
75
+ if schema.key?("$dynamicRef")
76
+ return false unless valid_reference?(node, dynamic_target(node, schema["$dynamicRef"]), instance)
77
+ end
78
+
79
+ keywords = node.keyword_mask
80
+ if keywords.zero? && format_asserted?(node)
81
+ return !instance.is_a?(String) || node.format.call(instance)
82
+ end
83
+
84
+ categories = Internal::Dialect
85
+ return false if (keywords & categories::TYPE) != 0 && !valid_type?(schema, instance)
86
+ return false if (keywords & categories::ENUM) != 0 && !valid_enum?(schema, instance)
87
+ return false if (keywords & categories::COMBINER) != 0 && !valid_combiners?(node, instance)
88
+
89
+ case instance
90
+ when Hash
91
+ (keywords & categories::OBJECT) == 0 || valid_object?(node, instance)
92
+ when Array
93
+ (keywords & categories::ARRAY) == 0 || valid_array?(node, instance)
94
+ when String
95
+ if (keywords & categories::STRING) != 0
96
+ valid_string?(node, instance)
97
+ elsif format_asserted?(node)
98
+ node.format.call(instance)
99
+ else
100
+ true
101
+ end
102
+ when Numeric
103
+ instance.is_a?(Complex) || (keywords & categories::NUMBER) == 0 || valid_number?(schema, instance)
104
+ else
105
+ true
106
+ end
107
+ rescue ResolutionError
108
+ false
109
+ ensure
110
+ @dynamic_scope.pop if entered_scope
111
+ end
112
+
113
+ private def valid_reference?(source, target, instance)
114
+ instances = active_instances(source)
115
+ instance_id = instance.object_id
116
+ return true if instances[instance_id]
117
+
118
+ instances[instance_id] = true
119
+ activated = true
120
+ evaluate_valid(target, instance)
121
+ ensure
122
+ instances&.delete(instance_id) if activated
123
+ end
124
+
125
+ private def valid_type?(schema, value)
126
+ types = schema["type"]
127
+ return types.any? { |type| type?(value, type) } if types.is_a?(Array)
128
+
129
+ type?(value, types)
130
+ end
131
+
132
+ private def valid_enum?(schema, value)
133
+ return false if schema.key?("enum") && !schema["enum"].any? { |candidate| json_equal?(candidate, value) }
134
+ return false if schema.key?("const") && !json_equal?(schema["const"], value)
135
+
136
+ true
137
+ end
138
+
139
+ private def valid_combiners?(node, value)
140
+ schema = node.schema
141
+ if schema.key?("allOf")
142
+ schema["allOf"].each_index do |index|
143
+ return false unless evaluate_valid(node.child("allOf", index), value)
144
+ end
145
+ end
146
+ if schema.key?("anyOf")
147
+ matched = schema["anyOf"].each_index.any? do |index|
148
+ evaluate_valid(node.child("anyOf", index), value)
149
+ end
150
+ return false unless matched
151
+ end
152
+ if schema.key?("oneOf")
153
+ matches = 0
154
+ schema["oneOf"].each_index do |index|
155
+ matches += 1 if evaluate_valid(node.child("oneOf", index), value)
156
+ return false if matches > 1
157
+ end
158
+ return false unless matches == 1
159
+ end
160
+ return false if schema.key?("not") && evaluate_valid(node.child("not"), value)
161
+
162
+ if schema.key?("if")
163
+ branch = evaluate_valid(node.child("if"), value) ? "then" : "else"
164
+ return false if schema.key?(branch) && !evaluate_valid(node.child(branch), value)
165
+ end
166
+ true
167
+ end
168
+
169
+ private def valid_number?(schema, value)
170
+ actual = nil
171
+ if schema.key?("maximum")
172
+ actual ||= decimal(value)
173
+ return false unless actual <= decimal(schema["maximum"])
174
+ end
175
+ if schema.key?("minimum")
176
+ actual ||= decimal(value)
177
+ return false unless actual >= decimal(schema["minimum"])
178
+ end
179
+ if schema.key?("exclusiveMaximum")
180
+ actual ||= decimal(value)
181
+ return false unless actual < decimal(schema["exclusiveMaximum"])
182
+ end
183
+ if schema.key?("exclusiveMinimum")
184
+ actual ||= decimal(value)
185
+ return false unless actual > decimal(schema["exclusiveMinimum"])
186
+ end
187
+ if schema.key?("multipleOf")
188
+ divisor = decimal(schema["multipleOf"])
189
+ return false unless divisor.positive?
190
+ return false unless (actual || decimal(value)).remainder(divisor).zero?
191
+ end
192
+ true
193
+ end
194
+
195
+ private def valid_string?(node, value)
196
+ schema = node.schema
197
+ length = value.length
198
+ return false if schema.key?("maxLength") && length > schema["maxLength"]
199
+ return false if schema.key?("minLength") && length < schema["minLength"]
200
+ return false if schema.key?("pattern") && !ecma_regexp(schema["pattern"]).match?(value)
201
+ if format_asserted?(node)
202
+ return false unless node.format.call(value)
203
+ end
204
+ return valid_content?(schema, value) if @validate_content
205
+
206
+ true
207
+ rescue RegexpError, IPAddr::InvalidAddressError
208
+ false
209
+ end
210
+
211
+ private def valid_content?(schema, value)
212
+ decoded = (schema["contentEncoding"] == "base64") ? Base64.strict_decode64(value) : value
213
+ JSON.parse(decoded) if schema["contentMediaType"] == "application/json"
214
+ true
215
+ rescue ArgumentError, JSON::ParserError
216
+ false
217
+ end
218
+
219
+ private def valid_array?(node, value)
220
+ schema = node.schema
221
+ length = value.length
222
+ return false if schema.key?("maxItems") && length > schema["maxItems"]
223
+ return false if schema.key?("minItems") && length < schema["minItems"]
224
+ if schema["uniqueItems"]
225
+ value.each_with_index do |item, index|
226
+ return false if value[0...index].any? { |previous| json_equal?(previous, item) }
227
+ end
228
+ end
229
+
230
+ prefix_items = schema["prefixItems"]
231
+ if prefix_items.is_a?(Array)
232
+ prefix_items.each_index do |index|
233
+ break if index >= length
234
+ return false unless evaluate_valid(node.child("prefixItems", index), value[index])
235
+ end
236
+ end
237
+
238
+ items = schema["items"]
239
+ if items.is_a?(Array)
240
+ items.each_index do |index|
241
+ break if index >= length
242
+ return false unless evaluate_valid(node.child("items", index), value[index])
243
+ end
244
+ if length > items.length && schema.key?("additionalItems")
245
+ additional = node.child("additionalItems")
246
+ (items.length...length).each do |index|
247
+ return false unless evaluate_valid(additional, value[index])
248
+ end
249
+ end
250
+ elsif !items.nil?
251
+ child = node.child("items")
252
+ start = prefix_items.is_a?(Array) ? prefix_items.length : 0
253
+ (start...length).each do |index|
254
+ return false unless evaluate_valid(child, value[index])
255
+ end
256
+ end
257
+
258
+ if schema.key?("contains")
259
+ child = node.child("contains")
260
+ if node.dialect.keywords.key?("minContains")
261
+ matches = value.count { |item| evaluate_valid(child, item) }
262
+ return false if matches < schema.fetch("minContains", 1)
263
+ return false if schema.key?("maxContains") && matches > schema["maxContains"]
264
+ else
265
+ return false unless value.any? { |item| evaluate_valid(child, item) }
266
+ end
267
+ end
268
+ true
269
+ end
270
+
271
+ private def valid_object?(node, value)
272
+ schema = node.schema
273
+ length = value.length
274
+ return false if schema.key?("maxProperties") && length > schema["maxProperties"]
275
+ return false if schema.key?("minProperties") && length < schema["minProperties"]
276
+ return false if schema.key?("required") && !schema["required"].all? { |name| value.key?(name) }
277
+
278
+ properties = schema["properties"]
279
+ patterns = schema["patternProperties"]
280
+ additional = node.child("additionalProperties") if schema.key?("additionalProperties")
281
+ value.each do |name, property_value|
282
+ matched = false
283
+ if properties&.key?(name)
284
+ matched = true
285
+ return false unless evaluate_valid(node.child("properties", name), property_value)
286
+ end
287
+ if patterns
288
+ patterns.each_key do |pattern|
289
+ next unless ecma_regexp(pattern).match?(name)
290
+ matched = true
291
+ return false unless evaluate_valid(node.child("patternProperties", pattern), property_value)
292
+ end
293
+ end
294
+ return false if !matched && additional && !evaluate_valid(additional, property_value)
295
+ end
296
+
297
+ if schema.key?("propertyNames")
298
+ child = node.child("propertyNames")
299
+ value.each_key { |name| return false unless evaluate_valid(child, name) }
300
+ end
301
+
302
+ if schema.key?("dependencies")
303
+ schema["dependencies"].each do |name, dependency|
304
+ next unless value.key?(name)
305
+ if dependency.is_a?(Array)
306
+ return false unless dependency.all? { |required_name| value.key?(required_name) }
307
+ else
308
+ return false unless evaluate_valid(node.child("dependencies", name), value)
309
+ end
310
+ end
311
+ end
312
+ if schema.key?("dependentRequired")
313
+ schema["dependentRequired"].each do |name, required_names|
314
+ next unless value.key?(name)
315
+ return false unless required_names.all? { |required_name| value.key?(required_name) }
316
+ end
317
+ end
318
+
319
+ if schema.key?("dependentSchemas")
320
+ schema["dependentSchemas"].each_key do |name|
321
+ next unless value.key?(name)
322
+ return false unless evaluate_valid(node.child("dependentSchemas", name), value)
323
+ end
324
+ end
325
+ true
326
+ end
327
+
328
+ private def evaluate(node, instance)
329
+ schema = node.schema
330
+ return Evaluation.valid if schema == true
331
+ if schema == false
332
+ add_error("falseSchema", "boolean schema is false", append_keyword: false)
333
+ return Evaluation.invalid
334
+ end
335
+ return Evaluation.valid unless schema.is_a?(Hash)
336
+
337
+ before = @error_count
338
+ evaluation = Evaluation.valid
339
+
340
+ if @track_dynamic_scope
341
+ entered_scope = @dynamic_scope.nil? || !@dynamic_scope.last.equal?(node.resource)
342
+ (@dynamic_scope ||= []) << node.resource if entered_scope
343
+ end
344
+
345
+ if schema.key?("$ref")
346
+ begin
347
+ target = @graph.resolve(node, schema["$ref"])
348
+ evaluation = evaluation.merge(
349
+ evaluate_reference(node, target, instance, "$ref")
350
+ )
351
+ rescue ResolutionError => e
352
+ add_error("$ref", e.message, append_keyword: false)
353
+ end
354
+ return (@error_count == before) ? evaluation : Evaluation.invalid unless node.dialect.ref_siblings?
355
+ end
356
+
357
+ if schema.key?("$recursiveRef")
358
+ target = recursive_target(node, schema["$recursiveRef"])
359
+ evaluation = evaluation.merge(evaluate_reference(node, target, instance, "$recursiveRef"))
360
+ end
361
+
362
+ if schema.key?("$dynamicRef")
363
+ target = dynamic_target(node, schema["$dynamicRef"])
364
+ evaluation = evaluation.merge(evaluate_reference(node, target, instance, "$dynamicRef"))
365
+ end
366
+
367
+ keywords = node.keyword_mask
368
+ categories = Internal::Dialect
369
+ check_type(schema, instance) if (keywords & categories::TYPE) != 0
370
+ check_enum(schema, instance) if (keywords & categories::ENUM) != 0
371
+ if (keywords & categories::COMBINER) != 0
372
+ evaluation = evaluation.merge(check_combiners(node, instance))
373
+ end
374
+
375
+ case instance
376
+ when Hash
377
+ if (keywords & categories::OBJECT) != 0
378
+ evaluation = evaluation.merge(check_object(node, instance, evaluation))
379
+ end
380
+ when Array
381
+ if (keywords & categories::ARRAY) != 0
382
+ evaluation = evaluation.merge(check_array(node, instance, evaluation))
383
+ end
384
+ when String
385
+ check_string(node, instance) if (keywords & categories::STRING) != 0 || format_asserted?(node)
386
+ when Numeric
387
+ check_number(schema, instance) if !instance.is_a?(Complex) && (keywords & categories::NUMBER) != 0
388
+ end
389
+
390
+ (@error_count == before) ? evaluation : Evaluation.invalid
391
+ ensure
392
+ @dynamic_scope.pop if entered_scope
393
+ end
394
+
395
+ private def evaluate_reference(node, target, instance, keyword)
396
+ instances = active_instances(node)
397
+ instance_id = instance.object_id
398
+ return Evaluation.valid if instances[instance_id]
399
+
400
+ instances[instance_id] = true
401
+ activated = true
402
+ evaluate_at(target, instance, MISSING_SEGMENT, keyword)
403
+ rescue ResolutionError => e
404
+ add_error(keyword, e.message, append_keyword: false)
405
+ Evaluation.invalid
406
+ ensure
407
+ instances&.delete(instance_id) if activated
408
+ end
409
+
410
+ private def recursive_target(node, reference)
411
+ target = @graph.resolve(node, reference)
412
+ return target unless reference.to_s.end_with?("#") && target.schema.is_a?(Hash) && target.schema["$recursiveAnchor"] == true
413
+
414
+ @dynamic_scope.filter_map { |resource| resource.root if resource.root.schema.is_a?(Hash) && resource.root.schema["$recursiveAnchor"] == true }.first || target
415
+ end
416
+
417
+ private def dynamic_target(node, reference)
418
+ target = @graph.resolve(node, reference)
419
+ raw_fragment = reference.to_s.split("#", 2)[1]
420
+ return target if raw_fragment.nil? || raw_fragment.empty? || raw_fragment.start_with?("/")
421
+ return target unless target.schema.is_a?(Hash) && target.schema["$dynamicAnchor"] == raw_fragment
422
+
423
+ @dynamic_scope.each do |resource|
424
+ dynamic = @graph.dynamic_anchor(resource, raw_fragment)
425
+ return dynamic if dynamic
426
+ end
427
+ target
428
+ end
429
+
430
+ private def active_instances(node)
431
+ active = (@active ||= {})
432
+ active[node.object_id] ||= {}
433
+ end
434
+
435
+ private def check_type(schema, value)
436
+ return unless schema.key?("type")
437
+
438
+ types = Array(schema["type"])
439
+ return if types.any? { |type| type?(value, type) }
440
+
441
+ add_error("type", "expected #{types.join(" or ")}")
442
+ end
443
+
444
+ private def check_enum(schema, value)
445
+ if schema.key?("enum") && !schema["enum"].any? { |candidate| json_equal?(candidate, value) }
446
+ add_error("enum", "value is not in enum")
447
+ end
448
+ if schema.key?("const") && !json_equal?(schema["const"], value)
449
+ add_error("const", "value does not equal const")
450
+ end
451
+ end
452
+
453
+ private def check_combiners(node, value)
454
+ schema = node.schema
455
+ evaluation = Evaluation.valid
456
+ if schema.key?("allOf")
457
+ schema["allOf"].each_index do |index|
458
+ evaluation = evaluation.merge(
459
+ evaluate_at(node.child("allOf", index), value, MISSING_SEGMENT, "allOf", index)
460
+ )
461
+ end
462
+ end
463
+
464
+ if schema.key?("anyOf")
465
+ matches = []
466
+ schema["anyOf"].each_index do |index|
467
+ result = trial_at(node.child("anyOf", index), value, MISSING_SEGMENT, "anyOf", index)
468
+ matches << result if result.valid?
469
+ end
470
+ if matches.empty?
471
+ add_error("anyOf", "no subschema matched")
472
+ else
473
+ matches.each { |result| evaluation = evaluation.merge(result) }
474
+ end
475
+ end
476
+
477
+ if schema.key?("oneOf")
478
+ matches = []
479
+ schema["oneOf"].each_index do |index|
480
+ result = trial_at(node.child("oneOf", index), value, MISSING_SEGMENT, "oneOf", index)
481
+ matches << result if result.valid?
482
+ end
483
+ if matches.length == 1
484
+ evaluation = evaluation.merge(matches.first)
485
+ else
486
+ add_error("oneOf", "expected exactly one match, got #{matches.length}")
487
+ end
488
+ end
489
+
490
+ if schema.key?("not") && trial_at(node.child("not"), value, MISSING_SEGMENT, "not").valid?
491
+ add_error("not", "subschema matched")
492
+ end
493
+
494
+ if schema.key?("if")
495
+ condition = trial_at(node.child("if"), value, MISSING_SEGMENT, "if")
496
+ branch = condition.valid? ? "then" : "else"
497
+ evaluation = evaluation.merge(condition) if condition.valid?
498
+ if schema.key?(branch)
499
+ evaluation = evaluation.merge(evaluate_at(node.child(branch), value, MISSING_SEGMENT, branch))
500
+ end
501
+ end
502
+ evaluation
503
+ end
504
+
505
+ private def check_number(schema, value)
506
+ compare(schema, "maximum", value) { |a, b| a <= b }
507
+ compare(schema, "minimum", value) { |a, b| a >= b }
508
+ compare(schema, "exclusiveMaximum", value) { |a, b| a < b }
509
+ compare(schema, "exclusiveMinimum", value) { |a, b| a > b }
510
+
511
+ return unless schema.key?("multipleOf")
512
+
513
+ divisor = decimal(schema["multipleOf"])
514
+ valid = divisor.positive? && decimal(value).remainder(divisor).zero?
515
+ add_error("multipleOf", "number is not a multiple") unless valid
516
+ end
517
+
518
+ private def compare(schema, keyword, value)
519
+ return unless schema.key?(keyword)
520
+ return if yield(decimal(value), decimal(schema[keyword]))
521
+
522
+ add_error(keyword, "numeric limit was exceeded")
523
+ end
524
+
525
+ private def check_string(node, value)
526
+ schema = node.schema
527
+ length = value.length
528
+ limit(schema, "maxLength", length) { |actual, expected| actual <= expected }
529
+ limit(schema, "minLength", length) { |actual, expected| actual >= expected }
530
+
531
+ if schema.key?("pattern")
532
+ matched = ecma_regexp(schema["pattern"]).match?(value)
533
+ add_error("pattern", "string does not match pattern") unless matched
534
+ end
535
+ if format_asserted?(node)
536
+ add_error("format", "string is not a valid #{node.format.name}") unless node.format.call(value)
537
+ end
538
+ check_content(schema, value) if @validate_content
539
+ rescue RegexpError
540
+ add_error("pattern", "invalid regular expression")
541
+ end
542
+
543
+ private def format_asserted?(node)
544
+ (@validate_format || node.dialect.format_assertion?) && !node.format.nil?
545
+ end
546
+
547
+ private def check_array(node, value, prior_evaluation)
548
+ schema = node.schema
549
+ evaluated = []
550
+ limit(schema, "maxItems", value.length) { |actual, expected| actual <= expected }
551
+ limit(schema, "minItems", value.length) { |actual, expected| actual >= expected }
552
+
553
+ if schema["uniqueItems"]
554
+ duplicate = value.each_with_index.any? do |item, index|
555
+ value[0...index].any? { |previous| json_equal?(previous, item) }
556
+ end
557
+ add_error("uniqueItems", "array items are not unique") if duplicate
558
+ end
559
+
560
+ prefix_items = schema["prefixItems"]
561
+ if prefix_items.is_a?(Array)
562
+ prefix_items.each_index do |index|
563
+ break if index >= value.length
564
+ evaluate_at(node.child("prefixItems", index), value[index], index, "prefixItems", index)
565
+ evaluated << index
566
+ end
567
+ end
568
+
569
+ items = schema["items"]
570
+ if items.is_a?(Array)
571
+ items.each_index do |index|
572
+ break if index >= value.length
573
+ evaluate_at(node.child("items", index), value[index], index, "items", index)
574
+ evaluated << index
575
+ end
576
+ if value.length > items.length && schema.key?("additionalItems")
577
+ additional = node.child("additionalItems")
578
+ (items.length...value.length).each do |index|
579
+ evaluate_at(additional, value[index], index, "additionalItems")
580
+ evaluated << index
581
+ end
582
+ end
583
+ elsif !items.nil?
584
+ start = prefix_items.is_a?(Array) ? prefix_items.length : 0
585
+ value.each_with_index do |item, index|
586
+ next if index < start
587
+ evaluate_at(node.child("items"), item, index, "items")
588
+ evaluated << index
589
+ end
590
+ end
591
+
592
+ if schema.key?("contains")
593
+ matched = value.each_index.select do |index|
594
+ trial_at(node.child("contains"), value[index], index, "contains").valid?
595
+ end
596
+ minimum = schema.fetch("minContains", 1)
597
+ maximum = schema.fetch("maxContains", Float::INFINITY)
598
+ unless matched.length.between?(minimum, maximum)
599
+ add_error("contains", "matched #{matched.length} array items")
600
+ end
601
+ evaluated.concat(matched)
602
+ end
603
+
604
+ combined = prior_evaluation.evaluated_items | evaluated
605
+ if schema.key?("unevaluatedItems")
606
+ unevaluated = (0...value.length).to_a - combined
607
+ unevaluated.each do |index|
608
+ evaluate_at(node.child("unevaluatedItems"), value[index], index, "unevaluatedItems")
609
+ end
610
+ evaluated.concat(unevaluated)
611
+ end
612
+ Evaluation.valid(evaluated_items: evaluated.uniq)
613
+ end
614
+
615
+ private def check_object(node, value, prior_evaluation)
616
+ schema = node.schema
617
+ evaluated = []
618
+ limit(schema, "maxProperties", value.length) { |actual, expected| actual <= expected }
619
+ limit(schema, "minProperties", value.length) { |actual, expected| actual >= expected }
620
+
621
+ Array(schema["required"]).each do |name|
622
+ add_error("required", "required property #{name.inspect} is missing") unless value.key?(name)
623
+ end
624
+
625
+ properties = schema.fetch("properties", {})
626
+ patterns = schema.fetch("patternProperties", {})
627
+ value.each do |name, property_value|
628
+ matched = false
629
+ if properties.key?(name)
630
+ matched = true
631
+ evaluate_at(node.child("properties", name), property_value, name, "properties", name)
632
+ evaluated << name
633
+ end
634
+ patterns.each do |pattern, subschema|
635
+ next unless ecma_regexp(pattern).match?(name)
636
+ matched = true
637
+ evaluate_at(node.child("patternProperties", pattern), property_value, name, "patternProperties", pattern)
638
+ evaluated << name
639
+ end
640
+ if !matched && schema.key?("additionalProperties")
641
+ evaluate_at(node.child("additionalProperties"), property_value, name, "additionalProperties")
642
+ evaluated << name
643
+ end
644
+ end
645
+
646
+ if schema.key?("propertyNames")
647
+ value.each_key do |name|
648
+ evaluate_at(node.child("propertyNames"), name, name, "propertyNames")
649
+ end
650
+ end
651
+
652
+ schema.fetch("dependencies", {}).each do |name, dependency|
653
+ next unless value.key?(name)
654
+ if dependency.is_a?(Array)
655
+ dependency.each do |required_name|
656
+ add_error("dependencies", "property #{required_name.inspect} is required by #{name.inspect}") unless value.key?(required_name)
657
+ end
658
+ else
659
+ result = evaluate_at(node.child("dependencies", name), value, MISSING_SEGMENT, "dependencies", name)
660
+ evaluated.concat(result.evaluated_properties) if result.valid?
661
+ end
662
+ end
663
+
664
+ schema.fetch("dependentRequired", {}).each do |name, required_names|
665
+ next unless value.key?(name)
666
+ required_names.each do |required_name|
667
+ unless value.key?(required_name)
668
+ add_error("dependentRequired", "property #{required_name.inspect} is required by #{name.inspect}")
669
+ end
670
+ end
671
+ end
672
+
673
+ schema.fetch("dependentSchemas", {}).each_key do |name|
674
+ next unless value.key?(name)
675
+ result = evaluate_at(node.child("dependentSchemas", name), value, MISSING_SEGMENT, "dependentSchemas", name)
676
+ evaluated.concat(result.evaluated_properties) if result.valid?
677
+ end
678
+
679
+ combined = prior_evaluation.evaluated_properties | evaluated
680
+ if schema.key?("unevaluatedProperties")
681
+ unevaluated = value.keys - combined
682
+ unevaluated.each do |name|
683
+ evaluate_at(node.child("unevaluatedProperties"), value[name], name, "unevaluatedProperties")
684
+ end
685
+ evaluated.concat(unevaluated)
686
+ end
687
+ Evaluation.valid(evaluated_properties: evaluated.uniq)
688
+ end
689
+
690
+ private def trial(node, value)
691
+ saved_callback = @error_callback
692
+ saved_error_count = @error_count
693
+ @error_callback = nil
694
+ @error_count = 0
695
+ result = evaluate(node, value)
696
+ result
697
+ ensure
698
+ @error_callback = saved_callback
699
+ @error_count = saved_error_count
700
+ end
701
+
702
+ private def limit(schema, keyword, actual)
703
+ return unless schema.key?(keyword)
704
+ return if yield(actual, schema[keyword])
705
+
706
+ add_error(keyword, "size limit was exceeded")
707
+ end
708
+
709
+ private def type?(value, type)
710
+ case type
711
+ when "null" then value.nil?
712
+ when "boolean" then value == true || value == false
713
+ when "object" then value.is_a?(Hash)
714
+ when "array" then value.is_a?(Array)
715
+ when "number" then number?(value)
716
+ when "integer" then number?(value) && value.finite? && value.to_i == value
717
+ when "string" then value.is_a?(String)
718
+ else false
719
+ end
720
+ end
721
+
722
+ private def number?(value)
723
+ value.is_a?(Numeric) && !value.is_a?(Complex)
724
+ end
725
+
726
+ private def json_equal?(left, right)
727
+ return false if json_kind(left) != json_kind(right)
728
+ case left
729
+ when Hash
730
+ left.length == right.length && left.all? { |key, value| right.key?(key) && json_equal?(value, right[key]) }
731
+ when Array
732
+ left.length == right.length && left.each_index.all? { |index| json_equal?(left[index], right[index]) }
733
+ else
734
+ left == right
735
+ end
736
+ end
737
+
738
+ private def json_kind(value)
739
+ return :number if number?(value)
740
+ return :boolean if value == true || value == false
741
+ value.class
742
+ end
743
+
744
+ private def decimal(value)
745
+ return value if value.is_a?(Integer) || value.is_a?(Rational)
746
+
747
+ Rational(value.to_s)
748
+ end
749
+
750
+ # Ruby and ECMA-262 differ in their ASCII character classes, anchors, and
751
+ # definition of whitespace. Draft 7 patterns use the ECMA behavior.
752
+ private def ecma_regexp(pattern)
753
+ regexps = (@regexps ||= {})
754
+ return regexps[pattern] if regexps.key?(pattern)
755
+
756
+ whitespace = "\\u0009-\\u000D\\u0020\\u00A0\\u1680\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000\\uFEFF"
757
+ translated = +""
758
+ escaped = false
759
+ in_class = false
760
+ pattern.each_char do |character|
761
+ if escaped
762
+ translated << case character
763
+ when "d" then in_class ? "0-9" : "[0-9]"
764
+ when "D" then in_class ? "^0-9" : "[^0-9]"
765
+ when "w" then in_class ? "A-Za-z0-9_" : "[A-Za-z0-9_]"
766
+ when "W" then in_class ? "^A-Za-z0-9_" : "[^A-Za-z0-9_]"
767
+ when "s" then in_class ? whitespace : "[#{whitespace}]"
768
+ when "S" then in_class ? "^#{whitespace}" : "[^#{whitespace}]"
769
+ else "\\#{character}"
770
+ end
771
+ escaped = false
772
+ elsif character == "\\"
773
+ escaped = true
774
+ elsif character == "["
775
+ in_class = true
776
+ translated << character
777
+ elsif character == "]"
778
+ in_class = false
779
+ translated << character
780
+ elsif character == "^" && !in_class
781
+ translated << "\\A"
782
+ elsif character == "$" && !in_class
783
+ translated << "\\z"
784
+ else
785
+ translated << character
786
+ end
787
+ end
788
+ translated << "\\" if escaped
789
+ regexps[pattern] = Regexp.new(translated)
790
+ end
791
+
792
+ private def check_content(schema, value)
793
+ decoded = value
794
+ if schema["contentEncoding"] == "base64"
795
+ decoded = Base64.strict_decode64(value)
796
+ end
797
+ return unless schema["contentMediaType"] == "application/json"
798
+
799
+ JSON.parse(decoded)
800
+ rescue ArgumentError, JSON::ParserError
801
+ keyword = (schema["contentEncoding"] == "base64") ? "contentEncoding" : "contentMediaType"
802
+ add_error(keyword, "string content is invalid")
803
+ end
804
+
805
+ private def add_error(keyword, message, append_keyword: true)
806
+ @error_count += 1
807
+ if @error_callback
808
+ schema_keyword = append_keyword ? keyword : MISSING_SEGMENT
809
+ @error_callback.call(
810
+ ValidationError.new(
811
+ keyword: keyword,
812
+ instance_path: pointer(@instance_path),
813
+ schema_path: pointer(@schema_path, schema_keyword),
814
+ message: message
815
+ )
816
+ )
817
+ end
818
+ false
819
+ end
820
+
821
+ private def evaluate_at(node, instance, instance_segment, schema_segment, schema_child_segment = MISSING_SEGMENT)
822
+ @instance_path << instance_segment unless instance_segment.equal?(MISSING_SEGMENT)
823
+ @schema_path << schema_segment
824
+ @schema_path << schema_child_segment unless schema_child_segment.equal?(MISSING_SEGMENT)
825
+ evaluate(node, instance)
826
+ ensure
827
+ @schema_path.pop unless schema_child_segment.equal?(MISSING_SEGMENT)
828
+ @schema_path.pop
829
+ @instance_path.pop unless instance_segment.equal?(MISSING_SEGMENT)
830
+ end
831
+
832
+ private def trial_at(node, instance, instance_segment, schema_segment, schema_child_segment = MISSING_SEGMENT)
833
+ @instance_path << instance_segment unless instance_segment.equal?(MISSING_SEGMENT)
834
+ @schema_path << schema_segment
835
+ @schema_path << schema_child_segment unless schema_child_segment.equal?(MISSING_SEGMENT)
836
+ trial(node, instance)
837
+ ensure
838
+ @schema_path.pop unless schema_child_segment.equal?(MISSING_SEGMENT)
839
+ @schema_path.pop
840
+ @instance_path.pop unless instance_segment.equal?(MISSING_SEGMENT)
841
+ end
842
+
843
+ private def pointer(path, final_segment = MISSING_SEGMENT)
844
+ pointer = +""
845
+ path.each { |segment| append_pointer_segment(pointer, segment) }
846
+ append_pointer_segment(pointer, final_segment) unless final_segment.equal?(MISSING_SEGMENT)
847
+ pointer
848
+ end
849
+
850
+ private def append_pointer_segment(pointer, segment)
851
+ pointer << "/" << segment.to_s.gsub("~", "~0").gsub("/", "~1")
852
+ end
853
+
854
+ private_constant :MISSING_SEGMENT
855
+ end
856
+ end
857
+ end