lutaml-model 0.3.3 → 0.3.4

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.
@@ -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