loc_mods 0.2.3 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2b206d76220e77a18fd6609c3f7646eaf8fb836ac2d69a7d6da019db5820f24d
4
- data.tar.gz: ffd23bb7508ad1fef1e0b165eb07620fc0a2420b6c436ac34cec8208affb6463
3
+ metadata.gz: c4e669e85342e73695662fb8685879a283bd8b166c44a180de849a1a868d5db3
4
+ data.tar.gz: 93bd3bce5771da37094b7b2f4431c733fd5024be93fbde51900ea3e76a95af1b
5
5
  SHA512:
6
- metadata.gz: 9f56bbb4bfdab2dd2820ef67d94461ac5caa47d62937762a5cbeacbea8e70497c75968d92b3ce23f5d7e79f6a737c98c43058d6e11b774d0777340f040808fea
7
- data.tar.gz: 0ac7feedcebe5df178f6ad313ba811004de126c82adc0f5289f44b43212a26ef2acfa3f09a45afa9cd4b158b7b4ef97976413ac357b4eb11749ab227c801a168
6
+ metadata.gz: 3597a8790c984fb0e4666d334d44727952e46bed0822b8c999dc4bfa3a32669ebfdf6150cc6ab4c37762b7483dfb376f716b9437e27634d594852dc10732de65
7
+ data.tar.gz: 3d44d27786cea8ddd7024065b9ea8f8c5a0b08eebf6aa93d35dc24c9575c67f21a9a3ed69887b667332fa9e20af84dd003739b6853ec41b92960bd587132a8fc
data/README.adoc CHANGED
@@ -28,15 +28,44 @@ LocMods::Collection.from_xml(File.read("reference/allrecords-MODS.xml"))
28
28
 
29
29
  === Command line interface
30
30
 
31
- LocMods provides a command-line interface (CLI) for various operations. The main
32
- executable is `loc-mods`.
31
+ LocMods provides a command-line interface (CLI) for various operations.
32
+
33
+ The main executable is `loc-mods`.
34
+
35
+ [source,shell]
36
+ ----
37
+ Commands:
38
+ loc-mods detect-duplicates PATH... # Detect duplicate records in MODS XML files or directories
39
+ loc-mods help [COMMAND] # Describe available commands or one specific command
40
+ ----
41
+
42
+
43
+
33
44
 
34
45
  ==== Detect duplicates
35
46
 
36
47
  The `detect-duplicates` command allows you to find duplicate MODS records based
37
48
  on using a "primary ID" that is their DOI (Digital Object Identifier).
38
49
 
50
+ NOTE: The library assumes that every record has a DOI. If that is not the case,
51
+ another way to setting the primary key needs to be defined.
52
+
53
+ Usage:
54
+
55
+ [source,shell]
56
+ ----
39
57
  Usage:
58
+ loc-mods detect-duplicates PATH...
59
+
60
+ Options:
61
+ [--show-unchanged], [--no-show-unchanged] # Show unchanged attributes in the diff output
62
+ # Default: false
63
+ [--highlight-diff], [--no-highlight-diff] # Highlight only the differences
64
+ # Default: false
65
+ [--color=COLOR] # Use colors in the diff output (auto, on, off)
66
+ # Default: auto
67
+ # Possible values: auto, on, off
68
+ ----
40
69
 
41
70
  [source,shell]
42
71
  ----
@@ -45,15 +74,28 @@ $ loc-mods detect-duplicates [OPTIONS] <file_or_directory_path>
45
74
 
46
75
  Options:
47
76
 
48
- * `-r, --recursive`: Search for MODS files recursively in subdirectories.
49
- * `-v, --verbose`: Display more detailed output.
50
- * `-o, --output FILE`: Write the results to a file instead of stdout.
77
+ `--show-unchanged`::
78
+ (default: `false`)
79
+ Show attributes of both objects even when they were not changed.
80
+
81
+ `--highlight-diff`::
82
+ (default: `false`)
83
+ Highlight values only when they differ between two records.
84
+
85
+ `--color=COLOR`::
86
+ (default: `auto`) Use colors in the diff output. Values:
87
+
88
+ `auto`::: the CLI will detect whether the terminal supports colors and display
89
+ with colors if it does.
90
+ `on`::: the CLI will always display with colors.
91
+ `off`::: the CLI will never display with colors.
92
+
51
93
 
