Problem Statement
Canon library currently has limitations when comparing XML with:
- Whitespace variations in mixed content (semantically equivalent but textually different)
- Non-UTF-8 encodings (Shift_JIS, ISO-8859-1) that fail Nokogiri parsing
These limitations cause false negative comparisons in lutaml-model tests.
Use Cases
Use Case 1: Whitespace Normalization
Scenario: Different XML adapters normalize whitespace differently
<!-- Input 1 (Nokogiri) -->
<Root>
Text <bold>B</bold> more
</Root>
<!-- Input 2 (Ox - normalized) -->
<Root>Text <bold>B</bold> more</Root>
Current Behavior: Canon reports as DIFFERENT
Expected Behavior: Canon reports as EQUIVALENT (whitespace has no semantic meaning without xml:space="preserve")
Use Case 2: Encoding-Agnostic Comparison
Scenario: Compare XML in different encodings
xml1 = "<root>手書き</root>".encode("UTF-8")
xml2 = "<root>手書き</root>".encode("Shift_JIS")
Canon.equivalent?(xml1, xml2) # Should be true
Current Behavior: Fails with encoding error
Expected Behavior: Transcodes to common encoding for comparison
Proposed Enhancements
Enhancement 1: Whitespace Normalization Option
API Design:
Canon::Comparison.equivalent?(xml1, xml2,
normalize_whitespace: true # New option
)
Implementation:
class Canon::Xml::DataModel
def normalize_whitespace!
# Normalize text nodes: collapse consecutive whitespace
text_nodes.each do |node|
next if node.parent.attribute('xml:space')&.value == 'preserve'
node.content = node.content.gsub(/\s+/, ' ').strip
end
end
end
W3C Compliance:
- Respects
xml:space="preserve" attribute
- Only normalizes where semantically safe
- Follows XML 1.0 specification section 2.10
Enhancement 2: Encoding Normalization
API Design:
Canon::Comparison.equivalent?(xml1, xml2,
normalize_encoding: 'UTF-8' # Transcode to this before comparison
)
Implementation:
class Canon::XmlComparator
def normalize_encoding(xml, target_encoding)
return xml if xml.encoding.to_s == target_encoding
# Transcode to target encoding
xml.encode(target_encoding,
invalid: :replace,
undef: :replace,
replace: '?'
)
end
end
Enhancement 3: Comparison Profiles
API Design:
# Predefined comparison profiles
Canon::Comparison.equivalent?(xml1, xml2, profile: :strict) # Current behavior
Canon::Comparison.equivalent?(xml1, xml2, profile: :semantic) # Whitespace + encoding normalized
Canon::Comparison.equivalent?(xml1, xml2, profile: :structural) # Structure only, ignore text
Profiles:
module Canon
module Profiles
STRICT = {
normalize_whitespace: false,
normalize_encoding: nil,
ignore_text_order: false
}
SEMANTIC = {
normalize_whitespace: true,
normalize_encoding: 'UTF-8',
ignore_text_order: false
}
STRUCTURAL = {
normalize_whitespace: true,
normalize_encoding: 'UTF-8',
ignore_text_order: true
}
end
end
Implementation Plan
Phase 1: Whitespace Normalization (2-3 hours)
- Add
normalize_whitespace option to Canon::Comparison
- Implement text node normalization in
Canon::Xml::DataModel
- Add test coverage for mixed content scenarios
- Update documentation
Phase 2: Encoding Normalization (1-2 hours)
- Add
normalize_encoding option
- Implement safe transcoding with fallbacks
- Add test coverage for multi-byte encodings
- Document encoding handling
Phase 3: Profiles (1 hour)
- Define profile constants
- Implement profile loading
- Add convenience methods
- Update RSpec matchers
Phase 4: Documentation (1 hour)
- Update README with new options
- Add encoding guide
- Add whitespace handling guide
- Provide migration examples
Total Estimate: 5-7 hours
Backward Compatibility
All enhancements are opt-in via options:
- Default behavior unchanged (strict comparison)
- Existing code continues to work
- New options accessible via keyword arguments
No breaking changes.
Alternative: Workaround in lutaml-model
Instead of enhancing Canon, lutaml-model could:
-
Pre-normalize before comparison:
def normalize_for_comparison(xml)
# Manually normalize whitespace
xml.gsub(/\s+/, ' ').gsub(/>\s+</, '><')
end
-
Skip canon for encoding tests:
if encoding != 'UTF-8'
expect(serialized.bytes).to eq(xml.bytes)
else
expect(serialized).to be_xml_equivalent_to(xml)
end
-
Adapter-specific expectations:
case adapter
when OxAdapter
expect(serialized).to eq(normalized_xml)
else
expect(serialized).to be_xml_equivalent_to(xml)
end
Trade-off: Workarounds are faster short-term but reduce test quality and maintainability.
Recommendation
Implement workarounds in lutaml-model now (pragmatic):
- Fixes immediate test failures
- Doesn't block lutaml-model development
- Low risk
Propose Canon enhancement for future (strategic):
- Benefits all Canon users
- Improves library capabilities
- Better long-term solution
Action Items:
- ✅ Document workarounds in lutaml-model tests
- ✅ Create this proposal for Canon maintainers
- ⏸️ Defer Canon implementation to separate project
- ✅ Use workarounds to achieve 0 lutaml-model failures
References
Appendix: Test Cases
Test Case 1: Whitespace Variations
RSpec.describe "Canon whitespace handling" do
it "recognizes whitespace-normalized XML as equivalent" do
xml1 = "<root>\n <text>Hello</text>\n</root>"
xml2 = "<root><text>Hello</text></root>"
expect(Canon.equivalent?(xml1, xml2, normalize_whitespace: true)).to be true
end
it "preserves whitespace when xml:space='preserve'" do
xml1 = "<root xml:space='preserve'>\n <text>Hello</text>\n</root>"
xml2 = "<root xml:space='preserve'><text>Hello</text></root>"
expect(Canon.equivalent?(xml1, xml2, normalize_whitespace: true)).to be false
end
end
Test Case 2: Encoding Normalization
RSpec.describe "Canon encoding handling" do
it "compares XML in different encodings" do
xml1 = "<root>日本語</root>".encode("UTF-8")
xml2 = "<root>日本語</root>".encode("Shift_JIS")
expect(Canon.equivalent?(xml1, xml2, normalize_encoding: 'UTF-8')).to be true
end
end
Problem Statement
Canon library currently has limitations when comparing XML with:
These limitations cause false negative comparisons in lutaml-model tests.
Use Cases
Use Case 1: Whitespace Normalization
Scenario: Different XML adapters normalize whitespace differently
Current Behavior: Canon reports as DIFFERENT
Expected Behavior: Canon reports as EQUIVALENT (whitespace has no semantic meaning without
xml:space="preserve")Use Case 2: Encoding-Agnostic Comparison
Scenario: Compare XML in different encodings
Current Behavior: Fails with encoding error
Expected Behavior: Transcodes to common encoding for comparison
Proposed Enhancements
Enhancement 1: Whitespace Normalization Option
API Design:
Implementation:
W3C Compliance:
xml:space="preserve"attributeEnhancement 2: Encoding Normalization
API Design:
Implementation:
Enhancement 3: Comparison Profiles
API Design:
Profiles:
Implementation Plan
Phase 1: Whitespace Normalization (2-3 hours)
normalize_whitespaceoption toCanon::ComparisonCanon::Xml::DataModelPhase 2: Encoding Normalization (1-2 hours)
normalize_encodingoptionPhase 3: Profiles (1 hour)
Phase 4: Documentation (1 hour)
Total Estimate: 5-7 hours
Backward Compatibility
All enhancements are opt-in via options:
No breaking changes.
Alternative: Workaround in lutaml-model
Instead of enhancing Canon, lutaml-model could:
Pre-normalize before comparison:
Skip canon for encoding tests:
Adapter-specific expectations:
Trade-off: Workarounds are faster short-term but reduce test quality and maintainability.
Recommendation
Implement workarounds in lutaml-model now (pragmatic):
Propose Canon enhancement for future (strategic):
Action Items:
References
Appendix: Test Cases
Test Case 1: Whitespace Variations
Test Case 2: Encoding Normalization