canon 0.3.114 → 0.3.116

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: e1fba0743a7127443ef7837004ae95b97be4760617237be10a63de01147e6812
4
- data.tar.gz: 7853cf8424906ab05947560615db5d3c3ff5620564a86ee38b8fe19f306e8dac
3
+ metadata.gz: 89df5f303938b98100a1d44399c7eb5fbbf793f0f91c445c4f0533a19234d5ec
4
+ data.tar.gz: 1308c9228b0a6efc15e81ebef134c7495aadd100e10978a23ee09b92ad558555
5
5
  SHA512:
6
- metadata.gz: 4b222586fe5831e76e443fa61bb4ee0601c31e5c048f3a9e39a37649437c738f03b9be5f89e7b97a823f311245a1ce1275cf215649a07758307715c86242aecb
7
- data.tar.gz: 32d38e3eaaa8703697f2e14ff7610f44b76a69ba8d7ff18a4951d57f0d36ba83d9e02525526643103c2d62f37bd37cc0f7174d707a1fb96d87de262430251bad
6
+ metadata.gz: 93cfb6bca734697c5cc79173dac20bbe53cb4fb17e72fc72e7aa726af366c187c09ae0c30e5a775d9ea8b9e94f1194b29657f9da3d44c58f6fbc116717e39909
7
+ data.tar.gz: 37f713fb17fe7961cb622f42c96605cf0bb19227a6831018d0505afdc33ad2e315e54b1a65145219457e30d8b0c6d06d001869611bbe94f55b443c2cbf0e4252
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canon
4
+ module Comparison
5
+ # W5.1 validate-then-compare: runs both documents through the
6
+ # engine's XSD assessment before the diff. Returns a hash of
7
+ # side-index (0 or 1) to Canon::ValidationError — empty when both
8
+ # documents pass and the comparison may proceed.
9
+ module SchemaGate
10
+ module_function
11
+
12
+ # @param obj1 [String] first XML document
13
+ # @param obj2 [String] second XML document
14
+ # @param schema [String, Leptris::XML::XSD::Schema] schema text
15
+ # (compiled once here) or a compiled schema
16
+ # @return [Hash{Integer=>Canon::ValidationError}]
17
+ def gate(obj1, obj2, schema)
18
+ schema = Canon::Validators::XsdValidator.compile(schema) if schema.is_a?(String)
19
+ failures = {}
20
+ [obj1, obj2].each_with_index do |obj, side|
21
+ Canon::Validators::XsdValidator.validate!(obj, schema: schema)
22
+ rescue Canon::ValidationError => e
23
+ failures[side] = e
24
+ end
25
+ failures
26
+ end
27
+ end
28
+ end
29
+ end
@@ -95,6 +95,7 @@ module Canon
95
95
  autoload :DiffNodeBuilder, "canon/comparison/diff_node_builder"
96
96
  autoload :Dimensions, "canon/comparison/dimensions"
97
97
  autoload :FormatDetector, "canon/comparison/format_detector"
98
+ autoload :SchemaGate, "canon/comparison/schema_gate"
98
99
  autoload :HtmlComparator, "canon/comparison/html_comparator"
99
100
  autoload :HtmlCompareProfile, "canon/comparison/html_compare_profile"
100
101
  autoload :HtmlParser, "canon/comparison/html_parser"
@@ -246,6 +247,33 @@ module Canon
246
247
  :semantic_tree))
247
248
  end
248
249
 
