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 +4 -4
- data/CLAUDE.md +1 -1
- data/lib/canon/commands/diff_command.rb +2 -2
- data/lib/canon/config/profile_loader.rb +1 -1
- data/lib/canon/formatters/yaml_formatter.rb +1 -1
- data/lib/canon/validators/yaml_validator.rb +1 -1
- data/lib/canon/version.rb +1 -1
- data/lib/canon/xml/element_matcher.rb +57 -0
- data/lib/canon/yaml_backend.rb +7 -7
- data/lib/canon/yaml_parsing.rb +5 -6
- data/lib/tasks/benchmark_runner.rb +21 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6094f49ebe4d3b32a5396a0db4d62dbc502156d277ed12115029d50eb36f37ff
|
|
4
|
+
data.tar.gz: 8cf4e6d0b0e1feaaddaed2b996d98f9fd36adb4b250c3460ac2ae20a5263689a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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: `:
|
|
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
|
-
|
|
202
|
+
Canon::JsonParsing.parse(content)
|
|
203
203
|
when :yaml
|
|
204
204
|
# Parse YAML to Ruby object
|
|
205
|
-
|
|
205
|
+
Canon::YamlParsing.safe_load(content)
|
|
206
206
|
else
|
|
207
207
|
abort "Error: Unsupported format '#{format}'"
|
|
208
208
|
end
|
|
@@ -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
|
@@ -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:)
|
data/lib/canon/yaml_backend.rb
CHANGED
|
@@ -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).
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# yeptris
|
|
13
|
-
#
|
|
14
|
-
#
|
|
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?
|
data/lib/canon/yaml_parsing.rb
CHANGED
|
@@ -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:
|
|
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
|