52
94
  Example:
53
95
 
54
96
  [source,shell]
55
97
  ----
56
- $ loc-mods detect-duplicates -r -v /path/to/mods/files
98
+ $ loc-mods detect-duplicates /path/to/mods/files
57
99
  ----
58
100
 
59
101
  This command will:
data/lib/loc_mods/cli.rb CHANGED
@@ -7,6 +7,10 @@ require "loc_mods"
7
7
  module LocMods
8
8
  class Cli < Thor
9
9
  desc "detect-duplicates PATH...", "Detect duplicate records in MODS XML files or directories"
10
+ method_option :show_unchanged, type: :boolean, default: false, desc: "Show unchanged attributes in the diff output"
11
+ method_option :highlight_diff, type: :boolean, default: false, desc: "Highlight only the differences"
12
+ method_option :color, type: :string, enum: %w[auto on off], default: "auto",
13
+ desc: "Use colors in the diff output (auto, on, off)"
10
14
 
11
15
  def detect_duplicates(*paths)
12
16
  all_records = []
@@ -46,12 +50,13 @@ module LocMods
46
50
  puts " Comparison #{index + 1}:"
47
51
  puts " File 1: #{record1[:file]}"
48
52
  puts " File 2: #{record2[:file]}"
49
- differences = record1[:record].compare(record2[:record])
50
- if differences
51
- puts " ----"
52
- print_differences(differences)
53
- puts " ----"
54
- end
53
+ print_differences(
54
+ record1[:record],
55
+ record2[:record],
56
+ options[:show_unchanged],
57
+ options[:highlight_diff],
58
+ color_enabled?
59
+ )
55
60
  puts "\n"
56
61
  end
57
62
  end
@@ -67,87 +72,51 @@ module LocMods
67
72
  end
68
73
  end
69
74
 
70
- def print_differences(differences, prefix = "", path = [])
71
- if differences.is_a?(Comparison)
72
- print_difference(differences, path)
73
- return
74
- end
75
-
76
- # Differences is a Hash here
77
- differences.each do |key, value|
78
- # puts "key #{key}, value #{value}"
79
- current_path = path + [key]
80
- # This is a comparison
81
- if value.is_a?(Comparison)
82
- print_difference(value, path)
83
- next
84
- end
75
+ def print_differences(record1, record2, show_unchanged, highlight_diff, use_colors)
76
+ diff_score, diff_tree = LocMods::BaseMapper.diff_with_score(
77
+ record1,
78
+ record2,
79
+ show_unchanged: show_unchanged,
80
+ highlight_diff: highlight_diff,
81
+ use_colors: use_colors,
82
+ indent: " ",
83
+ )
84
+ similarity_percentage = (1 - diff_score) * 100
85
+
86
+ puts " Differences:"
87
+ puts diff_tree
88
+ puts " Similarity score: #{similarity_percentage.round(2)}%"
89
+ end
85
90
 
86
- raise "Differences must be in form of a Hash" unless value.is_a?(Hash)
91
+ def color_enabled?
92
+ case options[:color]
93
+ when "on"
94
+ true
95
+ when "off"
96
+ false
97
+ else
98
+ supports_color?
99
+ end
100
+ end
87
101
 
88
- # This is not array, end here
89
- next unless value.keys.any? do |k|
90
- k.is_a?(Integer) || k == :_array_size_difference
91
- end
102
+ def supports_color?
103
+ return false unless $stdout.tty?
92
104
 
93
- # if value[:_array_size_difference]
94
- # puts " #{format_path(current_path)} [_array_size_difference]:"
95
- # puts " Record 1: #{format_value(value[:_array_size_difference].original)}"
96
- # puts " Record 2: #{format_value(value[:_array_size_difference].updated)}"
97
- # puts
98
- # end
99
-
100
- value.each do |subkey, subvalue|
101
- # if [:self, :other].include?(subkey)
102
- # raise "Subkey is self or other"
103
- # end
104
- # puts "subkey (#{subkey})"
105
- # puts "subvalue #{subvalue}, prefix #{prefix}, current_path + [subkey] #{current_path + [subkey]}"
106
-
107
- if subkey.is_a?(Integer)
108
- print_differences(subvalue, prefix, current_path + [subkey])
109
- else
110
- if subvalue.is_a?(Comparison)
111
- print_difference(subvalue, current_path + [subkey])
112
- next
113
- end
114
-
115
- raise "In an array diff but not an Integer!"
116
- end
117
- end
105
+ if RbConfig::CONFIG["host_os"] =~ /mswin|mingw|cygwin/
106
+ return true if ENV["ANSICON"]
107
+ return true if ENV["ConEmuANSI"] == "ON"
108
+ return true if ENV["TERM"] == "xterm"
118
109
  end
