class Lutaml::Model::ComparableModel::DiffContext

DiffContext handles the comparison between two objects

def calculate_array_score(arr1, arr2)

Returns:
  • (Float) - The diff score for the arrays

Parameters:
  • arr2 (Array) -- The array from the second object
  • arr1 (Array) -- The array from the first object
def calculate_array_score(arr1, arr2)
  max_length = [arr1.length, arr2.length].max
  return 0.0 if max_length.zero?
  total_score = max_length.times.sum do |i|
    if i < arr1.length && i < arr2.length
      if arr1[i] == arr2[i]
        0.0
      elsif arr1[i].is_a?(ComparableModel) && arr2[i].is_a?(ComparableModel)
        DiffContext.new(arr1[i], arr2[i],
                        show_unchanged: @show_unchanged).calculate_diff_score
      else
        calculate_attribute_score(arr1[i], arr2[i])
      end
    else
      1.0
    end
  end
  total_score / max_length
end

def calculate_attribute_score(value1, value2)

Returns:
  • (Float) - The diff score for the attribute

Parameters:
  • value2 (Object) -- The value of the attribute in the second object
  • value1 (Object) -- The value of the attribute in the first object
def calculate_attribute_score(value1, value2)
  if value1 == value2
    0
  elsif value1.is_a?(Array) && value2.is_a?(Array)
    calculate_array_score(value1, value2)
  else
    value1.instance_of?(value2.class) ? 0.5 : 1
  end
end

def calculate_diff_score

Returns:
  • (Float) - The normalized diff score
def calculate_diff_score
  total_score = 0
  total_attributes = 0
  traverse_diff do |_, _, value1, value2, _|
    total_score += calculate_attribute_score(value1, value2)
    total_attributes += 1
  end
  total_attributes.positive? ? total_score / total_attributes : 0
end

def colorize(text, color)

Returns:
  • (String) - The colored text

Parameters:
  • color (Symbol) -- The color to apply
  • text (String) -- The text to color
def colorize(text, color)
  return text unless @use_colors
  color_codes = { red: 31, green: 32, blue: 34 }
  "\e[#{color_codes[color]}m#{text}\e[0m"
end

def diff_tree(indent = "")

Returns:
  • (String) - A string representation of the diff tree
def diff_tree(indent = "")
  traverse_diff do |name, type, value1, value2, is_last|
    format_attribute_diff(name, type, value1, value2, is_last)
  end
  @root_tree.to_s(indent)
end

def format_added_item(item, _parent_node)

Returns:
  • (String) - Formatted output for the added item

Parameters:
  • index (Integer) -- The index of the added item
  • is_last (Boolean) -- Whether this is the last item in the current level
  • item (Object) -- The added item
def format_added_item(item, _parent_node)
  format_diff_item(item, :green)
end

def format_attribute_diff(name, type, value1, value2, _is_last)

Returns:
  • (String) - Formatted diff output for the attribute

Parameters:
  • is_last (Boolean) -- Whether this is the last attribute in the list
  • value2 (Object) -- The value of the attribute in the second object
  • value1 (Object) -- The value of the attribute in the first object
  • type (Symbol) -- The type of the attribute
  • name (String) -- The name of the attribute
def format_attribute_diff(name, type, value1, value2, _is_last)
  return if value1 == value2 && !@show_unchanged
  node = Tree.new("#{name} (#{obj1.class.attributes[name].collection? ? 'collection' : type_name(type)}):")
  @root_tree.add_child(node)
  if obj1.class.attributes[name].collection?
    format_collection(value1, value2, node)
  elsif value1 == value2
    format_single_value(value1, node, "")
  else
    format_value_tree(value1, value2, node, "", type_name(type))
  end
end

def format_collection(array1, array2, parent_node)

Returns:
  • (String) - Formatted diff output for the collection

Parameters:
  • array2 (Array) -- The second array to compare
  • array1 (Array) -- The first array to compare
def format_collection(array1, array2, parent_node)
  array2 = [] if array2.nil?
  max_length = [array1.length, array2.length].max
  if max_length.zero?
    parent_node.content += " (nil)"
    return
  end
  max_length.times do |index|
    item1 = array1[index]
    item2 = array2[index]
    next if item1 == item2 && !@show_unchanged
    prefix = if item2.nil?
               "- "
             else
               (item1.nil? ? "+ " : "")
             end
    color = if item2.nil?
              :red
            else
              (item1.nil? ? :green : nil)
            end
    type = item1&.class || item2&.class
    node = Tree.new("#{prefix}[#{index + 1}] (#{type_name(type)})",
                    color: color)
    parent_node.add_child(node)
    if item1.nil?
      format_diff_item(item2, :green, node)
    elsif item2.nil?
      format_diff_item(item1, :red, node)
    else
      format_value_tree(item1, item2, node, "")
    end
  end
