canon 0.1.3 → 0.1.5
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/.rubocop.yml +9 -1
- data/.rubocop_todo.yml +276 -7
- data/README.adoc +203 -138
- data/_config.yml +116 -0
- data/docs/ADVANCED_TOPICS.adoc +20 -0
- data/docs/BASIC_USAGE.adoc +16 -0
- data/docs/CHARACTER_VISUALIZATION.adoc +567 -0
- data/docs/CLI.adoc +493 -0
- data/docs/CUSTOMIZING_BEHAVIOR.adoc +19 -0
- data/docs/DIFF_ARCHITECTURE.adoc +435 -0
- data/docs/DIFF_FORMATTING.adoc +540 -0
- data/docs/FORMATS.adoc +447 -0
- data/docs/INDEX.adoc +222 -0
- data/docs/INPUT_VALIDATION.adoc +477 -0
- data/docs/MATCH_ARCHITECTURE.adoc +463 -0
- data/docs/MATCH_OPTIONS.adoc +719 -0
- data/docs/MODES.adoc +432 -0
- data/docs/NORMATIVE_INFORMATIVE_DIFFS.adoc +219 -0
- data/docs/OPTIONS.adoc +1387 -0
- data/docs/PREPROCESSING.adoc +491 -0
- data/docs/RSPEC.adoc +605 -0
- data/docs/RUBY_API.adoc +478 -0
- data/docs/SEMANTIC_DIFF_REPORT.adoc +528 -0
- data/docs/UNDERSTANDING_CANON.adoc +17 -0
- data/docs/VERBOSE.adoc +482 -0
- data/exe/canon +7 -0
- data/lib/canon/cli.rb +179 -0
- data/lib/canon/commands/diff_command.rb +195 -0
- data/lib/canon/commands/format_command.rb +113 -0
- data/lib/canon/comparison/base_comparator.rb +39 -0
- data/lib/canon/comparison/comparison_result.rb +79 -0
- data/lib/canon/comparison/html_comparator.rb +410 -0
- data/lib/canon/comparison/json_comparator.rb +212 -0
- data/lib/canon/comparison/match_options.rb +616 -0
- data/lib/canon/comparison/xml_comparator.rb +566 -0
- data/lib/canon/comparison/yaml_comparator.rb +93 -0
- data/lib/canon/comparison.rb +239 -0
- data/lib/canon/config.rb +172 -0
- data/lib/canon/diff/diff_block.rb +71 -0
- data/lib/canon/diff/diff_block_builder.rb +105 -0
- data/lib/canon/diff/diff_classifier.rb +46 -0
- data/lib/canon/diff/diff_context.rb +85 -0
- data/lib/canon/diff/diff_context_builder.rb +107 -0
- data/lib/canon/diff/diff_line.rb +77 -0
- data/lib/canon/diff/diff_node.rb +56 -0
- data/lib/canon/diff/diff_node_mapper.rb +148 -0
- data/lib/canon/diff/diff_report.rb +133 -0
- data/lib/canon/diff/diff_report_builder.rb +62 -0
- data/lib/canon/diff_formatter/by_line/base_formatter.rb +407 -0
- data/lib/canon/diff_formatter/by_line/html_formatter.rb +672 -0
- data/lib/canon/diff_formatter/by_line/json_formatter.rb +284 -0
- data/lib/canon/diff_formatter/by_line/simple_formatter.rb +190 -0
- data/lib/canon/diff_formatter/by_line/xml_formatter.rb +860 -0
- data/lib/canon/diff_formatter/by_line/yaml_formatter.rb +292 -0
- data/lib/canon/diff_formatter/by_object/base_formatter.rb +199 -0
- data/lib/canon/diff_formatter/by_object/json_formatter.rb +305 -0
- data/lib/canon/diff_formatter/by_object/xml_formatter.rb +248 -0
- data/lib/canon/diff_formatter/by_object/yaml_formatter.rb +17 -0
- data/lib/canon/diff_formatter/character_map.yml +197 -0
- data/lib/canon/diff_formatter/debug_output.rb +431 -0
- data/lib/canon/diff_formatter/diff_detail_formatter.rb +551 -0
- data/lib/canon/diff_formatter/legend.rb +141 -0
- data/lib/canon/diff_formatter.rb +520 -0
- data/lib/canon/errors.rb +56 -0
- data/lib/canon/formatters/html4_formatter.rb +17 -0
- data/lib/canon/formatters/html5_formatter.rb +17 -0
- data/lib/canon/formatters/html_formatter.rb +37 -0
- data/lib/canon/formatters/html_formatter_base.rb +163 -0
- data/lib/canon/formatters/json_formatter.rb +3 -0
- data/lib/canon/formatters/xml_formatter.rb +20 -55
- data/lib/canon/formatters/yaml_formatter.rb +4 -1
- data/lib/canon/pretty_printer/html.rb +57 -0
- data/lib/canon/pretty_printer/json.rb +25 -0
- data/lib/canon/pretty_printer/xml.rb +29 -0
- data/lib/canon/rspec_matchers.rb +222 -80
- data/lib/canon/validators/base_validator.rb +49 -0
- data/lib/canon/validators/html_validator.rb +138 -0
- data/lib/canon/validators/json_validator.rb +89 -0
- data/lib/canon/validators/xml_validator.rb +53 -0
- data/lib/canon/validators/yaml_validator.rb +73 -0
- data/lib/canon/version.rb +1 -1
- data/lib/canon/xml/attribute_handler.rb +80 -0
- data/lib/canon/xml/c14n.rb +36 -0
- data/lib/canon/xml/character_encoder.rb +38 -0
- data/lib/canon/xml/data_model.rb +225 -0
- data/lib/canon/xml/element_matcher.rb +196 -0
- data/lib/canon/xml/line_range_mapper.rb +158 -0
- data/lib/canon/xml/namespace_handler.rb +86 -0
- data/lib/canon/xml/node.rb +32 -0
- data/lib/canon/xml/nodes/attribute_node.rb +54 -0
- data/lib/canon/xml/nodes/comment_node.rb +23 -0
- data/lib/canon/xml/nodes/element_node.rb +56 -0
- data/lib/canon/xml/nodes/namespace_node.rb +38 -0
- data/lib/canon/xml/nodes/processing_instruction_node.rb +24 -0
- data/lib/canon/xml/nodes/root_node.rb +16 -0
- data/lib/canon/xml/nodes/text_node.rb +23 -0
- data/lib/canon/xml/processor.rb +151 -0
- data/lib/canon/xml/whitespace_normalizer.rb +72 -0
- data/lib/canon/xml/xml_base_handler.rb +188 -0
- data/lib/canon.rb +14 -3
- metadata +116 -21
data/docs/OPTIONS.adoc
ADDED
|
@@ -0,0 +1,1387 @@
|
|
|
1
|
+
= Comparison options reference
|
|
2
|
+
:toc: left
|
|
3
|
+
:toclevels: 3
|
|
4
|
+
:sectanchors:
|
|
5
|
+
:sectlinks:
|
|
6
|
+
|
|
7
|
+
== Overview
|
|
8
|
+
|
|
9
|
+
Canon provides a flexible, format-aware semantic comparison system for XML, HTML, JSON, and YAML documents. The comparison process follows a three-phase architecture that allows fine-grained control over how documents are compared.
|
|
10
|
+
|
|
11
|
+
This document describes all available options for controlling Canon's comparison behavior across three interfaces: CLI, Ruby API, and RSpec matchers.
|
|
12
|
+
|
|
13
|
+
== Architecture
|
|
14
|
+
|
|
15
|
+
Canon's comparison flow consists of three distinct phases:
|
|
16
|
+
|
|
17
|
+
[source]
|
|
18
|
+
----
|
|
19
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
20
|
+
│ CANON COMPARISON FLOW │
|
|
21
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
22
|
+
|
|
23
|
+
┌─────────────────────┐
|
|
24
|
+
│ Input Documents │
|
|
25
|
+
│ (File 1, File 2) │
|
|
26
|
+
└──────────┬──────────┘
|
|
27
|
+
│
|
|
28
|
+
▼
|
|
29
|
+
╔══════════════════════════════════════════════════════════════════╗
|
|
30
|
+
║ PHASE 1: PREPROCESSING ║
|
|
31
|
+
╠══════════════════════════════════════════════════════════════════╣
|
|
32
|
+
║ Options: ║
|
|
33
|
+
║ • none - No preprocessing ║
|
|
34
|
+
║ • c14n - Canonical form (XML C14N, JSON/YAML sorted) ║
|
|
35
|
+
║ • normalize - Normalize whitespace ║
|
|
36
|
+
║ • format - Pretty-print with standard formatting ║
|
|
37
|
+
╚══════════════════════════════════════════════════════════════════╝
|
|
38
|
+
│
|
|
39
|
+
▼
|
|
40
|
+
┌─────────────────────┐
|
|
41
|
+
│ Preprocessed Docs │
|
|
42
|
+
└──────────┬──────────┘
|
|
43
|
+
│
|
|
44
|
+
▼
|
|
45
|
+
╔══════════════════════════════════════════════════════════════════╗
|
|
46
|
+
║ PHASE 2: SEMANTIC MATCHING ║
|
|
47
|
+
╠══════════════════════════════════════════════════════════════════╣
|
|
48
|
+
║ Match Dimensions: ║
|
|
49
|
+
║ • text_content (strict|normalize|ignore) ║
|
|
50
|
+
║ • structural_whitespace (strict|normalize|ignore) ║
|
|
51
|
+
║ • attribute_whitespace (strict|normalize|ignore) [XML/HTML] ║
|
|
52
|
+
║ • attribute_order (strict|ignore) [XML/HTML] ║
|
|
53
|
+
║ • attribute_values (strict|normalize|ignore) [XML/HTML] ║
|
|
54
|
+
║ • key_order (strict|ignore) [JSON/YAML] ║
|
|
55
|
+
║ • comments (strict|normalize|ignore) ║
|
|
56
|
+
║ ║
|
|
57
|
+
║ Match Profiles: ║
|
|
58
|
+
║ • strict - All dimensions strict (exact matching) ║
|
|
59
|
+
║ • rendered - Mimics browser/CSS rendering behavior ║
|
|
60
|
+
║ • spec_friendly - Test-friendly (ignores formatting diffs) ║
|
|
61
|
+
║ • content_only - Only semantic content matters ║
|
|
62
|
+
╚══════════════════════════════════════════════════════════════════╝
|
|
63
|
+
│
|
|
64
|
+
├─ Equivalent? ──► Return true
|
|
65
|
+
│
|
|
66
|
+
├─ Different? ──┐
|
|
67
|
+
│ │
|
|
68
|
+
▼ ▼
|
|
69
|
+
╔══════════════════════════════════════════════════════════════════╗
|
|
70
|
+
║ PHASE 3: DIFF RENDERING ║
|
|
71
|
+
╠══════════════════════════════════════════════════════════════════╣
|
|
72
|
+
║ Diff Options: ║
|
|
73
|
+
║ • mode - by_line (XML/HTML) | by_object (JSON/YAML) ║
|
|
74
|
+
║ • use_color - Colorized output (terminal colors) ║
|
|
75
|
+
║ • context_lines - Lines of context around changes ║
|
|
76
|
+
║ • grouping_lines - Group nearby changes into blocks ║
|
|
77
|
+
╚══════════════════════════════════════════════════════════════════╝
|
|
78
|
+
│
|
|
79
|
+
▼
|
|
80
|
+
┌─────────────────────┐
|
|
81
|
+
│ Formatted Diff │
|
|
82
|
+
│ Output │
|
|
83
|
+
└─────────────────────┘
|
|
84
|
+
----
|
|
85
|
+
|
|
86
|
+
== Match Dimensions Reference
|
|
87
|
+
|
|
88
|
+
Match dimensions control which aspects of documents are compared and how strictly they are compared. Each dimension can be set to one of several behaviors.
|
|
89
|
+
|
|
90
|
+
=== text_content
|
|
91
|
+
|
|
92
|
+
*Purpose*: Controls how text content within elements is compared.
|
|
93
|
+
|
|
94
|
+
*Applies to*: XML, HTML, JSON, YAML
|
|
95
|
+
|
|
96
|
+
*Behaviors*:
|
|
97
|
+
|
|
98
|
+
* `strict` - Text must match exactly, character-for-character including all whitespace
|
|
99
|
+
* `normalize` - Whitespace is normalized (collapsed/trimmed) before comparison
|
|
100
|
+
* `ignore` - Text content is completely ignored in comparison
|
|
101
|
+
|
|
102
|
+
*XML Example*:
|
|
103
|
+
|
|
104
|
+
.Input Files
|
|
105
|
+
[source,xml]
|
|
106
|
+
----
|
|
107
|
+
<!-- File 1 -->
|
|
108
|
+
<message>Hello World</message>
|
|
109
|
+
|
|
110
|
+
<!-- File 2 -->
|
|
111
|
+
<message>Hello World</message>
|
|
112
|
+
----
|
|
113
|
+
|
|
114
|
+
.Comparison Results
|
|
115
|
+
|===
|
|
116
|
+
|Behavior |Result |Explanation
|
|
117
|
+
|
|
118
|
+
|`strict`
|
|
119
|
+
|Different
|
|
120
|
+
|Whitespace differs (3 spaces vs 1 space)
|
|
121
|
+
|
|
122
|
+
|`normalize`
|
|
123
|
+
|Equivalent
|
|
124
|
+
|Both normalize to "Hello World"
|
|
125
|
+
|
|
126
|
+
|`ignore`
|
|
127
|
+
|Equivalent
|
|
128
|
+
|Text content ignored, structure matches
|
|
129
|
+
|===
|
|
130
|
+
|
|
131
|
+
*JSON Example*:
|
|
132
|
+
|
|
133
|
+
.Input Files
|
|
134
|
+
[source,json]
|
|
135
|
+
----
|
|
136
|
+
// File 1
|
|
137
|
+
{"message": "Hello World"}
|
|
138
|
+
|
|
139
|
+
// File 2
|
|
140
|
+
{"message": "Hello World"}
|
|
141
|
+
----
|
|
142
|
+
|
|
143
|
+
.Comparison Results
|
|
144
|
+
|===
|
|
145
|
+
|Behavior |Result |Explanation
|
|
146
|
+
|
|
147
|
+
|`strict`
|
|
148
|
+
|Different
|
|
149
|
+
|String values differ
|
|
150
|
+
|
|
151
|
+
|`normalize`
|
|
152
|
+
|Equivalent
|
|
153
|
+
|Whitespace normalized in strings
|
|
154
|
+
|
|
155
|
+
|`ignore`
|
|
156
|
+
|Equivalent
|
|
157
|
+
|Only structure compared
|
|
158
|
+
|===
|
|
159
|
+
|
|
160
|
+
=== structural_whitespace
|
|
161
|
+
|
|
162
|
+
*Purpose*: Controls how whitespace between elements (indentation, newlines) is handled.
|
|
163
|
+
|
|
164
|
+
*Applies to*: XML, HTML, JSON, YAML
|
|
165
|
+
|
|
166
|
+
*Behaviors*:
|
|
167
|
+
|
|
168
|
+
* `strict` - All structural whitespace must match exactly
|
|
169
|
+
* `normalize` - Structural whitespace is normalized
|
|
170
|
+
* `ignore` - Structural whitespace is completely ignored
|
|
171
|
+
|
|
172
|
+
*XML Example*:
|
|
173
|
+
|
|
174
|
+
.Input Files
|
|
175
|
+
[source,xml]
|
|
176
|
+
----
|
|
177
|
+
<!-- File 1 -->
|
|
178
|
+
<root>
|
|
179
|
+
<item>A</item>
|
|
180
|
+
<item>B</item>
|
|
181
|
+
</root>
|
|
182
|
+
|
|
183
|
+
<!-- File 2 -->
|
|
184
|
+
<root><item>A</item><item>B</item></root>
|
|
185
|
+
----
|
|
186
|
+
|
|
187
|
+
.Comparison Results
|
|
188
|
+
|===
|
|
189
|
+
|Behavior |Result |Explanation
|
|
190
|
+
|
|
191
|
+
|`strict`
|
|
192
|
+
|Different
|
|
193
|
+
|Indentation and newlines differ
|
|
194
|
+
|
|
195
|
+
|`normalize`
|
|
196
|
+
|Equivalent
|
|
197
|
+
|Whitespace between elements normalized
|
|
198
|
+
|
|
199
|
+
|`ignore`
|
|
200
|
+
|Equivalent
|
|
201
|
+
|Only element structure compared
|
|
202
|
+
|===
|
|
203
|
+
|
|
204
|
+
*JSON Example*:
|
|
205
|
+
|
|
206
|
+
.Input Files
|
|
207
|
+
[source,json]
|
|
208
|
+
----
|
|
209
|
+
// File 1
|
|
210
|
+
{
|
|
211
|
+
"items": [
|
|
212
|
+
"A",
|
|
213
|
+
"B"
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// File 2
|
|
218
|
+
{"items":["A","B"]}
|
|
219
|
+
----
|
|
220
|
+
|
|
221
|
+
.Comparison Results
|
|
222
|
+
|===
|
|
223
|
+
|Behavior |Result |Explanation
|
|
224
|
+
|
|
225
|
+
|`strict`
|
|
226
|
+
|Different
|
|
227
|
+
|Formatting differs
|
|
228
|
+
|
|
229
|
+
|`normalize`
|
|
230
|
+
|Equivalent
|
|
231
|
+
|Both have same structure
|
|
232
|
+
|
|
233
|
+
|`ignore`
|
|
234
|
+
|Equivalent
|
|
235
|
+
|Structure identical
|
|
236
|
+
|===
|
|
237
|
+
|
|
238
|
+
=== attribute_whitespace
|
|
239
|
+
|
|
240
|
+
*Purpose*: Controls how whitespace in attribute values is handled.
|
|
241
|
+
|
|
242
|
+
*Applies to*: XML, HTML only
|
|
243
|
+
|
|
244
|
+
*Behaviors*:
|
|
245
|
+
|
|
246
|
+
* `strict` - Attribute value whitespace must match exactly
|
|
247
|
+
* `normalize` - Whitespace in attribute values is normalized
|
|
248
|
+
* `ignore` - Whitespace in attribute values is ignored
|
|
249
|
+
|
|
250
|
+
*XML Example*:
|
|
251
|
+
|
|
252
|
+
.Input Files
|
|
253
|
+
[source,xml]
|
|
254
|
+
----
|
|
255
|
+
<!-- File 1 -->
|
|
256
|
+
<div class="item active">Content</div>
|
|
257
|
+
|
|
258
|
+
<!-- File 2 -->
|
|
259
|
+
<div class="item active">Content</div>
|
|
260
|
+
----
|
|
261
|
+
|
|
262
|
+
.Comparison Results
|
|
263
|
+
|===
|
|
264
|
+
|Behavior |Result |Explanation
|
|
265
|
+
|
|
266
|
+
|`strict`
|
|
267
|
+
|Different
|
|
268
|
+
|Attribute value whitespace differs (2 spaces vs 1)
|
|
269
|
+
|
|
270
|
+
|`normalize`
|
|
271
|
+
|Equivalent
|
|
272
|
+
|"item active" normalizes to "item active"
|
|
273
|
+
|
|
274
|
+
|`ignore`
|
|
275
|
+
|Equivalent
|
|
276
|
+
|Only attribute presence compared
|
|
277
|
+
|===
|
|
278
|
+
|
|
279
|
+
*HTML Special Behavior*:
|
|
280
|
+
|
|
281
|
+
HTML's `class` attribute is space-separated, so normalization is particularly useful:
|
|
282
|
+
|
|
283
|
+
[source,html]
|
|
284
|
+
----
|
|
285
|
+
<!-- These are equivalent with normalize -->
|
|
286
|
+
<div class="btn primary active">Click</div>
|
|
287
|
+
<div class="btn primary active">Click</div>
|
|
288
|
+
----
|
|
289
|
+
|
|
290
|
+
=== attribute_order
|
|
291
|
+
|
|
292
|
+
*Purpose*: Controls whether attribute order matters.
|
|
293
|
+
|
|
294
|
+
*Applies to*: XML, HTML only
|
|
295
|
+
|
|
296
|
+
*Behaviors*:
|
|
297
|
+
|
|
298
|
+
* `strict` - Attributes must appear in the same order
|
|
299
|
+
* `ignore` - Attribute order doesn't matter (set-based comparison)
|
|
300
|
+
|
|
301
|
+
*XML Example*:
|
|
302
|
+
|
|
303
|
+
.Input Files
|
|
304
|
+
[source,xml]
|
|
305
|
+
----
|
|
306
|
+
<!-- File 1 -->
|
|
307
|
+
<element id="123" class="active" data-value="test"/>
|
|
308
|
+
|
|
309
|
+
<!-- File 2 -->
|
|
310
|
+
<element class="active" data-value="test" id="123"/>
|
|
311
|
+
----
|
|
312
|
+
|
|
313
|
+
.Comparison Results
|
|
314
|
+
|===
|
|
315
|
+
|Behavior |Result |Explanation
|
|
316
|
+
|
|
317
|
+
|`strict`
|
|
318
|
+
|Different
|
|
319
|
+
|Attribute order differs
|
|
320
|
+
|
|
321
|
+
|`ignore`
|
|
322
|
+
|Equivalent
|
|
323
|
+
|Same attributes present (unordered comparison)
|
|
324
|
+
|===
|
|
325
|
+
|
|
326
|
+
*HTML Special Behavior*:
|
|
327
|
+
|
|
328
|
+
HTML attributes are inherently unordered by the HTML spec, so the default for HTML formats is `ignore`:
|
|
329
|
+
|
|
330
|
+
[source,html]
|
|
331
|
+
----
|
|
332
|
+
<!-- These are always equivalent for HTML -->
|
|
333
|
+
<input type="text" id="name" class="form-control">
|
|
334
|
+
<input class="form-control" id="name" type="text">
|
|
335
|
+
----
|
|
336
|
+
|
|
337
|
+
=== attribute_values
|
|
338
|
+
|
|
339
|
+
*Purpose*: Controls how attribute values are compared.
|
|
340
|
+
|
|
341
|
+
*Applies to*: XML, HTML only
|
|
342
|
+
|
|
343
|
+
*Behaviors*:
|
|
344
|
+
|
|
345
|
+
* `strict` - Attribute values must match exactly
|
|
346
|
+
* `normalize` - Whitespace in values is normalized
|
|
347
|
+
* `ignore` - Only attribute presence is checked, values ignored
|
|
348
|
+
|
|
349
|
+
*XML Example*:
|
|
350
|
+
|
|
351
|
+
.Input Files
|
|
352
|
+
[source,xml]
|
|
353
|
+
----
|
|
354
|
+
<!-- File 1 -->
|
|
355
|
+
<element id="123" class="normative"/>
|
|
356
|
+
|
|
357
|
+
<!-- File 2 -->
|
|
358
|
+
<element id="456" class="informative"/>
|
|
359
|
+
----
|
|
360
|
+
|
|
361
|
+
.Comparison Results
|
|
362
|
+
|===
|
|
363
|
+
|Behavior |Result |Explanation
|
|
364
|
+
|
|
365
|
+
|`strict`
|
|
366
|
+
|Different
|
|
367
|
+
|Attribute values differ
|
|
368
|
+
|
|
369
|
+
|`normalize`
|
|
370
|
+
|Different
|
|
371
|
+
|Values still differ after normalization
|
|
372
|
+
|
|
373
|
+
|`ignore`
|
|
374
|
+
|Equivalent
|
|
375
|
+
|Both have `id` and `class` attributes (values ignored)
|
|
376
|
+
|===
|
|
377
|
+
|
|
378
|
+
*Use Case*: Useful when you want to verify that certain attributes exist but don't care about their specific values (e.g., testing that generated IDs are present).
|
|
379
|
+
|
|
380
|
+
=== key_order
|
|
381
|
+
|
|
382
|
+
*Purpose*: Controls whether object key order matters.
|
|
383
|
+
|
|
384
|
+
*Applies to*: JSON, YAML only
|
|
385
|
+
|
|
386
|
+
*Behaviors*:
|
|
387
|
+
|
|
388
|
+
* `strict` - Keys must appear in the same order
|
|
389
|
+
* `ignore` - Key order doesn't matter (unordered comparison)
|
|
390
|
+
|
|
391
|
+
*JSON Example*:
|
|
392
|
+
|
|
393
|
+
.Input Files
|
|
394
|
+
[source,json]
|
|
395
|
+
----
|
|
396
|
+
// File 1
|
|
397
|
+
{
|
|
398
|
+
"name": "John",
|
|
399
|
+
"age": 30,
|
|
400
|
+
"city": "NYC"
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// File 2
|
|
404
|
+
{
|
|
405
|
+
"city": "NYC",
|
|
406
|
+
"name": "John",
|
|
407
|
+
"age": 30
|
|
408
|
+
}
|
|
409
|
+
----
|
|
410
|
+
|
|
411
|
+
.Comparison Results
|
|
412
|
+
|===
|
|
413
|
+
|Behavior |Result |Explanation
|
|
414
|
+
|
|
415
|
+
|`strict`
|
|
416
|
+
|Different
|
|
417
|
+
|Key order differs
|
|
418
|
+
|
|
419
|
+
|`ignore`
|
|
420
|
+
|Equivalent
|
|
421
|
+
|Same keys and values (unordered)
|
|
422
|
+
|===
|
|
423
|
+
|
|
424
|
+
*YAML Example*:
|
|
425
|
+
|
|
426
|
+
.Input Files
|
|
427
|
+
[source,yaml]
|
|
428
|
+
----
|
|
429
|
+
# File 1
|
|
430
|
+
name: John
|
|
431
|
+
age: 30
|
|
432
|
+
city: NYC
|
|
433
|
+
|
|
434
|
+
# File 2
|
|
435
|
+
city: NYC
|
|
436
|
+
name: John
|
|
437
|
+
age: 30
|
|
438
|
+
----
|
|
439
|
+
|
|
440
|
+
.Comparison Results
|
|
441
|
+
|===
|
|
442
|
+
|Behavior |Result |Explanation
|
|
443
|
+
|
|
444
|
+
|`strict`
|
|
445
|
+
|Different
|
|
446
|
+
|Key order differs
|
|
447
|
+
|
|
448
|
+
|`ignore`
|
|
449
|
+
|Equivalent
|
|
450
|
+
|Same structure and values
|
|
451
|
+
|===
|
|
452
|
+
|
|
453
|
+
=== comments
|
|
454
|
+
|
|
455
|
+
*Purpose*: Controls how comments are compared.
|
|
456
|
+
|
|
457
|
+
*Applies to*: XML, HTML, YAML (JSON doesn't support comments in standard spec)
|
|
458
|
+
|
|
459
|
+
*Behaviors*:
|
|
460
|
+
|
|
461
|
+
* `strict` - Comments must match exactly (including whitespace)
|
|
462
|
+
* `normalize` - Whitespace in comments is normalized
|
|
463
|
+
* `ignore` - Comments are completely ignored
|
|
464
|
+
|
|
465
|
+
*XML Example*:
|
|
466
|
+
|
|
467
|
+
.Input Files
|
|
468
|
+
[source,xml]
|
|
469
|
+
----
|
|
470
|
+
<!-- File 1 -->
|
|
471
|
+
<root>
|
|
472
|
+
<!-- This is a comment -->
|
|
473
|
+
<element>Value</element>
|
|
474
|
+
</root>
|
|
475
|
+
|
|
476
|
+
<!-- File 2 -->
|
|
477
|
+
<root>
|
|
478
|
+
<element>Value</element>
|
|
479
|
+
</root>
|
|
480
|
+
----
|
|
481
|
+
|
|
482
|
+
.Comparison Results
|
|
483
|
+
|===
|
|
484
|
+
|Behavior |Result |Explanation
|
|
485
|
+
|
|
486
|
+
|`strict`
|
|
487
|
+
|Different
|
|
488
|
+
|File 1 has a comment, File 2 doesn't
|
|
489
|
+
|
|
490
|
+
|`normalize`
|
|
491
|
+
|Different
|
|
492
|
+
|Still different (comment present vs absent)
|
|
493
|
+
|
|
494
|
+
|`ignore`
|
|
495
|
+
|Equivalent
|
|
496
|
+
|Comments ignored, structure matches
|
|
497
|
+
|===
|
|
498
|
+
|
|
499
|
+
*YAML Example*:
|
|
500
|
+
|
|
501
|
+
.Input Files
|
|
502
|
+
[source,yaml]
|
|
503
|
+
----
|
|
504
|
+
# File 1
|
|
505
|
+
# Configuration file
|
|
506
|
+
name: test
|
|
507
|
+
# Database settings
|
|
508
|
+
database: prod
|
|
509
|
+
|
|
510
|
+
# File 2
|
|
511
|
+
name: test
|
|
512
|
+
database: prod
|
|
513
|
+
----
|
|
514
|
+
|
|
515
|
+
.Comparison Results
|
|
516
|
+
|===
|
|
517
|
+
|Behavior |Result |Explanation
|
|
518
|
+
|
|
519
|
+
|`strict`
|
|
520
|
+
|Different
|
|
521
|
+
|Comments differ
|
|
522
|
+
|
|
523
|
+
|`normalize`
|
|
524
|
+
|Different
|
|
525
|
+
|Comments still differ
|
|
526
|
+
|
|
527
|
+
|`ignore`
|
|
528
|
+
|Equivalent
|
|
529
|
+
|Comments ignored
|
|
530
|
+
|===
|
|
531
|
+
|
|
532
|
+
== Preprocessing Options
|
|
533
|
+
|
|
534
|
+
Preprocessing transforms documents before comparison. This happens in Phase 1 of the comparison flow.
|
|
535
|
+
|
|
536
|
+
=== none
|
|
537
|
+
|
|
538
|
+
*Description*: No preprocessing applied. Documents are compared as-is.
|
|
539
|
+
|
|
540
|
+
*Use when*: You want to compare the exact input without any modifications.
|
|
541
|
+
|
|
542
|
+
=== c14n
|
|
543
|
+
|
|
544
|
+
*Description*: Apply canonical form:
|
|
545
|
+
|
|
546
|
+
* *XML/HTML*: W3C XML Canonicalization (C14N) 1.1
|
|
547
|
+
* *JSON*: Sort keys alphabetically, normalize whitespace
|
|
548
|
+
* *YAML*: Sort keys, normalize to standard YAML format
|
|
549
|
+
|
|
550
|
+
*Use when*: You want to eliminate all formatting differences before comparison.
|
|
551
|
+
|
|
552
|
+
=== normalize
|
|
553
|
+
|
|
554
|
+
*Description*: Normalize whitespace throughout the document:
|
|
555
|
+
|
|
556
|
+
* Collapse multiple whitespace to single space
|
|
557
|
+
* Trim leading/trailing whitespace
|
|
558
|
+
* Normalize line endings
|
|
559
|
+
|
|
560
|
+
*Use when*: You want to ignore whitespace differences but preserve structure.
|
|
561
|
+
|
|
562
|
+
=== format
|
|
563
|
+
|
|
564
|
+
*Description*: Pretty-print the document with standard formatting:
|
|
565
|
+
|
|
566
|
+
* *XML/HTML*: 2-space indentation, one element per line
|
|
567
|
+
* *JSON*: 2-space indentation, standard JSON formatting
|
|
568
|
+
* *YAML*: Standard YAML formatting
|
|
569
|
+
|
|
570
|
+
*Use when*: You want both documents formatted consistently before comparison.
|
|
571
|
+
|
|
572
|
+
== Match Profiles
|
|
573
|
+
|
|
574
|
+
Match profiles are predefined combinations of match dimension settings. They provide convenient shortcuts for common comparison scenarios.
|
|
575
|
+
|
|
576
|
+
=== strict
|
|
577
|
+
|
|
578
|
+
*Description*: Exact matching - all dimensions are set to `strict`.
|
|
579
|
+
|
|
580
|
+
*Settings*:
|
|
581
|
+
[source,ruby]
|
|
582
|
+
----
|
|
583
|
+
{
|
|
584
|
+
preprocessing: :none,
|
|
585
|
+
text_content: :strict,
|
|
586
|
+
structural_whitespace: :strict,
|
|
587
|
+
attribute_whitespace: :strict,
|
|
588
|
+
attribute_order: :strict,
|
|
589
|
+
attribute_values: :strict,
|
|
590
|
+
key_order: :strict,
|
|
591
|
+
comments: :strict
|
|
592
|
+
}
|
|
593
|
+
----
|
|
594
|
+
|
|
595
|
+
*Use when*: You need character-perfect matching.
|
|
596
|
+
|
|
597
|
+
=== rendered
|
|
598
|
+
|
|
599
|
+
*Description*: Mimics how browsers/CSS engines render content.
|
|
600
|
+
|
|
601
|
+
*Settings*:
|
|
602
|
+
[source,ruby]
|
|
603
|
+
----
|
|
604
|
+
{
|
|
605
|
+
preprocessing: :none,
|
|
606
|
+
text_content: :normalize,
|
|
607
|
+
structural_whitespace: :normalize,
|
|
608
|
+
attribute_whitespace: :normalize,
|
|
609
|
+
attribute_order: :ignore,
|
|
610
|
+
attribute_values: :strict,
|
|
611
|
+
key_order: :ignore,
|
|
612
|
+
comments: :ignore
|
|
613
|
+
}
|
|
614
|
+
----
|
|
615
|
+
|
|
616
|
+
*Use when*: Comparing rendered output (HTML) where formatting doesn't affect display.
|
|
617
|
+
|
|
618
|
+
=== spec_friendly
|
|
619
|
+
|
|
620
|
+
*Description*: Test-friendly comparison that ignores most formatting differences.
|
|
621
|
+
|
|
622
|
+
*Settings*:
|
|
623
|
+
[source,ruby]
|
|
624
|
+
----
|
|
625
|
+
{
|
|
626
|
+
preprocessing: :normalize,
|
|
627
|
+
text_content: :normalize,
|
|
628
|
+
structural_whitespace: :ignore,
|
|
629
|
+
attribute_whitespace: :normalize,
|
|
630
|
+
attribute_order: :ignore,
|
|
631
|
+
attribute_values: :strict,
|
|
632
|
+
key_order: :ignore,
|
|
633
|
+
comments: :ignore
|
|
634
|
+
}
|
|
635
|
+
----
|
|
636
|
+
|
|
637
|
+
*Use when*: Writing tests where you care about content but not formatting.
|
|
638
|
+
|
|
639
|
+
=== content_only
|
|
640
|
+
|
|
641
|
+
*Description*: Only semantic content matters - maximum tolerance for formatting.
|
|
642
|
+
|
|
643
|
+
*Settings*:
|
|
644
|
+
[source,ruby]
|
|
645
|
+
----
|
|
646
|
+
{
|
|
647
|
+
preprocessing: :normalize,
|
|
648
|
+
text_content: :normalize,
|
|
649
|
+
structural_whitespace: :ignore,
|
|
650
|
+
attribute_whitespace: :ignore,
|
|
651
|
+
attribute_order: :ignore,
|
|
652
|
+
attribute_values: :ignore,
|
|
653
|
+
key_order: :ignore,
|
|
654
|
+
comments: :ignore
|
|
655
|
+
}
|
|
656
|
+
----
|
|
657
|
+
|
|
658
|
+
*Use when*: You only care about the structural content, not any formatting or attribute details.
|
|
659
|
+
|
|
660
|
+
== Format Defaults
|
|
661
|
+
|
|
662
|
+
Each format has sensible defaults based on its typical usage:
|
|
663
|
+
|
|
664
|
+
[cols="1,1,1,1,1,1,1,1",options="header"]
|
|
665
|
+
|===
|
|
666
|
+
|Dimension |XML |HTML |HTML4 |HTML5 |JSON |YAML
|
|
667
|
+
|
|
668
|
+
|preprocessing
|
|
669
|
+
|none
|
|
670
|
+
|none
|
|
671
|
+
|none
|
|
672
|
+
|none
|
|
673
|
+
|none
|
|
674
|
+
|none
|
|
675
|
+
|
|
676
|
+
|text_content
|
|
677
|
+
|strict
|
|
678
|
+
|normalize
|
|
679
|
+
|normalize
|
|
680
|
+
|normalize
|
|
681
|
+
|strict
|
|
682
|
+
|strict
|
|
683
|
+
|
|
684
|
+
|structural_whitespace
|
|
685
|
+
|strict
|
|
686
|
+
|normalize
|
|
687
|
+
|normalize
|
|
688
|
+
|normalize
|
|
689
|
+
|strict
|
|
690
|
+
|strict
|
|
691
|
+
|
|
692
|
+
|attribute_whitespace
|
|
693
|
+
|strict
|
|
694
|
+
|normalize
|
|
695
|
+
|normalize
|
|
696
|
+
|normalize
|
|
697
|
+
|—
|
|
698
|
+
|—
|
|
699
|
+
|
|
700
|
+
|attribute_order
|
|
701
|
+
|strict
|
|
702
|
+
|ignore
|
|
703
|
+
|ignore
|
|
704
|
+
|ignore
|
|
705
|
+
|—
|
|
706
|
+
|—
|
|
707
|
+
|
|
708
|
+
|attribute_values
|
|
709
|
+
|strict
|
|
710
|
+
|strict
|
|
711
|
+
|strict
|
|
712
|
+
|strict
|
|
713
|
+
|—
|
|
714
|
+
|—
|
|
715
|
+
|
|
716
|
+
|key_order
|
|
717
|
+
|—
|
|
718
|
+
|—
|
|
719
|
+
|—
|
|
720
|
+
|—
|
|
721
|
+
|strict
|
|
722
|
+
|strict
|
|
723
|
+
|
|
724
|
+
|comments
|
|
725
|
+
|strict
|
|
726
|
+
|ignore
|
|
727
|
+
|ignore
|
|
728
|
+
|ignore
|
|
729
|
+
|—
|
|
730
|
+
|strict
|
|
731
|
+
|===
|
|
732
|
+
|
|
733
|
+
*Diff Mode Defaults*:
|
|
734
|
+
|
|
735
|
+
* XML/HTML: `by_line`
|
|
736
|
+
* JSON/YAML: `by_object`
|
|
737
|
+
|
|
738
|
+
== Verbose Mode
|
|
739
|
+
|
|
740
|
+
When `verbose: true` is specified, Canon returns detailed information about the comparison results in a Hash structure instead of a simple boolean.
|
|
741
|
+
|
|
742
|
+
=== Return Structure
|
|
743
|
+
|
|
744
|
+
Verbose mode returns a Hash with two keys:
|
|
745
|
+
|
|
746
|
+
[source,ruby]
|
|
747
|
+
----
|
|
748
|
+
{
|
|
749
|
+
differences: Array, # Array of difference objects
|
|
750
|
+
preprocessed: Array # Two-element array of preprocessed content
|
|
751
|
+
}
|
|
752
|
+
----
|
|
753
|
+
|
|
754
|
+
=== differences
|
|
755
|
+
|
|
756
|
+
An array of difference objects. Each difference object contains:
|
|
757
|
+
|
|
758
|
+
* *For XML/HTML*:
|
|
759
|
+
- `:node1` - The node from the first document
|
|
760
|
+
- `:node2` - The node from the second document (or nil if missing)
|
|
761
|
+
- `:diff1` - Difference code (e.g., `Canon::Comparison::UNEQUAL_ELEMENTS`)
|
|
762
|
+
- `:diff2` - Corresponding code for second document
|
|
763
|
+
- Additional context depending on the difference type
|
|
764
|
+
|
|
765
|
+
* *For JSON/YAML*:
|
|
766
|
+
- `:path` - Path to the difference (e.g., "user.name" or "[2].id")
|
|
767
|
+
- `:value1` - Value from first document
|
|
768
|
+
- `:value2` - Value from second document
|
|
769
|
+
- `:diff_code` - Type of difference (e.g., `Canon::Comparison::UNEQUAL_PRIMITIVES`)
|
|
770
|
+
|
|
771
|
+
If documents are equivalent, `differences` will be an empty array.
|
|
772
|
+
|
|
773
|
+
=== preprocessed
|
|
774
|
+
|
|
775
|
+
A two-element array containing the preprocessed versions of both documents that were used for comparison.
|
|
776
|
+
|
|
777
|
+
**Important**: The preprocessed content respects match options. For example:
|
|
778
|
+
|
|
779
|
+
* When `comments: :ignore` is set, comments are removed from the preprocessed XML
|
|
780
|
+
* When `structural_whitespace: :ignore` is set, whitespace-only text nodes are filtered from XML
|
|
781
|
+
* For HTML with `preprocessing: :rendered`, the content is normalized to match browser rendering
|
|
782
|
+
|
|
783
|
+
This ensures that diff rendering shows only the content that was actually compared, avoiding confusion from showing differences in ignored content.
|
|
784
|
+
|
|
785
|
+
.Example: XML with comments ignored
|
|
786
|
+
[source,ruby]
|
|
787
|
+
----
|
|
788
|
+
xml1 = <<~XML
|
|
789
|
+
<root>
|
|
790
|
+
<!-- This comment will be filtered -->
|
|
791
|
+
<item>Value</item>
|
|
792
|
+
</root>
|
|
793
|
+
XML
|
|
794
|
+
|
|
795
|
+
xml2 = "<root><item>Value</item></root>"
|
|
796
|
+
|
|
797
|
+
result = Canon::Comparison.equivalent?(xml1, xml2,
|
|
798
|
+
match: { comments: :ignore },
|
|
799
|
+
verbose: true
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
# result[:preprocessed][0] will have the comment removed
|
|
803
|
+
# This ensures diff rendering doesn't show comment differences
|
|
804
|
+
# that were ignored during comparison
|
|
805
|
+
----
|
|
806
|
+
|
|
807
|
+
=== Using Verbose Results
|
|
808
|
+
|
|
809
|
+
.Check for equivalence
|
|
810
|
+
[source,ruby]
|
|
811
|
+
----
|
|
812
|
+
result = Canon::Comparison.equivalent?(doc1, doc2, verbose: true)
|
|
813
|
+
|
|
814
|
+
if result[:differences].empty?
|
|
815
|
+
puts "Documents are equivalent"
|
|
816
|
+
else
|
|
817
|
+
puts "Found #{result[:differences].size} differences"
|
|
818
|
+
end
|
|
819
|
+
----
|
|
820
|
+
|
|
821
|
+
.Access preprocessed content for custom processing
|
|
822
|
+
[source,ruby]
|
|
823
|
+
----
|
|
824
|
+
result = Canon::Comparison.equivalent?(xml1, xml2,
|
|
825
|
+
match: { structural_whitespace: :ignore },
|
|
826
|
+
verbose: true
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
preprocessed1, preprocessed2 = result[:preprocessed]
|
|
830
|
+
|
|
831
|
+
# Use preprocessed content for custom diff rendering
|
|
832
|
+
# or further analysis
|
|
833
|
+
----
|
|
834
|
+
|
|
835
|
+
.Iterate through differences
|
|
836
|
+
[source,ruby]
|
|
837
|
+
----
|
|
838
|
+
result = Canon::Comparison.equivalent?(json1, json2, verbose: true)
|
|
839
|
+
|
|
840
|
+
result[:differences].each do |diff|
|
|
841
|
+
puts "Path: #{diff[:path]}"
|
|
842
|
+
puts " Expected: #{diff[:value1].inspect}"
|
|
843
|
+
puts " Got: #{diff[:value2].inspect}"
|
|
844
|
+
end
|
|
845
|
+
----
|
|
846
|
+
|
|
847
|
+
== Usage
|
|
848
|
+
|
|
849
|
+
=== CLI Interface
|
|
850
|
+
|
|
851
|
+
==== Command Syntax
|
|
852
|
+
|
|
853
|
+
[source,bash]
|
|
854
|
+
----
|
|
855
|
+
canon diff FILE1 FILE2 [OPTIONS]
|
|
856
|
+
----
|
|
857
|
+
|
|
858
|
+
==== Match Control Options
|
|
859
|
+
|
|
860
|
+
[source,bash]
|
|
861
|
+
----
|
|
862
|
+
--match-profile PROFILE # strict|rendered|spec_friendly|content_only
|
|
863
|
+
--preprocessing MODE # none|c14n|normalize|format
|
|
864
|
+
|
|
865
|
+
# Match dimensions
|
|
866
|
+
--text-content BEHAVIOR # strict|normalize|ignore
|
|
867
|
+
--structural-whitespace BEHAVIOR # strict|normalize|ignore
|
|
868
|
+
--attribute-whitespace BEHAVIOR # strict|normalize|ignore (XML/HTML only)
|
|
869
|
+
--attribute-order BEHAVIOR # strict|ignore (XML/HTML only)
|
|
870
|
+
--attribute-values BEHAVIOR # strict|normalize|ignore (XML/HTML only)
|
|
871
|
+
--key-order BEHAVIOR # strict|ignore (JSON/YAML only)
|
|
872
|
+
--comments BEHAVIOR # strict|normalize|ignore
|
|
873
|
+
----
|
|
874
|
+
|
|
875
|
+
==== Diff Display Options
|
|
876
|
+
|
|
877
|
+
[source,bash]
|
|
878
|
+
----
|
|
879
|
+
--diff-mode MODE # by_line|by_object
|
|
880
|
+
--color / --no-color # Enable/disable colorized output
|
|
881
|
+
--context-lines N # Number of context lines around changes
|
|
882
|
+
--diff-grouping-lines N # Group changes within N lines
|
|
883
|
+
----
|
|
884
|
+
|
|
885
|
+
==== Format Specification
|
|
886
|
+
|
|
887
|
+
[source,bash]
|
|
888
|
+
----
|
|
889
|
+
--format FORMAT # xml|html|json|yaml (for both files)
|
|
890
|
+
--format1 FORMAT # Format of first file
|
|
891
|
+
--format2 FORMAT # Format of second file
|
|
892
|
+
----
|
|
893
|
+
|
|
894
|
+
==== CLI Examples
|
|
895
|
+
|
|
896
|
+
.Use a match profile
|
|
897
|
+
[source,bash]
|
|
898
|
+
----
|
|
899
|
+
canon diff file1.xml file2.xml --match-profile spec_friendly
|
|
900
|
+
----
|
|
901
|
+
|
|
902
|
+
.Override a specific dimension
|
|
903
|
+
[source,bash]
|
|
904
|
+
----
|
|
905
|
+
canon diff file1.xml file2.xml --text-content normalize
|
|
906
|
+
----
|
|
907
|
+
|
|
908
|
+
.Combine profile with overrides
|
|
909
|
+
[source,bash]
|
|
910
|
+
----
|
|
911
|
+
canon diff file1.xml file2.xml \
|
|
912
|
+
--match-profile spec_friendly \
|
|
913
|
+
--comments strict \
|
|
914
|
+
--diff-mode by_line
|
|
915
|
+
----
|
|
916
|
+
|
|
917
|
+
.Preprocess before comparing
|
|
918
|
+
[source,bash]
|
|
919
|
+
----
|
|
920
|
+
canon diff file1.xml file2.xml --preprocessing normalize
|
|
921
|
+
----
|
|
922
|
+
|
|
923
|
+
.Multiple dimension overrides
|
|
924
|
+
[source,bash]
|
|
925
|
+
----
|
|
926
|
+
canon diff file1.xml file2.xml \
|
|
927
|
+
--text-content normalize \
|
|
928
|
+
--structural-whitespace ignore \
|
|
929
|
+
--attribute-order ignore
|
|
930
|
+
----
|
|
931
|
+
|
|
932
|
+
.JSON comparison with custom diff settings
|
|
933
|
+
[source,bash]
|
|
934
|
+
----
|
|
935
|
+
canon diff config1.json config2.json \
|
|
936
|
+
--match-profile spec_friendly \
|
|
937
|
+
--key-order ignore \
|
|
938
|
+
--context-lines 5 \
|
|
939
|
+
--no-color
|
|
940
|
+
----
|
|
941
|
+
|
|
942
|
+
.HTML comparison with rendered profile
|
|
943
|
+
[source,bash]
|
|
944
|
+
----
|
|
945
|
+
canon diff page1.html page2.html \
|
|
946
|
+
--match-profile rendered \
|
|
947
|
+
--diff-grouping-lines 2
|
|
948
|
+
----
|
|
949
|
+
|
|
950
|
+
=== Ruby API Interface
|
|
951
|
+
|
|
952
|
+
==== Method Signature
|
|
953
|
+
|
|
954
|
+
[source,ruby]
|
|
955
|
+
----
|
|
956
|
+
Canon::Comparison.equivalent?(obj1, obj2, options = {})
|
|
957
|
+
----
|
|
958
|
+
|
|
959
|
+
==== Options Hash
|
|
960
|
+
|
|
961
|
+
[source,ruby]
|
|
962
|
+
----
|
|
963
|
+
{
|
|
964
|
+
# Match control
|
|
965
|
+
match_profile: Symbol, # :strict, :rendered, :spec_friendly, :content_only
|
|
966
|
+
preprocessing: Symbol, # :none, :c14n, :normalize, :format
|
|
967
|
+
match: Hash, # Hash of dimension => behavior
|
|
968
|
+
|
|
969
|
+
# Diff control
|
|
970
|
+
verbose: Boolean, # Return diff details if different
|
|
971
|
+
diff: Hash # Diff rendering options
|
|
972
|
+
}
|
|
973
|
+
----
|
|
974
|
+
|
|
975
|
+
==== Match Options Hash
|
|
976
|
+
|
|
977
|
+
[source,ruby]
|
|
978
|
+
----
|
|
979
|
+
{
|
|
980
|
+
text_content: Symbol, # :strict, :normalize, :ignore
|
|
981
|
+
structural_whitespace: Symbol, # :strict, :normalize, :ignore
|
|
982
|
+
attribute_whitespace: Symbol, # :strict, :normalize, :ignore (XML/HTML)
|
|
983
|
+
attribute_order: Symbol, # :strict, :ignore (XML/HTML)
|
|
984
|
+
attribute_values: Symbol, # :strict, :normalize, :ignore (XML/HTML)
|
|
985
|
+
key_order: Symbol, # :strict, :ignore (JSON/YAML)
|
|
986
|
+
comments: Symbol # :strict, :normalize, :ignore
|
|
987
|
+
}
|
|
988
|
+
----
|
|
989
|
+
|
|
990
|
+
==== Diff Options Hash
|
|
991
|
+
|
|
992
|
+
[source,ruby]
|
|
993
|
+
----
|
|
994
|
+
{
|
|
995
|
+
mode: Symbol, # :by_line, :by_object
|
|
996
|
+
use_color: Boolean, # Enable/disable colors
|
|
997
|
+
context_lines: Integer, # Lines of context
|
|
998
|
+
grouping_lines: Integer # Group changes within N lines
|
|
999
|
+
}
|
|
1000
|
+
----
|
|
1001
|
+
|
|
1002
|
+
==== Ruby API Examples
|
|
1003
|
+
|
|
1004
|
+
.Basic comparison with auto-detection
|
|
1005
|
+
[source,ruby]
|
|
1006
|
+
----
|
|
1007
|
+
require "canon/comparison"
|
|
1008
|
+
|
|
1009
|
+
xml1 = File.read("file1.xml")
|
|
1010
|
+
xml2 = File.read("file2.xml")
|
|
1011
|
+
|
|
1012
|
+
Canon::Comparison.equivalent?(xml1, xml2)
|
|
1013
|
+
# => true or false
|
|
1014
|
+
----
|
|
1015
|
+
|
|
1016
|
+
.Use a match profile
|
|
1017
|
+
[source,ruby]
|
|
1018
|
+
----
|
|
1019
|
+
Canon::Comparison.equivalent?(xml1, xml2,
|
|
1020
|
+
match_profile: :spec_friendly
|
|
1021
|
+
)
|
|
1022
|
+
----
|
|
1023
|
+
|
|
1024
|
+
.Preprocess before comparison
|
|
1025
|
+
[source,ruby]
|
|
1026
|
+
----
|
|
1027
|
+
Canon::Comparison.equivalent?(xml1, xml2,
|
|
1028
|
+
preprocessing: :normalize
|
|
1029
|
+
)
|
|
1030
|
+
----
|
|
1031
|
+
|
|
1032
|
+
.Override specific match dimensions
|
|
1033
|
+
[source,ruby]
|
|
1034
|
+
----
|
|
1035
|
+
Canon::Comparison.equivalent?(xml1, xml2,
|
|
1036
|
+
match: {
|
|
1037
|
+
text_content: :normalize,
|
|
1038
|
+
structural_whitespace: :ignore,
|
|
1039
|
+
comments: :ignore
|
|
1040
|
+
}
|
|
1041
|
+
)
|
|
1042
|
+
----
|
|
1043
|
+
|
|
1044
|
+
.Combine profile with dimension overrides
|
|
1045
|
+
[source,ruby]
|
|
1046
|
+
----
|
|
1047
|
+
Canon::Comparison.equivalent?(xml1, xml2,
|
|
1048
|
+
match_profile: :spec_friendly,
|
|
1049
|
+
match: {
|
|
1050
|
+
comments: :strict # Override profile setting
|
|
1051
|
+
}
|
|
1052
|
+
)
|
|
1053
|
+
----
|
|
1054
|
+
|
|
1055
|
+
.Get verbose diff output
|
|
1056
|
+
[source,ruby]
|
|
1057
|
+
----
|
|
1058
|
+
result = Canon::Comparison.equivalent?(xml1, xml2,
|
|
1059
|
+
match_profile: :spec_friendly,
|
|
1060
|
+
verbose: true
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
if result.is_a?(Hash)
|
|
1064
|
+
# Verbose mode returns a Hash with :differences and :preprocessed keys
|
|
1065
|
+
if result[:differences].empty?
|
|
1066
|
+
# Documents are equivalent
|
|
1067
|
+
puts "Files match!"
|
|
1068
|
+
else
|
|
1069
|
+
# Documents differ, result[:differences] contains diff details
|
|
1070
|
+
puts "Differences found:"
|
|
1071
|
+
puts result[:differences]
|
|
1072
|
+
|
|
1073
|
+
# result[:preprocessed] contains the preprocessed content
|
|
1074
|
+
# that was used for comparison, respecting match options
|
|
1075
|
+
# (e.g., whitespace-only nodes filtered for XML)
|
|
1076
|
+
preprocessed1, preprocessed2 = result[:preprocessed]
|
|
1077
|
+
end
|
|
1078
|
+
else
|
|
1079
|
+
# Non-verbose mode returns boolean
|
|
1080
|
+
puts result ? "Files match!" : "Files differ"
|
|
1081
|
+
end
|
|
1082
|
+
----
|
|
1083
|
+
|
|
1084
|
+
.Verbose with custom diff options
|
|
1085
|
+
[source,ruby]
|
|
1086
|
+
----
|
|
1087
|
+
result = Canon::Comparison.equivalent?(xml1, xml2,
|
|
1088
|
+
match_profile: :spec_friendly,
|
|
1089
|
+
verbose: true,
|
|
1090
|
+
diff: {
|
|
1091
|
+
mode: :by_line,
|
|
1092
|
+
use_color: true,
|
|
1093
|
+
context_lines: 5,
|
|
1094
|
+
grouping_lines: 2
|
|
1095
|
+
}
|
|
1096
|
+
)
|
|
1097
|
+
----
|
|
1098
|
+
|
|
1099
|
+
.JSON comparison
|
|
1100
|
+
[source,ruby]
|
|
1101
|
+
----
|
|
1102
|
+
json1 = File.read("config1.json")
|
|
1103
|
+
json2 = File.read("config2.json")
|
|
1104
|
+
|
|
1105
|
+
Canon::Comparison.equivalent?(json1, json2,
|
|
1106
|
+
match_profile: :spec_friendly,
|
|
1107
|
+
match: {
|
|
1108
|
+
key_order: :ignore
|
|
1109
|
+
}
|
|
1110
|
+
)
|
|
1111
|
+
----
|
|
1112
|
+
|
|
1113
|
+
.Explicit format specification
|
|
1114
|
+
[source,ruby]
|
|
1115
|
+
----
|
|
1116
|
+
Canon::Comparison::XmlComparator.equivalent?(xml1, xml2,
|
|
1117
|
+
match_profile: :strict,
|
|
1118
|
+
match: {
|
|
1119
|
+
attribute_order: :ignore
|
|
1120
|
+
}
|
|
1121
|
+
)
|
|
1122
|
+
----
|
|
1123
|
+
|
|
1124
|
+
=== RSpec Interface
|
|
1125
|
+
|
|
1126
|
+
==== Global Configuration
|
|
1127
|
+
|
|
1128
|
+
[source,ruby]
|
|
1129
|
+
----
|
|
1130
|
+
# spec/spec_helper.rb or spec/support/canon.rb
|
|
1131
|
+
require "canon/rspec_matchers"
|
|
1132
|
+
|
|
1133
|
+
Canon::RSpecMatchers.configure do |config|
|
|
1134
|
+
# Format-specific configuration
|
|
1135
|
+
config.<format>.match.profile = Symbol
|
|
1136
|
+
config.<format>.match.options = Hash
|
|
1137
|
+
config.<format>.preprocessing = Symbol
|
|
1138
|
+
config.<format>.diff.mode = Symbol
|
|
1139
|
+
config.<format>.diff.use_color = Boolean
|
|
1140
|
+
config.<format>.diff.context_lines = Integer
|
|
1141
|
+
config.<format>.diff.grouping_lines = Integer
|
|
1142
|
+
end
|
|
1143
|
+
----
|
|
1144
|
+
|
|
1145
|
+
*Supported formats*: `xml`, `html`, `json`, `yaml`
|
|
1146
|
+
|
|
1147
|
+
==== Match Configuration
|
|
1148
|
+
|
|
1149
|
+
For each format, set match profile or individual dimension options:
|
|
1150
|
+
|
|
1151
|
+
[source,ruby]
|
|
1152
|
+
----
|
|
1153
|
+
# Using a profile
|
|
1154
|
+
config.<format>.match.profile = :spec_friendly
|
|
1155
|
+
|
|
1156
|
+
# OR using individual dimension options
|
|
1157
|
+
config.<format>.match.options = {
|
|
1158
|
+
text_content: :normalize,
|
|
1159
|
+
structural_whitespace: :ignore,
|
|
1160
|
+
attribute_whitespace: :normalize, # XML/HTML only
|
|
1161
|
+
attribute_order: :ignore, # XML/HTML only
|
|
1162
|
+
attribute_values: :strict, # XML/HTML only
|
|
1163
|
+
key_order: :ignore, # JSON/YAML only
|
|
1164
|
+
comments: :ignore
|
|
1165
|
+
}
|
|
1166
|
+
----
|
|
1167
|
+
|
|
1168
|
+
==== Diff Configuration
|
|
1169
|
+
|
|
1170
|
+
For each format:
|
|
1171
|
+
|
|
1172
|
+
[source,ruby]
|
|
1173
|
+
----
|
|
1174
|
+
config.<format>.diff.mode = :by_line # :by_line or :by_object
|
|
1175
|
+
config.<format>.diff.use_color = true
|
|
1176
|
+
config.<format>.diff.context_lines = 3
|
|
1177
|
+
config.<format>.diff.grouping_lines = 10
|
|
1178
|
+
----
|
|
1179
|
+
|
|
1180
|
+
==== Matcher Syntax
|
|
1181
|
+
|
|
1182
|
+
[source,ruby]
|
|
1183
|
+
----
|
|
1184
|
+
# Basic matchers
|
|
1185
|
+
expect(actual).to be_equivalent_xml_to(expected)
|
|
1186
|
+
expect(actual).to be_equivalent_html_to(expected)
|
|
1187
|
+
expect(actual).to be_equivalent_json_to(expected)
|
|
1188
|
+
expect(actual).to be_equivalent_yaml_to(expected)
|
|
1189
|
+
|
|
1190
|
+
# With verbose output (shows diff on failure)
|
|
1191
|
+
expect(actual).to be_equivalent_xml_to(expected, verbose: true)
|
|
1192
|
+
|
|
1193
|
+
# Override with profile
|
|
1194
|
+
expect(actual).to be_equivalent_xml_to(expected, verbose: true)
|
|
1195
|
+
.with_profile(:rendered)
|
|
1196
|
+
|
|
1197
|
+
# Override with specific options
|
|
1198
|
+
expect(actual).to be_equivalent_xml_to(expected, verbose: true)
|
|
1199
|
+
.with_options(
|
|
1200
|
+
text_content: :strict,
|
|
1201
|
+
structural_whitespace: :strict
|
|
1202
|
+
)
|
|
1203
|
+
|
|
1204
|
+
# Combine profile and option overrides
|
|
1205
|
+
expect(actual).to be_equivalent_xml_to(expected, verbose: true)
|
|
1206
|
+
.with_profile(:spec_friendly)
|
|
1207
|
+
.with_options(comments: :strict)
|
|
1208
|
+
----
|
|
1209
|
+
|
|
1210
|
+
==== RSpec Examples
|
|
1211
|
+
|
|
1212
|
+
.Global configuration for XML
|
|
1213
|
+
[source,ruby]
|
|
1214
|
+
----
|
|
1215
|
+
Canon::RSpecMatchers.configure do |config|
|
|
1216
|
+
config.xml.match.profile = :spec_friendly
|
|
1217
|
+
config.xml.preprocessing = :normalize
|
|
1218
|
+
config.xml.match.options = {
|
|
1219
|
+
text_content: :normalize,
|
|
1220
|
+
structural_whitespace: :ignore,
|
|
1221
|
+
comments: :ignore
|
|
1222
|
+
}
|
|
1223
|
+
config.xml.diff.mode = :by_line
|
|
1224
|
+
config.xml.diff.use_color = true
|
|
1225
|
+
config.xml.diff.context_lines = 3
|
|
1226
|
+
end
|
|
1227
|
+
----
|
|
1228
|
+
|
|
1229
|
+
.Global configuration for HTML
|
|
1230
|
+
[source,ruby]
|
|
1231
|
+
----
|
|
1232
|
+
Canon::RSpecMatchers.configure do |config|
|
|
1233
|
+
config.html.match.profile = :rendered
|
|
1234
|
+
config.html.match.options = {
|
|
1235
|
+
structural_whitespace: :ignore
|
|
1236
|
+
}
|
|
1237
|
+
config.html.diff.mode = :by_line
|
|
1238
|
+
config.html.diff.grouping_lines = 2
|
|
1239
|
+
end
|
|
1240
|
+
----
|
|
1241
|
+
|
|
1242
|
+
.Global configuration for JSON
|
|
1243
|
+
[source,ruby]
|
|
1244
|
+
----
|
|
1245
|
+
Canon::RSpecMatchers.configure do |config|
|
|
1246
|
+
config.json.match.profile = :spec_friendly
|
|
1247
|
+
config.json.match.options = {
|
|
1248
|
+
key_order: :ignore
|
|
1249
|
+
}
|
|
1250
|
+
config.json.diff.mode = :by_object
|
|
1251
|
+
config.json.diff.context_lines = 5
|
|
1252
|
+
end
|
|
1253
|
+
----
|
|
1254
|
+
|
|
1255
|
+
.Basic spec usage
|
|
1256
|
+
[source,ruby]
|
|
1257
|
+
----
|
|
1258
|
+
RSpec.describe "XML comparison" do
|
|
1259
|
+
it "compares XML documents" do
|
|
1260
|
+
actual = File.read("actual.xml")
|
|
1261
|
+
expected = File.read("expected.xml")
|
|
1262
|
+
|
|
1263
|
+
expect(actual).to be_equivalent_xml_to(expected)
|
|
1264
|
+
end
|
|
1265
|
+
end
|
|
1266
|
+
----
|
|
1267
|
+
|
|
1268
|
+
.With verbose output
|
|
1269
|
+
[source,ruby]
|
|
1270
|
+
----
|
|
1271
|
+
it "shows diff on failure" do
|
|
1272
|
+
expect(actual).to be_equivalent_xml_to(expected, verbose: true)
|
|
1273
|
+
end
|
|
1274
|
+
----
|
|
1275
|
+
|
|
1276
|
+
.Override global config with profile
|
|
1277
|
+
[source,ruby]
|
|
1278
|
+
----
|
|
1279
|
+
it "uses strict matching for this test" do
|
|
1280
|
+
expect(actual).to be_equivalent_xml_to(expected, verbose: true)
|
|
1281
|
+
.with_profile(:strict)
|
|
1282
|
+
end
|
|
1283
|
+
----
|
|
1284
|
+
|
|
1285
|
+
.Override specific dimensions
|
|
1286
|
+
[source,ruby]
|
|
1287
|
+
----
|
|
1288
|
+
it "requires strict whitespace for this test" do
|
|
1289
|
+
expect(actual).to be_equivalent_xml_to(expected, verbose: true)
|
|
1290
|
+
.with_options(
|
|
1291
|
+
structural_whitespace: :strict,
|
|
1292
|
+
text_content: :strict
|
|
1293
|
+
)
|
|
1294
|
+
end
|
|
1295
|
+
----
|
|
1296
|
+
|
|
1297
|
+
.Combine profile and overrides
|
|
1298
|
+
[source,ruby]
|
|
1299
|
+
----
|
|
1300
|
+
it "uses spec_friendly but checks comments" do
|
|
1301
|
+
expect(actual).to be_equivalent_xml_to(expected, verbose: true)
|
|
1302
|
+
.with_profile(:spec_friendly)
|
|
1303
|
+
.with_options(comments: :strict)
|
|
1304
|
+
end
|
|
1305
|
+
----
|
|
1306
|
+
|
|
1307
|
+
.HTML comparison
|
|
1308
|
+
[source,ruby]
|
|
1309
|
+
----
|
|
1310
|
+
it "compares HTML with rendered profile" do
|
|
1311
|
+
expect(actual_html).to be_equivalent_html_to(expected_html, verbose: true)
|
|
1312
|
+
.with_profile(:rendered)
|
|
1313
|
+
end
|
|
1314
|
+
----
|
|
1315
|
+
|
|
1316
|
+
.JSON comparison
|
|
1317
|
+
[source,ruby]
|
|
1318
|
+
----
|
|
1319
|
+
it "compares JSON ignoring key order" do
|
|
1320
|
+
expect(actual_json).to be_equivalent_json_to(expected_json, verbose: true)
|
|
1321
|
+
.with_options(key_order: :ignore)
|
|
1322
|
+
end
|
|
1323
|
+
----
|
|
1324
|
+
|
|
1325
|
+
== Configuration Precedence
|
|
1326
|
+
|
|
1327
|
+
When options are specified in multiple places, Canon resolves them using the following precedence hierarchy (highest to lowest):
|
|
1328
|
+
|
|
1329
|
+
[source]
|
|
1330
|
+
----
|
|
1331
|
+
1. Per-test explicit options (highest priority)
|
|
1332
|
+
↓
|
|
1333
|
+
2. Per-test profile
|
|
1334
|
+
↓
|
|
1335
|
+
3. Global config explicit options
|
|
1336
|
+
↓
|
|
1337
|
+
4. Global config profile
|
|
1338
|
+
↓
|
|
1339
|
+
5. Format defaults (lowest priority)
|
|
1340
|
+
----
|
|
1341
|
+
|
|
1342
|
+
=== Precedence Example
|
|
1343
|
+
|
|
1344
|
+
.Global configuration
|
|
1345
|
+
[source,ruby]
|
|
1346
|
+
----
|
|
1347
|
+
Canon::RSpecMatchers.configure do |config|
|
|
1348
|
+
config.xml.match.profile = :spec_friendly # Sets multiple dimensions
|
|
1349
|
+
config.xml.match.options = { comments: :strict } # Explicit override
|
|
1350
|
+
end
|
|
1351
|
+
----
|
|
1352
|
+
|
|
1353
|
+
The `:spec_friendly` profile sets:
|
|
1354
|
+
- `text_content: :normalize`
|
|
1355
|
+
- `structural_whitespace: :ignore`
|
|
1356
|
+
- `comments: :ignore`
|
|
1357
|
+
|
|
1358
|
+
But the explicit `comments: :strict` in options overrides the profile setting.
|
|
1359
|
+
|
|
1360
|
+
.Per-test usage
|
|
1361
|
+
[source,ruby]
|
|
1362
|
+
----
|
|
1363
|
+
expect(actual).to be_equivalent_xml_to(expected)
|
|
1364
|
+
.with_profile(:rendered) # Sets: text_content=normalize, structural_whitespace=normalize
|
|
1365
|
+
.with_options(structural_whitespace: :ignore) # Explicit override
|
|
1366
|
+
----
|
|
1367
|
+
|
|
1368
|
+
*Final resolved options*:
|
|
1369
|
+
- `text_content: :normalize` (from `:rendered` per-test profile)
|
|
1370
|
+
- `structural_whitespace: :ignore` (from per-test explicit option)
|
|
1371
|
+
- `comments: :strict` (from global explicit option)
|
|
1372
|
+
- Other dimensions use `:rendered` profile or format defaults
|
|
1373
|
+
|
|
1374
|
+
=== Resolution Rules
|
|
1375
|
+
|
|
1376
|
+
. Explicit options always override profile settings
|
|
1377
|
+
. Per-test options override global configuration
|
|
1378
|
+
. Profile settings are merged (not replaced) - explicit options from lower precedence levels are preserved
|
|
1379
|
+
. Missing dimensions fall back to the next level in the hierarchy
|
|
1380
|
+
. At the bottom, format defaults provide the final fallback
|
|
1381
|
+
|
|
1382
|
+
This allows you to:
|
|
1383
|
+
|
|
1384
|
+
* Set sensible defaults globally
|
|
1385
|
+
* Use profiles for common scenarios
|
|
1386
|
+
* Override specific dimensions when needed
|
|
1387
|
+
* Maintain fine-grained control per test
|