canon 0.1.7 → 0.1.9

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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +69 -92
  3. data/README.adoc +13 -13
  4. data/docs/.lycheeignore +69 -0
  5. data/docs/Gemfile +1 -0
  6. data/docs/_config.yml +90 -1
  7. data/docs/advanced/diff-classification.adoc +82 -2
  8. data/docs/advanced/extending-canon.adoc +193 -0
  9. data/docs/features/match-options/index.adoc +239 -1
  10. data/docs/internals/diffnode-enrichment.adoc +611 -0
  11. data/docs/internals/index.adoc +251 -0
  12. data/docs/lychee.toml +13 -6
  13. data/docs/understanding/architecture.adoc +749 -33
  14. data/docs/understanding/comparison-pipeline.adoc +122 -0
  15. data/lib/canon/cache.rb +129 -0
  16. data/lib/canon/comparison/dimensions/attribute_order_dimension.rb +68 -0
  17. data/lib/canon/comparison/dimensions/attribute_presence_dimension.rb +68 -0
  18. data/lib/canon/comparison/dimensions/attribute_values_dimension.rb +171 -0
  19. data/lib/canon/comparison/dimensions/base_dimension.rb +107 -0
  20. data/lib/canon/comparison/dimensions/comments_dimension.rb +121 -0
  21. data/lib/canon/comparison/dimensions/element_position_dimension.rb +90 -0
  22. data/lib/canon/comparison/dimensions/registry.rb +77 -0
  23. data/lib/canon/comparison/dimensions/structural_whitespace_dimension.rb +119 -0
  24. data/lib/canon/comparison/dimensions/text_content_dimension.rb +96 -0
  25. data/lib/canon/comparison/dimensions.rb +54 -0
  26. data/lib/canon/comparison/format_detector.rb +87 -0
  27. data/lib/canon/comparison/html_comparator.rb +70 -26
  28. data/lib/canon/comparison/html_compare_profile.rb +8 -2
  29. data/lib/canon/comparison/html_parser.rb +80 -0
  30. data/lib/canon/comparison/json_comparator.rb +12 -0
  31. data/lib/canon/comparison/json_parser.rb +19 -0
  32. data/lib/canon/comparison/markup_comparator.rb +293 -0
  33. data/lib/canon/comparison/match_options/base_resolver.rb +150 -0
  34. data/lib/canon/comparison/match_options/json_resolver.rb +82 -0
  35. data/lib/canon/comparison/match_options/xml_resolver.rb +151 -0
  36. data/lib/canon/comparison/match_options/yaml_resolver.rb +87 -0
  37. data/lib/canon/comparison/match_options.rb +68 -463
  38. data/lib/canon/comparison/profile_definition.rb +149 -0
  39. data/lib/canon/comparison/ruby_object_comparator.rb +180 -0
  40. data/lib/canon/comparison/strategies/semantic_tree_match_strategy.rb +7 -10
  41. data/lib/canon/comparison/whitespace_sensitivity.rb +208 -0
  42. data/lib/canon/comparison/xml_comparator/attribute_comparator.rb +177 -0
  43. data/lib/canon/comparison/xml_comparator/attribute_filter.rb +136 -0
  44. data/lib/canon/comparison/xml_comparator/child_comparison.rb +197 -0
  45. data/lib/canon/comparison/xml_comparator/diff_node_builder.rb +115 -0
  46. data/lib/canon/comparison/xml_comparator/namespace_comparator.rb +186 -0
  47. data/lib/canon/comparison/xml_comparator/node_parser.rb +79 -0
  48. data/lib/canon/comparison/xml_comparator/node_type_comparator.rb +102 -0
  49. data/lib/canon/comparison/xml_comparator.rb +97 -684
  50. data/lib/canon/comparison/xml_node_comparison.rb +319 -0
  51. data/lib/canon/comparison/xml_parser.rb +19 -0
  52. data/lib/canon/comparison/yaml_comparator.rb +3 -3
  53. data/lib/canon/comparison.rb +265 -110
  54. data/lib/canon/diff/diff_classifier.rb +101 -2
  55. data/lib/canon/diff/diff_node.rb +32 -2
  56. data/lib/canon/diff/formatting_detector.rb +1 -1
  57. data/lib/canon/diff/node_serializer.rb +191 -0
  58. data/lib/canon/diff/path_builder.rb +143 -0
  59. data/lib/canon/diff_formatter/by_line/base_formatter.rb +251 -0
  60. data/lib/canon/diff_formatter/by_line/html_formatter.rb +6 -248
  61. data/lib/canon/diff_formatter/by_line/xml_formatter.rb +38 -229
  62. data/lib/canon/diff_formatter/diff_detail_formatter/color_helper.rb +30 -0
  63. data/lib/canon/diff_formatter/diff_detail_formatter/dimension_formatter.rb +579 -0
  64. data/lib/canon/diff_formatter/diff_detail_formatter/location_extractor.rb +121 -0
  65. data/lib/canon/diff_formatter/diff_detail_formatter/node_utils.rb +253 -0
  66. data/lib/canon/diff_formatter/diff_detail_formatter/text_utils.rb +61 -0
  67. data/lib/canon/diff_formatter/diff_detail_formatter.rb +31 -1028
  68. data/lib/canon/diff_formatter.rb +1 -1
  69. data/lib/canon/rspec_matchers.rb +38 -9
  70. data/lib/canon/tree_diff/operation_converter.rb +92 -338
  71. data/lib/canon/tree_diff/operation_converter_helpers/metadata_enricher.rb +71 -0
  72. data/lib/canon/tree_diff/operation_converter_helpers/post_processor.rb +103 -0
  73. data/lib/canon/tree_diff/operation_converter_helpers/reason_builder.rb +168 -0
  74. data/lib/canon/tree_diff/operation_converter_helpers/update_change_handler.rb +188 -0
  75. data/lib/canon/version.rb +1 -1
  76. data/lib/canon/xml/data_model.rb +24 -13
  77. metadata +48 -2
