schemurai 2.2.1 → 2.2.3
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.
- checksums.yaml +4 -4
- data/lib/schemurai/evaluator.rb +5 -11
- data/lib/schemurai/unique_items.rb +66 -0
- data/lib/schemurai/version.rb +1 -1
- data/lib/schemurai/vm/evaluator.rb +35 -61
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c4620544f45920ada46eda1beff7cbb6461fb1633031f78d2964c4e963716d82
|
|
4
|
+
data.tar.gz: b0a66f4e369181d9182a10715d1622924ebac68cfe32a1169b844714a7b6a038
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b6dd9a631fa90e9600e0a60ba4e2ecfca492497a69376a11801cf1a4bfb799fb71eea3546f417b394279b7c3795ace0d2ff8422c8fa8c0b9c9cf6e7530ca8f86
|
|
7
|
+
data.tar.gz: b8b2e1bdaccdfc420b1aa2cec9fc2bef856157d450ccb547dcf230693b4b959baa77f9901b9df920c5c73bd6ef982b468249655397c0160e55c3b57772b1c2c5
|
data/lib/schemurai/evaluator.rb
CHANGED
|
@@ -4,10 +4,13 @@ require "json"
|
|
|
4
4
|
require "base64"
|
|
5
5
|
require_relative "evaluation"
|
|
6
6
|
require_relative "error_message"
|
|
7
|
+
require_relative "unique_items"
|
|
7
8
|
|
|
8
9
|
module Schemurai
|
|
9
10
|
module Internal
|
|
10
11
|
class Evaluator
|
|
12
|
+
include UniqueItems
|
|
13
|
+
|
|
11
14
|
MISSING_SEGMENT = Object.new.freeze
|
|
12
15
|
|
|
13
16
|
def backend = :ruby
|
|
@@ -226,11 +229,7 @@ module Schemurai
|
|
|
226
229
|
length = value.length
|
|
227
230
|
return false if schema.key?("maxItems") && length > schema["maxItems"]
|
|
228
231
|
return false if schema.key?("minItems") && length < schema["minItems"]
|
|
229
|
-
if schema["uniqueItems"]
|
|
230
|
-
value.each_with_index do |item, index|
|
|
231
|
-
return false if value[0...index].any? { |previous| json_equal?(previous, item) }
|
|
232
|
-
end
|
|
233
|
-
end
|
|
232
|
+
return false if schema["uniqueItems"] && !unique_items?(value)
|
|
234
233
|
|
|
235
234
|
prefix_items = schema["prefixItems"]
|
|
236
235
|
if prefix_items.is_a?(Array)
|
|
@@ -555,12 +554,7 @@ module Schemurai
|
|
|
555
554
|
limit(schema, "maxItems", value.length) { |actual, expected| actual <= expected }
|
|
556
555
|
limit(schema, "minItems", value.length) { |actual, expected| actual >= expected }
|
|
557
556
|
|
|
558
|
-
if schema["uniqueItems"]
|
|
559
|
-
duplicate = value.each_with_index.any? do |item, index|
|
|
560
|
-
value[0...index].any? { |previous| json_equal?(previous, item) }
|
|
561
|
-
end
|
|
562
|
-
add_error("uniqueItems") { ErrorMessage.unique_items } if duplicate
|
|
563
|
-
end
|
|
557
|
+
add_error("uniqueItems") { ErrorMessage.unique_items } if schema["uniqueItems"] && !unique_items?(value)
|
|
564
558
|
|
|
565
559
|
prefix_items = schema["prefixItems"]
|
|
566
560
|
if prefix_items.is_a?(Array)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Schemurai
|
|
4
|
+
module Internal
|
|
5
|
+
module UniqueItems
|
|
6
|
+
private def unique_items?(values)
|
|
7
|
+
# Avoid building an index for small arrays and early duplicates.
|
|
8
|
+
if values.length <= 16
|
|
9
|
+
values.each_with_index do |item, index|
|
|
10
|
+
index.times { |previous| return false if json_equal?(values[previous], item) }
|
|
11
|
+
end
|
|
12
|
+
return true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
buckets = {}
|
|
16
|
+
# Store a single index until a collision needs a bucket array.
|
|
17
|
+
index = 0
|
|
18
|
+
while index < values.length
|
|
19
|
+
item = values[index]
|
|
20
|
+
fingerprint = json_fingerprint(item)
|
|
21
|
+
if (bucket = buckets[fingerprint])
|
|
22
|
+
if bucket.is_a?(Integer)
|
|
23
|
+
return false if json_equal?(values[bucket], item)
|
|
24
|
+
|
|
25
|
+
buckets[fingerprint] = [bucket, index]
|
|
26
|
+
else
|
|
27
|
+
return false if bucket.any? { |previous| json_equal?(values[previous], item) }
|
|
28
|
+
|
|
29
|
+
bucket << index
|
|
30
|
+
end
|
|
31
|
+
else
|
|
32
|
+
buckets[fingerprint] = index
|
|
33
|
+
end
|
|
34
|
+
index += 1
|
|
35
|
+
end
|
|
36
|
+
true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private def json_fingerprint(value)
|
|
40
|
+
case value
|
|
41
|
+
when Numeric
|
|
42
|
+
# JSON numbers compare across Integer/Float. Converting integral
|
|
43
|
+
# floats to integers also preserves precision for large integers.
|
|
44
|
+
return value.hash if value.is_a?(Integer)
|
|
45
|
+
|
|
46
|
+
number = value.to_f
|
|
47
|
+
(number.finite? && number == number.to_i) ? number.to_i.hash : number.hash
|
|
48
|
+
when Array
|
|
49
|
+
# Mask before multiplication to keep intermediate values immediate
|
|
50
|
+
# integers; hash each step to retain order without temporary arrays.
|
|
51
|
+
fingerprint = value.class.hash
|
|
52
|
+
value.each { |item| fingerprint = (((fingerprint & 0x1fffffff) * 31) ^ json_fingerprint(item)).hash }
|
|
53
|
+
fingerprint
|
|
54
|
+
when Hash
|
|
55
|
+
# Object key order is immaterial. Hash collisions are resolved using
|
|
56
|
+
# json_equal?, so this fingerprint never decides equality itself.
|
|
57
|
+
entries = 0
|
|
58
|
+
value.each_pair { |key, item| entries ^= (((key.hash & 0x1fffffff) * 31) ^ json_fingerprint(item)).hash }
|
|
59
|
+
(value.class.hash ^ value.length ^ entries).hash
|
|
60
|
+
else
|
|
61
|
+
value.hash
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
data/lib/schemurai/version.rb
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
require "base64"
|
|
4
4
|
require "json"
|
|
5
|
+
require "set" # rubocop:disable Lint/RedundantRequireStatement -- Required on Ruby 3.4.
|
|
5
6
|
require_relative "../evaluation"
|
|
6
7
|
require_relative "../error_message"
|
|
8
|
+
require_relative "../unique_items"
|
|
7
9
|
require_relative "compiler"
|
|
8
10
|
|
|
9
11
|
module Schemurai
|
|
@@ -12,8 +14,10 @@ module Schemurai
|
|
|
12
14
|
attr_reader :evaluated_properties, :evaluated_items
|
|
13
15
|
|
|
14
16
|
def initialize
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
# Applicators can overlap. Array membership makes collecting and merging
|
|
18
|
+
# their annotations quadratic in the number of instance locations.
|
|
19
|
+
@evaluated_properties = Set.new
|
|
20
|
+
@evaluated_items = Set.new
|
|
17
21
|
end
|
|
18
22
|
|
|
19
23
|
def valid? = true
|
|
@@ -25,30 +29,30 @@ module Schemurai
|
|
|
25
29
|
end
|
|
26
30
|
|
|
27
31
|
def record_property(name)
|
|
28
|
-
@evaluated_properties
|
|
32
|
+
@evaluated_properties.add(name)
|
|
29
33
|
self
|
|
30
34
|
end
|
|
31
35
|
|
|
32
36
|
def record_item(index)
|
|
33
|
-
@evaluated_items
|
|
37
|
+
@evaluated_items.add(index)
|
|
34
38
|
self
|
|
35
39
|
end
|
|
36
40
|
|
|
37
41
|
def merge(other)
|
|
38
42
|
return Evaluation.invalid unless other.valid?
|
|
39
43
|
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
properties = other.evaluated_properties
|
|
45
|
+
items = other.evaluated_items
|
|
46
|
+
@evaluated_properties.merge(properties) unless properties.empty?
|
|
47
|
+
@evaluated_items.merge(items) unless items.empty?
|
|
42
48
|
self
|
|
43
49
|
end
|
|
44
|
-
|
|
45
|
-
private def merge_locations(target, locations)
|
|
46
|
-
locations.each { |location| target << location unless target.include?(location) }
|
|
47
|
-
end
|
|
48
50
|
end
|
|
49
51
|
private_constant :EvaluationBuffer
|
|
50
52
|
|
|
51
53
|
class Evaluator
|
|
54
|
+
include Internal::UniqueItems
|
|
55
|
+
|
|
52
56
|
MISSING_SEGMENT = Object.new.freeze
|
|
53
57
|
DECIMAL_CACHE_LIMIT = 16
|
|
54
58
|
|
|
@@ -552,17 +556,7 @@ module Schemurai
|
|
|
552
556
|
length = value.length
|
|
553
557
|
return false if rules.max_items && length > rules.max_items
|
|
554
558
|
return false if rules.min_items && length < rules.min_items
|
|
555
|
-
if rules.unique
|
|
556
|
-
index = 1
|
|
557
|
-
while index < length
|
|
558
|
-
previous_index = 0
|
|
559
|
-
while previous_index < index
|
|
560
|
-
return false if json_equal?(value[previous_index], value[index])
|
|
561
|
-
previous_index += 1
|
|
562
|
-
end
|
|
563
|
-
index += 1
|
|
564
|
-
end
|
|
565
|
-
end
|
|
559
|
+
return false if rules.unique && !unique_items?(value)
|
|
566
560
|
|
|
567
561
|
if (prefix_items = rules.prefix_items)
|
|
568
562
|
prefix_items.each_with_index do |child, index|
|
|
@@ -616,25 +610,10 @@ module Schemurai
|
|
|
616
610
|
|
|
617
611
|
add_error("minItems") { Internal::ErrorMessage.size("minItems", rules.min_items, value.length) }
|
|
618
612
|
end
|
|
619
|
-
if rules.unique
|
|
620
|
-
|
|
621
|
-
index = 1
|
|
622
|
-
while index < value.length && !duplicate
|
|
623
|
-
previous_index = 0
|
|
624
|
-
while previous_index < index
|
|
625
|
-
if json_equal?(value[previous_index], value[index])
|
|
626
|
-
duplicate = true
|
|
627
|
-
break
|
|
628
|
-
end
|
|
629
|
-
previous_index += 1
|
|
630
|
-
end
|
|
631
|
-
index += 1
|
|
632
|
-
end
|
|
633
|
-
if duplicate
|
|
634
|
-
return Evaluation.invalid unless @errors
|
|
613
|
+
if rules.unique && !unique_items?(value)
|
|
614
|
+
return Evaluation.invalid unless @errors
|
|
635
615
|
|
|
636
|
-
|
|
637
|
-
end
|
|
616
|
+
add_error("uniqueItems") { Internal::ErrorMessage.unique_items }
|
|
638
617
|
end
|
|
639
618
|
|
|
640
619
|
if (prefix_items = rules.prefix_items)
|
|
@@ -674,7 +653,7 @@ module Schemurai
|
|
|
674
653
|
matched = nil
|
|
675
654
|
index = 0
|
|
676
655
|
while index < value.length
|
|
677
|
-
(matched ||= []) << index if
|
|
656
|
+
(matched ||= []) << index if trial(contains, value[index]).valid?
|
|
678
657
|
index += 1
|
|
679
658
|
end
|
|
680
659
|
matched_count = matched ? matched.length : 0
|
|
@@ -895,7 +874,7 @@ module Schemurai
|
|
|
895
874
|
matches = 0
|
|
896
875
|
index = 0
|
|
897
876
|
while index < operand.length
|
|
898
|
-
result =
|
|
877
|
+
result = trial(operand[index], value)
|
|
899
878
|
if result.valid?
|
|
900
879
|
matches += 1
|
|
901
880
|
evaluation = evaluation.merge(result)
|
|
@@ -908,7 +887,7 @@ module Schemurai
|
|
|
908
887
|
matched_evaluation = nil
|
|
909
888
|
index = 0
|
|
910
889
|
while index < operand.length
|
|
911
|
-
result =
|
|
890
|
+
result = trial(operand[index], value)
|
|
912
891
|
if result.valid?
|
|
913
892
|
matches += 1
|
|
914
893
|
matched_evaluation = result
|
|
@@ -921,9 +900,9 @@ module Schemurai
|
|
|
921
900
|
add_error("oneOf") { Internal::ErrorMessage.one_of(matches) }
|
|
922
901
|
end
|
|
923
902
|
when :not
|
|
924
|
-
add_error("not") { Internal::ErrorMessage.not } if
|
|
903
|
+
add_error("not") { Internal::ErrorMessage.not } if trial(operand, value).valid?
|
|
925
904
|
when :conditional
|
|
926
|
-
condition =
|
|
905
|
+
condition = trial(operand.condition, value)
|
|
927
906
|
condition_valid = condition.valid?
|
|
928
907
|
evaluation = evaluation.merge(condition) if condition_valid
|
|
929
908
|
if condition_valid && operand.then_branch
|
|
@@ -938,12 +917,24 @@ module Schemurai
|
|
|
938
917
|
private def trial(program, value)
|
|
939
918
|
saved_errors = @errors
|
|
940
919
|
saved_count = @error_count
|
|
920
|
+
saved_instance_path = @instance_path
|
|
941
921
|
@errors = nil
|
|
942
922
|
@error_count = 0
|
|
923
|
+
# Trials retain annotations, but discard errors. Descendants therefore
|
|
924
|
+
# need neither instance nor schema paths until the trial has returned.
|
|
925
|
+
if saved_instance_path
|
|
926
|
+
saved_schema_path = @schema_path
|
|
927
|
+
@instance_path = nil
|
|
928
|
+
@schema_path = nil
|
|
929
|
+
end
|
|
943
930
|
evaluate(program, value)
|
|
944
931
|
ensure
|
|
945
932
|
@errors = saved_errors
|
|
946
933
|
@error_count = saved_count
|
|
934
|
+
if saved_instance_path
|
|
935
|
+
@instance_path = saved_instance_path
|
|
936
|
+
@schema_path = saved_schema_path
|
|
937
|
+
end
|
|
947
938
|
end
|
|
948
939
|
|
|
949
940
|
private def evaluate_at(program, instance, instance_segment, schema_segment, child_segment = MISSING_SEGMENT)
|
|
@@ -971,23 +962,6 @@ module Schemurai
|
|
|
971
962
|
@instance_path.pop unless instance_segment.equal?(MISSING_SEGMENT)
|
|
972
963
|
end
|
|
973
964
|
|
|
974
|
-
private def trial_at(program, instance, instance_segment, schema_segment, child_segment = MISSING_SEGMENT)
|
|
975
|
-
return trial(program, instance) unless @instance_path
|
|
976
|
-
|
|
977
|
-
trial_at_with_path(program, instance, instance_segment, schema_segment, child_segment)
|
|
978
|
-
end
|
|
979
|
-
|
|
980
|
-
private def trial_at_with_path(program, instance, instance_segment, schema_segment, child_segment)
|
|
981
|
-
@instance_path << instance_segment unless instance_segment.equal?(MISSING_SEGMENT)
|
|
982
|
-
@schema_path << schema_segment
|
|
983
|
-
@schema_path << child_segment unless child_segment.equal?(MISSING_SEGMENT)
|
|
984
|
-
trial(program, instance)
|
|
985
|
-
ensure
|
|
986
|
-
@schema_path.pop unless child_segment.equal?(MISSING_SEGMENT)
|
|
987
|
-
@schema_path.pop
|
|
988
|
-
@instance_path.pop unless instance_segment.equal?(MISSING_SEGMENT)
|
|
989
|
-
end
|
|
990
|
-
|
|
991
965
|
private def add_error(keyword, message = nil, append_keyword: true)
|
|
992
966
|
@error_count += 1
|
|
993
967
|
return false unless @errors
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: schemurai
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.2.
|
|
4
|
+
version: 2.2.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kuya KOHARA (oakcask)
|
|
@@ -53,6 +53,7 @@ files:
|
|
|
53
53
|
- lib/schemurai/schema_domain.rb
|
|
54
54
|
- lib/schemurai/schema_graph.rb
|
|
55
55
|
- lib/schemurai/schema_node.rb
|
|
56
|
+
- lib/schemurai/unique_items.rb
|
|
56
57
|
- lib/schemurai/version.rb
|
|
57
58
|
- lib/schemurai/vm/compiler.rb
|
|
58
59
|
- lib/schemurai/vm/evaluator.rb
|
|
@@ -63,7 +64,7 @@ licenses:
|
|
|
63
64
|
metadata:
|
|
64
65
|
homepage_uri: https://github.com/oakcask/schemurai
|
|
65
66
|
source_code_uri: https://github.com/oakcask/schemurai/tree/main
|
|
66
|
-
documentation_uri: https://rubydoc.info/gems/schemurai/2.2.
|
|
67
|
+
documentation_uri: https://rubydoc.info/gems/schemurai/2.2.3
|
|
67
68
|
rubygems_mfa_required: 'true'
|
|
68
69
|
rdoc_options:
|
|
69
70
|
- "--main"
|
|
@@ -87,7 +88,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
87
88
|
- !ruby/object:Gem::Version
|
|
88
89
|
version: '0'
|
|
89
90
|
requirements: []
|
|
90
|
-
rubygems_version: 4.0.
|
|
91
|
+
rubygems_version: 4.0.20
|
|
91
92
|
specification_version: 4
|
|
92
93
|
summary: A fast, Ractor-safe, pure Ruby JSON Schema Draft 7+ validator
|
|
93
94
|
test_files: []
|