class Canon::Commands::DiffCommand
Command for semantic diffing of two files
def add_algorithm_option(opts)
-
opts(Hash) -- Options hash to modify
def add_algorithm_option(opts) opts[:diff_algorithm] = determine_algorithm end
def add_show_diffs_option(opts)
-
opts(Hash) -- Options hash to modify
def add_show_diffs_option(opts) return unless @options[:show_diffs] opts[:show_diffs] = @options[:show_diffs].to_sym end
def build_comparison_options
def build_comparison_options opts = build_profile_and_preprocessing_options match_opts = build_match_dimension_options opts[:match] = match_opts unless match_opts.empty? opts[:ignore_attr_order] = @options.fetch(:ignore_attr_order, true) # Always request verbose comparison to get ComparisonResult with differences # The CLI --verbose flag only affects output formatting, not comparison detail opts[:verbose] = true add_algorithm_option(opts) add_show_diffs_option(opts) opts end
def build_match_dimension_options
def build_match_dimension_options dimensions = %i[ text_content structural_whitespace attribute_whitespace attribute_order attribute_values comments key_order whitespace_type ] dimensions.each_with_object({}) do |dim, opts| opts[dim] = @options[dim].to_sym if @options[dim] end end
def build_profile_and_preprocessing_options
def build_profile_and_preprocessing_options opts = {} if @options[:match_profile] opts[:match_profile] = @options[:match_profile].to_sym end if @options[:preprocessing] opts[:preprocessing] = @options[:preprocessing].to_sym end opts end
def check_file_size(filename, format)
-
(Canon::SizeLimitExceededError)- if file exceeds limit
Parameters:
-
format(Symbol) -- File format -
filename(String) -- Path to file
def check_file_size(filename, format) file_size = File.size(filename) max_size = get_max_file_size(format) return unless max_size&.positive? return if file_size <= max_size raise Canon::SizeLimitExceededError.new(:file_size, file_size, max_size) end
def detect_format(filename)
def detect_format(filename) ext = File.extname(filename).downcase case ext when ".xml" :xml when ".html", ".htm" :html when ".json" :json when ".yaml", ".yml" :yaml else abort "Error: Cannot detect format from extension '#{ext}'. " \ "Please specify --format (xml, html, json, or yaml)" end end
def determine_algorithm
def determine_algorithm algo = @options[:diff_algorithm] || "dom" algo.to_sym end
def determine_mode(format)
def determine_mode(format) # Check for explicit --diff-mode flag (new approach) if @options[:diff_mode] return @options[:diff_mode].to_sym end # Backward compatibility: check --by-line flag (deprecated) if @options[:by_line] warn "WARNING: --by-line is deprecated. Use --diff-mode by_line instead." return :by_line end # Format-specific defaults case format when :html :by_line else :by_object end end
def get_max_file_size(format)
-
(Integer, nil)- Max file size in bytes
Parameters:
-
format(Symbol) -- File format
def get_max_file_size(format) config = Canon::Config.instance case format when :xml config.xml.diff.max_file_size when :html config.html.diff.max_file_size when :json config.json.diff.max_file_size when :yaml config.yaml.diff.max_file_size else 5_242_880 # Default 5MB end end
def initialize(options = {})
def initialize(options = {}) @options = options end
def parse_document_content(content, format)
def parse_document_content(content, format) case format when :xml # Return string for Canon::Comparison to parse content when :html # Return string for Canon::Comparison to parse content when :json # Parse JSON to Ruby object JSON.parse(content) when :yaml # Parse YAML to Ruby object YAML.safe_load(content) else abort "Error: Unsupported format '#{format}'" end end
def prepare_formatted_content(content1, content2, format, mode)
def prepare_formatted_content(content1, content2, format, mode) return [content1, content2] unless mode == :by_line case format when :xml formatted1 = Canon::PrettyPrinter::Xml.new(indent: 2).format(content1) formatted2 = Canon::PrettyPrinter::Xml.new(indent: 2).format(content2) [formatted1, formatted2] when :html formatted1 = Canon::PrettyPrinter::Html.new(indent: 2).format(content1) formatted2 = Canon::PrettyPrinter::Html.new(indent: 2).format(content2) [formatted1, formatted2] else [content1, content2] end end
def resolve_color_option
-
(Boolean)- true if colors should be used
def resolve_color_option return @options[:color] unless @options[:color].nil? # Auto-detect when no explicit choice was made ColorDetector.supports_color? end
def run(file1, file2)
rubocop:disable Metrics/MethodLength
def run(file1, file2) # Detect formats format1 = @options[:format1] || @options[:format] || detect_format(file1) format2 = @options[:format2] || @options[:format] || detect_format(file2) # Check file sizes before reading check_file_size(file1, format1) check_file_size(file2, format2) # Read raw content for potential by-line diff content1 = File.read(file1, encoding: "utf-8") content2 = File.read(file2, encoding: "utf-8") # Parse documents doc1 = parse_document_content(content1, format1) doc2 = parse_document_content(content2, format2) # Build comparison options comp_opts = build_comparison_options # Perform semantic comparison result = Canon::Comparison.equivalent?(doc1, doc2, comp_opts) # Determine diff mode mode = determine_mode(format1) # Prepare formatted content for by-line mode formatted1, formatted2 = prepare_formatted_content( content1, content2, format1, mode ) # Format and output results formatter = Canon::DiffFormatter.new( use_color: resolve_color_option, mode: mode, context_lines: @options.fetch(:context_lines, 3), diff_grouping_lines: @options[:diff_grouping_lines], show_diffs: @options[:show_diffs]&.to_sym || :all, show_raw_inputs: @options[:show_raw_inputs] || false, show_preprocessed_inputs: @options[:show_preprocessed_inputs] || false, show_preprocessed_expected: @options[:show_preprocessed_expected] || false, show_preprocessed_received: @options[:show_preprocessed_received] || false, show_prettyprint_inputs: @options[:show_prettyprint_inputs] || false, show_prettyprint_expected: @options[:show_prettyprint_expected] || false, show_prettyprint_received: @options[:show_prettyprint_received] || false, show_line_numbered_inputs: @options[:show_line_numbered_inputs] || false, ) # Show configuration in verbose mode using shared DebugOutput if @options[:verbose] config_output = Canon::DiffFormatter::DebugOutput.verbose_tables_only( result, { use_color: resolve_color_option, mode: mode, context_lines: @options.fetch(:context_lines, 3), diff_grouping_lines: @options[:diff_grouping_lines], show_diffs: @options[:show_diffs]&.to_sym || :all, verbose_diff: true, # Enable verbose table output }, ) puts config_output unless config_output.empty? end # Always show diff when files are not equivalent # result is always a ComparisonResult object when verbose: true output = formatter.format( result, format1, doc1: formatted1, doc2: formatted2, ) puts output exit result.equivalent? ? 0 : 1 rescue Errno::ENOENT => e abort "Error: #{e.message}" rescue JSON::ParserError => e abort "Error parsing JSON: #{e.message}" rescue Psych::SyntaxError => e abort "Error parsing YAML: #{e.message}" rescue Canon::Error => e abort "Error: #{e.message}" rescue StandardError => e abort "Error processing files: #{e.message}" end