class Lutaml::Model::ComparableModel::DiffContext
DiffContext handles the comparison between two objects
def calculate_array_score(arr1, arr2)
-
(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)
-
(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
-
(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)
-
(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 = "")
-
(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)
-
(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)
-
(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)
-
(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)
-
(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)
-
(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)
-
(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)
-
(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)
-
(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)
-
(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)
-
(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)
-
(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)
-
(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,
-
(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)
-
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
- 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)
-
(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
-
(String)
- Prefix for tree lines
def tree_prefix @tree_lines.map { |enabled| enabled ? "│ " : " " }.join end
def type_name(type)
-
(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