canon 0.3.31 → 0.3.33

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: 7c7f349890b131b4280956b65c069369fee17b526db3ffacaa4560c3b3beb485
4
- data.tar.gz: dc2179259f0a7212de6b6aaddd50e61a32fba4627d4b853d60e00172cb819c10
3
+ metadata.gz: 6094f49ebe4d3b32a5396a0db4d62dbc502156d277ed12115029d50eb36f37ff
4
+ data.tar.gz: 8cf4e6d0b0e1feaaddaed2b996d98f9fd36adb4b250c3460ac2ae20a5263689a
5
5
  SHA512:
6
- metadata.gz: 6e3085a46f5ae85c542a419ae35b961529fc76d1f232d72f3599eb54b1f6588e0bbc9d8f1ffcdb6e31e61a5dcf4a3d8c884ddf0999f9a1f2062e22997640052a
7
- data.tar.gz: 3573d0da07ad60bd4aac8a3e3ffb149d61c400a2708c10d8dcca9280abae0dc12ac7d442786fd12be62ea584345e67dd2807ca63bc598578a4013e9159cd0ad7
6
+ metadata.gz: 9b53fdc9296da8e354cbb99cc194f6dab11544a52c1cda09370a01f413ce57dc5b04d47d99d496d9a3dcb9558f934dd75a5124be10406de74106e6750b86f51a
7
+ data.tar.gz: b8cf46fd5d9380584db39732281d18e7186c33927518ac46e5a42fec09d7d3cc5aad167ca1a85c879f087a54a934e8a329b879c43b4cc3aade63512503d0fe80
data/CLAUDE.md CHANGED
@@ -149,7 +149,7 @@ Engine A/B testing: `CANON_XML_BACKEND=nokogiri bundle exec rspec` (or `=moxml`
149
149
 
150
150
  ### YAML Engines
151
151
 
152
- `Canon::YamlBackend` selects the YAML engine: `:psych` (default) or `:yeptris` (FFI over libyeptris, the YAML counterpart of the leptris XML stack — `CANON_YAML_BACKEND=yeptris` opts in; the optional `yeptris` Gemfile group must be enabled). `Canon::YamlParsing` is the single gateway for string loads; `Canon::JsonParsing` mirrors it for JSON (the same yeptris engine serves both — `Yeptris::YAML.load` auto-detects JSON and routes to the native C-API materializer; JSON falls back to stdlib unless `YamlBackend.yeptris_native?`, since the FFI ladder is ~29x slower than the stdlib C extension). `YAML.dump` stays on Psych everywhere — canonical output bytes are canon's product and the writers differ. **JSON defaults to the strict yeptris surface whenever the native materializer is installed** (yeptris 0.1.13.4 ships platform gems — zero compilation, zero env; parity spec-pinned to `JSON.parse` upstream). The YAML default stays `:psych` until the yeptris Psych-parity gaps close (yeptris-ruby#30 sexagesimal scalars, #31 >64-bit integers #29 empty documents was fixed in 0.1.12); `spec/canon/yaml_engine_parity_spec.rb` is the executable gate, with upstream-tracked cases pending. Never `require "yeptris/psych"` — it rebinds the global `::Psych` constant for the whole process; only the namespaced `Yeptris::YAML` API is used.
152
+ `Canon::YamlBackend` selects the YAML engine: `:yeptris` (default Marshal bulk materialization in yeptris 0.1.28, 4.5–5x Psych load) or `:psych` (`CANON_YAML_BACKEND=psych` forces the stdlib engine). The default follows availability: yeptris when loadable, Psych otherwise. `Canon::YamlParsing` is the single gateway for string loads; `Canon::JsonParsing` mirrors it for JSON (the same yeptris engine serves both — `Yeptris::YAML.load` auto-detects JSON and routes to the native C-API materializer; JSON falls back to stdlib unless `YamlBackend.yeptris_native?`, since the FFI ladder is ~29x slower than the stdlib C extension). `YAML.dump` stays on Psych everywhere — canonical output bytes are canon's product and the writers differ. **JSON defaults to the strict yeptris surface whenever the native materializer is installed** (yeptris 0.1.13.4 ships platform gems — zero compilation, zero env; parity spec-pinned to `JSON.parse` upstream). The parity gate (`spec/canon/yaml_engine_parity_spec.rb`) ran clean after yeptris-ruby #29/#30/#31/#37 all closed (0.1.15.1), so YAML loads flip to yeptris by default; the spec stays as the executable gate for future yeptris releases. Never `require "yeptris/psych"` — it rebinds the global `::Psych` constant for the whole process; only the namespaced `Yeptris::YAML` API is used.
153
153
 
154
154
  ### Format Detection
155
155
 
@@ -199,10 +199,10 @@ module Canon
199
199
  content
200
200
  when :json
201
201
  # Parse JSON to Ruby object
202
- JSON.parse(content)
202
+ Canon::JsonParsing.parse(content)
203
203
  when :yaml
204
204
  # Parse YAML to Ruby object
205
- YAML.safe_load(content)
205
+ Canon::YamlParsing.safe_load(content)
206
206
  else
207
207
  abort "Error: Unsupported format '#{format}'"
208
208
  end
@@ -126,7 +126,7 @@ module Canon
126
126
 
127
127
  def load_yaml(path)
128
128
  content = File.read(path)
129
- YAML.safe_load(content, permitted_classes: [Symbol]) || {}
129
+ Canon::YamlParsing.safe_load(content, permitted_classes: [Symbol]) || {}
130
130
  end
131
131
  end
132
132
  end
@@ -17,7 +17,7 @@ module Canon
17
17
  # Return as-is if already parsed
18
18
  return yaml if yaml.is_a?(Hash) || yaml.is_a?(Array)
19
19
 
20
- Canon::YamlParsing.safe_load(yaml)
20
+ Canon::YamlParsing.safe_load(yaml, aliases: true)
21
21
  end
22
22
 
23
23
  def self.sort_yaml_keys(obj)
@@ -22,7 +22,7 @@ module Canon
22
22
  return if input.is_a?(Hash) || input.is_a?(Array) # Already parsed
23
23
  return if input.strip.empty?
24
24
 
25
- Canon::YamlParsing.safe_load(input)
25
+ Canon::YamlParsing.safe_load(input, aliases: true)
26
26
  rescue Psych::SyntaxError => e
27
27
  location = extract_location(e)
28
28
 
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.31"
4
+ VERSION = "0.3.33"
5
5
  end
@@ -164,6 +164,21 @@ module Canon
164
164
  elems1 = children1.select { |n| n.node_type == :element }
165
165
  elems2 = children2.select { |n| n.node_type == :element }
166
166
 
167
+ # FAST PATH: pairwise-corresponding children. When both lists
168
+ # are the same length and every position pairs elements with
169
+ # equal name, namespace URI, and identity value, the full
170
+ # matcher's output is exactly the positional pairing: unique
171
+ # identity keys pair same-with-same, duplicate keys pair
172
+ # last-with-last in the identity phase and the rest pair in
173
+ # order in the positional phase. No deleted/inserted results
174
+ # either way. Skip the identity/positional/class machinery.
175
+ if !elems1.empty? && elems1.length == elems2.length &&
176
+ pairwise_corresponding?(elems1, elems2)
177
+ record_positional_matches(elems1, elems2, path,
178
+ recursive: recursive)
179
+ return
180
+ end
181
+
167
182
  # Positions by identity: elems.index(elem) was an O(n) scan per
168
183
  # recorded match — O(n²) per level on element-heavy parents.
169
184
  positions1 = {}
@@ -263,6 +278,48 @@ module Canon
263
278
  end
264
279
  end
265
280
 
281
+ # True when every position pairs elements with equal name,
282
+ # namespace URI, and identity value (nil-safe).
283
+ def pairwise_corresponding?(elems1, elems2)
284
+ elems1.each_index.all? do |i|
285
+ e1 = elems1[i]
286
+ e2 = elems2[i]
287
+ e1.name == e2.name && e1.namespace_uri == e2.namespace_uri &&
288
+ extract_identity(e1) == extract_identity(e2)
289
+ end
290
+ end
291
+
292
+ # Record the positional pairing as :matched MatchResults,
293
+ # descending per pair in recursive mode (match_trees' ordering
294
+ # contract).
295
+ def record_positional_matches(elems1, elems2, path, recursive:)
296
+ elems1.each_index do |i|
297
+ elem1 = elems1[i]
298
+ elem_path = element_path(path, elem1)
299
+ @matches << MatchResult.new(
300
+ status: :matched,
301
+ elem1: elem1,
302
+ elem2: elems2[i],
303
+ path: elem_path,
304
+ pos1: i,
305
+ pos2: i,
306
+ )
307
+ if recursive
308
+ match_children(elem1.children, elems2[i].children, elem_path)
309
+ end
310
+ end
311
+ end
312
+
313
+ # Path segment for an element: expanded under a namespace,
314
+ # bare otherwise.
315
+ def element_path(path, elem)
316
+ if elem.namespace_uri && !elem.namespace_uri.empty?
317
+ path + ["{#{elem.namespace_uri}}#{elem.name}"]
318
+ else
319
+ path + [elem.name]
320
+ end
321
+ end
322
+
266
323
  # Match remaining elements by name and position
267
324
  def match_by_position(elems1, elems2, path, matched1, matched2,
268
325
  recursive:)
@@ -6,12 +6,12 @@ module Canon
6
6
  # stack).
7
7
  #
8
8
  # Mirrors XmlBackend's discipline (MECE — this module owns selection,
9
- # YamlParsing owns the calls). The default stays :psych until the
10
- # yeptris Psych-safe_load parity gaps close (yeptris-ruby#29 empty
11
- # documents crash, #30 sexagesimal scalars, #31 >64-bit integers);
12
- # yeptris is 3.1x faster at loading (measured, 2,000-item document),
13
- # so CANON_YAML_BACKEND=yeptris opts in early and the default flips
14
- # once the parity spec runs clean.
9
+ # YamlParsing owns the calls). yeptris loads 4.5–5x faster than Psych
10
+ # (0.1.28 Marshal materialization, 2,000-item document) and the
11
+ # Psych-safe_load parity spec runs clean the former gate-blockers
12
+ # (yeptris-ruby#29/#30/#31) are all fixed upstream. The default
13
+ # therefore follows availability: yeptris when loadable, Psych
14
+ # otherwise. CANON_YAML_BACKEND=psych forces the stdlib engine.
15
15
  #
16
16
  # Only the namespaced API (Yeptris::YAML) is ever used — requiring
17
17
  # "yeptris/psych" rebinds the global ::Psych constant for the whole
@@ -22,7 +22,7 @@ module Canon
22
22
  class << self
23
23
  def active
24
24
  @active ||= begin
25
- wanted = forced || :psych
25
+ wanted = forced || (yeptris_available? ? :yeptris : :psych)
26
26
  # A forced yeptris without a loadable gem/native lib must
27
27
  # degrade to Psych, not NameError in the gateway.
28
28
  wanted = :psych if wanted == :yeptris && !yeptris_available?
@@ -14,18 +14,17 @@ module Canon
14
14
  # data with Symbol/Date/Time built in (aliases resolve), so the
15
15
  # permitted-class list is inherently satisfied; parse failures are
16
16
  # normalized to Psych::SyntaxError so canon's rescues hold on both
17
- # engines.
17
+ # engines. Canon always resolves anchors (aliases: true at every
18
+ # call site) — a canonicalizer treats anchors as content: yeptris
19
+ # resolves them unconditionally, so :true keeps both engines
20
+ # behavior-identical.
18
21
  def safe_load(yaml, permitted_classes: [Symbol, Date, Time],
19
- aliases: false)
22
+ aliases: true)
20
23
  if YamlBackend.yeptris?