250
+ # W5.1 schema gate: when a schema rides the options, both XML
251
+ # documents must pass the engine's XSD assessment BEFORE the
252
+ # diff runs — a validation failure is normative regardless of
253
+ # what the tree comparison would say.
254
+ if (xsd = opts[:xsd])
255
+ gate = SchemaGate.gate(obj1, obj2, xsd)
256
+ unless gate.empty?
257
+ diffs = gate.map do |side, error|
258
+ node = Diff::DiffNode.new(node1: side.zero? ? obj1 : obj2,
259
+ node2: side.zero? ? obj2 : obj1,
260
+ dimension: :schema_validation,
261
+ reason: error.message)
262
+ node.normative = true # a schema failure is normative by definition
263
+ node
264
+ end
265
+ return false unless opts[:verbose]
266
+
267
+ return ComparisonResult.new(
268
+ differences: diffs,
269
+ preprocessed_strings: [obj1.to_s, obj2.to_s],
270
+ format: opts[:format] || :xml,
271
+ algorithm: :dom,
272
+ original_strings: [obj1.to_s, obj2.to_s],
273
+ )
274
+ end
275
+ end
276
+
249
277
  if %i[semantic semantic_tree].include?(opts[:diff_algorithm])
250
278
  return semantic_diff(obj1, obj2, opts)
251
279
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module Canon
6
+ # W5.3 — canonical digests: the hash of a document's canonical form
7
+ # as a first-class API for deduplication and change detection.
8
+ #
9
+ # The digest is taken over the format's CANONICAL form — the form
10
+ # under which the comparison pipeline deems documents equivalent:
11
+ # XML rides Canonical XML 1.1 (attribute order sorted,
12
+ # namespace-normalized); JSON/YAML ride the key-sorted forms; HTML
13
+ # rides the whitespace-law form with sorted attributes. Two
14
+ # documents that compare equivalent under the canonical form
15
+ # produce identical digests. The digests are stable across releases
16
+ # for as long as the canonical form is (the digest-stability suite
17
+ # pins the bytes per release pair).
18
+ module Digest
19
+ DEFAULT_ALGORITHM = "sha256"
20
+
21
+ class << self
22
+ # @param content [String] the document
23
+ # @param format [Symbol] the format (see SUPPORTED_FORMATS)
24
+ # @param algorithm [String, Symbol] any OpenSSL digest name
25
+ # @return [String] the hex digest of the canonical form
26
+ def digest(content, format, algorithm: DEFAULT_ALGORITHM)
27
+ OpenSSL::Digest.new(algorithm.to_s)
28
+ .hexdigest(canonical_form(content, format))
29
+ end
30
+
31
+ # The canonical form the digest rides: XML's is C14N (the
32
+ # pretty-printed form preserves attribute order and is NOT
33
+ # canonical); every other format's formatter output IS the
34
+ # canonical form.
35
+ def canonical_form(content, format)
36
+ if format.to_sym == :xml
37
+ Formatters::XmlFormatter.format(content, pretty: false)
38
+ else
39
+ Canon.format(content, format)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "leptris" # the XSD lane rides the engine's tier-1 validator
4
+ # (libleptris >= 1.9.321, leptris#1075)
5
+
6
+ module Canon
7
+ module Validators
8
+ # XSD validation (W5.1) through the engine's tier-1 validator:
9
+ # schema compilation, whole-instance assessment, enumerated
10
+ # failures. The schema is a compiled `Leptris::XML::XSD::Schema`
11
+ # — callers validating many documents against one schema compile
12
+ # once and reuse (compilation is the expensive step).
13
+ #
14
+ # schema = Canon::Validators::XsdValidator.compile(schema_xml)
15
+ # Canon::Validators::XsdValidator.validate!(xml, schema: schema)
16
+ class XsdValidator < BaseValidator
17
+ # Compile a schema from XSD text. Raises Canon::ValidationError
18
+ # when the text is not well-formed XML, and when the engine
19
+ # reports schema-level problems.
20
+ #
21
+ # @param schema_xml [String] the XSD document
22
+ # @return [Leptris::XML::XSD::Schema] the compiled schema
23
+ def self.compile(schema_xml)
24
+ schema = Leptris::XML::XSD.compile(schema_xml)
25
+ if (detail = schema.error)
26
+ raise Canon::ValidationError.new(
27
+ "schema invalid: #{detail}",
28
+ format: :xml,
29
+ )
30
+ end
31
+ schema
32
+ end
33
+
34
+ # Validate an XML instance against a compiled schema
35
+ #
36
+ # @param input [String] the XML instance
37
+ # @param schema [Leptris::XML::XSD::Schema] a compiled schema
38
+ # @raise [Canon::ValidationError] if the instance fails
39
+ # assessment; the message carries the first failure, details
40
+ # carry the full enumerated list
41
+ # @return [void]
42
+ def self.validate!(input, schema:)
43
+ return if input.nil? || input.strip.empty?
44
+
45
+ doc = Leptris::XML.parse(input, readonly: true)
46
+ errors = schema.validate_errors(doc)
47
+ return if errors.empty?
48
+
49
+ raise Canon::ValidationError.new(
50
+ errors.first.to_s,
51
+ format: :xml,
52
+ details: errors.join("\n"),
53
+ )
54
+ end
55
+ end
56
+ end
57
+ end
@@ -7,6 +7,7 @@ module Canon
7
7
  autoload :HtmlValidator, "canon/validators/html_validator"
8
8
  autoload :JsonValidator, "canon/validators/json_validator"
9
9
  autoload :XmlValidator, "canon/validators/xml_validator"
10
+ autoload :XsdValidator, "canon/validators/xsd_validator"
10
11
  autoload :YamlValidator, "canon/validators/yaml_validator"
11
12
  end
12
13
  end
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.114"
4
+ VERSION = "0.3.116"
5
5
  end
data/lib/canon.rb CHANGED
@@ -32,6 +32,7 @@ module Canon
32
32
  autoload :Cli, "canon/cli"
33
33
  autoload :ColorDetector, "canon/color_detector"
34
34
  autoload :DiffFormatter, "canon/diff_formatter"
35
+ autoload :Digest, "canon/digest"
35
36
  autoload :Json, "canon/json"
36
37
 
37
38
  SUPPORTED_FORMATS = %i[xml yaml json html html4 html5 string].freeze
@@ -84,5 +85,15 @@ module Canon
84
85
  define_singleton_method("format_#{format}") do |content|
85
86
  format(content, format)
86
87
  end
88
+
89
+ define_singleton_method("digest_#{format}") do |content, algorithm: Canon::Digest::DEFAULT_ALGORITHM|
90
+ Canon::Digest.digest(content, format, algorithm: algorithm)
91
+ end
92
+ end
93
+
94
+ # The canonical digest of a document (W5.3): the hash of its
95
+ # canonical form. See Canon::Digest for the stability contract.
96
+ def self.digest(content, format = :xml, algorithm: Canon::Digest::DEFAULT_ALGORITHM)
97
+ Canon::Digest.digest(content, format, algorithm: algorithm)
87
98
  end
88
99
  end
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.114
4
+ version: 0.3.116
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-10-23 00:00:00.000000000 Z
11
+ date: 2026-10-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs
@@ -267,6 +267,7 @@ files:
267
267
  - lib/canon/comparison/pipeline.rb
268
268
  - lib/canon/comparison/profile_definition.rb
269
269
  - lib/canon/comparison/ruby_object_comparator.rb
270
+ - lib/canon/comparison/schema_gate.rb
270
271
  - lib/canon/comparison/strategies.rb
271
272
  - lib/canon/comparison/strategies/base_match_strategy.rb
272
273
  - lib/canon/comparison/strategies/match_strategy_factory.rb
@@ -338,6 +339,7 @@ files:
338
339
  - lib/canon/diff_formatter/legend.rb
339
340
  - lib/canon/diff_formatter/pretty_diff_formatter.rb
340
341
  - lib/canon/diff_formatter/theme.rb
342
+ - lib/canon/digest.rb
341
343
  - lib/canon/errors.rb
342
344
  - lib/canon/formatters.rb
343
345
  - lib/canon/formatters/html4_formatter.rb
@@ -404,6 +406,7 @@ files:
404
406
  - lib/canon/validators/html_validator.rb
405
407
  - lib/canon/validators/json_validator.rb
406
408
  - lib/canon/validators/xml_validator.rb
409
+ - lib/canon/validators/xsd_validator.rb
407
410
  - lib/canon/validators/yaml_validator.rb
408
411
  - lib/canon/version.rb
409
412
  - lib/canon/xml.rb