schemurai 3.1.0-aarch64-linux-gnu

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