end

def format_colored_content(content, color, is_last)

Returns:
  • (String) - Formatted and colored content

Parameters:
  • is_last (Boolean) -- Whether this is the last item in the current level
  • color (Symbol) -- The color to apply
  • content (String) -- The content to format and color
def format_colored_content(content, color, is_last)
  lines = content.split("\n")
  lines.map.with_index do |line, index|
    if index.zero?
      "" # Skip the first line as it's already been output
    else
      prefix = index == lines.length - 1 && is_last ? "└── " : "├── "
      tree_line(index == lines.length - 1 && is_last,
                colorize("#{prefix}#{line}", color))
    end
  end.join
end

def format_comparable_mapper(obj, parent_node, color = nil)

Returns:
  • (String) - Formatted ComparableModel object

Parameters:
  • obj (ComparableModel) -- The object to format
def format_comparable_mapper(obj, parent_node, color = nil)
  obj.class.attributes.each do |attr_name, attr_type|
    attr_value = obj.send(attr_name)
    attr_node = Tree.new("#{attr_name} (#{type_name(attr_type)}):",
                         color: color)
    parent_node.add_child(attr_node)
    if attr_value.is_a?(ComparableModel)
      format_comparable_mapper(attr_value, attr_node, color)
    else
      value_node = Tree.new(format_value(attr_value), color: color)
      attr_node.add_child(value_node)
    end
  end
end

def format_diff_item(item, color, parent_node)

Returns:
  • (String) - Formatted output for the diff item

Parameters:
  • prefix (String) -- The prefix to use for the item (+ or -)
  • color (Symbol) -- The color to use for the item
  • index (Integer) -- The index of the item
  • is_last (Boolean) -- Whether this is the last item in the current level
  • item (Object) -- The item to format
def format_diff_item(item, color, parent_node)
  if item.is_a?(ComparableModel)
    return format_comparable_mapper(item, parent_node, color)
  end
  parent_node.add_child(Tree.new(format_value(item), color: color))
end

def format_hash_tree(hash1, hash2, parent_node)

Returns:
  • (String) - Formatted hash tree

Parameters:
  • hash2 (Hash) -- The second hash to compare
  • hash1 (Hash) -- The first hash to compare
def format_hash_tree(hash1, hash2, parent_node)
  keys = (hash1.keys + hash2.keys).uniq
  keys.each do |key|
    value1 = hash1[key]
    value2 = hash2[key]
    if value1 == value2
      format_single_value(value1, parent_node, key) if @show_unchanged
    else
      format_value_tree(value1, value2, parent_node, key)
    end
  end
end

def format_object_attributes(obj1, obj2, parent_node)

Returns:
  • (String) - Formatted attributes of the objects

Parameters:
  • obj2 (Object) -- The second object
  • obj1 (Object) -- The first object
def format_object_attributes(obj1, obj2, parent_node)
  obj1.class.attributes.each_key do |attr|
    value1 = obj1.send(attr)
    value2 = obj2&.send(attr)
    attr_type = obj1.class.attributes[attr].collection? ? "collection" : type_name(obj1.class.attributes[attr])
    if value1 == value2
      if @show_unchanged
        format_single_value(value1, parent_node,
                            "#{attr} (#{attr_type})")
      end
    else
      format_value_tree(value1, value2, parent_node, attr, attr_type)
    end
  end
end

def format_object_content(obj)

Returns:
  • (String) - Formatted content of the object

Parameters:
  • obj (Object) -- The object to format
def format_object_content(obj)
  return format_value(obj) unless obj.is_a?(ComparableModel)
  obj.class.attributes.map do |attr, _|
    "#{attr}: #{format_value(obj.send(attr))}"
  end.join("\n")
end

def format_removed_item(item, _parent_node)

Returns:
  • (String) - Formatted output for the removed item

Parameters:
  • index (Integer) -- The index of the removed item
  • is_last (Boolean) -- Whether this is the last item in the current level
  • item (Object) -- The removed item
