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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ad9362bcb75b8b6f51fb74a2d15858766793620d348fc4924746c44eaa210fc3
4
- data.tar.gz: ba30c2b4f45525b6953019dda82a3f4b1cbcc41405925e19c2f4550b58dfccd1
3
+ metadata.gz: c4620544f45920ada46eda1beff7cbb6461fb1633031f78d2964c4e963716d82
4
+ data.tar.gz: b0a66f4e369181d9182a10715d1622924ebac68cfe32a1169b844714a7b6a038
5
5
  SHA512:
6
- metadata.gz: 1773b839b4b8de56dc0f0087e1070f6236602f3ad9a5aec39bcebe2586c5632a45865a6326365bba505f49728d006e1c9c6dbe63f9f1075d00a3398241d953d0
7
- data.tar.gz: bfd5d96cd648547bfc1a97bd7d0f566ba5d86a4f39e8ceb89a27ebc536ce45ac8563614024a2d10c73cb6c61dd568a0f271069a7525510d6bd214de52ba53a3c
6
+ metadata.gz: b6dd9a631fa90e9600e0a60ba4e2ecfca492497a69376a11801cf1a4bfb799fb71eea3546f417b394279b7c3795ace0d2ff8422c8fa8c0b9c9cf6e7530ca8f86
7
+ data.tar.gz: b8b2e1bdaccdfc420b1aa2cec9fc2bef856157d450ccb547dcf230693b4b959baa77f9901b9df920c5c73bd6ef982b468249655397c0160e55c3b57772b1c2c5
@@ -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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Schemurai
4
4
  # The installed Schemurai version.
5
- VERSION = "2.2.1"
5
+ VERSION = "2.2.3"
6
6
  end
@@ -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
- @evaluated_properties = []
16
- @evaluated_items = []
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 << name unless @evaluated_properties.include?(name)
32
+ @evaluated_properties.add(name)
29
33
  self
30
34
  end
31
35
 
32
36
  def record_item(index)
33
- @evaluated_items << index unless @evaluated_items.include?(index)
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
- merge_locations(@evaluated_properties, other.evaluated_properties)
41
- merge_locations(@evaluated_items, other.evaluated_items)
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
- duplicate = false
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
- add_error("uniqueItems") { Internal::ErrorMessage.unique_items }
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 trial_at(contains, value[index], index, "contains").valid?
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 = trial_at(operand[index], value, MISSING_SEGMENT, "anyOf", index)
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 = trial_at(operand[index], value, MISSING_SEGMENT, "oneOf", index)
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 trial_at(operand, value, MISSING_SEGMENT, "not").valid?
903
+ add_error("not") { Internal::ErrorMessage.not } if trial(operand, value).valid?
925
904
  when :conditional
926
- condition = trial_at(operand.condition, value, MISSING_SEGMENT, "if")
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.1
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.1
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.16
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: []