kennel 1.130.0 → 1.131.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d6ee3df09b69dd5a7af7285c219f4415c4e94ce2ab4f6533c5fff03be848e9c4
4
- data.tar.gz: d7f923b69ad9b63774141ac0e9c6c04de7a26a67abd732c41c5f19d478704656
3
+ metadata.gz: ac3eeb3c1ec0e2b03c7acbf2b2d7c68335207a7e79408775e9b876c6c4acd400
4
+ data.tar.gz: db96fddb04bce543efcea1a505a1d360447161259e9e9fdd481acfe785bad194
5
5
  SHA512:
6
- metadata.gz: 7c967bf54743cc757a0c0c911e981701d4b5b393f6ebac66538432541c7c6dfecef2cd8c1c0e5389f90c7d0b91a54e79a29e0e1abf9d16fd67adad4bcd6209fe
7
- data.tar.gz: 297239ea9305e4eadbe40a363160b0ca2aaf70dad237f95a448b00b30d3db9ed9f707b2736b2596ef73e8981d333b600af87da40978f5d1c9fc8e0dee3d7f9ea
6
+ metadata.gz: a710b2e2ae6932cf36bdd12fbb06c34f8b538944e693a484b294e7614b7d9941e7c2da498cf7cdcb56bda255a0b2c874383306437ee5b71456d900ae1e9a5c2f
7
+ data.tar.gz: 90e767722863bb640f3bd96436937c390d40d8c8b868d3ee51a444ff6e042e36f03048cc1b5c9cec49d96207d53d99df6f529318658c3881fd9078ed55c2b7d8
data/lib/kennel/api.rb CHANGED
@@ -7,6 +7,8 @@ module Kennel
7
7
 
8
8
  def self.tag(api_resource, reply)
9
9
  klass = Models::Record.api_resource_map[api_resource]