119
- end
120
110
 
121
- def print_difference(value, current_path)
122
- return unless value.original || value.updated
111
+ return true if ENV["COLORTERM"]
123
112
 
124
- puts " #{format_path(current_path)}:"
125
- puts " Record 1: #{format_value(value.original)}"
126
- puts " Record 2: #{format_value(value.updated)}"
127
- puts
128
- end
113
+ term = ENV["TERM"]
114
+ return false if term.nil? || term.empty?
129
115
 
130
- def format_path(path)
131
- path.map.with_index do |part, index|
132
- if index.zero?
133
- part.to_s
134
- elsif part.is_a?(Integer)
135
- "[#{part}]"
136
- else
137
- ".#{part}"
138
- end
139
- end.join
140
- end
116
+ color_terms = %w[ansi color console cygwin gnome konsole kterm
117
+ linux msys putty rxvt screen tmux vt100 xterm]
141
118
 
142
- def format_value(value)
143
- case value
144
- when nil, ComparableNil
145
- "(nil)"
146
- when String
147
- "\"#{value}\""
148
- else
149
- value.to_s
150
- end
119
+ color_terms.any? { |ct| term.include?(ct) }
151
120
  end
152
121
  end
153
122
  end
@@ -1,110 +1,501 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # lib/loc_mods/comparable_mapper.rb
4
-
5
3
  module LocMods
6
- # Enable comparison of two class models solely based on attribute values in
7
- # a recursive manner.
4
+ # ComparableMapper module provides functionality to compare and diff two objects
5
+ # of the same class, based on their attribute values.
8
6
  module ComparableMapper
9
- # TODO: Implement Comparable
10
- # include Comparable
11
- # def <=>(other)
12
- # attributes.foo <=> other.attributes.foo
13
- # end
14
- # def inspect
15
- # @foo
16
- # end
7
+ def self.included(base)
8
+ base.extend(ClassMethods)
9
+ end
17
10
 
11
+ # Checks if two objects are equal based on their attributes
12
+ # @param other [Object] The object to compare with
13
+ # @return [Boolean] True if objects are equal, false otherwise
18
14
  def eql?(other)
19
15
  other.class == self.class &&
20
- self.class.attributes.all? do |attr|
21
- send(attr) == other.send(attr)
22
- end
16
+ self.class.attributes.all? { |attr, _| send(attr) == other.send(attr) }
23
17
  end
24
18
 
25
19
  alias == eql?
26
20
 
21
+ # Generates a hash value for the object
22
+ # @return [Integer] The hash value
27
23
  def hash
28
- ([self.class] + self.class.attributes.map(&:hash)).hash
24
+ ([self.class] + self.class.attributes.map { |attr, _| send(attr).hash }).hash
29
25
  end
30
26
 
31
- def compare(other)
32
- differences = {}
27
+ # Class methods added to the class that includes ComparableMapper
28
+ module ClassMethods
29
+ # Generates a diff tree between two objects of the same class
30
+ # @param obj1 [Object] The first object to compare
31
+ # @param obj2 [Object] The second object to compare
32
+ # @param options [Hash] Options for diff generation
33
+ # @return [String] A string representation of the diff tree
34
+ def diff_tree
35
+ if @obj1.nil? && @obj2.nil?
36
+ @root_tree = Tree.new("Both objects are nil")
37
+ elsif @obj1.nil?
38
+ @root_tree = Tree.new("First object is nil")
39
+ format_single_value(@obj2, @root_tree, @obj2.class.to_s)
40
+ elsif @obj2.nil?
41
+ @root_tree = Tree.new("Second object is nil")
42
+ format_single_value(@obj1, @root_tree, @obj1.class.to_s)
43
+ else
44
+ traverse_diff do |name, type, value1, value2, is_last|
45
+ format_attribute_diff(name, type, value1, value2, is_last)
46
+ end
47
+ end
48
+
49
+ @root_tree.to_s
50
+ end
51
+
52
+ # Generates a diff tree and calculates a diff score between two objects of the same class
53
+ # @param obj1 [Object] The first object to compare
54
+ # @param obj2 [Object] The second object to compare
55
+ # @param options [Hash] Options for diff generation
56
+ # @return [Array<Float, String>] An array containing the normalized diff score and the diff tree
57
+ def diff_with_score(obj1, obj2, **options)
58
+ context = DiffContext.new(obj1, obj2, **options)
59
+ indent = options[:indent] || ""
60
+ [context.calculate_diff_score, context.diff_tree(indent)]
61
+ end
62
+ end
33
63
 
