canon 0.1.5 → 0.1.7
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_todo.yml +163 -67
- data/README.adoc +400 -7
- data/docs/Gemfile +9 -0
- data/docs/INDEX.adoc +99 -182
- data/docs/_config.yml +100 -0
- data/docs/advanced/diff-classification.adoc +547 -0
- data/docs/advanced/diff-pipeline.adoc +358 -0
- data/docs/advanced/index.adoc +214 -0
- data/docs/advanced/semantic-diff-report.adoc +390 -0
- data/docs/{VERBOSE.adoc → advanced/verbose-mode-architecture.adoc} +51 -53
- data/docs/features/diff-formatting/algorithm-specific-output.adoc +533 -0
- data/docs/{CHARACTER_VISUALIZATION.adoc → features/diff-formatting/character-visualization.adoc} +23 -62
- data/docs/features/diff-formatting/colors-and-symbols.adoc +606 -0
- data/docs/features/diff-formatting/context-and-grouping.adoc +490 -0
- data/docs/features/diff-formatting/display-filtering.adoc +472 -0
- data/docs/features/diff-formatting/index.adoc +140 -0
- data/docs/features/environment-configuration/index.adoc +327 -0
- data/docs/features/environment-configuration/override-system.adoc +436 -0
- data/docs/features/environment-configuration/size-limits.adoc +273 -0
- data/docs/features/index.adoc +173 -0
- data/docs/features/input-validation/index.adoc +521 -0
- data/docs/features/match-options/algorithm-specific-behavior.adoc +365 -0
- data/docs/features/match-options/html-policies.adoc +312 -0
- data/docs/features/match-options/index.adoc +621 -0
- data/docs/getting-started/index.adoc +83 -0
- data/docs/getting-started/quick-start.adoc +76 -0
- data/docs/guides/choosing-configuration.adoc +689 -0
- data/docs/guides/index.adoc +181 -0
- data/docs/{CLI.adoc → interfaces/cli/index.adoc} +18 -13
- data/docs/interfaces/index.adoc +101 -0
- data/docs/{RSPEC.adoc → interfaces/rspec/index.adoc} +242 -31
- data/docs/{RUBY_API.adoc → interfaces/ruby-api/index.adoc} +118 -16
- data/docs/lychee.toml +65 -0
- data/docs/reference/cli-options.adoc +418 -0
- data/docs/reference/environment-variables.adoc +375 -0
- data/docs/reference/index.adoc +204 -0
- data/docs/reference/options-across-interfaces.adoc +417 -0
- data/docs/understanding/algorithms/dom-diff.adoc +389 -0
- data/docs/understanding/algorithms/index.adoc +314 -0
- data/docs/understanding/algorithms/semantic-tree-diff.adoc +533 -0
- data/docs/understanding/architecture.adoc +447 -0
- data/docs/understanding/comparison-pipeline.adoc +317 -0
- data/docs/understanding/formats/html.adoc +380 -0
- data/docs/understanding/formats/index.adoc +261 -0
- data/docs/understanding/formats/json.adoc +390 -0
- data/docs/understanding/formats/xml.adoc +366 -0
- data/docs/understanding/formats/yaml.adoc +504 -0
- data/docs/understanding/index.adoc +130 -0
- data/lib/canon/cli.rb +42 -1
- data/lib/canon/commands/diff_command.rb +108 -23
- data/lib/canon/comparison/compare_profile.rb +101 -0
- data/lib/canon/comparison/comparison_result.rb +41 -2
- data/lib/canon/comparison/html_comparator.rb +292 -71
- data/lib/canon/comparison/html_compare_profile.rb +117 -0
- data/lib/canon/comparison/match_options.rb +42 -4
- data/lib/canon/comparison/strategies/base_match_strategy.rb +99 -0
- data/lib/canon/comparison/strategies/match_strategy_factory.rb +74 -0
- data/lib/canon/comparison/strategies/semantic_tree_match_strategy.rb +220 -0
- data/lib/canon/comparison/xml_comparator.rb +695 -91
- data/lib/canon/comparison.rb +207 -2
- data/lib/canon/config/env_provider.rb +71 -0
- data/lib/canon/config/env_schema.rb +58 -0
- data/lib/canon/config/override_resolver.rb +55 -0
- data/lib/canon/config/type_converter.rb +59 -0
- data/lib/canon/config.rb +158 -29
- data/lib/canon/data_model.rb +29 -0
- data/lib/canon/diff/diff_classifier.rb +74 -14
- data/lib/canon/diff/diff_context_builder.rb +41 -0
- data/lib/canon/diff/diff_line.rb +18 -2
- data/lib/canon/diff/diff_node.rb +18 -3
- data/lib/canon/diff/diff_node_mapper.rb +71 -12
- data/lib/canon/diff/formatting_detector.rb +53 -0
- data/lib/canon/diff_formatter/by_line/base_formatter.rb +60 -5
- data/lib/canon/diff_formatter/by_line/html_formatter.rb +68 -16
- data/lib/canon/diff_formatter/by_line/json_formatter.rb +0 -37
- data/lib/canon/diff_formatter/by_line/simple_formatter.rb +0 -42
- data/lib/canon/diff_formatter/by_line/xml_formatter.rb +116 -31
- data/lib/canon/diff_formatter/by_line/yaml_formatter.rb +0 -37
- data/lib/canon/diff_formatter/by_object/base_formatter.rb +126 -19
- data/lib/canon/diff_formatter/by_object/xml_formatter.rb +30 -1
- data/lib/canon/diff_formatter/debug_output.rb +7 -1
- data/lib/canon/diff_formatter/diff_detail_formatter.rb +674 -57
- data/lib/canon/diff_formatter/legend.rb +42 -0
- data/lib/canon/diff_formatter.rb +78 -9
- data/lib/canon/errors.rb +56 -0
- data/lib/canon/formatters/html_formatter_base.rb +35 -1
- data/lib/canon/formatters/json_formatter.rb +3 -0
- data/lib/canon/formatters/yaml_formatter.rb +3 -0
- data/lib/canon/html/data_model.rb +229 -0
- data/lib/canon/html.rb +9 -0
- data/lib/canon/options/cli_generator.rb +70 -0
- data/lib/canon/options/registry.rb +234 -0
- data/lib/canon/rspec_matchers.rb +34 -13
- data/lib/canon/tree_diff/adapters/html_adapter.rb +316 -0
- data/lib/canon/tree_diff/adapters/json_adapter.rb +204 -0
- data/lib/canon/tree_diff/adapters/xml_adapter.rb +285 -0
- data/lib/canon/tree_diff/adapters/yaml_adapter.rb +213 -0
- data/lib/canon/tree_diff/core/attribute_comparator.rb +84 -0
- data/lib/canon/tree_diff/core/matching.rb +241 -0
- data/lib/canon/tree_diff/core/node_signature.rb +164 -0
- data/lib/canon/tree_diff/core/node_weight.rb +135 -0
- data/lib/canon/tree_diff/core/tree_node.rb +450 -0
- data/lib/canon/tree_diff/matchers/hash_matcher.rb +258 -0
- data/lib/canon/tree_diff/matchers/similarity_matcher.rb +168 -0
- data/lib/canon/tree_diff/matchers/structural_propagator.rb +242 -0
- data/lib/canon/tree_diff/matchers/universal_matcher.rb +220 -0
- data/lib/canon/tree_diff/operation_converter.rb +631 -0
- data/lib/canon/tree_diff/operations/operation.rb +92 -0
- data/lib/canon/tree_diff/operations/operation_detector.rb +626 -0
- data/lib/canon/tree_diff/tree_diff_integrator.rb +140 -0
- data/lib/canon/tree_diff.rb +33 -0
- data/lib/canon/validators/json_validator.rb +3 -1
- data/lib/canon/validators/yaml_validator.rb +3 -1
- data/lib/canon/version.rb +1 -1
- data/lib/canon/xml/data_model.rb +22 -23
- data/lib/canon/xml/element_matcher.rb +128 -20
- data/lib/canon/xml/namespace_helper.rb +110 -0
- data/lib/canon.rb +3 -0
- metadata +81 -23
- data/_config.yml +0 -116
- data/docs/ADVANCED_TOPICS.adoc +0 -20
- data/docs/BASIC_USAGE.adoc +0 -16
- data/docs/CUSTOMIZING_BEHAVIOR.adoc +0 -19
- data/docs/DIFF_ARCHITECTURE.adoc +0 -435
- data/docs/DIFF_FORMATTING.adoc +0 -540
- data/docs/FORMATS.adoc +0 -447
- data/docs/INPUT_VALIDATION.adoc +0 -477
- data/docs/MATCH_ARCHITECTURE.adoc +0 -463
- data/docs/MATCH_OPTIONS.adoc +0 -719
- data/docs/MODES.adoc +0 -432
- data/docs/NORMATIVE_INFORMATIVE_DIFFS.adoc +0 -219
- data/docs/OPTIONS.adoc +0 -1387
- data/docs/PREPROCESSING.adoc +0 -491
- data/docs/SEMANTIC_DIFF_REPORT.adoc +0 -528
- data/docs/UNDERSTANDING_CANON.adoc +0 -17
data/lib/canon/comparison.rb
CHANGED
|
@@ -100,8 +100,215 @@ module Canon
|
|
|
100
100
|
# @param obj2 [Object] Second object to compare
|
|
101
101
|
# @param opts [Hash] Comparison options
|
|
102
102
|
# - :format - Format hint (:xml, :html, :html4, :html5, :json, :yaml, :string)
|
|
103
|
+
# - :diff_algorithm - Algorithm to use (:dom or :semantic)
|
|
103
104
|
# @return [Boolean, Array] true if equivalent, or array of diffs if verbose
|
|
104
105
|
def equivalent?(obj1, obj2, opts = {})
|
|
106
|
+
# Check if semantic tree diff is requested
|
|
107
|
+
# Support both :semantic and :semantic_tree for backward compatibility
|
|
108
|
+
if %i[semantic semantic_tree].include?(opts[:diff_algorithm])
|
|
109
|
+
return semantic_diff(obj1, obj2, opts)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Otherwise use DOM-based comparison (default)
|
|
113
|
+
dom_diff(obj1, obj2, opts)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
# Perform semantic tree diff comparison
|
|
119
|
+
def semantic_diff(obj1, obj2, opts = {})
|
|
120
|
+
require_relative "tree_diff"
|
|
121
|
+
|
|
122
|
+
# Detect format for both objects
|
|
123
|
+
format1 = opts[:format] || detect_format(obj1)
|
|
124
|
+
format2 = opts[:format] || detect_format(obj2)
|
|
125
|
+
|
|
126
|
+
# Handle string format (plain text comparison) - semantic tree doesn't support it
|
|
127
|
+
if format1 == :string
|
|
128
|
+
if opts[:verbose]
|
|
129
|
+
return obj1.to_s == obj2.to_s ? [] : [:different]
|
|
130
|
+
else
|
|
131
|
+
return obj1.to_s == obj2.to_s
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Ensure formats match
|
|
136
|
+
unless format1 == format2
|
|
137
|
+
raise Canon::CompareFormatMismatchError.new(format1, format2)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Resolve match options for the format
|
|
141
|
+
match_opts_hash = resolve_match_options(format1, opts)
|
|
142
|
+
|
|
143
|
+
# Delegate parsing to comparators (reuses existing preprocessing logic)
|
|
144
|
+
doc1, doc2 = parse_with_comparator(obj1, obj2, format1, match_opts_hash)
|
|
145
|
+
|
|
146
|
+
# Normalize format for TreeDiff (html4/html5 -> html)
|
|
147
|
+
tree_diff_format = normalize_format_for_tree_diff(format1)
|
|
148
|
+
|
|
149
|
+
# Create TreeDiff integrator for the format
|
|
150
|
+
# CRITICAL: Use match_opts_hash (resolved options with profile) not opts[:match]
|
|
151
|
+
integrator = Canon::TreeDiff::TreeDiffIntegrator.new(
|
|
152
|
+
format: tree_diff_format,
|
|
153
|
+
options: match_opts_hash,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Perform diff
|
|
157
|
+
tree_diff_result = integrator.diff(doc1, doc2)
|
|
158
|
+
|
|
159
|
+
# Convert operations to DiffNodes for unified pipeline
|
|
160
|
+
# CRITICAL: Use match_opts_hash (resolved options with profile) not opts[:match]
|
|
161
|
+
converter = Canon::TreeDiff::OperationConverter.new(
|
|
162
|
+
format: format1,
|
|
163
|
+
match_options: match_opts_hash,
|
|
164
|
+
)
|
|
165
|
+
diff_nodes = converter.convert(tree_diff_result[:operations])
|
|
166
|
+
|
|
167
|
+
# CRITICAL: Use strategy's preprocess_for_display to ensure proper line-breaking
|
|
168
|
+
# This matches DOM diff preprocessing pattern (xml_comparator.rb:106-109)
|
|
169
|
+
require_relative "comparison/strategies/semantic_tree_match_strategy"
|
|
170
|
+
strategy = Comparison::Strategies::SemanticTreeMatchStrategy.new(
|
|
171
|
+
format: format1, match_options: match_opts_hash,
|
|
172
|
+
)
|
|
173
|
+
str1, str2 = strategy.preprocess_for_display(doc1, doc2)
|
|
174
|
+
|
|
175
|
+
# Store tree diff data in match_options for access via result
|
|
176
|
+
enhanced_match_options = match_opts_hash.merge(
|
|
177
|
+
tree_diff_operations: tree_diff_result[:operations],
|
|
178
|
+
tree_diff_statistics: tree_diff_result[:statistics],
|
|
179
|
+
tree_diff_matching: tree_diff_result[:matching],
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Create ComparisonResult for unified handling
|
|
183
|
+
result = Canon::Comparison::ComparisonResult.new(
|
|
184
|
+
differences: diff_nodes,
|
|
185
|
+
preprocessed_strings: [str1, str2],
|
|
186
|
+
format: format1,
|
|
187
|
+
html_version: %i[html4 html5].include?(format1) ? format1 : nil,
|
|
188
|
+
match_options: enhanced_match_options,
|
|
189
|
+
algorithm: :semantic,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Return boolean or ComparisonResult based on verbose flag
|
|
193
|
+
if opts[:verbose]
|
|
194
|
+
result
|
|
195
|
+
else
|
|
196
|
+
result.equivalent?
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Resolve match options for a format
|
|
201
|
+
#
|
|
202
|
+
# @param format [Symbol] Format type
|
|
203
|
+
# @param opts [Hash] User options
|
|
204
|
+
# @return [Hash] Resolved match options
|
|
205
|
+
def resolve_match_options(format, opts)
|
|
206
|
+
case format
|
|
207
|
+
when :xml, :html, :html4, :html5
|
|
208
|
+
MatchOptions::Xml.resolve(
|
|
209
|
+
format: format,
|
|
210
|
+
match_profile: opts[:match_profile],
|
|
211
|
+
match: opts[:match],
|
|
212
|
+
preprocessing: opts[:preprocessing],
|
|
213
|
+
global_profile: opts[:global_profile],
|
|
214
|
+
global_options: opts[:global_options],
|
|
215
|
+
)
|
|
216
|
+
when :json
|
|
217
|
+
MatchOptions::Json.resolve(
|
|
218
|
+
format: format,
|
|
219
|
+
match_profile: opts[:match_profile],
|
|
220
|
+
match: opts[:match],
|
|
221
|
+
preprocessing: opts[:preprocessing],
|
|
222
|
+
global_profile: opts[:global_profile],
|
|
223
|
+
global_options: opts[:global_options],
|
|
224
|
+
)
|
|
225
|
+
when :yaml
|
|
226
|
+
MatchOptions::Yaml.resolve(
|
|
227
|
+
format: format,
|
|
228
|
+
match_profile: opts[:match_profile],
|
|
229
|
+
match: opts[:match],
|
|
230
|
+
preprocessing: opts[:preprocessing],
|
|
231
|
+
global_profile: opts[:global_profile],
|
|
232
|
+
global_options: opts[:global_options],
|
|
233
|
+
)
|
|
234
|
+
else
|
|
235
|
+
opts[:match] || {}
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Parse documents using comparator's parse logic (reuses preprocessing)
|
|
240
|
+
#
|
|
241
|
+
# @param obj1 [Object] First object
|
|
242
|
+
# @param obj2 [Object] Second object
|
|
243
|
+
# @param format [Symbol] Format type
|
|
244
|
+
# @param match_opts_hash [Hash] Resolved match options
|
|
245
|
+
# @return [Array<Object, Object>] Parsed documents
|
|
246
|
+
def parse_with_comparator(obj1, obj2, format, match_opts_hash)
|
|
247
|
+
preprocessing = match_opts_hash[:preprocessing] || :none
|
|
248
|
+
|
|
249
|
+
case format
|
|
250
|
+
when :xml
|
|
251
|
+
# Delegate to XmlComparator's parse_node - returns Canon::Xml::Node
|
|
252
|
+
# Adapter now handles Canon::Xml::Node directly
|
|
253
|
+
doc1 = XmlComparator.send(:parse_node, obj1, preprocessing)
|
|
254
|
+
doc2 = XmlComparator.send(:parse_node, obj2, preprocessing)
|
|
255
|
+
[doc1, doc2]
|
|
256
|
+
when :html, :html4, :html5
|
|
257
|
+
# Delegate to HtmlComparator's parse_node_for_semantic for Canon::Xml::Node
|
|
258
|
+
[
|
|
259
|
+
HtmlComparator.send(:parse_node_for_semantic, obj1, preprocessing),
|
|
260
|
+
HtmlComparator.send(:parse_node_for_semantic, obj2, preprocessing),
|
|
261
|
+
]
|
|
262
|
+
when :json
|
|
263
|
+
# Delegate to JsonComparator's parse_json
|
|
264
|
+
[
|
|
265
|
+
JsonComparator.send(:parse_json, obj1),
|
|
266
|
+
JsonComparator.send(:parse_json, obj2),
|
|
267
|
+
]
|
|
268
|
+
when :yaml
|
|
269
|
+
# Delegate to YamlComparator's parse_yaml
|
|
270
|
+
[
|
|
271
|
+
YamlComparator.send(:parse_yaml, obj1),
|
|
272
|
+
YamlComparator.send(:parse_yaml, obj2),
|
|
273
|
+
]
|
|
274
|
+
else
|
|
275
|
+
[obj1, obj2]
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Normalize format for TreeDiff (html4/html5 -> html)
|
|
280
|
+
#
|
|
281
|
+
# @param format [Symbol] Original format
|
|
282
|
+
# @return [Symbol] Normalized format for TreeDiff
|
|
283
|
+
def normalize_format_for_tree_diff(format)
|
|
284
|
+
case format
|
|
285
|
+
when :html4, :html5
|
|
286
|
+
:html
|
|
287
|
+
else
|
|
288
|
+
format
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Serialize document back to string
|
|
293
|
+
def serialize_document(doc, format)
|
|
294
|
+
case format
|
|
295
|
+
when :xml, :html, :html4, :html5
|
|
296
|
+
doc.respond_to?(:to_html) ? doc.to_html : doc.to_xml
|
|
297
|
+
when :json
|
|
298
|
+
require "json"
|
|
299
|
+
JSON.pretty_generate(doc)
|
|
300
|
+
when :yaml
|
|
301
|
+
require "yaml"
|
|
302
|
+
doc.to_yaml
|
|
303
|
+
else
|
|
304
|
+
doc.to_s
|
|
305
|
+
end
|
|
306
|
+
rescue StandardError
|
|
307
|
+
doc.to_s
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Perform DOM-based comparison (original behavior)
|
|
311
|
+
def dom_diff(obj1, obj2, opts = {})
|
|
105
312
|
# Use format hint if provided
|
|
106
313
|
if opts[:format]
|
|
107
314
|
format1 = format2 = opts[:format]
|
|
@@ -159,8 +366,6 @@ module Canon
|
|
|
159
366
|
end
|
|
160
367
|
end
|
|
161
368
|
|
|
162
|
-
private
|
|
163
|
-
|
|
164
369
|
# Parse HTML string into Nokogiri document
|
|
165
370
|
#
|
|
166
371
|
# @param content [String, Object] Content to parse (returns as-is if not a string)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "env_schema"
|
|
4
|
+
require_relative "type_converter"
|
|
5
|
+
|
|
6
|
+
module Canon
|
|
7
|
+
class Config
|
|
8
|
+
# Provides environment variable values for configuration
|
|
9
|
+
# Reads and parses CANON_* environment variables
|
|
10
|
+
class EnvProvider
|
|
11
|
+
class << self
|
|
12
|
+
# Load environment overrides for a specific format's diff config
|
|
13
|
+
def load_diff_for_format(format)
|
|
14
|
+
load_config_for_format(format, :diff, EnvSchema.all_diff_attributes)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Load environment overrides for a specific format's match config
|
|
18
|
+
def load_match_for_format(format)
|
|
19
|
+
load_config_for_format(format, :match, EnvSchema.all_match_attributes)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Load environment overrides for a specific format's format config
|
|
23
|
+
def load_format_for_format(format)
|
|
24
|
+
load_config_for_format(format, :format,
|
|
25
|
+
EnvSchema.all_format_attributes)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Load global environment overrides (apply to all formats)
|
|
29
|
+
def load_global_diff
|
|
30
|
+
load_global_config(EnvSchema.all_diff_attributes)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def load_config_for_format(format, config_type, attributes)
|
|
36
|
+
result = {}
|
|
37
|
+
attributes.each do |attr|
|
|
38
|
+
# Try format-specific ENV var first
|
|
39
|
+
env_key = EnvSchema.env_key(format, config_type, attr)
|
|
40
|
+
value = ENV[env_key]
|
|
41
|
+
|
|
42
|
+
# Fall back to global ENV var if format-specific not set
|
|
43
|
+
if value.nil?
|
|
44
|
+
global_key = EnvSchema.global_env_key(attr)
|
|
45
|
+
value = ENV[global_key]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Convert and store if value exists
|
|
49
|
+
if value
|
|
50
|
+
result[attr] = TypeConverter.convert(attr, value)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
result
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def load_global_config(attributes)
|
|
57
|
+
result = {}
|
|
58
|
+
attributes.each do |attr|
|
|
59
|
+
global_key = EnvSchema.global_env_key(attr)
|
|
60
|
+
value = ENV[global_key]
|
|
61
|
+
|
|
62
|
+
if value
|
|
63
|
+
result[attr] = TypeConverter.convert(attr, value)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
result
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Canon
|
|
4
|
+
class Config
|
|
5
|
+
# Schema definition for configuration attributes
|
|
6
|
+
# Defines attribute types and ENV variable mappings
|
|
7
|
+
class EnvSchema
|
|
8
|
+
ATTRIBUTE_TYPES = {
|
|
9
|
+
# DiffConfig attributes
|
|
10
|
+
mode: :symbol,
|
|
11
|
+
use_color: :boolean,
|
|
12
|
+
context_lines: :integer,
|
|
13
|
+
grouping_lines: :integer,
|
|
14
|
+
show_diffs: :symbol,
|
|
15
|
+
verbose_diff: :boolean,
|
|
16
|
+
algorithm: :symbol,
|
|
17
|
+
|
|
18
|
+
# MatchConfig attributes
|
|
19
|
+
profile: :symbol,
|
|
20
|
+
|
|
21
|
+
# FormatConfig attributes
|
|
22
|
+
preprocessing: :string,
|
|
23
|
+
|
|
24
|
+
# Size limits to prevent hangs on large files
|
|
25
|
+
max_file_size: :integer,
|
|
26
|
+
max_node_count: :integer,
|
|
27
|
+
max_diff_lines: :integer,
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
def type_for(attribute)
|
|
32
|
+
ATTRIBUTE_TYPES[attribute.to_sym]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def env_key(format, config_type, attribute)
|
|
36
|
+
"CANON_#{format.to_s.upcase}_#{config_type.to_s.upcase}_#{attribute.to_s.upcase}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def global_env_key(attribute)
|
|
40
|
+
"CANON_#{attribute.to_s.upcase}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def all_diff_attributes
|
|
44
|
+
%i[mode use_color context_lines grouping_lines show_diffs
|
|
45
|
+
verbose_diff algorithm max_file_size max_node_count max_diff_lines]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def all_match_attributes
|
|
49
|
+
%i[profile]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def all_format_attributes
|
|
53
|
+
%i[preprocessing]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Canon
|
|
4
|
+
class Config
|
|
5
|
+
# Resolves configuration values using priority chain
|
|
6
|
+
# Priority: ENV > programmatic > defaults
|
|
7
|
+
class OverrideResolver
|
|
8
|
+
attr_reader :defaults, :programmatic, :env
|
|
9
|
+
|
|
10
|
+
def initialize(defaults: {}, programmatic: {}, env: {})
|
|
11
|
+
@defaults = defaults
|
|
12
|
+
@programmatic = programmatic
|
|
13
|
+
@env = env
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Resolve a single value using priority chain
|
|
17
|
+
# Uses .key? to properly handle false values
|
|
18
|
+
def resolve(key)
|
|
19
|
+
return @env[key] if @env.key?(key)
|
|
20
|
+
return @programmatic[key] if @programmatic.key?(key)
|
|
21
|
+
|
|
22
|
+
@defaults[key]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Update programmatic value
|
|
26
|
+
def set_programmatic(key, value)
|
|
27
|
+
@programmatic[key] = value
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Update ENV override
|
|
31
|
+
def set_env(key, value)
|
|
32
|
+
@env[key] = value
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Check if value is set by ENV
|
|
36
|
+
def env_set?(key)
|
|
37
|
+
@env.key?(key)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Check if value is set programmatically
|
|
41
|
+
def programmatic_set?(key)
|
|
42
|
+
@programmatic.key?(key)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Get the source of a value
|
|
46
|
+
def source_for(key)
|
|
47
|
+
return :env if @env.key?(key)
|
|
48
|
+
return :programmatic if @programmatic.key?(key)
|
|
49
|
+
return :default if @defaults.key?(key)
|
|
50
|
+
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Canon
|
|
4
|
+
class Config
|
|
5
|
+
# Type converter for environment variable values
|
|
6
|
+
# Converts string ENV values to appropriate Ruby types
|
|
7
|
+
class TypeConverter
|
|
8
|
+
BOOLEAN_VALUES = {
|
|
9
|
+
"true" => true,
|
|
10
|
+
"1" => true,
|
|
11
|
+
"yes" => true,
|
|
12
|
+
"false" => false,
|
|
13
|
+
"0" => false,
|
|
14
|
+
"no" => false,
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def convert(attribute, value)
|
|
19
|
+
return nil if value.nil? || value.empty?
|
|
20
|
+
|
|
21
|
+
type = EnvSchema.type_for(attribute)
|
|
22
|
+
case type
|
|
23
|
+
when :boolean
|
|
24
|
+
convert_boolean(value)
|
|
25
|
+
when :integer
|
|
26
|
+
convert_integer(value)
|
|
27
|
+
when :symbol
|
|
28
|
+
convert_symbol(value)
|
|
29
|
+
when :string
|
|
30
|
+
value
|
|
31
|
+
else
|
|
32
|
+
value
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def convert_boolean(value)
|
|
39
|
+
normalized = value.to_s.downcase
|
|
40
|
+
return BOOLEAN_VALUES[normalized] if BOOLEAN_VALUES.key?(normalized)
|
|
41
|
+
|
|
42
|
+
raise ArgumentError,
|
|
43
|
+
"Invalid boolean value: #{value}. " \
|
|
44
|
+
"Valid values: #{BOOLEAN_VALUES.keys.join(', ')}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def convert_integer(value)
|
|
48
|
+
Integer(value)
|
|
49
|
+
rescue ArgumentError
|
|
50
|
+
raise ArgumentError, "Invalid integer value: #{value}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def convert_symbol(value)
|
|
54
|
+
value.to_sym
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
data/lib/canon/config.rb
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "config/env_provider"
|
|
4
|
+
require_relative "config/override_resolver"
|
|
5
|
+
|
|
3
6
|
module Canon
|
|
4
7
|
# Global configuration for Canon
|
|
5
8
|
# Provides unified configuration across CLI, Ruby API, and RSpec interfaces
|
|
@@ -93,8 +96,8 @@ module Canon
|
|
|
93
96
|
|
|
94
97
|
def initialize(format)
|
|
95
98
|
@format = format
|
|
96
|
-
@match = MatchConfig.new
|
|
97
|
-
@diff = DiffConfig.new
|
|
99
|
+
@match = MatchConfig.new(format)
|
|
100
|
+
@diff = DiffConfig.new(format)
|
|
98
101
|
@preprocessing = nil
|
|
99
102
|
end
|
|
100
103
|
|
|
@@ -107,11 +110,11 @@ module Canon
|
|
|
107
110
|
|
|
108
111
|
# Match configuration for comparison behavior
|
|
109
112
|
class MatchConfig
|
|
110
|
-
attr_accessor :profile
|
|
111
113
|
attr_reader :options
|
|
112
114
|
|
|
113
|
-
def initialize
|
|
114
|
-
@
|
|
115
|
+
def initialize(format = nil)
|
|
116
|
+
@format = format
|
|
117
|
+
@resolver = build_resolver(format)
|
|
115
118
|
@options = {}
|
|
116
119
|
end
|
|
117
120
|
|
|
@@ -120,53 +123,179 @@ module Canon
|
|
|
120
123
|
end
|
|
121
124
|
|
|
122
125
|
def reset!
|
|
123
|
-
@
|
|
126
|
+
@resolver = build_resolver(@format)
|
|
124
127
|
@options = {}
|
|
125
128
|
end
|
|
126
129
|
|
|
130
|
+
# Profile accessor with ENV override support
|
|
131
|
+
def profile
|
|
132
|
+
@resolver.resolve(:profile)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def profile=(value)
|
|
136
|
+
@resolver.set_programmatic(:profile, value)
|
|
137
|
+
end
|
|
138
|
+
|
|
127
139
|
# Build match options from profile and options
|
|
128
140
|
def to_h
|
|
129
141
|
result = {}
|
|
130
|
-
result[:match_profile] =
|
|
142
|
+
result[:match_profile] = profile if profile
|
|
131
143
|
result[:match] = @options if @options && !@options.empty?
|
|
132
144
|
result
|
|
133
145
|
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
def build_resolver(format)
|
|
150
|
+
defaults = {
|
|
151
|
+
profile: nil,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
env = format ? EnvProvider.load_match_for_format(format) : {}
|
|
155
|
+
|
|
156
|
+
OverrideResolver.new(
|
|
157
|
+
defaults: defaults,
|
|
158
|
+
programmatic: {},
|
|
159
|
+
env: env,
|
|
160
|
+
)
|
|
161
|
+
end
|
|
134
162
|
end
|
|
135
163
|
|
|
136
164
|
# Diff configuration for output formatting
|
|
137
165
|
class DiffConfig
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def initialize
|
|
142
|
-
@mode = :by_line
|
|
143
|
-
@use_color = true
|
|
144
|
-
@context_lines = 3
|
|
145
|
-
@grouping_lines = 10
|
|
146
|
-
@show_diffs = :all
|
|
147
|
-
@verbose_diff = false
|
|
166
|
+
def initialize(format = nil)
|
|
167
|
+
@format = format
|
|
168
|
+
@resolver = build_resolver(format)
|
|
148
169
|
end
|
|
149
170
|
|
|
150
171
|
def reset!
|
|
151
|
-
@
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
@
|
|
172
|
+
@resolver = build_resolver(@format)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Accessors with ENV override support
|
|
176
|
+
def mode
|
|
177
|
+
@resolver.resolve(:mode)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def mode=(value)
|
|
181
|
+
@resolver.set_programmatic(:mode, value)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def use_color
|
|
185
|
+
@resolver.resolve(:use_color)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def use_color=(value)
|
|
189
|
+
@resolver.set_programmatic(:use_color, value)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def context_lines
|
|
193
|
+
@resolver.resolve(:context_lines)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def context_lines=(value)
|
|
197
|
+
@resolver.set_programmatic(:context_lines, value)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def grouping_lines
|
|
201
|
+
@resolver.resolve(:grouping_lines)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def grouping_lines=(value)
|
|
205
|
+
@resolver.set_programmatic(:grouping_lines, value)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def show_diffs
|
|
209
|
+
@resolver.resolve(:show_diffs)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def show_diffs=(value)
|
|
213
|
+
@resolver.set_programmatic(:show_diffs, value)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def verbose_diff
|
|
217
|
+
@resolver.resolve(:verbose_diff)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def verbose_diff=(value)
|
|
221
|
+
@resolver.set_programmatic(:verbose_diff, value)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def algorithm
|
|
225
|
+
@resolver.resolve(:algorithm)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def algorithm=(value)
|
|
229
|
+
@resolver.set_programmatic(:algorithm, value)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# File size limit in bytes (default 5MB)
|
|
233
|
+
def max_file_size
|
|
234
|
+
@resolver.resolve(:max_file_size)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def max_file_size=(value)
|
|
238
|
+
@resolver.set_programmatic(:max_file_size, value)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Maximum node count in tree (default 10,000)
|
|
242
|
+
def max_node_count
|
|
243
|
+
@resolver.resolve(:max_node_count)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def max_node_count=(value)
|
|
247
|
+
@resolver.set_programmatic(:max_node_count, value)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Maximum diff output lines (default 10,000)
|
|
251
|
+
def max_diff_lines
|
|
252
|
+
@resolver.resolve(:max_diff_lines)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def max_diff_lines=(value)
|
|
256
|
+
@resolver.set_programmatic(:max_diff_lines, value)
|
|
157
257
|
end
|
|
158
258
|
|
|
159
259
|
# Build diff options
|
|
160
260
|
def to_h
|
|
161
261
|
{
|
|
162
|
-
diff:
|
|
163
|
-
use_color:
|
|
164
|
-
context_lines:
|
|
165
|
-
grouping_lines:
|
|
166
|
-
show_diffs:
|
|
167
|
-
verbose_diff:
|
|
262
|
+
diff: mode,
|
|
263
|
+
use_color: use_color,
|
|
264
|
+
context_lines: context_lines,
|
|
265
|
+
grouping_lines: grouping_lines,
|
|
266
|
+
show_diffs: show_diffs,
|
|
267
|
+
verbose_diff: verbose_diff,
|
|
268
|
+
diff_algorithm: algorithm,
|
|
269
|
+
max_file_size: max_file_size,
|
|
270
|
+
max_node_count: max_node_count,
|
|
271
|
+
max_diff_lines: max_diff_lines,
|
|
168
272
|
}
|
|
169
273
|
end
|
|
274
|
+
|
|
275
|
+
private
|
|
276
|
+
|
|
277
|
+
def build_resolver(format)
|
|
278
|
+
defaults = {
|
|
279
|
+
mode: :by_line,
|
|
280
|
+
use_color: true,
|
|
281
|
+
context_lines: 3,
|
|
282
|
+
grouping_lines: 10,
|
|
283
|
+
show_diffs: :all,
|
|
284
|
+
verbose_diff: false,
|
|
285
|
+
algorithm: :dom,
|
|
286
|
+
max_file_size: 5_242_880, # 5MB in bytes
|
|
287
|
+
max_node_count: 10_000, # Maximum nodes in tree
|
|
288
|
+
max_diff_lines: 10_000, # Maximum diff output lines
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
env = format ? EnvProvider.load_diff_for_format(format) : {}
|
|
292
|
+
|
|
293
|
+
OverrideResolver.new(
|
|
294
|
+
defaults: defaults,
|
|
295
|
+
programmatic: {},
|
|
296
|
+
env: env,
|
|
297
|
+
)
|
|
298
|
+
end
|
|
170
299
|
end
|
|
171
300
|
end
|
|
172
301
|
end
|