21
24
  begin
22
25
  ::Yeptris::YAML.load(yaml)
23
26
  rescue ::Yeptris::ParseError => e
24
27
  raise Psych::SyntaxError.new(nil, 0, 0, 0, e.message, "")
25
- rescue ::FFI::NullPointerError
26
- # yeptris-ruby#29: empty/comment-only input. Psych returns
27
- # nil; mirror that until the upstream fix lands.
28
- nil
29
28
  end
30
29
  else
31
30
  YAML.safe_load(yaml, permitted_classes: permitted_classes,
@@ -225,6 +225,13 @@ compare_against: nil)
225
225
  failure_means: "Slow formatting affects serialization performance. C14N is critical for digital signatures and XML canonicalization.",
226
226
  compare_against: "Previous branch (main).",
227
227
  },
228
+ data_comparison: {
229
+ name: "Data Comparison",
230
+ icon: "🧮",
231
+ description: "JSON and YAML semantic comparison. Both formats load through their engine gateways (yeptris/stdlib), so this referees engine choice.",
232
+ failure_means: "Slow data comparison affects validation pipelines and test suites. A regression here can also signal an engine lane flip gone wrong.",
233
+ compare_against: "Previous branch (main). Inputs are freshly generated (different values), so the comparison does real work.",
234
+ },
228
235
  }.freeze
