canon 0.1.6 → 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.
Files changed (136) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +163 -67
  3. data/README.adoc +400 -7
  4. data/docs/Gemfile +9 -0
  5. data/docs/INDEX.adoc +99 -182
  6. data/docs/_config.yml +100 -0
  7. data/docs/advanced/diff-classification.adoc +547 -0
  8. data/docs/advanced/diff-pipeline.adoc +358 -0
  9. data/docs/advanced/index.adoc +214 -0
  10. data/docs/advanced/semantic-diff-report.adoc +390 -0
  11. data/docs/{VERBOSE.adoc → advanced/verbose-mode-architecture.adoc} +51 -53
  12. data/docs/features/diff-formatting/algorithm-specific-output.adoc +533 -0
  13. data/docs/{CHARACTER_VISUALIZATION.adoc → features/diff-formatting/character-visualization.adoc} +23 -62
  14. data/docs/features/diff-formatting/colors-and-symbols.adoc +606 -0
  15. data/docs/features/diff-formatting/context-and-grouping.adoc +490 -0
  16. data/docs/features/diff-formatting/display-filtering.adoc +472 -0
  17. data/docs/features/diff-formatting/index.adoc +140 -0
  18. data/docs/features/environment-configuration/index.adoc +327 -0
  19. data/docs/features/environment-configuration/override-system.adoc +436 -0
  20. data/docs/features/environment-configuration/size-limits.adoc +273 -0
  21. data/docs/features/index.adoc +173 -0
  22. data/docs/features/input-validation/index.adoc +521 -0
  23. data/docs/features/match-options/algorithm-specific-behavior.adoc +365 -0
  24. data/docs/features/match-options/html-policies.adoc +312 -0
  25. data/docs/features/match-options/index.adoc +621 -0
  26. data/docs/getting-started/index.adoc +83 -0
  27. data/docs/getting-started/quick-start.adoc +76 -0
  28. data/docs/guides/choosing-configuration.adoc +689 -0
  29. data/docs/guides/index.adoc +181 -0
  30. data/docs/{CLI.adoc → interfaces/cli/index.adoc} +18 -13
  31. data/docs/interfaces/index.adoc +101 -0
  32. data/docs/{RSPEC.adoc → interfaces/rspec/index.adoc} +242 -31
  33. data/docs/{RUBY_API.adoc → interfaces/ruby-api/index.adoc} +118 -16
  34. data/docs/lychee.toml +65 -0
  35. data/docs/reference/cli-options.adoc +418 -0
  36. data/docs/reference/environment-variables.adoc +375 -0
  37. data/docs/reference/index.adoc +204 -0
  38. data/docs/reference/options-across-interfaces.adoc +417 -0
  39. data/docs/understanding/algorithms/dom-diff.adoc +389 -0
  40. data/docs/understanding/algorithms/index.adoc +314 -0
  41. data/docs/understanding/algorithms/semantic-tree-diff.adoc +533 -0
  42. data/docs/understanding/architecture.adoc +447 -0
  43. data/docs/understanding/comparison-pipeline.adoc +317 -0
  44. data/docs/understanding/formats/html.adoc +380 -0
  45. data/docs/understanding/formats/index.adoc +261 -0
  46. data/docs/understanding/formats/json.adoc +390 -0
  47. data/docs/understanding/formats/xml.adoc +366 -0
  48. data/docs/understanding/formats/yaml.adoc +504 -0
  49. data/docs/understanding/index.adoc +130 -0
  50. data/lib/canon/cli.rb +42 -1
  51. data/lib/canon/commands/diff_command.rb +108 -23
  52. data/lib/canon/comparison/compare_profile.rb +101 -0
  53. data/lib/canon/comparison/comparison_result.rb +41 -2
  54. data/lib/canon/comparison/html_comparator.rb +292 -71
  55. data/lib/canon/comparison/html_compare_profile.rb +117 -0
  56. data/lib/canon/comparison/match_options.rb +42 -4
  57. data/lib/canon/comparison/strategies/base_match_strategy.rb +99 -0
  58. data/lib/canon/comparison/strategies/match_strategy_factory.rb +74 -0
  59. data/lib/canon/comparison/strategies/semantic_tree_match_strategy.rb +220 -0
  60. data/lib/canon/comparison/xml_comparator.rb +695 -91
  61. data/lib/canon/comparison.rb +207 -2
  62. data/lib/canon/config/env_provider.rb +71 -0
  63. data/lib/canon/config/env_schema.rb +58 -0
  64. data/lib/canon/config/override_resolver.rb +55 -0
  65. data/lib/canon/config/type_converter.rb +59 -0
  66. data/lib/canon/config.rb +158 -29
  67. data/lib/canon/data_model.rb +29 -0
  68. data/lib/canon/diff/diff_classifier.rb +74 -14
  69. data/lib/canon/diff/diff_context_builder.rb +41 -0
  70. data/lib/canon/diff/diff_line.rb +18 -2
  71. data/lib/canon/diff/diff_node.rb +18 -3
  72. data/lib/canon/diff/diff_node_mapper.rb +71 -12
  73. data/lib/canon/diff/formatting_detector.rb +53 -0
  74. data/lib/canon/diff_formatter/by_line/base_formatter.rb +60 -5
  75. data/lib/canon/diff_formatter/by_line/html_formatter.rb +68 -16
  76. data/lib/canon/diff_formatter/by_line/json_formatter.rb +0 -37
  77. data/lib/canon/diff_formatter/by_line/simple_formatter.rb +0 -42
  78. data/lib/canon/diff_formatter/by_line/xml_formatter.rb +116 -31
  79. data/lib/canon/diff_formatter/by_line/yaml_formatter.rb +0 -37
  80. data/lib/canon/diff_formatter/by_object/base_formatter.rb +126 -19
  81. data/lib/canon/diff_formatter/by_object/xml_formatter.rb +30 -1
  82. data/lib/canon/diff_formatter/debug_output.rb +7 -1
  83. data/lib/canon/diff_formatter/diff_detail_formatter.rb +674 -57
  84. data/lib/canon/diff_formatter/legend.rb +42 -0
  85. data/lib/canon/diff_formatter.rb +78 -9
  86. data/lib/canon/errors.rb +56 -0
  87. data/lib/canon/formatters/html_formatter_base.rb +35 -1
  88. data/lib/canon/formatters/json_formatter.rb +3 -0
  89. data/lib/canon/formatters/yaml_formatter.rb +3 -0
  90. data/lib/canon/html/data_model.rb +229 -0
  91. data/lib/canon/html.rb +9 -0
  92. data/lib/canon/options/cli_generator.rb +70 -0
  93. data/lib/canon/options/registry.rb +234 -0
  94. data/lib/canon/rspec_matchers.rb +34 -13
  95. data/lib/canon/tree_diff/adapters/html_adapter.rb +316 -0
  96. data/lib/canon/tree_diff/adapters/json_adapter.rb +204 -0
  97. data/lib/canon/tree_diff/adapters/xml_adapter.rb +285 -0
  98. data/lib/canon/tree_diff/adapters/yaml_adapter.rb +213 -0
  99. data/lib/canon/tree_diff/core/attribute_comparator.rb +84 -0
  100. data/lib/canon/tree_diff/core/matching.rb +241 -0
  101. data/lib/canon/tree_diff/core/node_signature.rb +164 -0
  102. data/lib/canon/tree_diff/core/node_weight.rb +135 -0
  103. data/lib/canon/tree_diff/core/tree_node.rb +450 -0
  104. data/lib/canon/tree_diff/matchers/hash_matcher.rb +258 -0
  105. data/lib/canon/tree_diff/matchers/similarity_matcher.rb +168 -0
  106. data/lib/canon/tree_diff/matchers/structural_propagator.rb +242 -0
  107. data/lib/canon/tree_diff/matchers/universal_matcher.rb +220 -0
  108. data/lib/canon/tree_diff/operation_converter.rb +631 -0
  109. data/lib/canon/tree_diff/operations/operation.rb +92 -0
  110. data/lib/canon/tree_diff/operations/operation_detector.rb +626 -0
  111. data/lib/canon/tree_diff/tree_diff_integrator.rb +140 -0
  112. data/lib/canon/tree_diff.rb +33 -0
  113. data/lib/canon/validators/json_validator.rb +3 -1
  114. data/lib/canon/validators/yaml_validator.rb +3 -1
  115. data/lib/canon/version.rb +1 -1
  116. data/lib/canon/xml/data_model.rb +22 -23
  117. data/lib/canon/xml/element_matcher.rb +128 -20
  118. data/lib/canon/xml/namespace_helper.rb +110 -0
  119. data/lib/canon.rb +3 -0
  120. metadata +81 -23
  121. data/_config.yml +0 -116
  122. data/docs/ADVANCED_TOPICS.adoc +0 -20
  123. data/docs/BASIC_USAGE.adoc +0 -16
  124. data/docs/CUSTOMIZING_BEHAVIOR.adoc +0 -19
  125. data/docs/DIFF_ARCHITECTURE.adoc +0 -435
  126. data/docs/DIFF_FORMATTING.adoc +0 -540
  127. data/docs/FORMATS.adoc +0 -447
  128. data/docs/INPUT_VALIDATION.adoc +0 -477
  129. data/docs/MATCH_ARCHITECTURE.adoc +0 -463
  130. data/docs/MATCH_OPTIONS.adoc +0 -719
  131. data/docs/MODES.adoc +0 -432
  132. data/docs/NORMATIVE_INFORMATIVE_DIFFS.adoc +0 -219
  133. data/docs/OPTIONS.adoc +0 -1387
  134. data/docs/PREPROCESSING.adoc +0 -491
  135. data/docs/SEMANTIC_DIFF_REPORT.adoc +0 -528
  136. data/docs/UNDERSTANDING_CANON.adoc +0 -17
@@ -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
- @profile = nil
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
- @profile = nil
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] = @profile if @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
- attr_accessor :mode, :use_color, :context_lines, :grouping_lines,
139
- :show_diffs, :verbose_diff
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
- @mode = :by_line
152
- @use_color = true
153
- @context_lines = 3
154
- @grouping_lines = 10
155
- @show_diffs = :all
156
- @verbose_diff = false
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: @mode,
163
- use_color: @use_color,
164
- context_lines: @context_lines,
165
- grouping_lines: @grouping_lines,
166
- show_diffs: @show_diffs,
167
- verbose_diff: @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