lutaml-model 0.3.3 → 0.3.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "lutaml/model"
5
+
6
+ module Lutaml
7
+ module Model
8
+ # Command line interface for detecting duplicate records readable by Lutaml::Model
9
+ class Cli < Thor
10
+ # desc "detect-duplicates PATH...",
11
+ # "Detect duplicate records readable by Lutaml::Model, files or directories"
12
+ # method_option :show_unchanged, type: :boolean, default: false,
13
+ # desc: "Show unchanged attributes in the diff output"
14
+ # method_option :highlight_diff, type: :boolean, default: false,
15
+ # desc: "Highlight only the differences"
16
+ # method_option :color, type: :string, enum: %w[auto on off], default: "auto",
17
+ # desc: "Use colors in the diff output (auto, on, off)"
18
+
19
+ # def detect_duplicates(*paths)
20
+ # all_records = []
21
+ # paths.each do |path|
22
+ # if File.directory?(path)
23
+ # Dir.glob(File.join(path, "*.xml")).each do |file|
24
+ # process_file(file, all_records)
25
+ # end
26
+ # elsif File.file?(path) && path.end_with?(".xml")
27
+ # process_file(path, all_records)
28
+ # else
29
+ # puts "Warning: Skipping invalid path: #{path}"
30
+ # end
31
+ # end
32
+
33
+ # # TODO: Change using URL to a configurable primary key entered by user
34
+ # records_by_url = {}
35
+ # all_records.each do |record|
36
+ # urls = record[:record].location.flat_map do |loc|
37
+ # loc.url.map(&:content)
38
+ # end.compact
39
+ # unless urls.any?
40
+ # puts "Warning: Record without URL found in file: #{record[:file]}"
41
+ # next
42
+ # end
43
+
44
+ # urls.each do |url|
45
+ # records_by_url[url] ||= []
46
+ # records_by_url[url] << record
47
+ # end
48
+ # end
49
+
50
+ # duplicate_count = 0
51
+ # records_by_url.each do |url, records|
52
+ # next unless records.size > 1
53
+
54
+ # duplicate_count += 1
55
+ # puts "Duplicate set ##{duplicate_count} found for URL: #{url}"
56
+ # records.combination(2).each_with_index do |(record1, record2), index|
57
+ # puts " Comparison #{index + 1}:"
58
+ # puts " File 1: #{record1[:file]}"
59
+ # puts " File 2: #{record2[:file]}"
60
+ # print_differences(
61
+ # record1[:record],
62
+ # record2[:record],
63
+ # options[:show_unchanged],
64
+ # options[:highlight_diff],
65
+ # color_enabled?,
66
+ # )
67
+ # puts "\n"
68
+ # end
69
+ # end
70
+ # end
71
+
72
+ # private
73
+
74
+ # def process_file(file, all_records)
75
+ # xml_content = File.read(file)
76
+ # collection = Lutaml::Model.from_xml(xml_content)
77
+ # collection.mods.each do |record|
78
+ # all_records << { record: record, file: file }
79
+ # end
80
+ # end
81
+
82
+ # def print_differences(record1, record2, show_unchanged, highlight_diff,
83
+ # use_colors)
84
+ # diff_score, diff_tree = Lutaml::Model.diff_with_score(
85
+ # record1,
86
+ # record2,
87
+ # show_unchanged: show_unchanged,
88
+ # highlight_diff: highlight_diff,
89
+ # use_colors: use_colors,
90
+ # indent: " ",
91
+ # )
92
+ # similarity_percentage = (1 - diff_score) * 100
93
+
94
+ # puts " Differences:"
95
+ # puts diff_tree
96
+ # puts " Similarity score: #{similarity_percentage.round(2)}%"
97
+ # end
98
+
99
+ # def color_enabled?
100
+ # case options[:color]
101
+ # when "on"
102
+ # true
103
+ # when "off"
104
+ # false
105
+ # else
106
+ # supports_color?
107
+ # end
108
+ # end
109
+
110
+ # def supports_color?
111
+ # return false unless $stdout.tty?
112
+
113
+ # if /mswin|mingw|cygwin/.match?(RbConfig::CONFIG["host_os"])
114
+ # return true if ENV["ANSICON"]
115
+ # return true if ENV["ConEmuANSI"] == "ON"
116
+ # return true if ENV["TERM"] == "xterm"
117
+ # end
118
+
119
+ # return true if ENV["COLORTERM"]
120
+
121
+ # term = ENV.fetch("TERM", nil)
122
+ # return false if term.nil? || term.empty?
123
+
124
+ # color_terms = %w[ansi color console cygwin gnome konsole kterm
125
+ # linux msys putty rxvt screen tmux vt100 xterm]
126
+
127
+ # color_terms.any? { |ct| term.include?(ct) }
128
+ # end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,528 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ # ComparableModel module provides functionality to compare and diff two objects
6
+ # of the same class, based on their attribute values.
7
+ module ComparableModel
8
+ def self.included(base)
9
+ base.extend(ClassMethods)
10
+ end
11
+
12
+ # Checks if two objects are equal based on their attributes
13
+ # @param other [Object] The object to compare with
14
+ # @return [Boolean] True if objects are equal, false otherwise
15
+ def eql?(other)
16
+ other.class == self.class &&
17
+ self.class.attributes.all? do |attr, _|
18
+ send(attr) == other.send(attr)
19
+ end
20
+ end
21
+
22
+ alias == eql?
23
+
24
+ # Generates a hash value for the object
25
+ # @return [Integer] The hash value
26
+ def hash
27
+ ([self.class] + self.class.attributes.map do |attr, _|
28
+ send(attr).hash
29
+ end).hash
30
+ end
31
+
32
+ # Class methods added to the class that includes ComparableModel
33
+ module ClassMethods
34
+ # Generates a diff tree between two objects of the same class
35
+ # @param obj1 [Object] The first object to compare
36
+ # @param obj2 [Object] The second object to compare
37
+ # @param options [Hash] Options for diff generation
38
+ # @return [String] A string representation of the diff tree
39
+ def diff_tree
40
+ if @obj1.nil? && @obj2.nil?
41
+ @root_tree = Tree.new("Both objects are nil")
42
+ elsif @obj1.nil?
43
+ @root_tree = Tree.new("First object is nil")
44
+ format_single_value(@obj2, @root_tree, @obj2.class.to_s)
45
+ elsif @obj2.nil?
46
+ @root_tree = Tree.new("Second object is nil")
47
+ format_single_value(@obj1, @root_tree, @obj1.class.to_s)
48
+ else
49
+ traverse_diff do |name, type, value1, value2, is_last|
50
+ format_attribute_diff(name, type, value1, value2, is_last)
51
+ end
52
+ end
53
+
54
+ @root_tree.to_s
55
+ end
56
+
57
+ # Generates a diff tree and calculates a diff score between two objects of the same class
58
+ # @param obj1 [Object] The first object to compare
59
+ # @param obj2 [Object] The second object to compare
60
+ # @param options [Hash] Options for diff generation
61
+ # @return [Array<Float, String>] An array containing the normalized diff score and the diff tree
62
+ def diff_with_score(obj1, obj2, **options)
63
+ context = DiffContext.new(obj1, obj2, **options)
64
+ indent = options[:indent] || ""
65
+ [context.calculate_diff_score, context.diff_tree(indent)]
66
+ end
67
+ end
68
+
69
+ class Tree
70
+ attr_accessor :content, :children, :color
71
+
72
+ def initialize(content, color: nil)
73
+ @content = content
74
+ @children = []
75
+ @color = color
76
+ end
77
+
78
+ def add_child(child)
79
+ @children << child
80
+ end
81
+
82
+ def to_s(indent = "", is_last = true)
83
+ prefix = is_last ? "└── " : "├── "
84
+ result = "#{indent}#{colorize(prefix + @content, @color)}\n"
85
+ @children.each_with_index do |child, index|
86
+ is_last_child = index == @children.size - 1
87
+ child_indent = indent + (if is_last
88
+ " "
89
+ else
90
+ "#{colorize('│',
91
+ @color)} "
92
+ end)
93
+ result += child.to_s(child_indent, is_last_child)
94
+ end
95
+ result
96
+ end
97
+
98
+ private
99
+
100
+ def colorize(text, color)
101
+ return text unless color
102
+
103
+ color_codes = { red: 31, green: 32, blue: 34 }
104
+ "\e[#{color_codes[color]}m#{text}\e[0m"
105
+ end
106
+ end
107
+
108
+ # DiffContext handles the comparison between two objects
109
+ class DiffContext
110
+ attr_reader :obj1, :obj2, :show_unchanged, :highlight_diff, :use_colors
111
+ attr_accessor :level, :tree_lines, :root_tree
112
+
113
+ # Initializes a new DiffContext
114
+ # @param obj1 [Object] The first object to compare
115
+ # @param obj2 [Object] The second object to compare
116
+ # @param options [Hash] Options for diff generation
117
+ def initialize(obj1, obj2, **options)
118
+ @obj1 = obj1
119
+ @obj2 = obj2
120
+ @show_unchanged = options.fetch(:show_unchanged, false)
121
+ @highlight_diff = options.fetch(:highlight_diff, false)
122
+ @use_colors = options.fetch(:use_colors, true)
123
+ @level = 0
124
+ @tree_lines = []
125
+ @root_tree = Tree.new(obj1.class.to_s)
126
+ end
127
+
128
+ # Generates a diff tree between the two objects
129
+ # @return [String] A string representation of the diff tree
130
+ def diff_tree(indent = "")
131
+ traverse_diff do |name, type, value1, value2, is_last|
132
+ format_attribute_diff(name, type, value1, value2, is_last)
133
+ end
134
+ @root_tree.to_s(indent)
135
+ end
136
+
137
+ # Calculates the normalized diff score
138
+ # @return [Float] The normalized diff score
139
+ def calculate_diff_score
140
+ total_score = 0
141
+ total_attributes = 0
142
+ traverse_diff do |_, _, value1, value2, _|
143
+ total_score += calculate_attribute_score(value1, value2)
144
+ total_attributes += 1
145
+ end
146
+ total_attributes.positive? ? total_score / total_attributes : 0
147
+ end
148
+
149
+ private
150
+
151
+ # Applies color to text if colors are enabled
152
+ # @param text [String] The text to color
153
+ # @param color [Symbol] The color to apply
154
+ # @return [String] The colored text
155
+ def colorize(text, color)
156
+ return text unless @use_colors
157
+
158
+ color_codes = { red: 31, green: 32, blue: 34 }
159
+ "\e[#{color_codes[color]}m#{text}\e[0m"
160
+ end
161
+
162
+ # Traverses the attributes of the objects and yields each attribute's details
163
+ # @yield [String, Symbol, Object, Object, Boolean] Yields the name, type, value1, value2, and is_last for each attribute
164
+ def traverse_diff
165
+ return yield nil, nil, obj1, obj2, true if obj1.class != obj2.class
166
+
167
+ obj1.class.attributes.each_with_index do |(name, type), index|
168
+ yield name, type, obj1.send(name), obj2.send(name), index == obj1.class.attributes.length - 1
169
+ end
170
+ end
171
+
172
+ # Generates the prefix for tree lines
173
+ # @return [String] Prefix for tree lines
174
+ def tree_prefix
175
+ @tree_lines.map { |enabled| enabled ? "│ " : " " }.join
176
+ end
177
+
178
+ # Formats a line in the tree structure
179
+ # @param is_last [Boolean] Whether this is the last item in the current level
180
+ # @param content [String] The content to be displayed in the line
181
+ # @return [String] Formatted tree line
182
+ def tree_line(is_last, content)
183
+ "#{tree_prefix}#{is_last ? '└── ' : '├── '}#{content}\n"
184
+ end
185
+
186
+ # Calculates the diff score for a single attribute
187
+ # @param value1 [Object] The value of the attribute in the first object
188
+ # @param value2 [Object] The value of the attribute in the second object
189
+ # @return [Float] The diff score for the attribute
190
+ def calculate_attribute_score(value1, value2)
191
+ if value1 == value2
192
+ 0
193
+ elsif value1.is_a?(Array) && value2.is_a?(Array)
194
+ calculate_array_score(value1, value2)
195
+ else
196
+ value1.instance_of?(value2.class) ? 0.5 : 1
197
+ end
198
+ end
199
+
200
+ # Calculates the diff score for array attributes
201
+ # @param arr1 [Array] The array from the first object
202
+ # @param arr2 [Array] The array from the second object
203
+ # @return [Float] The diff score for the arrays
204
+ def calculate_array_score(arr1, arr2)
205
+ max_length = [arr1.length, arr2.length].max
206
+ return 0.0 if max_length.zero?
207
+
208
+ total_score = max_length.times.sum do |i|
209
+ if i < arr1.length && i < arr2.length
210
+ if arr1[i] == arr2[i]
211
+ 0.0
212
+ elsif arr1[i].is_a?(ComparableModel) && arr2[i].is_a?(ComparableModel)
213
+ DiffContext.new(arr1[i], arr2[i],
214
+ show_unchanged: @show_unchanged).calculate_diff_score
215
+ else
216
+ calculate_attribute_score(arr1[i], arr2[i])
217
+ end
218
+ else
219
+ 1.0
220
+ end
221
+ end
222
+
223
+ total_score / max_length
224
+ end
225
+
226
+ # Formats a value for display in the diff output
227
+ # @param value [Object] The value to format
228
+ # @return [String] Formatted value
229
+ def format_value(value)
230
+ case value
231
+ when nil
232
+ "(nil)"
233
+ when String
234
+ "(String) \"#{value}\""
235
+ when Array
236
+ if value.empty?
237
+ "(Array) 0 items"
238
+ else
239
+ items = value.map { |item| format_value(item) }.join(", ")
240
+ "(Array) [#{items}]"
241
+ end
242
+ when Hash
243
+ "(Hash) #{value.keys.length} keys"
244
+ when ComparableModel
245
+ "(#{value.class})"
246
+ else
247
+ "(#{value.class}) #{value}"
248
+ end
249
+ end
250
+
251
+ # Formats the diff output for a single attribute
252
+ # @param name [String] The name of the attribute
253
+ # @param type [Symbol] The type of the attribute
254
+ # @param value1 [Object] The value of the attribute in the first object
255
+ # @param value2 [Object] The value of the attribute in the second object
256
+ # @param is_last [Boolean] Whether this is the last attribute in the list
257
+ # @return [String] Formatted diff output for the attribute
258
+ def format_attribute_diff(name, type, value1, value2, _is_last)
259
+ return if value1 == value2 && !@show_unchanged
260
+
261
+ node = Tree.new("#{name} (#{obj1.class.attributes[name].collection? ? 'collection' : type_name(type)}):")
262
+ @root_tree.add_child(node)
263
+
264
+ if obj1.class.attributes[name].collection?
265
+ format_collection(value1, value2, node)
266
+ elsif value1 == value2
267
+ format_single_value(value1, node, "")
268
+ else
269
+ format_value_tree(value1, value2, node, "", type_name(type))
270
+ end
271
+ end
272
+
273
+ # Formats a collection (array) for diff output
274
+ # @param array1 [Array] The first array to compare
275
+ # @param array2 [Array] The second array to compare
276
+ # @return [String] Formatted diff output for the collection
277
+ def format_collection(array1, array2, parent_node)
278
+ array2 = [] if array2.nil?
279
+ max_length = [array1.length, array2.length].max
280
+
281
+ if max_length.zero?
282
+ parent_node.content += " (nil)"
283
+ return
284
+ end
285
+
286
+ max_length.times do |index|
287
+ item1 = array1[index]
288
+ item2 = array2[index]
289
+
290
+ next if item1 == item2 && !@show_unchanged
291
+
292
+ prefix = if item2.nil?
293
+ "- "
294
+ else
295
+ (item1.nil? ? "+ " : "")
296
+ end
297
+ color = if item2.nil?
298
+ :red
299
+ else
300
+ (item1.nil? ? :green : nil)
301
+ end
302
+ type = item1&.class || item2&.class
303
+
304
+ node = Tree.new("#{prefix}[#{index + 1}] (#{type_name(type)})",
305
+ color: color)
306
+ parent_node.add_child(node)
307
+
308
+ if item1.nil?
309
+ format_diff_item(item2, :green, node)
310
+ elsif item2.nil?
311
+ format_diff_item(item1, :red, node)
312
+ else
313
+ format_value_tree(item1, item2, node, "")
314
+ end
315
+ end
316
+ end
317
+
318
+ # Formats a removed item in the diff output
319
+ # @param item [Object] The removed item
320
+ # @param is_last [Boolean] Whether this is the last item in the current level
321
+ # @param index [Integer] The index of the removed item
322
+ # @return [String] Formatted output for the removed item
323
+ def format_removed_item(item, _parent_node)
324
+ format_diff_item(item, :red)
325
+ end
326
+
327
+ # Formats an added item in the diff output
328
+ # @param item [Object] The added item
329
+ # @param is_last [Boolean] Whether this is the last item in the current level
330
+ # @param index [Integer] The index of the added item
331
+ # @return [String] Formatted output for the added item
332
+ def format_added_item(item, _parent_node)
333
+ format_diff_item(item, :green)
334
+ end
335
+
336
+ # Formats a diff item (added or removed)
337
+ # @param item [Object] The item to format
338
+ # @param is_last [Boolean] Whether this is the last item in the current level
339
+ # @param index [Integer] The index of the item
340
+ # @param color [Symbol] The color to use for the item
341
+ # @param prefix [String] The prefix to use for the item (+ or -)
342
+ # @return [String] Formatted output for the diff item
343
+ def format_diff_item(item, color, parent_node)
344
+ if item.is_a?(ComparableModel)
345
+ return format_comparable_mapper(item, parent_node, color)
346
+ end
347
+
348
+ parent_node.add_child(Tree.new(format_value(item), color: color))
349
+ end
350
+
351
+ # Formats the content of an object for diff output
352
+ # @param obj [Object] The object to format
353
+ # @return [String] Formatted content of the object
354
+ def format_object_content(obj)
355
+ return format_value(obj) unless obj.is_a?(ComparableModel)
356
+
357
+ obj.class.attributes.map do |attr, _|
358
+ "#{attr}: #{format_value(obj.send(attr))}"
359
+ end.join("\n")
360
+ end
361
+
362
+ # Formats and colors the content for diff output
363
+ # @param content [String] The content to format and color
364
+ # @param color [Symbol] The color to apply
365
+ # @param is_last [Boolean] Whether this is the last item in the current level
366
+ # @return [String] Formatted and colored content
367
+ def format_colored_content(content, color, is_last)
368
+ lines = content.split("\n")
369
+ lines.map.with_index do |line, index|
370
+ if index.zero?
371
+ "" # Skip the first line as it's already been output
372
+ else
373
+ prefix = index == lines.length - 1 && is_last ? "└── " : "├── "
374
+ tree_line(index == lines.length - 1 && is_last,
375
+ colorize("#{prefix}#{line}", color))
376
+ end
377
+ end.join
378
+ end
379
+
380
+ # Gets the name of a type
381
+ # @param type [Class, Object] The type to get the name for
382
+ # @return [String] The name of the type
383
+ def type_name(type)
384
+ if type.is_a?(Class)
385
+ type.name
386
+ elsif type.respond_to?(:type)
387
+ type.type.name
388
+ else
389
+ type.class.name
390
+ end
391
+ end
392
+
393
+ # Formats the attributes of an object for diff output
394
+ # @param obj1 [Object] The first object
395
+ # @param obj2 [Object] The second object
396
+ # @return [String] Formatted attributes of the objects
397
+ def format_object_attributes(obj1, obj2, parent_node)
398
+ obj1.class.attributes.each_key do |attr|
399
+ value1 = obj1.send(attr)
400
+ value2 = obj2&.send(attr)
401
+
402
+ attr_type = obj1.class.attributes[attr].collection? ? "collection" : type_name(obj1.class.attributes[attr])
403
+
404
+ if value1 == value2
405
+ if @show_unchanged
406
+ format_single_value(value1, parent_node,
407
+ "#{attr} (#{attr_type})")
408
+ end
409
+ else
410
+ format_value_tree(value1, value2, parent_node, attr, attr_type)
411
+ end
412
+ end
413
+ end
414
+
415
+ # Formats the value tree for diff output
416
+ # @param value1 [Object] The first value
417
+ # @param value2 [Object] The second value
418
+ # @param is_last [Boolean] Whether this is the last item in the current level
419
+ # @param label [String] The label for the value
420
+ # @param type_info [String, nil] Additional type information
421
+ # @return [String] Formatted value tree
422
+ def format_value_tree(value1, value2, parent_node, label,
423
+ type_info = nil)
424
+ return if value1 == value2 && !@show_unchanged
425
+
426
+ if value1 == value2
427
+ if @show_unchanged
428
+ return format_single_value(
429
+ value1,
430
+ parent_node,
431
+ "#{label}#{type_info ? " (#{type_info})" : ''}",
432
+ )
433
+ end
434
+
435
+ return if @highlight_diff
436
+ end
437
+
438
+ case value1
439
+ when Array
440
+ format_collection(value1, value2, parent_node)
441
+ when Hash
442
+ format_hash_tree(value1, value2, parent_node)
443
+ when ComparableModel
444
+ format_object_attributes(value1, value2, parent_node)
445
+ else
446
+ node = Tree.new("#{label}#{type_info ? " (#{type_info})" : ''}:")
447
+ parent_node.add_child(node)
448
+ node.add_child(Tree.new("- #{format_value(value1)}", color: :red))
449
+ node.add_child(Tree.new("+ #{format_value(value2)}", color: :green))
450
+ end
451
+ end
452
+
453
+ # Formats a single value for diff output
454
+ # @param value [Object] The value to format
455
+ # @param is_last [Boolean] Whether this is the last item in the current level
456
+ # @param label [String] The label for the value
457
+ # @return [String] Formatted single value
458
+ def format_single_value(value, parent_node, label, color = nil)
459
+ node = Tree.new("#{label}#{label.empty? ? '' : ':'}", color: color)
460
+ parent_node.add_child(node)
461
+
462
+ case value
463
+ when ComparableModel
464
+ format_comparable_mapper(value, node, color)
465
+ when Array
466
+ if value.empty?
467
+ node.add_child(Tree.new("(nil)", color: color))
468
+ else
469
+ format_collection(value, value, node)
470
+ end
471
+ else
472
+ node.content += " #{format_value(value)}"
473
+ end
474
+ end
475
+
476
+ # Formats a ComparableModel object for diff output
477
+ # @param obj [ComparableModel] The object to format
478
+ # @return [String] Formatted ComparableModel object
479
+ def format_comparable_mapper(obj, parent_node, color = nil)
480
+ obj.class.attributes.each do |attr_name, attr_type|
481
+ attr_value = obj.send(attr_name)
482
+ attr_node = Tree.new("#{attr_name} (#{type_name(attr_type)}):",
483
+ color: color)
484
+ parent_node.add_child(attr_node)
485
+ if attr_value.is_a?(ComparableModel)
486
+ format_comparable_mapper(attr_value, attr_node, color)
487
+ else
488
+ value_node = Tree.new(format_value(attr_value), color: color)
489
+ attr_node.add_child(value_node)
490
+ end
491
+ end
492
+ end
493
+
494
+ # Formats a hash tree for diff output
495
+ # @param hash1 [Hash] The first hash to compare
496
+ # @param hash2 [Hash] The second hash to compare
497
+ # @return [String] Formatted hash tree
498
+ def format_hash_tree(hash1, hash2, parent_node)
499
+ keys = (hash1.keys + hash2.keys).uniq
500
+ keys.each do |key|
501
+ value1 = hash1[key]
502
+ value2 = hash2[key]
503
+
504
+ if value1 == value2
505
+ format_single_value(value1, parent_node, key) if @show_unchanged
506
+ else
507
+ format_value_tree(value1, value2, parent_node, key)
508
+ end
509
+ end
510
+ end
511
+ end
512
+ end
513
+
514
+ # Generates a tree representation of the object
515
+ # @return [String] A string representation of the object's attribute tree
516
+ def to_tree
517
+ output = "#{self.class}\n"
518
+ self.class.attributes.each_with_index do |(name, type), index|
519
+ value = send(name)
520
+ is_last = index == self.class.attributes.length - 1
521
+ context = DiffContext.new(nil, nil, show_unchanged: false)
522
+ formatted = context.format_value(value)
523
+ output << context.tree_line(is_last, "#{name} (#{type}): #{formatted}")
524
+ end
525
+ output
526
+ end
527
+ end
528
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "serializable"
4
+
5
+ module Lutaml
6
+ module Model
7
+ # Nil class substitute for comparison
8
+ class ComparableNil < ::Lutaml::Model::Serializable
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ # Comparison of two values for ComparableMapper
6
+ class Comparison
7
+ attr_accessor :original, :updated
8
+
9
+ def initialize(original:, updated:)
10
+ @original = original
11
+ @updated = updated
12
+ end
13
+ end
14
+ end
15
+ end