34
- # puts "Debugging: Attributes for #{self.class.name}"
35
- # pp self.class.attributes.keys
64
+ class Tree
65
+ attr_accessor :content, :children, :color
36
66
 
37
- self.class.attributes.each_key do |attr|
38
- self_value = respond_to?(attr) ? send(attr) : nil
39
- other_value = other.respond_to?(attr) ? other.send(attr) : nil
67
+ def initialize(content, color: nil)
68
+ @content = content
69
+ @children = []
70
+ @color = color
71
+ end
40
72
 
41
- # puts "Debugging: Comparing attribute '#{attr}'"
42
- # puts " Self value: #{self_value.inspect}"
43
- # puts " Other value: #{other_value.inspect}"
73
+ def add_child(child)
74
+ @children << child
75
+ end
44
76
 
45
- compared = compare_values(self_value, other_value)
46
- if compared
47
- # puts "DETECTED DIFFERENCE! #{compared.inspect}"
48
- differences[attr] = compared
77
+ def to_s(indent = "", is_last = true)
78
+ prefix = is_last ? "└── " : "├── "
79
+ result = "#{indent}#{colorize(prefix + @content, @color)}\n"
80
+ @children.each_with_index do |child, index|
81
+ is_last_child = index == @children.size - 1
82
+ child_indent = indent + (is_last ? " " : "#{colorize("│", @color)} ")
83
+ result += child.to_s(child_indent, is_last_child)
49
84
  end
85
+ result
50
86
  end
51
87
 
52
- # unless differences.empty?
53
- # puts "DIFFERENCES ARE"
54
- # pp differences
55
- # end
88
+ private
89
+
90
+ def colorize(text, color)
91
+ return text unless color
56
92
 
57
- differences.empty? ? nil : differences
93
+ color_codes = { red: 31, green: 32, blue: 34 }
94
+ "\e[#{color_codes[color]}m#{text}\e[0m"
95
+ end
58
96
  end
59
97
 
60
- private
98
+ # DiffContext handles the comparison between two objects
99
+ class DiffContext
100
+ attr_reader :obj1, :obj2, :show_unchanged, :highlight_diff, :use_colors
101
+ attr_accessor :level, :tree_lines, :root_tree
102
+
103
+ # Initializes a new DiffContext
104
+ # @param obj1 [Object] The first object to compare
105
+ # @param obj2 [Object] The second object to compare
106
+ # @param options [Hash] Options for diff generation
107
+ def initialize(obj1, obj2, **options)
108
+ @obj1 = obj1
109
+ @obj2 = obj2
110
+ @show_unchanged = options.fetch(:show_unchanged, false)
111
+ @highlight_diff = options.fetch(:highlight_diff, false)
112
+ @use_colors = options.fetch(:use_colors, true)
113
+ @level = 0
114
+ @tree_lines = []
115
+ @root_tree = Tree.new(obj1.class.to_s)
116
+ end
61
117
 
62
- def compare_values(self_value, other_value)
63
- # puts "compare_values (self_value: #{self_value}, other_value: #{other_value})"
64
- case self_value
65
- when Array
66
- # puts "compare_values case 1"
67
- compare_arrays(self_value, other_value || [])
68
- when ComparableMapper
69
- # puts "compare_values case 2"
70
- self_value.compare(other_value)
71
- else
72
- if self_value != other_value
73
- # puts "compare_values case 3"
74
- Comparison.new(original: self_value, updated: other_value)
118
+ # Generates a diff tree between the two objects
119
+ # @return [String] A string representation of the diff tree
120
+ def diff_tree(indent = "")
121
+ traverse_diff do |name, type, value1, value2, is_last|
122
+ format_attribute_diff(name, type, value1, value2, is_last)
75
123
  end