@@ -0,0 +1,319 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canon
4
+ module Comparison
5
+ # XML Node Comparison Utilities
6
+ #
7
+ # Provides public comparison methods for XML/HTML nodes.
8
+ # This module extracts shared comparison logic that was previously
9
+ # accessed via send() from HtmlComparator.
10
+ module XmlNodeComparison
11
+ # Main comparison dispatcher for XML nodes
12
+ #
13
+ # This method handles the high-level comparison logic, delegating
14
+ # to specific comparison methods based on node types.
15
+ #
16
+ # @param node1 [Object] First node
17
+ # @param node2 [Object] Second node
18
+ # @param opts [Hash] Comparison options
19
+ # @param child_opts [Hash] Options for child comparison
20
+ # @param diff_children [Boolean] Whether to diff children
21
+ # @param differences [Array] Array to append differences to
22
+ # @return [Symbol] Comparison result constant
23
+ def self.compare_nodes(node1, node2, opts, child_opts, diff_children,
24
+ differences)
25
+ # Handle DocumentFragment nodes - compare their children instead
26
+ if node1.is_a?(Nokogiri::XML::DocumentFragment) &&
27
+ node2.is_a?(Nokogiri::XML::DocumentFragment)
28
+ return compare_document_fragments(node1, node2, opts, child_opts,
29
+ diff_children, differences)
30
+ end
31
+
32
+ # Check if nodes should be excluded
33
+ return Comparison::EQUIVALENT if node_excluded?(node1, opts) &&
34
+ node_excluded?(node2, opts)
35
+
36
+ if node_excluded?(node1, opts) || node_excluded?(node2, opts)
37
+ add_difference(node1, node2, Comparison::MISSING_NODE,
38
+ Comparison::MISSING_NODE, :text_content, opts,
39
+ differences)
40
+ return Comparison::MISSING_NODE
41
+ end
42
+
43
+ # Check node types match
44
+ unless same_node_type?(node1, node2)
45
+ add_difference(node1, node2, Comparison::UNEQUAL_NODES_TYPES,
46
+ Comparison::UNEQUAL_NODES_TYPES, :text_content, opts,
47
+ differences)
48
+ return Comparison::UNEQUAL_NODES_TYPES
49
+ end
50
+
51
+ # Dispatch based on node type
52
+ dispatch_by_node_type(node1, node2, opts, child_opts, diff_children,
53
+ differences)
54
+ end
55
+
56
+ # Filter children based on options
57
+ #
58
+ # Removes nodes that should be excluded from comparison based on
59
+ # options like :ignore_nodes, :ignore_comments, etc.
60
+ #
61
+ # @param children [Array] Array of child nodes
62
+ # @param opts [Hash] Comparison options
63
+ # @return [Array] Filtered array of children
64
+ def self.filter_children(children, opts)
65
+ children.reject do |child|
66
+ node_excluded?(child, opts)
67
+ end
68
+ end
69
+
70
+ # Compare document fragments by comparing their children
71
+ #
72
+ # @param node1 [Nokogiri::XML::DocumentFragment] First fragment
73
+ # @param node2 [Nokogiri::XML::DocumentFragment] Second fragment
74
+ # @param opts [Hash] Comparison options
75
+ # @param child_opts [Hash] Options for child comparison
76
+ # @param diff_children [Boolean] Whether to diff children
77
+ # @param differences [Array] Array to append differences to
78
+ # @return [Symbol] Comparison result constant
79
+ def self.compare_document_fragments(node1, node2, opts, child_opts,
80
+ diff_children, differences)
81
+ childrenode1 = node1.children.to_a
82
+ childrenode2 = node2.children.to_a
83
+
84
+ if childrenode1.length != childrenode2.length
85
+ add_difference(node1, node2, Comparison::UNEQUAL_ELEMENTS,
86
+ Comparison::UNEQUAL_ELEMENTS, :text_content, opts,
87
+ differences)
88
+ Comparison::UNEQUAL_ELEMENTS
89
+ elsif childrenode1.empty?
90
+ Comparison::EQUIVALENT
91
+ else
92
+ # Compare each pair of children
93
+ result = Comparison::EQUIVALENT
94
+ childrenode1.zip(childrenode2).each do |child1, child2|
95
+ child_result = compare_nodes(child1, child2, opts, child_opts,
96
+ diff_children, differences)
97
+ result = child_result unless result == Comparison::EQUIVALENT
98
+ end
99
+ result
100
+ end
101
+ end
102
+
103
+ # Dispatch comparison based on node type
104
+ #
105
+ # @param node1 [Object] First node
106
+ # @param node2 [Object] Second node
107
+ # @param opts [Hash] Comparison options
108
+ # @param child_opts [Hash] Options for child comparison
109
+ # @param diff_children [Boolean] Whether to diff children
110
+ # @param differences [Array] Array to append differences to
111
+ # @return [Symbol] Comparison result constant
112
+ def self.dispatch_by_node_type(node1, node2, opts, child_opts,
113
+ diff_children, differences)
114
+ # Canon::Xml::Node types use .node_type method that returns symbols
115
+ # Nokogiri also has .node_type but returns integers, so check for Symbol
116
+ if node1.respond_to?(:node_type) && node2.respond_to?(:node_type) &&
117
+ node1.node_type.is_a?(Symbol) && node2.node_type.is_a?(Symbol)
118
+ dispatch_canon_node_type(node1, node2, opts, child_opts,
119
+ diff_children, differences)
120
+ # Moxml/Nokogiri types use .element?, .text?, etc. methods
121
+ else
122
+ dispatch_legacy_node_type(node1, node2, opts, child_opts,
123
+ diff_children, differences)
124
+ end
125
+ end
126
+
127
+ # Private helper methods
128
+
129
+ # Check if a node should be excluded from comparison
130
+ #
131
+ # @param node [Object] Node to check
132
+ # @param opts [Hash] Comparison options
133
+ # @return [Boolean] true if node should be excluded
134
+ def self.node_excluded?(node, opts)
135
+ return false if node.nil?
136
+ return true if opts[:ignore_nodes]&.include?(node)
137
+ return true if opts[:ignore_comments] && comment_node?(node)
138
+ return true if opts[:ignore_text_nodes] && text_node?(node)
139
+
140
+ # Check structural_whitespace match option
141
+ match_opts = opts[:match_opts]
142
+ return false unless match_opts
143
+
144
+ # Filter out whitespace-only text nodes based on structural_whitespace setting
145
+ # - :ignore or :normalize: Filter all whitespace-only text nodes
146
+ # - :strict: Preserve all whitespace-only text nodes (don't filter any)
147
+ if text_node?(node) && %i[ignore
148
+ normalize].include?(match_opts[:structural_whitespace])
149
+ text = node_text(node)
150
+ return true if MatchOptions.normalize_text(text).empty?
151
+ end
152
+
153
+ false
154
+ end
155
+
156
+ # Check if two nodes are of the same type
157
+ #
158
+ # @param node1 [Object] First node
159
+ # @param node2 [Object] Second node
160
+ # @return [Boolean] true if nodes are same type
161
+ def self.same_node_type?(node1, node2)
162
+ return false if node1.class != node2.class
163
+
164
+ # For Nokogiri/Canon::Xml nodes, check node type
165
+ if node1.respond_to?(:node_type) && node2.respond_to?(:node_type)
166
+ node1.node_type == node2.node_type
167
+ else
168
+ true
169
+ end
170
+ end
171
+
172
+ # Check if a node is a comment node
173
+ #
174
+ # @param node [Object] Node to check
175
+ # @return [Boolean] true if node is a comment
176
+ def self.comment_node?(node)
177
+ node.respond_to?(:comment?) && node.comment? ||
178
+ node.respond_to?(:node_type) && node.node_type == :comment
179
+ end
180
+
181
+ # Check if a node is a text node
182
+ #
183
+ # @param node [Object] Node to check
184
+ # @return [Boolean] true if node is a text node
185
+ def self.text_node?(node)
186
+ node.respond_to?(:text?) && node.text? &&
187
+ !node.respond_to?(:element?) ||
188
+ node.respond_to?(:node_type) && node.node_type == :text
189
+ end
190
+
191
+ # Extract text content from a node
192
+ #
193
+ # @param node [Object] Node to extract text from
194
+ # @return [String] Text content
195
+ def self.node_text(node)
196
+ return "" unless node
197
+
198
+ if node.respond_to?(:content)
199
+ node.content.to_s
200
+ elsif node.respond_to?(:text)
201
+ node.text.to_s
202
+ elsif node.respond_to?(:value)
203
+ node.value.to_s
204
+ else
205
+ ""
206
+ end
207
+ end
208
+
209
+ # Dispatch by Canon::Xml::Node type
210
+ def self.dispatch_canon_node_type(node1, node2, opts, child_opts,
211
+ diff_children, differences)
212
+ # Import XmlComparator to use its comparison methods
213
+ require_relative "xml_comparator"
214
+
215
+ case node1.node_type
216
+ when :root
217
+ XmlComparator.compare_children(node1, node2, opts, child_opts,
218
+ diff_children, differences)
219
+ when :element
220
+ XmlComparator.compare_element_nodes(node1, node2, opts, child_opts,
221
+ diff_children, differences)
222
+ when :text
223
+ XmlComparator.compare_text_nodes(node1, node2, opts, differences)
224
+ when :comment
225
+ XmlComparator.compare_comment_nodes(node1, node2, opts, differences)
226
+ when :cdata
227
+ XmlComparator.compare_text_nodes(node1, node2, opts, differences)
228
+ when :processing_instruction
229
+ XmlComparator.compare_processing_instruction_nodes(node1, node2,
230
+ opts, differences)
231
+ else
232
+ Comparison::EQUIVALENT
233
+ end
234
+ end
235
+
236
+ # Dispatch by legacy Nokogiri/Moxml node type
237
+ def self.dispatch_legacy_node_type(node1, node2, opts, child_opts,
238
+ diff_children, differences)
239
+ # Import XmlComparator to use its comparison methods
240
+ require_relative "xml_comparator"
241
+
242
+ if node1.respond_to?(:element?) && node1.element?
243
+ XmlComparator.compare_element_nodes(node1, node2, opts, child_opts,
244
+ diff_children, differences)
245
+ elsif node1.respond_to?(:text?) && node1.text?
246
+ XmlComparator.compare_text_nodes(node1, node2, opts, differences)
247
+ elsif node1.respond_to?(:comment?) && node1.comment?
248
+ XmlComparator.compare_comment_nodes(node1, node2, opts, differences)
249
+ elsif node1.respond_to?(:cdata?) && node1.cdata?
250
+ XmlComparator.compare_text_nodes(node1, node2, opts, differences)
251
+ elsif node1.respond_to?(:processing_instruction?) && node1.processing_instruction?
252
+ XmlComparator.compare_processing_instruction_nodes(node1, node2,
253
+ opts, differences)
254
+ elsif node1.respond_to?(:root)
255
+ XmlComparator.compare_document_nodes(node1, node2, opts, child_opts,
256
+ diff_children, differences)
257
+ else
258
+ Comparison::EQUIVALENT
259
+ end
260
+ end
261
+
262
+ # Add a difference to the differences array
263
+ #
264
+ # @param node1 [Object] First node
265
+ # @param node2 [Object] Second node
266
+ # @param diff1 [Symbol] Difference type for node1
267
+ # @param diff2 [Symbol] Difference type for node2
268
+ # @param dimension [Symbol] The dimension of the difference
269
+ # @param opts [Hash] Comparison options
270
+ # @param differences [Array] Array to append difference to
271
+ def self.add_difference(node1, node2, diff1, diff2, dimension, opts,
272
+ differences)
273
+ return unless opts[:verbose]
274
+
275
+ require_relative "xml_comparator"
276
+ XmlComparator.add_difference(node1, node2, diff1, diff2, dimension,
277
+ opts, differences)
278
+ end
279
+
280
+ # Serialize a Canon::Xml::Node to XML string
281
+ #
282
+ # This utility method handles serialization of different node types
283
+ # to their string representation for display and debugging purposes.
284
+ #
285
+ # @param node [Canon::Xml::Node, Object] Node to serialize
286
+ # @return [String] XML string representation
287
+ def self.serialize_node_to_xml(node)
288
+ if node.is_a?(Canon::Xml::Nodes::RootNode)
289
+ # Serialize all children of root
290
+ node.children.map { |child| serialize_node_to_xml(child) }.join
291
+ elsif node.is_a?(Canon::Xml::Nodes::ElementNode)
292
+ # Serialize element with attributes and children
293
+ attrs = node.attribute_nodes.map do |a|
294
+ " #{a.name}=\"#{a.value}\""
295
+ end.join
296
+ children_xml = node.children.map do |c|
297
+ serialize_node_to_xml(c)
298
+ end.join
299
+
300
+ if children_xml.empty?
301
+ "<#{node.name}#{attrs}/>"
302
+ else
303
+ "<#{node.name}#{attrs}>#{children_xml}</#{node.name}>"
304
+ end
305
+ elsif node.is_a?(Canon::Xml::Nodes::TextNode)
306
+ node.value
307
+ elsif node.is_a?(Canon::Xml::Nodes::CommentNode)
308
+ "<!--#{node.value}-->"
309
+ elsif node.is_a?(Canon::Xml::Nodes::ProcessingInstructionNode)
310
+ "<?#{node.target} #{node.data}?>"
311
+ elsif node.respond_to?(:to_xml)
312
+ node.to_xml
313
+ else
314
+ node.to_s
315
+ end
316
+ end
317
+ end
318
+ end
319
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canon
4
+ module Comparison
5
+ # Public API for XML parsing operations
6
+ # Provides access to parsing functionality without using send()
7
+ class XmlParser
8
+ # Parse an object to Canon::Xml::Node with preprocessing
9
+ #
10
+ # @param obj [String, Object] Object to parse
11
+ # @param preprocessing [Symbol] Preprocessing mode
12
+ # @return [Canon::Xml::Node] Parsed node
13
+ def self.parse_node(obj, preprocessing = :none)
14
+ # Delegate to XmlComparator's private method via public API
15
+ XmlComparator::NodeParser.parse(obj, preprocessing)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "yaml"
4
- require_relative "json_comparator"
5
4
  require_relative "match_options"
6
5
  require_relative "comparison_result"
6
+ require_relative "ruby_object_comparator"
7
7
 
8
8
  module Canon
9
9
  module Comparison
@@ -60,8 +60,8 @@ module Canon
60
60
  obj2 = parse_yaml(yaml2)
61
61
 
62
62
  differences = []
63
- result = JsonComparator.send(:compare_ruby_objects, obj1, obj2, opts,
64
- differences, "")
63
+ result = RubyObjectComparator.compare_objects(obj1, obj2, opts,
64
+ differences, "")
65
65
 
66
66
  if opts[:verbose]
67
67
  # Format YAML for display