abide_dev_utils 0.14.2 → 0.15.0

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,347 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AbideDevUtils
4
+ module XCCDF
5
+ module Parser
6
+ module Objects
7
+ # Methods for converting an object to a hash
8
+ module DiffableObject
9
+ # Generic error class for diffable objects
10
+ class DiffableObjectError < StandardError
11
+ def initialize(self_obj = nil, other_obj = nil, custom_msg: nil)
12
+ if custom_msg.nil?
13
+ super("This object (#{self_obj}) is not diffable with other object (#{other_obj})")
14
+ else
15
+ super(custom_msg)
16
+ end
17
+ @self_obj = self_obj
18
+ @other_obj = other_obj
19
+ @custom_msg = custom_msg
20
+ end
21
+ end
22
+
23
+ # Raised when the diffed objects are not instances of the same class
24
+ class NotSameClassError < DiffableObjectError
25
+ def initialize(self_obj = nil, other_obj = nil)
26
+ super(
27
+ self_obj,
28
+ other_obj,
29
+ custom_msg: "This object's class (#{self_obj.class}) does not match other object's class (#{other_obj.class})",
30
+ )
31
+ end
32
+ end
33
+
34
+ # Holds the result of a diff on a per-item basis.
35
+ class DiffChangeResult
36
+ attr_accessor :type, :details
37
+ attr_reader :old_value, :new_value, :element
38
+
39
+ def initialize(type, old_value, new_value, element, details = {})
40
+ @type = type
41
+ @old_value = old_value
42
+ @new_value = new_value
43
+ @element = element
44
+ @details = details
45
+ end
46
+
47
+ def to_h(hash_objs: false)
48
+ return new_hash(type, old_value, new_value, element, details) unless hash_objs
49
+
50
+ old_val = old_value.respond_to?(:to_s) ? old_value.to_s : old_value.inspect
51
+ new_val = new_value.respond_to?(:to_s) ? new_value.to_s : new_value.inspect
52
+ details_hash = details.transform_values do |val|
53
+ case val
54
+ when DiffChangeResult
55
+ val.to_h(hash_objs: hash_objs)
56
+ when Array
57
+ val.map { |v| v.is_a?(DiffChangeResult) ? v.to_h(hash_objs: hash_objs) : v.to_s }
58
+ else
59
+ val.to_s
60
+ end
61
+ end
62
+ new_hash(type, old_val, new_val, element, details_hash)
63
+ end
64
+
65
+ def to_yaml
66
+ require 'yaml'
67
+ to_h(hash_objs: true).to_yaml
68
+ end
69
+
70
+ def to_a
71
+ [type, old_value, new_value, element, details]
72
+ end
73
+
74
+ def to_s
75
+ "#{element} #{type}: #{old_value} -> #{new_value}, #{details}"
76
+ end
77
+
78
+ private
79
+
80
+ def new_hash(change_type, old_val, new_val, element, details_hash)
81
+ hsh = {}
82
+ hsh[:type] = change_type
83
+ hsh[:old_value] = old_val unless old_val.nil? || (old_val.respond_to?(:empty?) && old_val.empty?)
84
+ hsh[:new_value] = new_val unless new_val.nil? || (old_val.respond_to?(:empty?) && new_val.empty?)
85
+ hsh[:element] = element
86
+ hsh[:details] = details_hash unless details_hash.empty?
87
+ hsh
88
+ end
89
+ end
90
+
91
+ def diff_change_result(type, old_value, new_value, element = nil, details = {})
92
+ element = if element.nil?
93
+ get_element_from = old_value || new_value
94
+ get_element_from.respond_to?(:label) ? get_element_from.label.to_sym : :plain_object
95
+ else
96
+ element
97
+ end
98
+ DiffChangeResult.new(type, old_value, new_value, element, details)
99
+ end
100
+
101
+ def check_diffable!(other)
102
+ raise DiffableObjectError.new(self, other) unless other.class.included_modules.include?(DiffableObject)
103
+ raise NotSameClassError.new(self, other) unless other.instance_of?(self.class)
104
+ end
105
+
106
+ # Diff two parser objects
107
+ # @param other [Object] The object to compare to. Must be an instance
108
+ # of the same class as the object being diffed.
109
+ # @return [DiffChangeResult] The result of the diff
110
+ def diff(other)
111
+ return if other.nil?
112
+
113
+ check_diffable!(other)
114
+ case self
115
+ when AbideDevUtils::XCCDF::Parser::Objects::AttributeValue,
116
+ AbideDevUtils::XCCDF::Parser::Objects::ControlURI,
117
+ AbideDevUtils::XCCDF::Parser::Objects::Description,
118
+ AbideDevUtils::XCCDF::Parser::Objects::Fixtext,
119
+ AbideDevUtils::XCCDF::Parser::Objects::LongText,
120
+ AbideDevUtils::XCCDF::Parser::Objects::Platform,
121
+ AbideDevUtils::XCCDF::Parser::Objects::Rationale,
122
+ AbideDevUtils::XCCDF::Parser::Objects::ShortText,
123
+ AbideDevUtils::XCCDF::Parser::Objects::System,
124
+ AbideDevUtils::XCCDF::Parser::Objects::Title,
125
+ AbideDevUtils::XCCDF::Parser::Objects::Version
126
+ diff_str_obj(self, other)
127
+ when AbideDevUtils::XCCDF::Parser::Objects::Rule
128
+ diff_rule(self, other)
129
+ when AbideDevUtils::XCCDF::Parser::Objects::Check
130
+ diff_check(self, other)
131
+ when AbideDevUtils::XCCDF::Parser::Objects::ComplexCheck
132
+ diff_complex_check(self, other)
133
+ when AbideDevUtils::XCCDF::Parser::Objects::XccdfSelect
134
+ diff_xccdf_select(self, other)
135
+ when AbideDevUtils::XCCDF::Parser::Objects::Value
136
+ diff_value(self, other)
137
+ when AbideDevUtils::XCCDF::Parser::Objects::Group
138
+ diff_group(self, other)
139
+ when AbideDevUtils::XCCDF::Parser::Objects::Profile
140
+ diff_profile(self, other)
141
+ when AbideDevUtils::XCCDF::Parser::Objects::Benchmark
142
+ diff_benchmark(self, other)
143
+ else
144
+ diff_ambiguous(self, other, called_from_diff: true)
145
+ end
146
+ end
147
+
148
+ def diff_ambiguous(self_obj, other_obj, called_from_diff: false)
149
+ return if other_obj.nil?
150
+
151
+ if self_obj.is_a?(Array) && other_obj.is_a?(Array)
152
+ diff_array_obj(self_obj, other_obj)
153
+ elsif self_obj.respond_to?(:diff) && other_obj.respond_to?(:diff) && !called_from_diff
154
+ self_obj.diff(other_obj)
155
+ elsif self_obj.respond_to?(:to_s) && other_obj.respond_to?(:to_s)
156
+ diff_str_obj(self_obj, other_obj)
157
+ else
158
+ diff_plain_obj(self_obj, other_obj)
159
+ end
160
+ end
161
+
162
+ def diff_plain_obj(self_obj, other_obj)
163
+ result = self_obj == other_obj ? :equal : :not_equal
164
+ d_hash = {
165
+ self_ivars: self_obj.iv_to_h,
166
+ other_ivars: other_obj.iv_to_h,
167
+ }
168
+ diff_change_result(result, self_obj, other_obj, d_hash)
169
+ end
170
+
171
+ def diff_str_obj(self_obj, other_obj)
172
+ result = self_obj.to_s == other_obj.to_s ? :equal : :not_equal
173
+ diff_change_result(result, self_obj, other_obj)
174
+ end
175
+
176
+ def diff_array_obj(self_ary, other_ary)
177
+ sorted_self = self_ary.sort
178
+ sorted_other = other_ary.sort
179
+ added_ary = (sorted_other - sorted_self).map { |i| diff_change_result(:added, nil, i) }
180
+ removed_ary = (sorted_self - sorted_other).map { |i| diff_change_result(:removed, i, nil) }
181
+ changed_ary = correlate_added_removed(added_ary, removed_ary)
182
+ diffable_self = sorted_self - (changed_ary.map(&:old_value) + changed_ary.map(&:new_value)).compact
183
+ diffable_other = sorted_other - (changed_ary.map(&:old_value) + changed_ary.map(&:new_value)).compact
184
+ diff_ary = diffable_self.zip(diffable_other).filter_map do |(self_obj, other_obj)|
185
+ change = diff_ambiguous(self_obj, other_obj)
186
+ if change.type == :equal
187
+ nil
188
+ else
189
+ change
190
+ end
191
+ end
192
+ diff_ary + changed_ary
193
+ end
194
+
195
+ def correlate_added_removed(added_ary, removed_ary)
196
+ return [] if added_ary.empty? && removed_ary.empty?
197
+
198
+ actual_added = added_ary.dup
199
+ actual_removed = removed_ary.dup
200
+ correlated = added_ary.each_with_object([]) do |added, ary|
201
+ similarity = nil
202
+ removed = removed_ary.find do |r|
203
+ similarity = added.new_value.find_similarity(r.old_value)
204
+ similarity.any? { |s| s[3] }
205
+ end
206
+ next if removed.nil?
207
+
208
+ details_hash = {}
209
+ similarity.each do |similar|
210
+ details_hash[similar[0]] = similar[1..2] unless similar[3]
211
+ end
212
+ ary << diff_change_result(:not_equal, removed.old_value, added.new_value, nil, details_hash)
213
+ actual_added.delete(added)
214
+ actual_removed.delete(removed)
215
+ end
216
+ (correlated + actual_added + actual_removed).uniq
217
+ end
218
+
219
+ def diff_rule(self_rule, other_rule, diff_properties: %i[number title])
220
+ d_hash = diff_properties.each_with_object({}) do |prop, hsh|
221
+ hsh[prop] = diff_ambiguous(self_rule.send(prop), other_rule.send(prop))
222
+ end
223
+ result = result_from_details_hash(d_hash)
224
+ d_hash = process_details_hash!(d_hash)
225
+ diff_change_result(result, self_rule, other_rule, :rule, d_hash)
226
+ end
227
+
228
+ def diff_check(self_check, other_check)
229
+ d_hash = {
230
+ system: self_check.system.diff(other_check.system),
231
+ check_export: diff_ambiguous(self_check.check_export, other_check.check_export),
232
+ check_content_ref: diff_ambiguous(self_check.check_content_ref, other_check.check_content_ref),
233
+ }
234
+ result = result_from_details_hash(d_hash)
235
+ d_hash = process_details_hash!(d_hash)
236
+ diff_change_result(result, self_check, other_check, :check, d_hash)
237
+ end
238
+
239
+ def diff_complex_check(self_complex_check, other_complex_check)
240
+ d_hash = {
241
+ operator: self_complex_check.operator.diff(other_complex_check.operator),
242
+ check: diff_array_obj(self_complex_check.check, other_complex_check.check),
243
+ }
244
+ result = result_from_details_hash(d_hash)
245
+ d_hash = process_details_hash!(d_hash)
246
+ diff_change_result(result, self_complex_check, other_complex_check, :complex_check, d_hash)
247
+ end
248
+
249
+ def diff_xccdf_select(self_select, other_select)
250
+ d_hash = {
251
+ idref: diff_str_obj(self_select.to_s, other_select.to_s),
252
+ selected: diff_str_obj(self_select.selected, other_select.selected),
253
+ }
254
+ result = result_from_details_hash(d_hash)
255
+ d_hash = process_details_hash!(d_hash)
256
+ diff_change_result(result, self_select, other_select, :xccdf_select, d_hash)
257
+ end
258
+
259
+ def diff_value(self_value, other_value)
260
+ d_hash = {
261
+ title: self_value.title.diff(other_value.title),
262
+ description: self_value.description.diff(other_value.description),
263
+ text: diff_str_obj(self_value.text.to_s, other_value.text.to_s),
264
+ operator: self_value.operator.diff(other_value.operator),
265
+ type: self_value.type.diff(other_value.type),
266
+ }
267
+ result = result_from_details_hash(d_hash)
268
+ d_hash = process_details_hash!(d_hash)
269
+ diff_change_result(result, self_value, other_value, :value, d_hash)
270
+ end
271
+
272
+ def diff_group(self_group, other_group)
273
+ d_hash = {
274
+ title: self_group.title.diff(other_group.title),
275
+ description: self_group.description.diff(other_group.description),
276
+ }
277
+ if self_group.respond_to?(:group) && other_group.respond_to?(:group)
278
+ d_hash[:group] ||= []
279
+ g_diff = diff_ambiguous(self_group.group, other_group.group)
280
+ if g_diff.is_a?(Array)
281
+ d_hash[:group] += g_diff
282
+ else
283
+ d_hash[:group] << g_diff
284
+ end
285
+ end
286
+ if self_group.respond_to?(:rule) && other_group.respond_to?(:rule)
287
+ d_hash[:rule] ||= []
288
+ r_diff = diff_ambiguous(self_group.rule, other_group.rule)
289
+ if r_diff.is_a?(Array)
290
+ d_hash[:rule] += r_diff
291
+ else
292
+ d_hash[:rule] << r_diff
293
+ end
294
+ end
295
+ result = result_from_details_hash(d_hash)
296
+ d_hash = process_details_hash!(d_hash)
297
+ diff_change_result(result, self_group, other_group, :group, d_hash)
298
+ end
299
+
300
+ def diff_profile(self_profile, other_profile)
301
+ d_hash = {
302
+ title: self_profile.title.diff(other_profile.title),
303
+ description: self_profile.description.diff(other_profile.description),
304
+ level: diff_str_obj(self_profile.level, other_profile.level),
305
+ xccdf_select: diff_ambiguous(self_profile.xccdf_select, other_profile.xccdf_select),
306
+ }
307
+ result = result_from_details_hash(d_hash)
308
+ d_hash = process_details_hash!(d_hash)
309
+ diff_change_result(result, self_profile, other_profile, :profile, d_hash)
310
+ end
311
+
312
+ def diff_benchmark(self_benchmark, other_benchmark)
313
+ d_hash = {}
314
+ d_hash[:title] = self_benchmark.title.diff(other_benchmark.title)
315
+ d_hash[:description] = self_benchmark.description.diff(other_benchmark.description)
316
+ d_hash[:platform] = self_benchmark.platform.diff(other_benchmark.platform)
317
+ d_hash[:profile] = diff_ambiguous(self_benchmark.profile, other_benchmark.profile)
318
+ d_hash[:group] = diff_ambiguous(self_benchmark.group, other_benchmark.group)
319
+ d_hash[:value] = diff_ambiguous(self_benchmark.value, other_benchmark.value)
320
+ result = result_from_details_hash(d_hash)
321
+ d_hash = process_details_hash!(d_hash)
322
+ diff_change_result(result, self_benchmark, other_benchmark, :benchmark, d_hash)
323
+ end
324
+
325
+ def process_details_hash!(d_hash, _element = :plain_object)
326
+ d_hash.reject! do |_, v|
327
+ v.nil? || (v.empty? if v.respond_to?(:empty?)) || (v.type == :equal if v.respond_to?(:type))
328
+ end
329
+ d_hash
330
+ end
331
+
332
+ def result_from_details_hash(d_hash)
333
+ changed_types = %i[not_equal added removed]
334
+ results = d_hash.values.find do |v|
335
+ if v.is_a?(Array)
336
+ v.map(&:type).any? { |i| changed_types.include?(i) }
337
+ elsif v.respond_to?(:type)
338
+ changed_types.include?(v&.type)
339
+ end
340
+ end
341
+ results.nil? ? :equal : :not_equal
342
+ end
343
+ end
344
+ end
345
+ end
346
+ end
347
+ end