124
+ @root_tree.to_s(indent)
125
+ end
126
+
127
+ # Calculates the normalized diff score
128
+ # @return [Float] The normalized diff score
129
+ def calculate_diff_score
130
+ total_score = 0
131
+ total_attributes = 0
132
+ traverse_diff do |_, _, value1, value2, _|
133
+ total_score += calculate_attribute_score(value1, value2)
134
+ total_attributes += 1
135
+ end
136
+ total_attributes.positive? ? total_score / total_attributes : 0
137
+ end
138
+
139
+ private
140
+
141
+ # Applies color to text if colors are enabled
142
+ # @param text [String] The text to color
143
+ # @param color [Symbol] The color to apply
144
+ # @return [String] The colored text
145
+ def colorize(text, color)
146
+ return text unless @use_colors
147
+
148
+ color_codes = { red: 31, green: 32, blue: 34 }
149
+ "\e[#{color_codes[color]}m#{text}\e[0m"
76
150
  end
77
- end
78
151
 
79
- def compare_arrays(self_array, other_array)
80
- differences = {}
81
- max_length = [self_array.size, other_array.size].max
152
+ # Traverses the attributes of the objects and yields each attribute's details
153
+ # @yield [String, Symbol, Object, Object, Boolean] Yields the name, type, value1, value2, and is_last for each attribute
154
+ def traverse_diff
155
+ return yield nil, nil, obj1, obj2, true if obj1.class != obj2.class
82
156
 
83
- max_length.times do |index|
84
- self_item = self_array[index] || ComparableNil.new
85
- other_item = other_array[index] || ComparableNil.new
157
+ obj1.class.attributes.each_with_index do |(name, type), index|
158
+ yield name, type, obj1.send(name), obj2.send(name), index == obj1.class.attributes.length - 1
159
+ end
160
+ end
161
+
162
+ # Generates the prefix for tree lines
163
+ # @return [String] Prefix for tree lines
164
+ def tree_prefix
165
+ @tree_lines.map { |enabled| enabled ? "│ " : " " }.join
166
+ end
167
+
168
+ # Formats a line in the tree structure
169
+ # @param is_last [Boolean] Whether this is the last item in the current level
170
+ # @param content [String] The content to be displayed in the line
171
+ # @return [String] Formatted tree line
172
+ def tree_line(is_last, content)
173
+ "#{tree_prefix}#{is_last ? "└── " : "├── "}#{content}\n"
174
+ end
86
175
 
87
- compared = compare_values(self_item, other_item)
88
- differences[index] = compared if compared
176
+ # Calculates the diff score for a single attribute
177
+ # @param value1 [Object] The value of the attribute in the first object
178
+ # @param value2 [Object] The value of the attribute in the second object
179
+ # @return [Float] The diff score for the attribute
180
+ def calculate_attribute_score(value1, value2)
181
+ if value1 == value2
182
+ 0
183
+ elsif value1.is_a?(Array) && value2.is_a?(Array)
184
+ calculate_array_score(value1, value2)
185
+ else
186
+ value1.instance_of?(value2.class) ? 0.5 : 1
187
+ end
188
+ end
189
+
190
+ # Calculates the diff score for array attributes
191
+ # @param arr1 [Array] The array from the first object
192
+ # @param arr2 [Array] The array from the second object
193
+ # @return [Float] The diff score for the arrays
194
+ def calculate_array_score(arr1, arr2)
195
+ max_length = [arr1.length, arr2.length].max
196
+ return 0.0 if max_length.zero?
89
197
 
