senko 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.
@@ -0,0 +1,1391 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bigdecimal'
4
+ require 'set'
5
+
6
+ require_relative 'compiler/instruction'
7
+ require_relative 'errors'
8
+ require_relative 'format'
9
+ require_relative 'result'
10
+
11
+ module Senko
12
+ class Validator
13
+ MESSAGES = {
14
+ false_schema: 'boolean schema false does not allow any value',
15
+ type: 'expected %{expected}, got %{actual}',
16
+ required: "missing required property '%{property}'",
17
+ minimum: 'value must be >= %{minimum}',
18
+ exclusive_minimum: 'value must be > %{minimum}',
19
+ maximum: 'value must be <= %{maximum}',
20
+ exclusive_maximum: 'value must be < %{maximum}',
21
+ min_length: 'string length must be >= %{min_length}',
22
+ max_length: 'string length must be <= %{max_length}',
23
+ pattern: "string does not match pattern '%{pattern}'",
24
+ min_items: 'array length must be >= %{min_items}',
25
+ max_items: 'array length must be <= %{max_items}',
26
+ unique_items: 'array items must be unique',
27
+ min_contains: 'array must contain at least %{min_contains} matching item(s)',
28
+ max_contains: 'array must contain at most %{max_contains} matching item(s)',
29
+ min_properties: 'object must have >= %{min_properties} properties',
30
+ max_properties: 'object must have <= %{max_properties} properties',
31
+ enum: 'value must be one of: %{values}',
32
+ const: 'value must be %{const}',
33
+ multiple_of: 'value must be a multiple of %{multiple_of}',
34
+ format: "value does not match format '%{format}'",
35
+ not: 'value should not match the schema',
36
+ one_of_none: 'value must match exactly one schema in oneOf (matched none)',
37
+ one_of_multiple: 'value must match exactly one schema in oneOf (matched %{count})',
38
+ any_of: 'value must match at least one schema in anyOf',
39
+ discriminator: "value does not match discriminator property '%{property}'",
40
+ additional_properties: "unexpected property '%{property}'",
41
+ property_names: "property name '%{property}' is invalid",
42
+ dependent_required: "property '%{property}' requires property '%{dependency}'",
43
+ unevaluated_properties: "unevaluated property '%{property}'",
44
+ unevaluated_items: 'unevaluated item at index %{index}',
45
+ custom_keyword: "value failed custom keyword '%{keyword}'"
46
+ }.freeze
47
+
48
+ class EvaluationContext
49
+ EMPTY_EVALUATED = {}.freeze
50
+
51
+ def initialize(dynamic_scopes = [])
52
+ @dynamic_scopes = dynamic_scopes.dup
53
+ end
54
+
55
+ def evaluated_properties_by_location
56
+ @evaluated_properties_by_location || EMPTY_EVALUATED
57
+ end
58
+
59
+ def evaluated_items_by_location
60
+ @evaluated_items_by_location || EMPTY_EVALUATED
61
+ end
62
+
63
+ def mark_property(location, name)
64
+ evaluated_properties_for_write[location][name.to_s] = true
65
+ end
66
+
67
+ def track_evaluation?
68
+ true
69
+ end
70
+
71
+ def mark_item(location, index)
72
+ evaluated_items_for_write[location][index] = true
73
+ end
74
+
75
+ def property_evaluated?(location, name)
76
+ return false unless @evaluated_properties_by_location
77
+
78
+ properties = @evaluated_properties_by_location.fetch(location, nil)
79
+ properties ? properties.key?(name.to_s) : false
80
+ end
81
+
82
+ def item_evaluated?(location, index)
83
+ return false unless @evaluated_items_by_location
84
+
85
+ items = @evaluated_items_by_location.fetch(location, nil)
86
+ items ? items.key?(index) : false
87
+ end
88
+
89
+ def evaluation_empty?
90
+ (!@evaluated_properties_by_location || @evaluated_properties_by_location.empty?) &&
91
+ (!@evaluated_items_by_location || @evaluated_items_by_location.empty?)
92
+ end
93
+
94
+ def push_dynamic_scope(anchors)
95
+ @dynamic_scopes << anchors
96
+ end
97
+
98
+ def pop_dynamic_scope
99
+ @dynamic_scopes.pop
100
+ end
101
+
102
+ def dynamic_target(anchor)
103
+ scope = @dynamic_scopes.find { |candidate| candidate.key?(anchor) }
104
+ scope&.fetch(anchor)
105
+ end
106
+
107
+ def fork(track_evaluation: true)
108
+ track_evaluation ? self.class.new(@dynamic_scopes) : NoopEvaluationContext.new(@dynamic_scopes)
109
+ end
110
+
111
+ def merge(child)
112
+ child.evaluated_properties_by_location.each do |location, properties|
113
+ evaluated_properties_for_write[location].update(properties)
114
+ end
115
+ child.evaluated_items_by_location.each do |location, items|
116
+ evaluated_items_for_write[location].update(items)
117
+ end
118
+ end
119
+
120
+ private
121
+
122
+ def evaluated_properties_for_write
123
+ @evaluated_properties_by_location ||= Hash.new { |hash, location| hash[location] = {} }
124
+ end
125
+
126
+ def evaluated_items_for_write
127
+ @evaluated_items_by_location ||= Hash.new { |hash, location| hash[location] = {} }
128
+ end
129
+ end
130
+
131
+ class NoopEvaluationContext
132
+ def initialize(dynamic_scopes = [])
133
+ @dynamic_scopes = dynamic_scopes.dup
134
+ end
135
+
136
+ def mark_property(_location, _name); end
137
+
138
+ def track_evaluation?
139
+ false
140
+ end
141
+
142
+ def mark_item(_location, _index); end
143
+
144
+ def property_evaluated?(_location, _name)
145
+ false
146
+ end
147
+
148
+ def item_evaluated?(_location, _index)
149
+ false
150
+ end
151
+
152
+ def evaluation_empty?
153
+ true
154
+ end
155
+
156
+ def push_dynamic_scope(anchors)
157
+ @dynamic_scopes << anchors
158
+ end
159
+
160
+ def pop_dynamic_scope
161
+ @dynamic_scopes.pop
162
+ end
163
+
164
+ def dynamic_target(anchor)
165
+ scope = @dynamic_scopes.find { |candidate| candidate.key?(anchor) }
166
+ scope&.fetch(anchor)
167
+ end
168
+
169
+ def fork(track_evaluation: false)
170
+ track_evaluation ? EvaluationContext.new(@dynamic_scopes) : self.class.new(@dynamic_scopes)
171
+ end
172
+
173
+ def merge(_child); end
174
+ end
175
+
176
+ class << self
177
+ def requires_evaluation_tracking?(instructions, seen = {})
178
+ return false unless instructions.is_a?(Array)
179
+ return false if seen[instructions.object_id]
180
+
181
+ seen[instructions.object_id] = true
182
+ instructions.any? { |instruction| instruction_requires_evaluation_tracking?(instruction, seen) }
183
+ end
184
+
185
+ private
186
+
187
+ def instruction_requires_evaluation_tracking?(instruction, seen)
188
+ return true if %i[unevaluated_properties unevaluated_items].include?(instruction.op)
189
+ return true if instruction.op == :dynamic_ref && instruction.payload[:dynamic]
190
+
191
+ nested_instruction_sets(instruction).any? do |child_instructions|
192
+ requires_evaluation_tracking?(child_instructions, seen)
193
+ end
194
+ end
195
+
196
+ def nested_instruction_sets(instruction)
197
+ payload = instruction.payload
198
+
199
+ case instruction.op
200
+ when :prefix_items, :all_of, :any_of, :one_of
201
+ payload[:schemas]
202
+ when :properties, :dependent_schemas, :discriminator
203
+ payload[:schemas]&.values || payload[:mapping]&.values || []
204
+ when :pattern_properties
205
+ payload[:patterns].map { |entry| entry[:schema] }
206
+ when :items, :contains, :property_names, :not, :additional_properties
207
+ [payload[:schema]].grep(Array)
208
+ when :if_then_else
209
+ [payload[:if_schema], payload[:then_schema], payload[:else_schema]].compact
210
+ when :ref, :dynamic_ref
211
+ [payload[:instructions]]
212
+ when :dynamic_scope
213
+ payload[:anchors].values
214
+ else
215
+ []
216
+ end
217
+ end
218
+ end
219
+
220
+ def initialize(options = {})
221
+ @options = options
222
+ @custom_formats = options[:custom_formats] || {}
223
+ @messages = MESSAGES.merge(symbolized_messages(options[:messages] || {}))
224
+ @tracking_cache = {}
225
+ end
226
+
227
+ def validate(instructions, data, fail_fast: false)
228
+ result = Result.new(fail_fast: fail_fast)
229
+ validate_instructions(instructions, data, result, '', EvaluationContext.new)
230
+ result
231
+ end
232
+
233
+ def valid?(instructions, data, track_evaluation: self.class.requires_evaluation_tracking?(instructions))
234
+ context = track_evaluation ? EvaluationContext.new : NoopEvaluationContext.new
235
+ valid_instructions?(instructions, data, '', context)
236
+ end
237
+
238
+ private
239
+
240
+ def validate_instructions(instructions, data, result, instance_location, context)
241
+ start_error_count = result.errors.length
242
+ pushed_scopes = push_dynamic_scopes(instructions, context)
243
+
244
+ begin
245
+ instructions.each do |instruction|
246
+ next if instruction.op == :dynamic_scope
247
+
248
+ execute(instruction, data, result, instance_location, context)
249
+ return false if result.fail_fast? && result.errors.length > start_error_count
250
+ end
251
+
252
+ result.errors.length == start_error_count
253
+ ensure
254
+ pushed_scopes.times { context.pop_dynamic_scope }
255
+ end
256
+ end
257
+
258
+ def execute(instruction, data, result, instance_location, context)
259
+ case instruction.op
260
+ when :false_schema then add_error(result, instruction, data, :false_schema, {}, instance_location)
261
+ when :type then execute_type(instruction, data, result, instance_location)
262
+ when :enum then execute_enum(instruction, data, result, instance_location)
263
+ when :const then execute_const(instruction, data, result, instance_location)
264
+ when :multiple_of then execute_multiple_of(instruction, data, result, instance_location)
265
+ when :maximum then execute_maximum(instruction, data, result, instance_location)
266
+ when :minimum then execute_minimum(instruction, data, result, instance_location)
267
+ when :max_length then execute_max_length(instruction, data, result, instance_location)
268
+ when :min_length then execute_min_length(instruction, data, result, instance_location)
269
+ when :pattern then execute_pattern(instruction, data, result, instance_location)
270
+ when :format then execute_format(instruction, data, result, instance_location)
271
+ when :max_items then execute_max_items(instruction, data, result, instance_location)
272
+ when :min_items then execute_min_items(instruction, data, result, instance_location)
273
+ when :unique_items then execute_unique_items(instruction, data, result, instance_location)
274
+ when :prefix_items then execute_prefix_items(instruction, data, result, instance_location, context)
275
+ when :items then execute_items(instruction, data, result, instance_location, context)
276
+ when :contains then execute_contains(instruction, data, result, instance_location, context)
277
+ when :max_properties then execute_max_properties(instruction, data, result, instance_location)
278
+ when :min_properties then execute_min_properties(instruction, data, result, instance_location)
279
+ when :required then execute_required(instruction, data, result, instance_location)
280
+ when :properties then execute_properties(instruction, data, result, instance_location, context)
281
+ when :pattern_properties then execute_pattern_properties(instruction, data, result, instance_location, context)
282
+ when :additional_properties then execute_additional_properties(instruction, data, result, instance_location,
283
+ context)
284
+ when :property_names then execute_property_names(instruction, data, result, instance_location, context)
285
+ when :dependent_required then execute_dependent_required(instruction, data, result, instance_location)
286
+ when :dependent_schemas then execute_dependent_schemas(instruction, data, result, instance_location, context)
287
+ when :all_of then execute_all_of(instruction, data, result, instance_location, context)
288
+ when :any_of then execute_any_of(instruction, data, result, instance_location, context)
289
+ when :one_of then execute_one_of(instruction, data, result, instance_location, context)
290
+ when :not then execute_not(instruction, data, result, instance_location, context)
291
+ when :if_then_else then execute_if_then_else(instruction, data, result, instance_location, context)
292
+ when :discriminator then execute_discriminator(instruction, data, result, instance_location, context)
293
+ when :ref then validate_instructions(instruction.payload[:instructions], data, result, instance_location, context)
294
+ when :dynamic_ref then execute_dynamic_ref(instruction, data, result, instance_location, context)
295
+ when :unevaluated_properties then execute_unevaluated_properties(instruction, data, result, instance_location,
296
+ context)
297
+ when :unevaluated_items then execute_unevaluated_items(instruction, data, result, instance_location, context)
298
+ when :custom_keyword then execute_custom_keyword(instruction, data, result, instance_location)
299
+ end
300
+ end
301
+
302
+ def valid_instructions?(instructions, data, instance_location, context)
303
+ unless dynamic_scope_prefix?(instructions)
304
+ return valid_unscoped_instructions?(instructions, data, instance_location,
305
+ context)
306
+ end
307
+
308
+ valid_scoped_instructions?(instructions, data, instance_location, context)
309
+ end
310
+
311
+ def valid_unscoped_instructions?(instructions, data, instance_location, context)
312
+ instructions.each do |instruction|
313
+ return false unless valid_instruction?(instruction, data, instance_location, context)
314
+ end
315
+
316
+ true
317
+ end
318
+
319
+ def valid_scoped_instructions?(instructions, data, instance_location, context)
320
+ pushed_scopes = push_dynamic_scopes(instructions, context)
321
+
322
+ begin
323
+ instructions.each do |instruction|
324
+ next if instruction.op == :dynamic_scope
325
+
326
+ return false unless valid_instruction?(instruction, data, instance_location, context)
327
+ end
328
+
329
+ true
330
+ ensure
331
+ pushed_scopes.times { context.pop_dynamic_scope }
332
+ end
333
+ end
334
+
335
+ def dynamic_scope_prefix?(instructions)
336
+ !instructions.empty? && instructions.first.op == :dynamic_scope
337
+ end
338
+
339
+ def valid_instruction?(instruction, data, instance_location, context)
340
+ payload = instruction.payload
341
+
342
+ case instruction.op
343
+ when :false_schema
344
+ false
345
+ when :type
346
+ type_mask_for(data).anybits?(payload[:mask])
347
+ when :enum
348
+ payload[:values].any? { |value| json_equal?(value, data) }
349
+ when :const
350
+ json_equal?(payload[:value], data)
351
+ when :multiple_of
352
+ !numeric_instance?(data) || multiple_of?(data, payload[:factor])
353
+ when :maximum
354
+ return true unless numeric_instance?(data)
355
+
356
+ payload[:exclusive] ? native_numeric_lt?(data, payload[:limit]) : native_numeric_lte?(data, payload[:limit])
357
+ when :minimum
358
+ return true unless numeric_instance?(data)
359
+
360
+ payload[:exclusive] ? native_numeric_gt?(data, payload[:limit]) : native_numeric_gte?(data, payload[:limit])
361
+ when :max_length
362
+ !data.is_a?(String) || string_length(data) <= payload[:limit]
363
+ when :min_length
364
+ !data.is_a?(String) || string_length(data) >= payload[:limit]
365
+ when :pattern
366
+ !data.is_a?(String) || data.match?(payload[:pattern])
367
+ when :format
368
+ !data.is_a?(String) || !payload[:assertion] || Senko::Format.valid?(payload[:format], data, @custom_formats)
369
+ when :max_items
370
+ !data.is_a?(Array) || data.length <= payload[:limit]
371
+ when :min_items
372
+ !data.is_a?(Array) || data.length >= payload[:limit]
373
+ when :unique_items
374
+ !data.is_a?(Array) || unique_items?(data)
375
+ when :prefix_items
376
+ valid_prefix_items?(payload, data, instance_location, context)
377
+ when :items
378
+ valid_items?(payload, data, instance_location, context)
379
+ when :contains
380
+ valid_contains?(payload, data, instance_location, context)
381
+ when :max_properties
382
+ !data.is_a?(Hash) || data.length <= payload[:limit]
383
+ when :min_properties
384
+ !data.is_a?(Hash) || data.length >= payload[:limit]
385
+ when :required
386
+ !data.is_a?(Hash) || payload[:keys].all? { |key| data.key?(key) }
387
+ when :properties
388
+ valid_properties?(payload, data, instance_location, context)
389
+ when :pattern_properties
390
+ valid_pattern_properties?(payload, data, instance_location, context)
391
+ when :additional_properties
392
+ valid_additional_properties?(payload, data, instance_location, context)
393
+ when :property_names
394
+ valid_property_names?(payload, data, instance_location, context)
395
+ when :dependent_required
396
+ valid_dependent_required?(payload, data)
397
+ when :dependent_schemas
398
+ valid_dependent_schemas?(payload, data, instance_location, context)
399
+ when :all_of
400
+ valid_all_of?(payload, data, instance_location, context)
401
+ when :any_of
402
+ valid_any_of?(payload, data, instance_location, context)
403
+ when :one_of
404
+ valid_one_of?(payload, data, instance_location, context)
405
+ when :not
406
+ child_context = context.track_evaluation? ? context.fork : context
407
+ !valid_instructions?(payload[:schema], data, instance_location, child_context)
408
+ when :if_then_else
409
+ valid_if_then_else?(payload, data, instance_location, context)
410
+ when :discriminator
411
+ valid_discriminator?(payload, data, instance_location, context)
412
+ when :ref
413
+ valid_instructions?(payload[:instructions], data, instance_location, context)
414
+ when :dynamic_ref
415
+ valid_dynamic_ref?(payload, data, instance_location, context)
416
+ when :unevaluated_properties
417
+ valid_unevaluated_properties?(payload, data, instance_location, context)
418
+ when :unevaluated_items
419
+ valid_unevaluated_items?(payload, data, instance_location, context)
420
+ when :custom_keyword
421
+ custom_keyword_result(instruction, data) == true
422
+ else
423
+ true
424
+ end
425
+ end
426
+
427
+ def valid_prefix_items?(payload, data, instance_location, context)
428
+ return true unless data.is_a?(Array)
429
+
430
+ payload[:schemas].each_with_index do |schema, index|
431
+ break if index >= data.length
432
+
433
+ child_location = child_instance_location(context, instance_location, index)
434
+ return false unless valid_child_instance_schema?(schema, data[index], child_location, context)
435
+
436
+ context.mark_item(instance_location, index)
437
+ end
438
+
439
+ true
440
+ end
441
+
442
+ def valid_items?(payload, data, instance_location, context)
443
+ return true unless data.is_a?(Array)
444
+
445
+ data.each_with_index do |item, index|
446
+ next if index < payload[:start_index]
447
+
448
+ child_location = child_instance_location(context, instance_location, index)
449
+ return false unless valid_child_instance_schema?(payload[:schema], item, child_location, context)
450
+
451
+ context.mark_item(instance_location, index)
452
+ end
453
+
454
+ true
455
+ end
456
+
457
+ def valid_contains?(payload, data, instance_location, context)
458
+ return true unless data.is_a?(Array)
459
+
460
+ count = 0
461
+ data.each_with_index do |item, index|
462
+ child_location = child_instance_location(context, instance_location, index)
463
+ child_context = context.track_evaluation? ? context.fork : context
464
+ next unless valid_instructions?(payload[:schema], item, child_location, child_context)
465
+
466
+ count += 1
467
+ context.mark_item(instance_location, index)
468
+ context.merge(child_context) if context.track_evaluation?
469
+ end
470
+
471
+ count >= payload[:min] && (!payload[:max] || count <= payload[:max])
472
+ end
473
+
474
+ def valid_properties?(payload, data, instance_location, context)
475
+ return true unless data.is_a?(Hash)
476
+
477
+ payload[:schemas].each do |key, schema|
478
+ next unless data.key?(key)
479
+
480
+ child_location = child_instance_location(context, instance_location, key)
481
+ return false unless valid_child_instance_schema?(schema, data[key], child_location, context)
482
+
483
+ context.mark_property(instance_location, key)
484
+ end
485
+
486
+ true
487
+ end
488
+
489
+ def valid_pattern_properties?(payload, data, instance_location, context)
490
+ return true unless data.is_a?(Hash)
491
+
492
+ data.each do |key, value|
493
+ payload[:patterns].each do |entry|
494
+ next unless key.match?(entry[:pattern])
495
+
496
+ child_location = child_instance_location(context, instance_location, key)
497
+ return false unless valid_child_instance_schema?(entry[:schema], value, child_location, context)
498
+
499
+ context.mark_property(instance_location, key)
500
+ end
501
+ end
502
+
503
+ true
504
+ end
505
+
506
+ def valid_additional_properties?(payload, data, instance_location, context)
507
+ return true unless data.is_a?(Hash)
508
+
509
+ data.each do |key, value|
510
+ next if payload[:known].include?(key)
511
+ next if payload[:patterns].any? { |pattern| key.match?(pattern) }
512
+ return false if payload[:schema] == false
513
+
514
+ child_location = child_instance_location(context, instance_location, key)
515
+ return false unless valid_child_instance_schema?(payload[:schema], value, child_location, context)
516
+
517
+ context.mark_property(instance_location, key)
518
+ end
519
+
520
+ true
521
+ end
522
+
523
+ def valid_property_names?(payload, data, instance_location, context)
524
+ return true unless data.is_a?(Hash)
525
+
526
+ data.each_key do |key|
527
+ child_location = child_instance_location(context, instance_location, key)
528
+ child_context = context.track_evaluation? ? context.fork : context
529
+ return false unless valid_instructions?(payload[:schema], key, child_location, child_context)
530
+ end
531
+
532
+ true
533
+ end
534
+
535
+ def valid_dependent_required?(payload, data)
536
+ return true unless data.is_a?(Hash)
537
+
538
+ payload[:requirements].each do |property, dependencies|
539
+ next unless data.key?(property)
540
+
541
+ dependencies.each do |dependency|
542
+ return false unless data.key?(dependency)
543
+ end
544
+ end
545
+
546
+ true
547
+ end
548
+
549
+ def valid_dependent_schemas?(payload, data, instance_location, context)
550
+ return true unless data.is_a?(Hash)
551
+
552
+ payload[:schemas].each do |property, schema|
553
+ next unless data.key?(property)
554
+
555
+ return false unless valid_child_schema?(schema, data, instance_location, context)
556
+ end
557
+
558
+ true
559
+ end
560
+
561
+ def valid_all_of?(payload, data, instance_location, context)
562
+ if payload[:schemas].length == 1 && context.evaluation_empty?
563
+ return valid_instructions?(payload[:schemas].first, data, instance_location, context)
564
+ end
565
+
566
+ payload[:schemas].each do |schema|
567
+ return false unless valid_child_schema?(schema, data, instance_location, context)
568
+ end
569
+
570
+ true
571
+ end
572
+
573
+ def valid_any_of?(payload, data, instance_location, context)
574
+ unless context.track_evaluation?
575
+ return payload[:schemas].any? { |schema| valid_instructions?(schema, data, instance_location, context) }
576
+ end
577
+
578
+ valid_contexts = []
579
+
580
+ payload[:schemas].each do |schema|
581
+ child_context = context.fork
582
+ valid_contexts << child_context if valid_instructions?(schema, data, instance_location, child_context)
583
+ end
584
+
585
+ return false if valid_contexts.empty?
586
+
587
+ valid_contexts.each { |child_context| context.merge(child_context) }
588
+ true
589
+ end
590
+
591
+ def valid_one_of?(payload, data, instance_location, context)
592
+ unless context.track_evaluation?
593
+ matches = 0
594
+ payload[:schemas].each do |schema|
595
+ next unless valid_instructions?(schema, data, instance_location, context)
596
+
597
+ matches += 1
598
+ return false if matches > 1
599
+ end
600
+
601
+ return matches == 1
602
+ end
603
+
604
+ match_context = nil
605
+
606
+ payload[:schemas].each do |schema|
607
+ child_context = context.fork
608
+ next unless valid_instructions?(schema, data, instance_location, child_context)
609
+ return false if match_context
610
+
611
+ match_context = child_context
612
+ end
613
+
614
+ return false unless match_context
615
+
616
+ context.merge(match_context)
617
+ true
618
+ end
619
+
620
+ def valid_if_then_else?(payload, data, instance_location, context)
621
+ condition_context = context.track_evaluation? ? context.fork : context
622
+ condition_passed = valid_instructions?(payload[:if_schema], data, instance_location, condition_context)
623
+ selected_schema = condition_passed ? payload[:then_schema] : payload[:else_schema]
624
+
625
+ unless selected_schema
626
+ context.merge(condition_context) if condition_passed
627
+ return true
628
+ end
629
+
630
+ return false unless valid_child_schema?(selected_schema, data, instance_location, context)
631
+
632
+ context.merge(condition_context) if condition_passed
633
+ true
634
+ end
635
+
636
+ def valid_discriminator?(payload, data, instance_location, context)
637
+ property = payload[:property]
638
+ return false unless data.is_a?(Hash) && data.key?(property)
639
+
640
+ schema = payload[:mapping][data[property]] || payload[:mapping][data[property].to_s]
641
+ return false unless schema
642
+
643
+ return false unless valid_child_schema?(schema, data, instance_location, context)
644
+
645
+ true
646
+ end
647
+
648
+ def valid_dynamic_ref?(payload, data, instance_location, context)
649
+ target = (context.dynamic_target(payload[:anchor]) if payload[:dynamic] && payload[:anchor])
650
+ target ||= payload[:instructions]
651
+
652
+ valid_instructions?(target, data, instance_location, context)
653
+ end
654
+
655
+ def valid_unevaluated_properties?(payload, data, instance_location, context)
656
+ return true unless data.is_a?(Hash)
657
+
658
+ data.each do |key, value|
659
+ next if context.property_evaluated?(instance_location, key)
660
+ return false if payload[:schema] == false
661
+
662
+ return false unless valid_child_instance_schema?(payload[:schema], value,
663
+ join_instance(instance_location, key), context)
664
+
665
+ context.mark_property(instance_location, key)
666
+ end
667
+
668
+ true
669
+ end
670
+
671
+ def valid_unevaluated_items?(payload, data, instance_location, context)
672
+ return true unless data.is_a?(Array)
673
+
674
+ data.each_with_index do |item, index|
675
+ next if context.item_evaluated?(instance_location, index)
676
+ return false if payload[:schema] == false
677
+
678
+ return false unless valid_child_instance_schema?(payload[:schema], item,
679
+ join_instance(instance_location, index), context)
680
+
681
+ context.mark_item(instance_location, index)
682
+ end
683
+
684
+ true
685
+ end
686
+
687
+ def push_dynamic_scopes(instructions, context)
688
+ pushed = 0
689
+ instructions.each do |instruction|
690
+ break unless instruction.op == :dynamic_scope
691
+
692
+ context.push_dynamic_scope(instruction.payload[:anchors])
693
+ pushed += 1
694
+ end
695
+ pushed
696
+ end
697
+
698
+ def execute_type(instruction, data, result, instance_location)
699
+ return if type_mask_for(data).anybits?(instruction.payload[:mask])
700
+
701
+ add_error(
702
+ result,
703
+ instruction,
704
+ data,
705
+ :type,
706
+ { expected: instruction.payload[:expected].join(' or '), actual: actual_type(data) },
707
+ instance_location
708
+ )
709
+ end
710
+
711
+ def execute_enum(instruction, data, result, instance_location)
712
+ return if instruction.payload[:values].any? { |value| json_equal?(value, data) }
713
+
714
+ add_error(result, instruction, data, :enum, { values: instruction.payload[:values].inspect }, instance_location)
715
+ end
716
+
717
+ def execute_const(instruction, data, result, instance_location)
718
+ return if json_equal?(instruction.payload[:value], data)
719
+
720
+ add_error(result, instruction, data, :const, { const: instruction.payload[:value].inspect }, instance_location)
721
+ end
722
+
723
+ def execute_multiple_of(instruction, data, result, instance_location)
724
+ return unless numeric_instance?(data)
725
+ return if multiple_of?(data, instruction.payload[:factor])
726
+
727
+ add_error(result, instruction, data, :multiple_of, { multiple_of: instruction.payload[:factor] },
728
+ instance_location)
729
+ end
730
+
731
+ def execute_maximum(instruction, data, result, instance_location)
732
+ return unless numeric_instance?(data)
733
+
734
+ limit = instruction.payload[:limit]
735
+ exclusive = instruction.payload[:exclusive]
736
+ return if exclusive ? native_numeric_lt?(data, limit) : native_numeric_lte?(data, limit)
737
+
738
+ key = exclusive ? :exclusive_maximum : :maximum
739
+ add_error(result, instruction, data, key, { maximum: limit }, instance_location)
740
+ end
741
+
742
+ def execute_minimum(instruction, data, result, instance_location)
743
+ return unless numeric_instance?(data)
744
+
745
+ limit = instruction.payload[:limit]
746
+ exclusive = instruction.payload[:exclusive]
747
+ return if exclusive ? native_numeric_gt?(data, limit) : native_numeric_gte?(data, limit)
748
+
749
+ key = exclusive ? :exclusive_minimum : :minimum
750
+ add_error(result, instruction, data, key, { minimum: limit }, instance_location)
751
+ end
752
+
753
+ def execute_max_length(instruction, data, result, instance_location)
754
+ return unless data.is_a?(String)
755
+ return if string_length(data) <= instruction.payload[:limit]
756
+
757
+ add_error(result, instruction, data, :max_length, { max_length: instruction.payload[:limit] }, instance_location)
758
+ end
759
+
760
+ def execute_min_length(instruction, data, result, instance_location)
761
+ return unless data.is_a?(String)
762
+ return if string_length(data) >= instruction.payload[:limit]
763
+
764
+ add_error(result, instruction, data, :min_length, { min_length: instruction.payload[:limit] }, instance_location)
765
+ end
766
+
767
+ def execute_pattern(instruction, data, result, instance_location)
768
+ return unless data.is_a?(String)
769
+ return if data.match?(instruction.payload[:pattern])
770
+
771
+ add_error(result, instruction, data, :pattern, { pattern: instruction.payload[:source] }, instance_location)
772
+ end
773
+
774
+ def execute_format(instruction, data, result, instance_location)
775
+ return unless data.is_a?(String)
776
+
777
+ if instruction.payload[:assertion]
778
+ return if Senko::Format.valid?(instruction.payload[:format], data, @custom_formats)
779
+
780
+ add_error(result, instruction, data, :format, { format: instruction.payload[:format] }, instance_location)
781
+ else
782
+ result.add_annotation(
783
+ 'keywordLocation' => instruction.keyword_location,
784
+ 'instanceLocation' => instance_location,
785
+ 'annotation' => instruction.payload[:format]
786
+ )
787
+ end
788
+ end
789
+
790
+ def execute_max_items(instruction, data, result, instance_location)
791
+ return unless data.is_a?(Array)
792
+ return if data.length <= instruction.payload[:limit]
793
+
794
+ add_error(result, instruction, data, :max_items, { max_items: instruction.payload[:limit] }, instance_location)
795
+ end
796
+
797
+ def execute_min_items(instruction, data, result, instance_location)
798
+ return unless data.is_a?(Array)
799
+ return if data.length >= instruction.payload[:limit]
800
+
801
+ add_error(result, instruction, data, :min_items, { min_items: instruction.payload[:limit] }, instance_location)
802
+ end
803
+
804
+ def execute_unique_items(instruction, data, result, instance_location)
805
+ return unless data.is_a?(Array)
806
+ return if unique_items?(data)
807
+
808
+ add_error(result, instruction, data, :unique_items, {}, instance_location)
809
+ end
810
+
811
+ def execute_prefix_items(instruction, data, result, instance_location, context)
812
+ return unless data.is_a?(Array)
813
+
814
+ instruction.payload[:schemas].each_with_index do |schema, index|
815
+ break if index >= data.length
816
+
817
+ child_context = context.fork
818
+ before = result.errors.length
819
+ valid = validate_instructions(schema, data[index], result, join_instance(instance_location, index),
820
+ child_context)
821
+ if valid
822
+ context.mark_item(instance_location, index)
823
+ context.merge(child_context)
824
+ end
825
+ return if result.fail_fast? && result.errors.length > before
826
+ end
827
+ end
828
+
829
+ def execute_items(instruction, data, result, instance_location, context)
830
+ return unless data.is_a?(Array)
831
+
832
+ data.each_with_index do |item, index|
833
+ next if index < instruction.payload[:start_index]
834
+
835
+ child_context = context.fork
836
+ before = result.errors.length
837
+ valid = validate_instructions(instruction.payload[:schema], item, result,
838
+ join_instance(instance_location, index), child_context)
839
+ if valid
840
+ context.mark_item(instance_location, index)
841
+ context.merge(child_context)
842
+ end
843
+ return if result.fail_fast? && result.errors.length > before
844
+ end
845
+ end
846
+
847
+ def execute_contains(instruction, data, result, instance_location, context)
848
+ return unless data.is_a?(Array)
849
+
850
+ count = 0
851
+ data.each_with_index do |item, index|
852
+ child_context = context.fork
853
+ next unless schema_valid?(instruction.payload[:schema], item, join_instance(instance_location, index),
854
+ child_context)
855
+
856
+ count += 1
857
+ context.mark_item(instance_location, index)
858
+ context.merge(child_context)
859
+ end
860
+
861
+ min = instruction.payload[:min]
862
+ max = instruction.payload[:max]
863
+
864
+ if count < min
865
+ add_error(result, instruction, data, :min_contains, { min_contains: min }, instance_location)
866
+ elsif max && count > max
867
+ add_error(result, instruction, data, :max_contains, { max_contains: max }, instance_location)
868
+ end
869
+ end
870
+
871
+ def execute_max_properties(instruction, data, result, instance_location)
872
+ return unless data.is_a?(Hash)
873
+ return if data.length <= instruction.payload[:limit]
874
+
875
+ add_error(result, instruction, data, :max_properties, { max_properties: instruction.payload[:limit] },
876
+ instance_location)
877
+ end
878
+
879
+ def execute_min_properties(instruction, data, result, instance_location)
880
+ return unless data.is_a?(Hash)
881
+ return if data.length >= instruction.payload[:limit]
882
+
883
+ add_error(result, instruction, data, :min_properties, { min_properties: instruction.payload[:limit] },
884
+ instance_location)
885
+ end
886
+
887
+ def execute_required(instruction, data, result, instance_location)
888
+ return unless data.is_a?(Hash)
889
+
890
+ instruction.payload[:keys].each do |key|
891
+ next if data.key?(key)
892
+
893
+ add_error(result, instruction, data, :required, { property: key }, instance_location)
894
+ return if result.fail_fast?
895
+ end
896
+ end
897
+
898
+ def execute_properties(instruction, data, result, instance_location, context)
899
+ return unless data.is_a?(Hash)
900
+
901
+ instruction.payload[:schemas].each do |key, schema|
902
+ next unless data.key?(key)
903
+
904
+ child_context = context.fork
905
+ before = result.errors.length
906
+ valid = validate_instructions(schema, data[key], result, join_instance(instance_location, key), child_context)
907
+ if valid
908
+ context.mark_property(instance_location, key)
909
+ context.merge(child_context)
910
+ end
911
+ return if result.fail_fast? && result.errors.length > before
912
+ end
913
+ end
914
+
915
+ def execute_pattern_properties(instruction, data, result, instance_location, context)
916
+ return unless data.is_a?(Hash)
917
+
918
+ data.each do |key, value|
919
+ instruction.payload[:patterns].each do |entry|
920
+ next unless key.match?(entry[:pattern])
921
+
922
+ child_context = context.fork
923
+ before = result.errors.length
924
+ valid = validate_instructions(entry[:schema], value, result, join_instance(instance_location, key),
925
+ child_context)
926
+ if valid
927
+ context.mark_property(instance_location, key)
928
+ context.merge(child_context)
929
+ end
930
+ return if result.fail_fast? && result.errors.length > before
931
+ end
932
+ end
933
+ end
934
+
935
+ def execute_additional_properties(instruction, data, result, instance_location, context)
936
+ return unless data.is_a?(Hash)
937
+
938
+ data.each do |key, value|
939
+ next if instruction.payload[:known].include?(key)
940
+ next if instruction.payload[:patterns].any? { |pattern| key.match?(pattern) }
941
+
942
+ if instruction.payload[:schema] == false
943
+ add_error(
944
+ result,
945
+ instruction,
946
+ value,
947
+ :additional_properties,
948
+ { property: key },
949
+ join_instance(instance_location, key)
950
+ )
951
+ return if result.fail_fast?
952
+ else
953
+ child_context = context.fork
954
+ before = result.errors.length
955
+ valid = validate_instructions(instruction.payload[:schema], value, result,
956
+ join_instance(instance_location, key), child_context)
957
+ if valid
958
+ context.mark_property(instance_location, key)
959
+ context.merge(child_context)
960
+ end
961
+ return if result.fail_fast? && result.errors.length > before
962
+ end
963
+ end
964
+ end
965
+
966
+ def execute_property_names(instruction, data, result, instance_location, context)
967
+ return unless data.is_a?(Hash)
968
+
969
+ data.each_key do |key|
970
+ next if schema_valid?(instruction.payload[:schema], key, join_instance(instance_location, key), context.fork)
971
+
972
+ add_error(result, instruction, key, :property_names, { property: key }, join_instance(instance_location, key))
973
+ return if result.fail_fast?
974
+ end
975
+ end
976
+
977
+ def execute_dependent_required(instruction, data, result, instance_location)
978
+ return unless data.is_a?(Hash)
979
+
980
+ instruction.payload[:requirements].each do |property, dependencies|
981
+ next unless data.key?(property)
982
+
983
+ dependencies.each do |dependency|
984
+ next if data.key?(dependency)
985
+
986
+ add_error(
987
+ result,
988
+ instruction,
989
+ data,
990
+ :dependent_required,
991
+ { property: property, dependency: dependency },
992
+ instance_location
993
+ )
994
+ return if result.fail_fast?
995
+ end
996
+ end
997
+ end
998
+
999
+ def execute_dependent_schemas(instruction, data, result, instance_location, context)
1000
+ return unless data.is_a?(Hash)
1001
+
1002
+ instruction.payload[:schemas].each do |property, schema|
1003
+ next unless data.key?(property)
1004
+
1005
+ child_context = context.fork
1006
+ before = result.errors.length
1007
+ valid = validate_instructions(schema, data, result, instance_location, child_context)
1008
+ context.merge(child_context) if valid
1009
+ return if result.fail_fast? && result.errors.length > before
1010
+ end
1011
+ end
1012
+
1013
+ def execute_all_of(instruction, data, result, instance_location, context)
1014
+ instruction.payload[:schemas].each do |schema|
1015
+ child_context = context.fork
1016
+ before = result.errors.length
1017
+ valid = validate_instructions(schema, data, result, instance_location, child_context)
1018
+ context.merge(child_context) if valid
1019
+ return if result.fail_fast? && result.errors.length > before
1020
+ end
1021
+ end
1022
+
1023
+ def execute_any_of(instruction, data, result, instance_location, context)
1024
+ valid_contexts = []
1025
+
1026
+ instruction.payload[:schemas].each do |schema|
1027
+ child_context = context.fork
1028
+ valid_contexts << child_context if schema_valid?(schema, data, instance_location, child_context)
1029
+ end
1030
+
1031
+ if valid_contexts.empty?
1032
+ add_error(result, instruction, data, :any_of, {}, instance_location)
1033
+ else
1034
+ valid_contexts.each { |child_context| context.merge(child_context) }
1035
+ end
1036
+ end
1037
+
1038
+ def execute_one_of(instruction, data, result, instance_location, context)
1039
+ matches = []
1040
+
1041
+ instruction.payload[:schemas].each do |schema|
1042
+ child_context = context.fork
1043
+ next unless schema_valid?(schema, data, instance_location, child_context)
1044
+
1045
+ matches << child_context
1046
+ break if result.fail_fast? && matches.length > 1
1047
+ end
1048
+
1049
+ if matches.length == 1
1050
+ context.merge(matches.first)
1051
+ elsif matches.empty?
1052
+ add_error(result, instruction, data, :one_of_none, {}, instance_location)
1053
+ else
1054
+ add_error(result, instruction, data, :one_of_multiple, { count: matches.length }, instance_location)
1055
+ end
1056
+ end
1057
+
1058
+ def execute_not(instruction, data, result, instance_location, context)
1059
+ return unless schema_valid?(instruction.payload[:schema], data, instance_location, context.fork)
1060
+
1061
+ add_error(result, instruction, data, :not, {}, instance_location)
1062
+ end
1063
+
1064
+ def execute_discriminator(instruction, data, result, instance_location, context)
1065
+ property = instruction.payload[:property]
1066
+ unless data.is_a?(Hash) && data.key?(property)
1067
+ add_discriminator_error(instruction, data, result, instance_location, property)
1068
+ return
1069
+ end
1070
+
1071
+ schema = instruction.payload[:mapping][data[property]] || instruction.payload[:mapping][data[property].to_s]
1072
+ unless schema
1073
+ add_discriminator_error(instruction, data, result, instance_location, property)
1074
+ return
1075
+ end
1076
+
1077
+ child_context = context.fork
1078
+ valid = validate_instructions(schema, data, result, instance_location, child_context)
1079
+ context.merge(child_context) if valid
1080
+ end
1081
+
1082
+ def execute_dynamic_ref(instruction, data, result, instance_location, context)
1083
+ target = if instruction.payload[:dynamic] && instruction.payload[:anchor]
1084
+ context.dynamic_target(instruction.payload[:anchor])
1085
+ end
1086
+ target ||= instruction.payload[:instructions]
1087
+
1088
+ validate_instructions(target, data, result, instance_location, context)
1089
+ end
1090
+
1091
+ def add_discriminator_error(instruction, data, result, instance_location, property)
1092
+ key = instruction.payload[:mode] == :any_of ? :any_of : :one_of_none
1093
+ values = key == :any_of ? {} : { property: property }
1094
+ add_error(result, instruction, data, key, values, instance_location)
1095
+ end
1096
+
1097
+ def execute_if_then_else(instruction, data, result, instance_location, context)
1098
+ condition_context = context.fork
1099
+ condition_passed = schema_valid?(instruction.payload[:if_schema], data, instance_location, condition_context)
1100
+ selected_schema = condition_passed ? instruction.payload[:then_schema] : instruction.payload[:else_schema]
1101
+ if selected_schema.nil?
1102
+ context.merge(condition_context) if condition_passed
1103
+ return
1104
+ end
1105
+
1106
+ child_context = context.fork
1107
+ before = result.errors.length
1108
+ valid = validate_instructions(selected_schema, data, result, instance_location, child_context)
1109
+ if valid
1110
+ context.merge(condition_context) if condition_passed
1111
+ context.merge(child_context)
1112
+ end
1113
+ nil unless result.fail_fast? && result.errors.length > before
1114
+ end
1115
+
1116
+ def execute_unevaluated_properties(instruction, data, result, instance_location, context)
1117
+ return unless data.is_a?(Hash)
1118
+
1119
+ data.each do |key, value|
1120
+ next if context.property_evaluated?(instance_location, key)
1121
+
1122
+ if instruction.payload[:schema] == false
1123
+ add_error(
1124
+ result,
1125
+ instruction,
1126
+ value,
1127
+ :unevaluated_properties,
1128
+ { property: key },
1129
+ join_instance(instance_location, key)
1130
+ )
1131
+ return if result.fail_fast?
1132
+ else
1133
+ child_context = context.fork
1134
+ before = result.errors.length
1135
+ valid = validate_instructions(instruction.payload[:schema], value, result,
1136
+ join_instance(instance_location, key), child_context)
1137
+ if valid
1138
+ context.mark_property(instance_location, key)
1139
+ context.merge(child_context)
1140
+ end
1141
+ return if result.fail_fast? && result.errors.length > before
1142
+ end
1143
+ end
1144
+ end
1145
+
1146
+ def execute_unevaluated_items(instruction, data, result, instance_location, context)
1147
+ return unless data.is_a?(Array)
1148
+
1149
+ data.each_with_index do |item, index|
1150
+ next if context.item_evaluated?(instance_location, index)
1151
+
1152
+ if instruction.payload[:schema] == false
1153
+ add_error(result, instruction, item, :unevaluated_items, { index: index },
1154
+ join_instance(instance_location, index))
1155
+ return if result.fail_fast?
1156
+ else
1157
+ child_context = context.fork
1158
+ before = result.errors.length
1159
+ valid = validate_instructions(instruction.payload[:schema], item, result,
1160
+ join_instance(instance_location, index), child_context)
1161
+ if valid
1162
+ context.mark_item(instance_location, index)
1163
+ context.merge(child_context)
1164
+ end
1165
+ return if result.fail_fast? && result.errors.length > before
1166
+ end
1167
+ end
1168
+ end
1169
+
1170
+ def execute_custom_keyword(instruction, data, result, instance_location)
1171
+ valid = custom_keyword_result(instruction, data)
1172
+
1173
+ return if valid == true
1174
+
1175
+ message = valid.is_a?(String) ? valid : nil
1176
+ add_error(
1177
+ result,
1178
+ instruction,
1179
+ data,
1180
+ :custom_keyword,
1181
+ { keyword: instruction.payload[:keyword] },
1182
+ instance_location,
1183
+ message
1184
+ )
1185
+ end
1186
+
1187
+ def schema_valid?(instructions, data, instance_location, context)
1188
+ valid_instructions?(instructions, data, instance_location, context)
1189
+ end
1190
+
1191
+ def add_error(result, instruction, data, key, values, instance_location, explicit_message = nil)
1192
+ result.add_error(
1193
+ Error.new(
1194
+ message: explicit_message || Kernel.format(@messages.fetch(key), values),
1195
+ instance_location: instance_location,
1196
+ keyword_location: instruction.keyword_location,
1197
+ keyword: instruction.keyword,
1198
+ schema: instruction.schema,
1199
+ data: data
1200
+ )
1201
+ )
1202
+ end
1203
+
1204
+ def custom_keyword_result(instruction, data)
1205
+ validator = instruction.payload[:validator]
1206
+ return true unless validator.respond_to?(:call)
1207
+
1208
+ keyword_value = instruction.payload[:value]
1209
+ case validator.arity
1210
+ when 1 then validator.call(data)
1211
+ when 2 then validator.call(data, keyword_value)
1212
+ else validator.call(data, keyword_value, instruction)
1213
+ end
1214
+ end
1215
+
1216
+ def multiple_of?(value, factor)
1217
+ if value.is_a?(Integer) && factor.is_a?(Integer)
1218
+ (value % factor).zero?
1219
+ else
1220
+ (decimal(value) % decimal(factor)).zero?
1221
+ end
1222
+ end
1223
+
1224
+ def decimal(value)
1225
+ BigDecimal(value.to_s)
1226
+ end
1227
+
1228
+ def unique_items?(items)
1229
+ return Senko::Native.unique_array?(items) if native_available?
1230
+ return small_unique_items?(items) if items.length <= 16
1231
+
1232
+ seen = {}
1233
+ items.each do |item|
1234
+ key = canonical_json(item)
1235
+ return false if seen.key?(key)
1236
+
1237
+ seen[key] = true
1238
+ end
1239
+ true
1240
+ end
1241
+
1242
+ def small_unique_items?(items)
1243
+ items.each_with_index do |left, left_index|
1244
+ ((left_index + 1)...items.length).each do |right_index|
1245
+ return false if json_equal?(left, items[right_index])
1246
+ end
1247
+ end
1248
+ true
1249
+ end
1250
+
1251
+ def canonical_json(value)
1252
+ case value
1253
+ when Hash
1254
+ ['object', value.keys.sort.map { |key| [key, canonical_json(value[key])] }]
1255
+ when Array
1256
+ ['array', value.map { |item| canonical_json(item) }]
1257
+ when Integer, Float, BigDecimal
1258
+ ['number', decimal(value).to_s('F')]
1259
+ else
1260
+ [value.class.name, value]
1261
+ end
1262
+ end
1263
+
1264
+ def json_equal?(left, right)
1265
+ left == right
1266
+ end
1267
+
1268
+ def numeric_instance?(value)
1269
+ value.is_a?(Integer) || value.is_a?(Float) || value.is_a?(BigDecimal)
1270
+ end
1271
+
1272
+ def integer_number?(value)
1273
+ case value
1274
+ when Integer
1275
+ true
1276
+ when Float, BigDecimal
1277
+ value.finite? && value == value.to_i
1278
+ else
1279
+ false
1280
+ end
1281
+ end
1282
+
1283
+ def type_mask_for(value)
1284
+ return Senko::Native.type_mask(value) if native_available?
1285
+
1286
+ case value
1287
+ when NilClass
1288
+ Instructions::TYPE_NULL
1289
+ when TrueClass, FalseClass
1290
+ Instructions::TYPE_BOOLEAN
1291
+ when Integer
1292
+ Instructions::TYPE_INTEGER
1293
+ when Float, BigDecimal
1294
+ integer_number?(value) ? Instructions::TYPE_INTEGER : (Instructions::TYPE_NUMBER & ~Instructions::TYPE_INTEGER)
1295
+ when String
1296
+ Instructions::TYPE_STRING
1297
+ when Array
1298
+ Instructions::TYPE_ARRAY
1299
+ when Hash
1300
+ Instructions::TYPE_OBJECT
1301
+ else
1302
+ 0
1303
+ end
1304
+ end
1305
+
1306
+ def actual_type(value)
1307
+ case value
1308
+ when NilClass then 'null'
1309
+ when TrueClass, FalseClass then 'boolean'
1310
+ when Integer then 'integer'
1311
+ when Float, BigDecimal then integer_number?(value) ? 'integer' : 'number'
1312
+ when String then 'string'
1313
+ when Array then 'array'
1314
+ when Hash then 'object'
1315
+ else value.class.name
1316
+ end
1317
+ end
1318
+
1319
+ def string_length(value)
1320
+ native_available? ? Senko::Native.string_length(value) : value.length
1321
+ end
1322
+
1323
+ def native_numeric_lte?(left, right)
1324
+ native_available? ? Senko::Native.numeric_lte?(left, right) : left <= right
1325
+ end
1326
+
1327
+ def native_numeric_lt?(left, right)
1328
+ native_available? ? Senko::Native.numeric_lt?(left, right) : left < right
1329
+ end
1330
+
1331
+ def native_numeric_gte?(left, right)
1332
+ native_available? ? Senko::Native.numeric_gte?(left, right) : left >= right
1333
+ end
1334
+
1335
+ def native_numeric_gt?(left, right)
1336
+ native_available? ? Senko::Native.numeric_gt?(left, right) : left > right
1337
+ end
1338
+
1339
+ def native_available?
1340
+ defined?(Senko::Native) &&
1341
+ Senko::Native.respond_to?(:type_mask) &&
1342
+ Senko::Native.respond_to?(:unique_array?)
1343
+ end
1344
+
1345
+ def symbolized_messages(messages)
1346
+ messages.each_with_object({}) do |(key, value), result|
1347
+ result[key.to_sym] = value
1348
+ end
1349
+ end
1350
+
1351
+ def child_instance_location(context, base, token)
1352
+ context.track_evaluation? ? join_instance(base, token) : base
1353
+ end
1354
+
1355
+ def valid_child_schema?(schema, data, instance_location, context)
1356
+ return valid_instructions?(schema, data, instance_location, context) unless context.track_evaluation?
1357
+
1358
+ child_context = context.fork
1359
+ return false unless valid_instructions?(schema, data, instance_location, child_context)
1360
+
1361
+ context.merge(child_context)
1362
+ true
1363
+ end
1364
+
1365
+ def valid_child_instance_schema?(schema, data, instance_location, context)
1366
+ return valid_instructions?(schema, data, instance_location, context) unless context.track_evaluation?
1367
+ if schema_requires_evaluation_tracking?(schema)
1368
+ return valid_child_schema?(schema, data, instance_location,
1369
+ context)
1370
+ end
1371
+
1372
+ valid_instructions?(schema, data, instance_location, context.fork(track_evaluation: false))
1373
+ end
1374
+
1375
+ def schema_requires_evaluation_tracking?(schema)
1376
+ return false unless schema.is_a?(Array)
1377
+
1378
+ @tracking_cache.fetch(schema.object_id) do
1379
+ @tracking_cache[schema.object_id] = self.class.requires_evaluation_tracking?(schema)
1380
+ end
1381
+ end
1382
+
1383
+ def join_instance(base, token)
1384
+ "#{base}/#{escape_pointer_token(token)}"
1385
+ end
1386
+
1387
+ def escape_pointer_token(token)
1388
+ token.to_s.gsub('~', '~0').gsub('/', '~1')
1389
+ end
1390
+ end
1391
+ end