loc_mods 0.2.4 → 0.2.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/.rubocop_todo.yml +83 -0
  4. data/lib/loc_mods/abstract.rb +9 -9
  5. data/lib/loc_mods/access_condition.rb +11 -11
  6. data/lib/loc_mods/alternative_name.rb +10 -10
  7. data/lib/loc_mods/area.rb +3 -3
  8. data/lib/loc_mods/cartographic_extension.rb +4 -4
  9. data/lib/loc_mods/cartographics.rb +8 -8
  10. data/lib/loc_mods/city_section.rb +3 -3
  11. data/lib/loc_mods/classification.rb +8 -8
  12. data/lib/loc_mods/cli.rb +1 -2
  13. data/lib/loc_mods/collection.rb +3 -3
  14. data/lib/loc_mods/copy_information.rb +5 -5
  15. data/lib/loc_mods/date.rb +8 -8
  16. data/lib/loc_mods/date_other.rb +4 -4
  17. data/lib/loc_mods/detail.rb +7 -7
  18. data/lib/loc_mods/edition.rb +4 -4
  19. data/lib/loc_mods/enumeration_and_chronology.rb +4 -4
  20. data/lib/loc_mods/extent.rb +4 -4
  21. data/lib/loc_mods/extent_definition.rb +7 -7
  22. data/lib/loc_mods/form.rb +5 -5
  23. data/lib/loc_mods/genre.rb +8 -8
  24. data/lib/loc_mods/geographic_code.rb +6 -6
  25. data/lib/loc_mods/hierarchical_geographic.rb +6 -6
  26. data/lib/loc_mods/hierarchical_part.rb +8 -8
  27. data/lib/loc_mods/holding_simple.rb +2 -2
  28. data/lib/loc_mods/identifier.rb +8 -8
  29. data/lib/loc_mods/item_identifier.rb +4 -4
  30. data/lib/loc_mods/language.rb +9 -9
  31. data/lib/loc_mods/language_term.rb +7 -7
  32. data/lib/loc_mods/location.rb +8 -8
  33. data/lib/loc_mods/name.rb +19 -19
  34. data/lib/loc_mods/name_part.rb +4 -4
  35. data/lib/loc_mods/non_sort.rb +4 -4
  36. data/lib/loc_mods/note.rb +7 -7
  37. data/lib/loc_mods/occupation.rb +6 -6
  38. data/lib/loc_mods/origin_info.rb +10 -10
  39. data/lib/loc_mods/part.rb +10 -10
  40. data/lib/loc_mods/physical_description.rb +10 -10
  41. data/lib/loc_mods/physical_description_note.rb +7 -7
  42. data/lib/loc_mods/physical_location.rb +5 -5
  43. data/lib/loc_mods/place.rb +3 -3
  44. data/lib/loc_mods/place_term.rb +7 -7
  45. data/lib/loc_mods/publisher.rb +6 -6
  46. data/lib/loc_mods/record.rb +4 -4
  47. data/lib/loc_mods/record_content_source.rb +4 -4
  48. data/lib/loc_mods/record_identifier.rb +4 -4
  49. data/lib/loc_mods/record_info.rb +9 -9
  50. data/lib/loc_mods/record_info_note.rb +8 -8
  51. data/lib/loc_mods/region.rb +3 -3
  52. data/lib/loc_mods/related_item.rb +10 -10
  53. data/lib/loc_mods/role.rb +2 -2
  54. data/lib/loc_mods/role_term.rb +4 -4
  55. data/lib/loc_mods/script_term.rb +4 -4
  56. data/lib/loc_mods/string_plus_language.rb +6 -6
  57. data/lib/loc_mods/string_plus_language_plus_authority.rb +6 -6
  58. data/lib/loc_mods/string_plus_language_plus_supplied.rb +4 -4
  59. data/lib/loc_mods/subject.rb +15 -15
  60. data/lib/loc_mods/subject_name.rb +14 -14
  61. data/lib/loc_mods/subject_title_info.rb +16 -16
  62. data/lib/loc_mods/table_of_contents.rb +9 -9
  63. data/lib/loc_mods/target_audience.rb +5 -5
  64. data/lib/loc_mods/temporal.rb +5 -5
  65. data/lib/loc_mods/text.rb +5 -5
  66. data/lib/loc_mods/title_info.rb +22 -22
  67. data/lib/loc_mods/type_of_resource.rb +8 -8
  68. data/lib/loc_mods/url.rb +8 -8
  69. data/lib/loc_mods/version.rb +1 -1
  70. data/lib/loc_mods.rb +7 -6
  71. metadata +5 -6
  72. data/lib/loc_mods/base_mapper.rb +0 -26
  73. data/lib/loc_mods/comparable_mapper.rb +0 -501
@@ -1,501 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LocMods
4
- # ComparableMapper module provides functionality to compare and diff two objects
5
- # of the same class, based on their attribute values.
6
- module ComparableMapper
7
- def self.included(base)
8
- base.extend(ClassMethods)
9
- end
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
14
- def eql?(other)
15
- other.class == self.class &&
16
- self.class.attributes.all? { |attr, _| send(attr) == other.send(attr) }
17
- end
18
-
19
- alias == eql?
20
-
21
- # Generates a hash value for the object
22
- # @return [Integer] The hash value
23
- def hash
24
- ([self.class] + self.class.attributes.map { |attr, _| send(attr).hash }).hash
25
- end
26
-
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
63
-
64
- class Tree
65
- attr_accessor :content, :children, :color
66
-
67
- def initialize(content, color: nil)
68
- @content = content
69
- @children = []
70
- @color = color
71
- end
72
-
73
- def add_child(child)
74
- @children << child
75
- end
76
-
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)
84
- end
85
- result
86
- end
87
-
88
- private
89
-
90
- def colorize(text, color)
91
- return text unless color
92
-
93
- color_codes = { red: 31, green: 32, blue: 34 }
94
- "\e[#{color_codes[color]}m#{text}\e[0m"
95
- end
96
- end
97
-
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
117
-
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)
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"
150
- end
151
-
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
156
-
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
175
-
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?
197
-
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
213
- end
214
-
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
238
- end
239
-
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}")
498
- end
499
- output
500
- end
501
- end