90
- # if index >= self_array.size
91
- # puts "case 1 #{compared}"
92
- # # differences[index] = compared
93
- # elsif index >= other_array.size
94
- # puts "case 2.1 #{self_item}"
95
- # puts "case 2.2 #{compared}"
96
- # # differences[index] = compared
97
- # end
198
+ total_score = max_length.times.sum do |i|
199
+ if i < arr1.length && i < arr2.length
200
+ if arr1[i] == arr2[i]
201
+ 0.0
202
+ elsif arr1[i].is_a?(ComparableMapper) && arr2[i].is_a?(ComparableMapper)
203
+ DiffContext.new(arr1[i], arr2[i], show_unchanged: @show_unchanged).calculate_diff_score
204
+ else
205
+ calculate_attribute_score(arr1[i], arr2[i])
206
+ end
207
+ else
208
+ 1.0
209
+ end
210
+ end
211
+
212
+ total_score / max_length
98
213
  end
99
214
 
100
- if self_array.size != other_array.size
101
- differences[:_array_size_difference] = Comparison.new(
102
- original: self_array.size,
103
- updated: other_array.size
104
- )
215
+ # Formats a value for display in the diff output
216
+ # @param value [Object] The value to format
217
+ # @return [String] Formatted value
218
+ def format_value(value)
219
+ case value
220
+ when nil
221
+ "(nil)"
222
+ when String
223
+ "(String) \"#{value}\""
224
+ when Array
225
+ if value.empty?
226
+ "(Array) 0 items"
227
+ else
228
+ items = value.map { |item| format_value(item) }.join(", ")
229
+ "(Array) [#{items}]"
230
+ end
231
+ when Hash
232
+ "(Hash) #{value.keys.length} keys"
233
+ when ComparableMapper
234
+ "(#{value.class})"
235
+ else
236
+ "(#{value.class}) #{value}"
237
+ end
105
238
  end
106
239
 