def format_removed_item(item, _parent_node)
  format_diff_item(item, :red)
end

def format_single_value(value, parent_node, label, color = nil)

Returns:
  • (String) - Formatted single value

Parameters:
  • label (String) -- The label for the value
  • is_last (Boolean) -- Whether this is the last item in the current level
  • value (Object) -- The value to format
def format_single_value(value, parent_node, label, color = nil)
  node = Tree.new("#{label}#{label.empty? ? '' : ':'}", color: color)
  parent_node.add_child(node)
  case value
  when ComparableModel
    format_comparable_mapper(value, node, color)
  when Array
    if value.empty?
      node.add_child(Tree.new("(nil)", color: color))
    else
      format_collection(value, value, node)
    end
  else
    node.content += " #{format_value(value)}"
  end
end

def format_value(value)

Returns:
  • (String) - Formatted value

Parameters:
  • value (Object) -- The value to format
def format_value(value)
  case value
  when nil
    "(nil)"
  when String
    "(String) \"#{value}\""
  when Array
    if value.empty?
      "(Array) 0 items"
    else
      items = value.map { |item| format_value(item) }.join(", ")
      "(Array) [#{items}]"
    end
  when Hash
    "(Hash) #{value.keys.length} keys"
  when ComparableModel
    "(#{value.class})"
  else
    "(#{value.class}) #{value}"
  end
end

def format_value_tree(value1, value2, parent_node, label,

Returns:
  • (String) - Formatted value tree

Parameters:
  • type_info (String, nil) -- Additional type information
  • label (String) -- The label for the value
  • is_last (Boolean) -- Whether this is the last item in the current level
  • value2 (Object) -- The second value
  • value1 (Object) -- The first value
def format_value_tree(value1, value2, parent_node, label,
o = nil)
  return if value1 == value2 && !@show_unchanged
  if value1 == value2
    if @show_unchanged
      return format_single_value(
        value1,
        parent_node,
        "#{label}#{type_info ? " (#{type_info})" : ''}",
      )
    end
    return if @highlight_diff
  end
  case value1
  when Array
    format_collection(value1, value2, parent_node)
  when Hash
    format_hash_tree(value1, value2, parent_node)
  when ComparableModel
    format_object_attributes(value1, value2, parent_node)
  else
    node = Tree.new("#{label}#{type_info ? " (#{type_info})" : ''}:")
    parent_node.add_child(node)
    node.add_child(Tree.new("- #{format_value(value1)}", color: :red))
    node.add_child(Tree.new("+ #{format_value(value2)}", color: :green))
  end
end

def initialize(obj1, obj2, **options)

Parameters:
  • options (Hash) -- Options for diff generation
  • obj2 (Object) -- The second object to compare
  • obj1 (Object) -- The first object to compare
def initialize(obj1, obj2, **options)
  @obj1 = obj1
  @obj2 = obj2
  @show_unchanged = options.fetch(:show_unchanged, false)
  @highlight_diff = options.fetch(:highlight_diff, false)
  @use_colors = options.fetch(:use_colors, true)
  @level = 0
  @tree_lines = []
  @root_tree = Tree.new(obj1.class.to_s)
end

def traverse_diff

Other tags:
    Yield: - Yields the name, type, value1, value2, and is_last for each attribute
def traverse_diff
  return yield nil, nil, obj1, obj2, true if obj1.class != obj2.class
  obj1.class.attributes.each_with_index do |(name, type), index|
    yield name, type, obj1.send(name), obj2.send(name), index == obj1.class.attributes.length - 1
  end
end

def tree_line(is_last, content)

Returns:
  • (String) - Formatted tree line

Parameters:
  • content (String) -- The content to be displayed in the line
  • is_last (Boolean) -- Whether this is the last item in the current level
def tree_line(is_last, content)
  "#{tree_prefix}#{is_last ? '└── ' : '├── '}#{content}\n"
end

def tree_prefix

Returns:
  • (String) - Prefix for tree lines
def tree_prefix
  @tree_lines.map { |enabled| enabled ? "│   " : "    " }.join
end

def type_name(type)

Returns:
  • (String) - The name of the type

Parameters:
  • type (Class, Object) -- The type to get the name for
def type_name(type)
  if type.is_a?(Class)
    type.name
  elsif type.respond_to?(:type)
    type.type.name
  else
    type.class.name
  end
end