10
+ return reply unless klass # do not blow up on unknown models
11
+
10
12
  reply.merge(
11
13
  klass: klass,
12
14
  tracking_id: klass.parse_tracking_id(reply)
@@ -57,7 +57,9 @@ module Kennel
57
57
  # - delete expired keys
58
58
  # - delete what would be deleted anyway when updating
59
59
  def expire_old_data
60
- @data.reject! { |_, (_, (_, cv), expires)| expires < @now || cv != @cache_version }
60
+ @data.reject! do |(_api_resource, _id), (_value, (_key_version, cache_version), expires)|
61
+ expires < @now || cache_version != @cache_version
62
+ end
61
63
  end
62
64
  end
63
65
  end
@@ -202,7 +202,7 @@ module Kennel
202
202
  end
203
203
  end
204
204
 
205
- def validate_update!(_actuals, diffs)
205
+ def validate_update!(diffs)
206
206
  _, path, from, to = diffs.find { |diff| diff[1] == "layout_type" }
207
207
  invalid_update!(path, from, to) if path
208
208
  end
@@ -137,7 +137,7 @@ module Kennel
137
137
  end
138
138
  end
139
139
 
140
- def validate_update!(_actuals, diffs)
140
+ def validate_update!(diffs)
141
141
  # ensure type does not change, but not if it's metric->query which is supported and used by importer.rb
142
142
  _, path, from, to = diffs.detect { |_, path, _, _| path == "type" }
143
143
  if path && !(from == "metric alert" && to == "query alert")
@@ -167,7 +167,7 @@ module Kennel
167
167
  end
168
168
 
169
169
  # Can raise DisallowedUpdateError
170
- def validate_update!(*)
170
+ def validate_update!(_diffs)
171
171
  end
172
172
 
173
173
  def invalid_update!(field, old_value, new_value)
data/lib/kennel/syncer.rb CHANGED
@@ -6,47 +6,43 @@ module Kennel
6
6
  LINE_UP = "\e[1A\033[K" # go up and clear
7
7
 
8
8
  Plan = Struct.new(:changes, keyword_init: true)
9
+
10
+ InternalPlan = Struct.new(:creates, :updates, :deletes) do
11
+ def empty?
12
+ creates.empty? && updates.empty? && deletes.empty?
13
+ end
14
+ end
15
+
9
16
  Change = Struct.new(:type, :api_resource, :tracking_id, :id)
10
17
 
11
18
  def initialize(api, expected, actual, strict_imports: true, project_filter: nil, tracking_id_filter: nil)
12
19
  @api = api
13
- @expected = Set.new expected # need Set to speed up deletion
14
- @actual = actual
15
20
  @strict_imports = strict_imports
16
21
  @project_filter = project_filter
17
22
  @tracking_id_filter = tracking_id_filter
18
23
 
19
- @attribute_differ = AttributeDiffer.new
24
+ @resolver = Resolver.new(expected: expected, project_filter: @project_filter, tracking_id_filter: @tracking_id_filter)
20
25
 
21
- calculate_changes
22
- validate_changes
23
- prevent_irreversible_partial_updates
26
+ internal_plan = calculate_changes(expected: expected, actual: actual)
27
+ validate_changes(internal_plan)
28
+ @internal_plan = internal_plan
24
29
 
25
30
  @warnings.each { |message| Kennel.out.puts Console.color(:yellow, "Warning: #{message}") }
26
31
  end
27
32
 
28
33
  def plan
34
+ ip = @internal_plan
29
35
  Plan.new(
30
- changes:
31
- @create.map { |_id, e, _a| Change.new(:create, e.class.api_resource, e.tracking_id, nil) } +
32
- @update.map { |id, e, _a| Change.new(:update, e.class.api_resource, e.tracking_id, id) } +
33
- @delete.map { |id, _e, a| Change.new(:delete, a.fetch(:klass).api_resource, a.fetch(:tracking_id), id) }
36
+ changes: (ip.creates + ip.updates + ip.deletes).map(&:change)
34
37
  )
35
38
  end
36
39
 
37
40
  def print_plan
38
- Kennel.out.puts "Plan:"
39
- if noop?
40
- Kennel.out.puts Console.color(:green, "Nothing to do")
41
- else
42
- print_changes "Create", @create, :green
43
- print_changes "Update", @update, :yellow
44
- print_changes "Delete", @delete, :red
45
- end
41
+ PlanDisplayer.new.display(internal_plan)
46
42
  end
47
43
 
48
44
  def confirm
49
- return false if noop?
45
+ return false if internal_plan.empty?
50
46
  return true if ENV["CI"] || !STDIN.tty? || !Kennel.err.tty?
51
47
  Console.ask?("Execute Plan ?")
52
48
  end
@@ -54,121 +50,77 @@ module Kennel
54
50
  def update
55
51
  changes = []
56
52
 
57
- each_resolved @create do |_, e|
58
- message = "#{e.class.api_resource} #{e.tracking_id}"
53
+ internal_plan.deletes.each do |item|
54
+ message = "#{item.api_resource} #{item.tracking_id} #{item.id}"
55
+ Kennel.out.puts "Deleting #{message}"
56
+ @api.delete item.api_resource, item.id
57
+ changes << item.change
58
+ Kennel.out.puts "#{LINE_UP}Deleted #{message}"
59
+ end
60
+
61
+ resolver.each_resolved internal_plan.creates do |item|
62
+ message = "#{item.api_resource} #{item.tracking_id}"
59
63
  Kennel.out.puts "Creating #{message}"
60
- reply = @api.create e.class.api_resource, e.as_json
64
+ reply = @api.create item.api_resource, item.expected.as_json
61
65
  id = reply.fetch(:id)
62
- changes << Change.new(:create, e.class.api_resource, e.tracking_id, id)
63
- populate_id_map [], [reply] # allow resolving ids we could previously no resolve
64
- Kennel.out.puts "#{LINE_UP}Created #{message} #{e.class.url(id)}"
66
+ changes << item.change(id)
67
+ resolver.add_actual [reply] # allow resolving ids we could previously not resolve
68
+ Kennel.out.puts "#{LINE_UP}Created #{message} #{item.url(id)}"
65
69
  end
66
70
 
67
- each_resolved @update do |id, e|
68
- message = "#{e.class.api_resource} #{e.tracking_id} #{e.class.url(id)}"
71
+ resolver.each_resolved internal_plan.updates do |item|
72
+ message = "#{item.api_resource} #{item.tracking_id} #{item.url}"
69
73
  Kennel.out.puts "Updating #{message}"
70
- @api.update e.class.api_resource, id, e.as_json
71
- changes << Change.new(:update, e.class.api_resource, e.tracking_id, id)
74
+ @api.update item.api_resource, item.id, item.expected.as_json
75
+ changes << item.change
72
76
  Kennel.out.puts "#{LINE_UP}Updated #{message}"
73
77
  end
74
78
 
75
- @delete.each do |id, _, a|
76
- klass = a.fetch(:klass)
77
- message = "#{klass.api_resource} #{a.fetch(:tracking_id)} #{id}"
78
- Kennel.out.puts "Deleting #{message}"
79
- @api.delete klass.api_resource, id
80
- changes << Change.new(:delete, klass.api_resource, a.fetch(:tracking_id), id)
81
- Kennel.out.puts "#{LINE_UP}Deleted #{message}"
82
- end
83
-
84
79
  Plan.new(changes: changes)
85
80
  end
86
81
 
87
82
  private
88
83
 
89
- # loop over items until everything is resolved or crash when we get stuck
90
- # this solves cases like composite monitors depending on each other or monitor->monitor slo->slo monitor chains
91
- def each_resolved(list)
92
- list = list.dup
93
- loop do
94
- return if list.empty?
95
- list.reject! do |id, e|
96
- if resolved?(e)
97
- yield id, e
98
- true
99
- else
100
- false
101
- end
102
- end ||
103
- assert_resolved(list[0][1]) # resolve something or show a circular dependency error
104
- end
105
- end
106
-
107
- # TODO: optimize by storing an instance variable if already resolved
108
- def resolved?(e)
109
- assert_resolved e
110
- true
111
- rescue UnresolvableIdError
112
- false
113
- end
114
-
115
- # raises UnresolvableIdError when not resolved
116
- def assert_resolved(e)
117
- resolve_linked_tracking_ids! [e], force: true
118
- end
119
-
120
- def noop?
121
- @create.empty? && @update.empty? && @delete.empty?
122
- end
84
+ attr_reader :resolver, :internal_plan
123
85
 
124
- def calculate_changes
86
+ def calculate_changes(expected:, actual:)
125
87
  @warnings = []
126
- @id_map = IdMap.new
127
88
 
128
89
  Progress.progress "Diffing" do
129
- populate_id_map @expected, @actual
130
- filter_actual! @actual
131
- resolve_linked_tracking_ids! @expected # resolve dependencies to avoid diff
132
- @expected.each(&:add_tracking_id) # avoid diff with actual, which has tracking_id
90
+ resolver.add_actual actual
91
+ filter_actual! actual
92
+ resolver.resolve_as_much_as_possible(expected) # resolve as many dependencies as possible to reduce the diff
133
93
 
134
94
  # see which expected match the actual
135
- matching, unmatched_expected, unmatched_actual = partition_matched_expected
95
+ matching, unmatched_expected, unmatched_actual = MatchedExpected.partition(expected, actual)
136
96
  validate_expected_id_not_missing unmatched_expected
137
97
  fill_details! matching # need details to diff later
138
98
 
139
99
  # update matching if needed
140
- @update = matching.map do |e, a|
100
+ updates = matching.map do |e, a|
101
+ # Refuse to "adopt" existing items into kennel while running with a filter (i.e. on a branch).
102
+ # Without this, we'd adopt an item, then the next CI run would delete it
103
+ # (instead of "unadopting" it).
104
+ e.add_tracking_id unless @project_filter && a.fetch(:tracking_id).nil?
141
105
  id = a.fetch(:id)
142
106
  diff = e.diff(a)
143
- [id, e, a, diff] if diff.any?
107
+ a[:id] = id
108
+ Types::PlannedUpdate.new(e, a, diff) if diff.any?
144
109
  end.compact
145
110
 
146
111
  # delete previously managed
147
- @delete = unmatched_actual.map { |a| [a.fetch(:id), nil, a] if a.fetch(:tracking_id) }.compact
112
+ deletes = unmatched_actual.map { |a| Types::PlannedDelete.new(a) if a.fetch(:tracking_id) }.compact
148
113
 
149
114
  # unmatched expected need to be created
150
- @create = unmatched_expected.map { |e| [nil, e] }
115
+ unmatched_expected.each(&:add_tracking_id)
116
+ creates = unmatched_expected.map { |e| Types::PlannedCreate.new(e) }
151
117
 
152
118
  # order to avoid deadlocks
153
- @delete.sort_by! { |_, _, a| DELETE_ORDER.index a.fetch(:klass).api_resource }
154
- @update.sort_by! { |_, e, _| DELETE_ORDER.index e.class.api_resource } # slo needs to come before slo alert
155
- end
156
- end
119
+ deletes.sort_by! { |item| DELETE_ORDER.index item.api_resource }
120
+ updates.sort_by! { |item| DELETE_ORDER.index item.api_resource } # slo needs to come before slo alert
157
121
 
158
- def partition_matched_expected
159
- lookup_map = matching_expected_lookup_map
160
- unmatched_expected = @expected.dup
161
- unmatched_actual = []
162
- matched = []
163
- @actual.each do |a|
164
- e = matching_expected(a, lookup_map)
165
- if e && unmatched_expected.delete?(e)
166
- matched << [e, a]
167
- else
168
- unmatched_actual << a
169
- end
170
- end.compact
171
- [matched, unmatched_expected, unmatched_actual]
122
+ InternalPlan.new(creates, updates, deletes)
123
+ end
172
124
  end
173
125
 
174
126
  # fill details of things we need to compare
@@ -189,108 +141,242 @@ module Kennel
189
141
  end
190
142
  end
191
143
 
192
- # index list by all the thing we look up by: tracking id and actual id
193
- def matching_expected_lookup_map
194
- @expected.each_with_object({}) do |e, all|
195
- keys = [e.tracking_id]
196
- keys << "#{e.class.api_resource}:#{e.id}" if e.id
197
- keys.compact.each do |key|
198
- raise "Lookup #{key} is duplicated" if all[key]
199
- all[key] = e
200
- end
144
+ # We've already validated the desired objects ('generated') in isolation.
145
+ # Now that we have made the plan, we can perform some more validation.
146
+ def validate_changes(internal_plan)
147
+ internal_plan.updates.each do |item|
148
+ item.expected.validate_update!(item.diff)
201
149
  end
202
150
  end
203
151
 
204
- def matching_expected(a, map)
205
- klass = a.fetch(:klass)
206
- map["#{klass.api_resource}:#{a.fetch(:id)}"] || map[a.fetch(:tracking_id)]
152
+ def filter_actual!(actual)
153
+ if @tracking_id_filter
154
+ actual.select! do |a|
155
+ tracking_id = a.fetch(:tracking_id)
156
+ !tracking_id || @tracking_id_filter.include?(tracking_id)
157
+ end
158
+ elsif @project_filter
159
+ project_prefixes = @project_filter.map { |p| "#{p}:" }
160
+ actual.select! do |a|
161
+ tracking_id = a.fetch(:tracking_id)
162
+ !tracking_id || tracking_id.start_with?(*project_prefixes)
163
+ end
164
+ end
207
165
  end
208
166
 
209
- def print_changes(step, list, color)
210
- return if list.empty?
211
- list.each do |_, e, a, diff|
212
- klass = (e ? e.class : a.fetch(:klass))
213
- Kennel.out.puts Console.color(color, "#{step} #{klass.api_resource} #{e&.tracking_id || a.fetch(:tracking_id)}")
214
- diff&.each { |args| Kennel.out.puts @attribute_differ.format(*args) } # only for update
167
+ class PlanDisplayer
168
+ def initialize
169
+ @attribute_differ = AttributeDiffer.new
215
170
  end
216
- end
217
171
 
218
- # We've already validated the desired objects ('generated') in isolation.
219
- # Now that we have made the plan, we can perform some more validation.
220
- def validate_changes
221
- @update.each do |_, expected, actuals, diffs|
222
- expected.validate_update!(actuals, diffs)
172
+ def display(internal_plan)
173
+ Kennel.out.puts "Plan:"
174
+ if internal_plan.empty?
175
+ Kennel.out.puts Console.color(:green, "Nothing to do")
176
+ else
177
+ print_changes "Create", internal_plan.creates, :green
178
+ print_changes "Update", internal_plan.updates, :yellow
179
+ print_changes "Delete", internal_plan.deletes, :red
180
+ end
181
+ end
182
+
183
+ private
184
+
185
+ def print_changes(step, list, color)
186
+ return if list.empty?
187
+ list.each do |item|
188
+ Kennel.out.puts Console.color(color, "#{step} #{item.api_resource} #{item.tracking_id}")
189
+ if item.class::TYPE == :update
190
+ item.diff.each { |args| Kennel.out.puts @attribute_differ.format(*args) } # only for update
191
+ end
192
+ end
223
193
  end
224
194
  end
225
195
 
226
- # - do not add tracking-id when working with existing ids on a branch,
227
- # so resource do not get deleted when running an update on master (for example merge->CI)
228
- # - ideally we'd never add tracking in the first place, but when adding tracking we do not know the diff yet
229
- def prevent_irreversible_partial_updates
230
- return unless @project_filter # full update, so we are not on a branch
231
- @update.select! do |_, e, _, diff| # ensure clean diff, by removing noop-update
232
- next true unless e.id # safe to add tracking when not having id
233
-
234
- diff.select! do |field_diff|
235
- (_, field, actual) = field_diff
236
- # TODO: refactor this so TRACKING_FIELD stays record-private
237
- next true if e.class::TRACKING_FIELD != field.to_sym # need to sym here because Hashdiff produces strings
238
- next true if e.class.parse_tracking_id(field.to_sym => actual) # already has tracking id
239
-
240
- field_diff[3] = e.remove_tracking_id # make `rake plan` output match what we are sending
241
- actual != field_diff[3] # discard diff if now nothing changes
196
+ class Resolver
197
+ def initialize(expected:, project_filter:, tracking_id_filter:)
198
+ @id_map = IdMap.new
199
+ @project_filter = project_filter
200
+ @tracking_id_filter = tracking_id_filter
201
+
202
+ # mark everything as new
203
+ expected.each do |e|
204
+ id_map.set(e.class.api_resource, e.tracking_id, IdMap::NEW)
205
+ if e.class.api_resource == "synthetics/tests"
206
+ id_map.set(Kennel::Models::Monitor.api_resource, e.tracking_id, IdMap::NEW)
207
+ end
242
208
  end
209
+ end
243
210
 
244
- diff.any?
211
+ def add_actual(actual)
212
+ # override resources that exist with their id
213
+ project_prefixes = project_filter&.map { |p| "#{p}:" }
214
+
215
+ actual.each do |a|
216
+ # ignore when not managed by kennel
217
+ next unless tracking_id = a.fetch(:tracking_id)
218
+
219
+ # ignore when deleted from the codebase
220
+ # (when running with filters we cannot see the other resources in the codebase)
221
+ api_resource = a.fetch(:klass).api_resource
222
+ next if
223
+ !id_map.get(api_resource, tracking_id) &&
224
+ (!project_prefixes || tracking_id.start_with?(*project_prefixes)) &&
225
+ (!tracking_id_filter || tracking_id_filter.include?(tracking_id))
226
+
227
+ id_map.set(api_resource, tracking_id, a.fetch(:id))
228
+ if a.fetch(:klass).api_resource == "synthetics/tests"
229
+ id_map.set(Kennel::Models::Monitor.api_resource, tracking_id, a.fetch(:monitor_id))
230
+ end
231
+ end
245
232
  end
246
- end
247
233
 
248
- def populate_id_map(expected, actual)
249
- # mark everything as new
250
- expected.each do |e|
251
- @id_map.set(e.class.api_resource, e.tracking_id, IdMap::NEW)
252
- if e.class.api_resource == "synthetics/tests"
253
- @id_map.set(Kennel::Models::Monitor.api_resource, e.tracking_id, IdMap::NEW)
234
+ def resolve_as_much_as_possible(expected)
235
+ expected.each do |e|
236
+ e.resolve_linked_tracking_ids!(id_map, force: false)
254
237
  end
255
238
  end
256
239
 
257
- # override resources that exist with their id
258
- project_prefixes = @project_filter&.map { |p| "#{p}:" }
259
- actual.each do |a|
260
- # ignore when not managed by kennel
261
- next unless tracking_id = a.fetch(:tracking_id)
262
-
263
- # ignore when deleted from the codebase
264
- # (when running with filters we cannot see the other resources in the codebase)
265
- api_resource = a.fetch(:klass).api_resource
266
- next if
267
- !@id_map.get(api_resource, tracking_id) &&
268
- (!project_prefixes || tracking_id.start_with?(*project_prefixes)) &&
269
- (!@tracking_id_filter || @tracking_id_filter.include?(tracking_id))
270
-
271
- @id_map.set(api_resource, tracking_id, a.fetch(:id))
272
- if a.fetch(:klass).api_resource == "synthetics/tests"
273
- @id_map.set(Kennel::Models::Monitor.api_resource, tracking_id, a.fetch(:monitor_id))
240
+ # loop over items until everything is resolved or crash when we get stuck
241
+ # this solves cases like composite monitors depending on each other or monitor->monitor slo->slo monitor chains
242
+ def each_resolved(list)
243
+ list = list.dup
244
+ loop do
245
+ return if list.empty?
246
+ list.reject! do |item|
247
+ if resolved?(item.expected)
248
+ yield item
249
+ true
250
+ else
251
+ false
252
+ end
253
+ end ||
254
+ assert_resolved(list[0].expected) # resolve something or show a circular dependency error
274
255
  end
275
256
  end
257
+
258
+ private
259
+
260
+ attr_reader :id_map, :project_filter, :tracking_id_filter
261
+
262
+ # TODO: optimize by storing an instance variable if already resolved
263
+ def resolved?(e)
264
+ assert_resolved e
265
+ true
266
+ rescue UnresolvableIdError
267
+ false
268
+ end
269
+
270
+ # raises UnresolvableIdError when not resolved
271
+ def assert_resolved(e)
272
+ e.resolve_linked_tracking_ids!(id_map, force: true)
273
+ end
276
274
  end
277
275
 
278
- def resolve_linked_tracking_ids!(list, force: false)
279
- list.each { |e| e.resolve_linked_tracking_ids!(@id_map, force: force) }
276
+ module MatchedExpected
277
+ class << self
278
+ def partition(expected, actual)
279
+ lookup_map = matching_expected_lookup_map(expected)
280
+ unmatched_expected = Set.new(expected) # for efficient deletion
281
+ unmatched_actual = []
282
+ matched = []
283
+ actual.each do |a|
284
+ e = matching_expected(a, lookup_map)
285
+ if e && unmatched_expected.delete?(e)
286
+ matched << [e, a]
287
+ else
288
+ unmatched_actual << a
289
+ end
290
+ end.compact
291
+ [matched, unmatched_expected.to_a, unmatched_actual]
292
+ end
293
+
294
+ private
295
+
296
+ # index list by all the thing we look up by: tracking id and actual id
297
+ def matching_expected_lookup_map(expected)
298
+ expected.each_with_object({}) do |e, all|
299
+ keys = [e.tracking_id]
300
+ keys << "#{e.class.api_resource}:#{e.id}" if e.id
301
+ keys.compact.each do |key|
302
+ raise "Lookup #{key} is duplicated" if all[key]
303
+ all[key] = e
304
+ end
305
+ end
306
+ end
307
+
308
+ def matching_expected(a, map)
309
+ klass = a.fetch(:klass)
310
+ map["#{klass.api_resource}:#{a.fetch(:id)}"] || map[a.fetch(:tracking_id)]
311
+ end
312
+ end
280
313
  end
281
314
 
282
- def filter_actual!(actual)
283
- if @tracking_id_filter
284
- actual.select! do |a|
285
- tracking_id = a.fetch(:tracking_id)
286
- !tracking_id || @tracking_id_filter.include?(tracking_id)
315
+ module Types
316
+ class PlannedChange
317
+ def initialize(klass, tracking_id)
318
+ @klass = klass
319
+ @tracking_id = tracking_id
287
320
  end
288
- elsif @project_filter
289
- project_prefixes = @project_filter.map { |p| "#{p}:" }
290
- actual.select! do |a|
291
- tracking_id = a.fetch(:tracking_id)
292
- !tracking_id || tracking_id.start_with?(*project_prefixes)
321
+
322
+ def api_resource
323
+ klass.api_resource
324
+ end
325
+
326
+ def url(id = nil)
327
+ klass.url(id || self.id)
328
+ end
329
+
330
+ def change(id = nil)
331
+ Change.new(self.class::TYPE, api_resource, tracking_id, id)
293
332
  end
333
+
334
+ attr_reader :klass, :tracking_id
335
+ end
336
+
337
+ class PlannedCreate < PlannedChange
338
+ TYPE = :create
339
+
340
+ def initialize(expected)
341
+ super(expected.class, expected.tracking_id)
342
+ @expected = expected
343
+ end
344
+
345
+ attr_reader :expected
346
+ end
347
+
348
+ class PlannedUpdate < PlannedChange
349
+ TYPE = :update
350
+
351
+ def initialize(expected, actual, diff)
352
+ super(expected.class, expected.tracking_id)
353
+ @expected = expected
354
+ @actual = actual
355
+ @diff = diff
356
+ @id = actual.fetch(:id)
357
+ end
358
+
359
+ def change
360
+ super(id)
361
+ end
362
+
363
+ attr_reader :expected, :actual, :diff, :id
364
+ end
365
+
366
+ class PlannedDelete < PlannedChange
367
+ TYPE = :delete
368
+
369
+ def initialize(actual)
370
+ super(actual.fetch(:klass), actual.fetch(:tracking_id))
371
+ @actual = actual
372
+ @id = actual.fetch(:id)
373
+ end
374
+
375
+ def change
376
+ super(id)
377
+ end
378
+
379
+ attr_reader :actual, :id
294
380
  end
295
381
  end
296
382
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Kennel
3
- VERSION = "1.130.0"
3
+ VERSION = "1.131.0"
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kennel
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.130.0
4
+ version: 1.131.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Grosser
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-12-30 00:00:00.000000000 Z
11
+ date: 2023-01-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs