canon 0.3.37 → 0.3.39

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: fc8ab08578b3b1ceed40cb8f14ee8e83fdb116d26214a4a546789108950802dc
4
- data.tar.gz: 435c4f9300160c8b502b8441096b2eea9aec0e182f025e84789f8f0fd492451d
3
+ metadata.gz: 6b65514ddc21b35416e7856a21f17f084cde6f6176a0f84dd8ee03a3ec7c8604
4
+ data.tar.gz: e6390a45d84dc78708aa3f043f053c41bba8b68c30170b11a58282581365c09d
5
5
  SHA512:
6
- metadata.gz: a9c49d99613a801415eec95e663caa577319e9c4ce63546c1708c0599c891289e225c415279131c2652f83e9cec17b225c15f0bbc620da1a1237cc321eb2d381
7
- data.tar.gz: fa8e1ea33c6527b7a215c0b8e097f4266d3db0cc00e865bc39c883b8b3de8a8683533beb66bccf1538d89059aeb348a716c5c1d9fb2b4eefd14d257b23cc993c
6
+ metadata.gz: 338db190efc246cb7a23f06b4a6f3dc620e969e3029cc2384f52bf8b082f6903dd21883b29316bcff676bb36f446e1fcad8cab434d8590f40b57a32a6d56baca
7
+ data.tar.gz: 602303bc515970ac9085e1a5184d8684604b80c5047d65cc3bf6bb6aa2854fdce1bd9eb3a61b4ee8e0f0c74f3f347d63d62133c9e8df93b51d146eca67807da9
@@ -29,9 +29,7 @@ module Canon
29
29
  # difference exists (strict key_order compares insertion
30
30
  # order, which keys == keys preserves). Most object pairs in
31
31
  # similar documents hit this and skip the path-building walk.
32
- if obj1 == obj2 && (!obj1.is_a?(Hash) || obj1.keys == obj2.keys)
33
- return Comparison::EQUIVALENT
34
- end
32
+ return Comparison::EQUIVALENT if deep_equal?(obj1, obj2)
35
33
 
36
34
  case obj1
37
35
  when Hash
@@ -68,8 +66,16 @@ module Canon
68
66
  # Sort keys if order should be ignored (based on match options)
69
67
  match_opts = opts[:match_opts]
70
68
  if match_opts && match_opts[:key_order] != :strict
71
- keys1 = keys1.sort_by(&:to_s)
72
- keys2 = keys2.sort_by(&:to_s)
69
+ # Identical key arrays: one sort replaces two (the sorted
70
+ # forms are trivially equal, and the set algebra below
71
+ # degenerates — missing/extra empty, common = the sorted
72
+ # keys themselves).
73
+ if keys1 == keys2
74
+ keys1 = keys2 = keys1.sort_by(&:to_s)
75
+ else
76
+ keys1 = keys1.sort_by(&:to_s)
77
+ keys2 = keys2.sort_by(&:to_s)
78
+ end
73
79
  elsif match_opts && match_opts[:key_order] == :strict
74
80
  # Strict mode: key order matters
75
81
  # Check if keys are in same order
@@ -85,8 +91,8 @@ module Canon
85
91
  end
86
92
 
87
93
  # Check for missing keys
88
- missing_in_second = keys1 - keys2
89
- missing_in_first = keys2 - keys1
94
+ missing_in_second = keys1 == keys2 ? EMPTY_KEYS : keys1 - keys2
95
+ missing_in_first = keys1 == keys2 ? EMPTY_KEYS : keys2 - keys1
90
96
 
91
97
  missing_in_second.each do |key|
92
98
  key_path = path.empty? ? key.to_s : "#{path}.#{key}"
@@ -102,13 +108,19 @@ module Canon
102
108
 
103
109
  has_missing_keys = !missing_in_first.empty? || !missing_in_second.empty?
104
110
 
105
- # Compare common keys
106
- common_keys = keys1 & keys2
111
+ # Compare common keys. The path string is built lazily: deep-equal
112
+ # pairs (the overwhelming majority in similar documents) answer
113
+ # equivalent without ever needing it.
114
+ common_keys = keys1 == keys2 ? keys1 : keys1 & keys2
107
115
  all_equivalent = true
108
116
  common_keys.each do |key|
117
+ value1 = hash1[key]
118
+ value2 = hash2[key]
119
+ next if deep_equal?(value1, value2)
120
+
109
121
  key_path = path.empty? ? key.to_s : "#{path}.#{key}"
110
- result = compare_objects(hash1[key], hash2[key], opts,
111
- differences, key_path)
122
+ result = compare_objects(value1, value2, opts, differences,
123
+ key_path)
112
124
  all_equivalent = false unless result == Comparison::EQUIVALENT
113
125
  end
114
126
 
@@ -138,6 +150,8 @@ module Canon
138
150
  all_equivalent = true
139
151
  arr1.each_with_index do |elem1, index|
140
152
  elem2 = arr2[index]
153
+ next if deep_equal?(elem1, elem2)
154
+
141
155
  elem_path = "#{path}[#{index}]"
142
156
  result = compare_objects(elem1, elem2, opts, differences,
143
157
  elem_path)
@@ -166,6 +180,16 @@ module Canon
166
180
  end
167
181
  end
168
182
 
183
+ # Deep-equal with identical key order — compare_objects returns
184
+ # EQUIVALENT without consulting the path for such pairs, which
185
+ # is what makes lazy path construction in the hash/array walks
186
+ # behavior-identical.
187
+ def self.deep_equal?(obj1, obj2)
188
+ obj1 == obj2 && (!obj1.is_a?(Hash) || obj1.keys == obj2.keys)
189
+ end
190
+
191
+ EMPTY_KEYS = [].freeze
192
+
169
193
  # Add a Ruby object difference
170
194
  #
171
195
  # @param path [String] Path to the difference
data/lib/canon/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Canon
4
- VERSION = "0.3.37"
4
+ VERSION = "0.3.39"
5
5
  end
@@ -78,7 +78,7 @@ module Canon
78
78
  return nil unless root
79
79
 
80
80
  skeleton = doc.children.filter_map do |child|
81
- next if child.equal?(root)
81
+ next if Canon::XmlParsing.same_engine_node?(child, root)
82
82
 
83
83
  case child
84
84
  when Moxml::Comment, Moxml::ProcessingInstruction then child.to_s
@@ -149,7 +149,7 @@ module Canon
149
149
  def add_document_children(root, children, document_element,
150
150
  skip_types = [])
151
151
  children.each do |child|
152
- next if child.equal?(document_element)
152
+ next if Canon::XmlParsing.same_engine_node?(child, document_element)
153
153
  next if skip_types.any? { |type| child.is_a?(type) }
154
154
 
155
155
  node = yield child
@@ -63,6 +63,33 @@ module Canon
63
63
  end
64
64
  end
65
65
 
66
+ # Same underlying engine node? Wrapper identity first (correct
67
+ # everywhere wrappers are unified). Fallback: moxml 0.5.36-0.5.39
68
+ # wrap Document#root and Document#children entries in DIFFERENT
69
+ # wrapper classes for one node (NativeNode vs the FFI Element —
70
+ # moxml#216), so identity fails exactly where consumers exclude
71
+ # the document element from children; the shared C pointer
72
+ # address is the stable identity. Inert once wrappers unify.
73
+ def same_engine_node?(left, right)
74
+ return true if left.equal?(right)
75
+ return false unless defined?(::Moxml::Node) &&
76
+ left.is_a?(::Moxml::Node) && right.is_a?(::Moxml::Node)
77
+
78
+ address_left = moxml_node_address(left)
79
+ address_right = moxml_node_address(right)
80
+ !address_left.nil? && address_left == address_right
81
+ end
82
+
83
+ def moxml_node_address(wrapper)
84
+ native = wrapper.native
85
+ case native
86
+ when ::Leptris::XML::NativeNode then native.address
87
+ when ::Leptris::XML::Node then native.c_ptr.address
88
+ end
89
+ rescue StandardError
90
+ nil
91
+ end
92
+
66
93
  # --- Type checks (any recognized engine node) ---
67
94
 
68
95
  def document?(obj)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: canon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.37
4
+ version: 0.3.39
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-09-14 00:00:00.000000000 Z
11
+ date: 2026-09-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 0.5.0
47
+ version: 0.5.41
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: 0.5.0
54
+ version: 0.5.41
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: nokogiri
57
57
  requirement: !ruby/object:Gem::Requirement