229
236
 
230
237
  # Test definitions
@@ -265,6 +272,12 @@ compare_against: nil)
265
272
  { name: "JSON", method: :json_format, desc: "JSON formatting" },
266
273
  { name: "YAML", method: :yaml_format, desc: "YAML formatting" },
267
274
  ],
275
+ data_comparison: [
276
+ { name: "JSON", method: :json_compare_equivalent,
277
+ desc: "JSON equivalence" },
278
+ { name: "YAML", method: :yaml_compare_equivalent,
279
+ desc: "YAML equivalence" },
280
+ ],
268
281
  }.freeze
269
282
 
270
283
  # Test data generators
@@ -555,6 +568,14 @@ compare_against: nil)
555
568
  yaml = DataGenerator.generate_yaml(items: @items)
556
569
  data = YAML.safe_load(yaml, permitted_classes: [Time])
557
570
  measure { Canon.format_yaml(data) }
571
+ when :json_compare_equivalent
572
+ json1 = DataGenerator.generate_json(items: @items)
573
+ json2 = DataGenerator.generate_json(items: @items)
574
+ measure { Canon::Comparison.equivalent?(json1, json2, format: :json) }
575
+ when :yaml_compare_equivalent
576
+ yaml1 = DataGenerator.generate_yaml(items: @items)
577
+ yaml2 = DataGenerator.generate_yaml(items: @items)
578
+ measure { Canon::Comparison.equivalent?(yaml1, yaml2, format: :yaml) }
558
579
  else
559
580
  raise "Unknown benchmark: #{method}"
560
581
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: canon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.31
4
+ version: 0.3.33
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.