kennel 1.130.0 → 1.131.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.
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