loc_mods 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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