107
- differences.empty? ? nil : differences
240
+ # Formats the diff output for a single attribute
241
+ # @param name [String] The name of the attribute
242
+ # @param type [Symbol] The type of the attribute
243
+ # @param value1 [Object] The value of the attribute in the first object
244
+ # @param value2 [Object] The value of the attribute in the second object
245
+ # @param is_last [Boolean] Whether this is the last attribute in the list
246
+ # @return [String] Formatted diff output for the attribute
247
+ def format_attribute_diff(name, type, value1, value2, _is_last)
248
+ return if value1 == value2 && !@show_unchanged
249
+
250
+ node = Tree.new("#{name} (#{obj1.class.attributes[name].collection? ? "collection" : type_name(type)}):")
251
+ @root_tree.add_child(node)
252
+
253
+ if obj1.class.attributes[name].collection?
254
+ format_collection(value1, value2, node)
255
+ elsif value1 == value2
256
+ format_single_value(value1, node, "")
257
+ else
258
+ format_value_tree(value1, value2, node, "", type_name(type))
259
+ end
260
+ end
261
+
262
+ # Formats a collection (array) for diff output
263
+ # @param array1 [Array] The first array to compare
264
+ # @param array2 [Array] The second array to compare
265
+ # @return [String] Formatted diff output for the collection
266
+ def format_collection(array1, array2, parent_node)
267
+ array2 = [] if array2.nil?
268
+ max_length = [array1.length, array2.length].max
269
+
270
+ if max_length.zero?
271
+ parent_node.content += " (nil)"
272
+ return
273
+ end
274
+
275
+ max_length.times do |index|
276
+ item1 = array1[index]
277
+ item2 = array2[index]
278
+
279
+ next if item1 == item2 && !@show_unchanged
280
+
281
+ prefix = item2.nil? ? "- " : (item1.nil? ? "+ " : "")
282
+ color = item2.nil? ? :red : (item1.nil? ? :green : nil)
283
+ type = item1&.class || item2&.class
284
+
285
+ node = Tree.new("#{prefix}[#{index + 1}] (#{type_name(type)})", color: color)
286
+ parent_node.add_child(node)
287
+
288
+ if item1.nil?
289
+ format_diff_item(item2, :green, node)
290
+ elsif item2.nil?
291
+ format_diff_item(item1, :red, node)
292
+ else
293
+ format_value_tree(item1, item2, node, "")
294
+ end
295
+ end
296
+ end
297
+
298
+ # Formats a removed item in the diff output
299
+ # @param item [Object] The removed item
300
+ # @param is_last [Boolean] Whether this is the last item in the current level
301
+ # @param index [Integer] The index of the removed item
302
+ # @return [String] Formatted output for the removed item
303
+ def format_removed_item(item, _parent_node)
304
+ format_diff_item(item, :red)
305
+ end
306
+
307
+ # Formats an added item in the diff output
308
+ # @param item [Object] The added item
309
+ # @param is_last [Boolean] Whether this is the last item in the current level
310
+ # @param index [Integer] The index of the added item
311
+ # @return [String] Formatted output for the added item
312
+ def format_added_item(item, _parent_node)
313
+ format_diff_item(item, :green)
314
+ end
315
+
316
+ # Formats a diff item (added or removed)
317
+ # @param item [Object] The item to format
318
+ # @param is_last [Boolean] Whether this is the last item in the current level
319
+ # @param index [Integer] The index of the item
320
+ # @param color [Symbol] The color to use for the item
321
+ # @param prefix [String] The prefix to use for the item (+ or -)
322
+ # @return [String] Formatted output for the diff item
323
+ def format_diff_item(item, color, parent_node)
324
+ if item.is_a?(ComparableMapper)
325
+ return format_comparable_mapper(item, parent_node, color)
326
+ end
327
+
328
+ parent_node.add_child(Tree.new(format_value(item), color: color))
329
+ end
330
+
331
+ # Formats the content of an object for diff output
332
+ # @param obj [Object] The object to format
333
+ # @return [String] Formatted content of the object
334
+ def format_object_content(obj)
335
+ return format_value(obj) unless obj.is_a?(ComparableMapper)
336
+
337
+ obj.class.attributes.map do |attr, _|
338
+ "#{attr}: #{format_value(obj.send(attr))}"
339
+ end.join("\n")
340
+ end
341
+
342
+ # Formats and colors the content for diff output
343
+ # @param content [String] The content to format and color
344
+ # @param color [Symbol] The color to apply
345
+ # @param is_last [Boolean] Whether this is the last item in the current level
346
+ # @return [String] Formatted and colored content
347
+ def format_colored_content(content, color, is_last)
348
+ lines = content.split("\n")
349
+ lines.map.with_index do |line, index|
350
+ if index.zero?
351
+ "" # Skip the first line as it's already been output
352
+ else
353
+ prefix = index == lines.length - 1 && is_last ? "└── " : "├── "
354
+ tree_line(index == lines.length - 1 && is_last, colorize("#{prefix}#{line}", color))
355
+ end
356
+ end.join
357
+ end
358
+
359
+ # Gets the name of a type
360
+ # @param type [Class, Object] The type to get the name for
361
+ # @return [String] The name of the type
362
+ def type_name(type)
363
+ if type.is_a?(Class)
364
+ type.name
365
+ elsif type.respond_to?(:type)
366
+ type.type.name
367
+ else
368
+ type.class.name
369
+ end
370
+ end
371
+
372
+ # Formats the attributes of an object for diff output
373
+ # @param obj1 [Object] The first object
374
+ # @param obj2 [Object] The second object
375
+ # @return [String] Formatted attributes of the objects
376
+ def format_object_attributes(obj1, obj2, parent_node)
377
+ obj1.class.attributes.each_key do |attr|
378
+ value1 = obj1.send(attr)
379
+ value2 = obj2&.send(attr)
380
+
381
+ attr_type = obj1.class.attributes[attr].collection? ? "collection" : type_name(obj1.class.attributes[attr])
382
+
383
+ if value1 == value2
384
+ format_single_value(value1, parent_node, "#{attr} (#{attr_type})") if @show_unchanged
385
+ else
386
+ format_value_tree(value1, value2, parent_node, attr, attr_type)
387
+ end
388
+ end
389
+ end
390
+
391
+ # Formats the value tree for diff output
392
+ # @param value1 [Object] The first value
393
+ # @param value2 [Object] The second value
394
+ # @param is_last [Boolean] Whether this is the last item in the current level
395
+ # @param label [String] The label for the value
396
+ # @param type_info [String, nil] Additional type information
397
+ # @return [String] Formatted value tree
398
+ def format_value_tree(value1, value2, parent_node, label, type_info = nil)
399
+ return if value1 == value2 && !@show_unchanged
400
+
401
+ if value1 == value2
402
+ if @show_unchanged
403
+ return format_single_value(
404
+ value1,
405
+ parent_node,
406
+ "#{label}#{type_info ? " (#{type_info})" : ""}"
407
+ )
408
+ end
409
+
410
+ return if @highlight_diff
411
+ end
412
+
413
+ case value1
414
+ when Array
415
+ format_collection(value1, value2, parent_node)
416
+ when Hash
417
+ format_hash_tree(value1, value2, parent_node)
418
+ when ComparableMapper
419
+ format_object_attributes(value1, value2, parent_node)
420
+ else
421
+ node = Tree.new("#{label}#{type_info ? " (#{type_info})" : ""}:")
422
+ parent_node.add_child(node)
423
+ node.add_child(Tree.new("- #{format_value(value1)}", color: :red))
424
+ node.add_child(Tree.new("+ #{format_value(value2)}", color: :green))
425
+ end
426
+ end
427
+
428
+ # Formats a single value for diff output
429
+ # @param value [Object] The value to format
430
+ # @param is_last [Boolean] Whether this is the last item in the current level
431
+ # @param label [String] The label for the value
432
+ # @return [String] Formatted single value
433
+ def format_single_value(value, parent_node, label, color = nil)
434
+ node = Tree.new("#{label}#{label.empty? ? "" : ":"}", color: color)
435
+ parent_node.add_child(node)
436
+
437
+ case value
438
+ when ComparableMapper
439
+ format_comparable_mapper(value, node, color)
440
+ when Array
441
+ if value.empty?
442
+ node.add_child(Tree.new("(nil)", color: color))
443
+ else
444
+ format_collection(value, value, node)
445
+ end
446
+ else
447
+ node.content += " #{format_value(value)}"
448
+ end
449
+ end
450
+
451
+ # Formats a ComparableMapper object for diff output
452
+ # @param obj [ComparableMapper] The object to format
453
+ # @return [String] Formatted ComparableMapper object
454
+ def format_comparable_mapper(obj, parent_node, color = nil)
455
+ obj.class.attributes.each do |attr_name, attr_type|
456
+ attr_value = obj.send(attr_name)
457
+ attr_node = Tree.new("#{attr_name} (#{type_name(attr_type)}):", color: color)
458
+ parent_node.add_child(attr_node)
459
+ if attr_value.is_a?(ComparableMapper)
460
+ format_comparable_mapper(attr_value, attr_node, color)
461
+ else
462
+ value_node = Tree.new(format_value(attr_value), color: color)
463
+ attr_node.add_child(value_node)
464
+ end
465
+ end
466
+ end
467
+
468
+ # Formats a hash tree for diff output
469
+ # @param hash1 [Hash] The first hash to compare
470
+ # @param hash2 [Hash] The second hash to compare
471
+ # @return [String] Formatted hash tree
472
+ def format_hash_tree(hash1, hash2, parent_node)
473
+ keys = (hash1.keys + hash2.keys).uniq
474
+ keys.each do |key|
475
+ value1 = hash1[key]
476
+ value2 = hash2[key]
477
+
478
+ if value1 == value2
479
+ format_single_value(value1, parent_node, key) if @show_unchanged
480
+ else
481
+ format_value_tree(value1, value2, parent_node, key)
482
+ end
483
+ end
484
+ end
485
+ end
486
+ end
487
+
488
+ # Generates a tree representation of the object
489
+ # @return [String] A string representation of the object's attribute tree
490
+ def to_tree
491
+ output = "#{self.class}\n"
492
+ self.class.attributes.each_with_index do |(name, type), index|
493
+ value = send(name)
494
+ is_last = index == self.class.attributes.length - 1
495
+ context = DiffContext.new(nil, nil, show_unchanged: false)
496
+ formatted = context.format_value(value)
497
+ output << context.tree_line(is_last, "#{name} (#{type}): #{formatted}")
108
498
  end
499
+ output
109
500
  end
110
501
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LocMods
4
- VERSION = "0.2.3"
4
+ VERSION = "0.2.4"
5
5
  end
@@ -1863916,4 +1863916,4 @@
1863916
1863916
  </languageOfCataloging>
1863917
1863917
  </recordInfo>
1863918
1863918
  </mods>
1863919
- </modsCollection>
1863919
+ </modsCollection>
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: loc_mods
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-07-14 00:00:00.000000000 Z
11
+ date: 2024-08-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri