disposable 0.0.9 → 0.1.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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -5
  3. data/CHANGES.md +4 -0
  4. data/Gemfile +1 -1
  5. data/README.md +154 -1
  6. data/database.sqlite3 +0 -0
  7. data/disposable.gemspec +7 -7
  8. data/gemfiles/Gemfile.rails-3.0.lock +10 -8
  9. data/gemfiles/Gemfile.rails-3.2.lock +9 -7
  10. data/gemfiles/Gemfile.rails-4.0.lock +9 -7
  11. data/gemfiles/Gemfile.rails-4.1.lock +10 -8
  12. data/lib/disposable.rb +6 -7
  13. data/lib/disposable/callback.rb +174 -0
  14. data/lib/disposable/composition.rb +21 -58
  15. data/lib/disposable/expose.rb +49 -0
  16. data/lib/disposable/twin.rb +85 -38
  17. data/lib/disposable/twin/builder.rb +12 -30
  18. data/lib/disposable/twin/changed.rb +50 -0
  19. data/lib/disposable/twin/collection.rb +95 -0
  20. data/lib/disposable/twin/composition.rb +43 -15
  21. data/lib/disposable/twin/option.rb +1 -1
  22. data/lib/disposable/twin/persisted.rb +20 -0
  23. data/lib/disposable/twin/property_processor.rb +29 -0
  24. data/lib/disposable/twin/representer.rb +42 -14
  25. data/lib/disposable/twin/save.rb +19 -34
  26. data/lib/disposable/twin/schema.rb +31 -0
  27. data/lib/disposable/twin/setup.rb +38 -0
  28. data/lib/disposable/twin/sync.rb +114 -0
  29. data/lib/disposable/version.rb +1 -1
  30. data/test/api_semantics_test.rb +263 -0
  31. data/test/callback_group_test.rb +222 -0
  32. data/test/callbacks_test.rb +450 -0
  33. data/test/example.rb +40 -0
  34. data/test/expose_test.rb +92 -0
  35. data/test/persisted_test.rb +101 -0
  36. data/test/test_helper.rb +64 -0
  37. data/test/twin/benchmarking.rb +33 -0
  38. data/test/twin/builder_test.rb +32 -0
  39. data/test/twin/changed_test.rb +108 -0
  40. data/test/twin/collection_test.rb +223 -0
  41. data/test/twin/composition_test.rb +56 -25
  42. data/test/twin/expose_test.rb +73 -0
  43. data/test/twin/feature_test.rb +61 -0
  44. data/test/twin/from_test.rb +37 -0
  45. data/test/twin/inherit_test.rb +57 -0
  46. data/test/twin/option_test.rb +27 -0
  47. data/test/twin/readable_test.rb +57 -0
  48. data/test/twin/save_test.rb +192 -0
  49. data/test/twin/schema_test.rb +69 -0
  50. data/test/twin/setup_test.rb +139 -0
  51. data/test/twin/skip_unchanged_test.rb +64 -0
  52. data/test/twin/struct_test.rb +168 -0
  53. data/test/twin/sync_option_test.rb +228 -0
  54. data/test/twin/sync_test.rb +128 -0
  55. data/test/twin/twin_test.rb +49 -128
  56. data/test/twin/writeable_test.rb +56 -0
  57. metadata +106 -20
  58. data/STUFF +0 -4
  59. data/lib/disposable/twin/finders.rb +0 -29
  60. data/lib/disposable/twin/new.rb +0 -30
  61. data/lib/disposable/twin/save_.rb +0 -21
  62. data/test/composition_test.rb +0 -102
@@ -0,0 +1,222 @@
1
+ require "test_helper"
2
+ require "disposable/callback"
3
+ require "pp"
4
+
5
+ class CallbackGroupTest < MiniTest::Spec
6
+ class Group < Disposable::Callback::Group
7
+ attr_reader :output
8
+
9
+ on_change :change!
10
+
11
+ collection :songs do
12
+ on_add :notify_album!
13
+ on_add :reset_song!
14
+
15
+ # on_delete :notify_deleted_author! # in Update!
16
+
17
+ def notify_album!(twin)
18
+ @output = "added to songs"
19
+ end
20
+
21
+ def reset_song!(twin)
22
+ @output << "added to songs, reseting"
23
+ end
24
+ end
25
+
26
+ on_change :rehash_name!, property: :title
27
+
28
+
29
+ on_create :expire_cache! # on_change
30
+ on_update :expire_cache!
31
+
32
+ def change!(twin)
33
+ @output = "Album has changed!"
34
+ end
35
+ end
36
+
37
+
38
+ class AlbumTwin < Disposable::Twin
39
+ feature Sync, Save
40
+ feature Persisted, Changed
41
+
42
+ property :name
43
+
44
+ property :artist do
45
+ property :name
46
+ end
47
+
48
+ collection :songs do
49
+ property :title
50
+ end
51
+ end
52
+
53
+
54
+ # empty.
55
+ it do
56
+ album = Album.new(songs: [Song.new(title: "Dead To Me"), Song.new(title: "Diesel Boy")])
57
+ twin = AlbumTwin.new(album)
58
+
59
+ Group.new(twin).().invocations.must_equal [
60
+ [:on_change, :change!, []],
61
+ [:on_add, :notify_album!, []],
62
+ [:on_add, :reset_song!, []],
63
+ [:on_change, :rehash_name!, []],
64
+ [:on_create, :expire_cache!, []],
65
+ [:on_update, :expire_cache!, []],
66
+ ]
67
+ end
68
+
69
+ it do
70
+ twin = AlbumTwin.new(Album.new)
71
+ twin.songs << Song.new(title: "Dead To Me")
72
+ twin.songs << Song.new(title: "Diesel Boy")
73
+
74
+ twin.name = "Dear Landlord"
75
+
76
+ group = Group.new(twin).()
77
+ # Disposable::Callback::Dispatch.new(twin).on_change{ |twin| puts twin;puts }
78
+
79
+ # pp group.invocations
80
+
81
+ group.invocations.must_equal [
82
+ [:on_change, :change!, [twin]],
83
+ [:on_add, :notify_album!, [twin.songs[0], twin.songs[1]]],
84
+ [:on_add, :reset_song!, [twin.songs[0], twin.songs[1]]],
85
+ [:on_change, :rehash_name!, []],
86
+ [:on_create, :expire_cache!, []],
87
+ [:on_update, :expire_cache!, []],
88
+ ]
89
+
90
+ group.output.must_equal "Album has changed!"
91
+ end
92
+
93
+ # context.
94
+ class Operation
95
+ attr_reader :output
96
+
97
+ def change!(twin)
98
+ @output = "changed!"
99
+ end
100
+
101
+ def notify_album!(twin)
102
+ @output << "notify_album!"
103
+ end
104
+
105
+ def reset_song!(twin)
106
+ @output << "reset_song!"
107
+ end
108
+ end
109
+
110
+ it do
111
+ twin = AlbumTwin.new(Album.new)
112
+ twin.songs << Song.new(title: "Dead To Me")
113
+
114
+ twin.name = "Dear Landlord"
115
+
116
+ group = Group.new(twin).(context: context = Operation.new)
117
+ # Disposable::Callback::Dispatch.new(twin).on_change{ |twin| puts twin;puts }
118
+
119
+ # pp group.invocations
120
+
121
+ group.invocations.must_equal [
122
+ [:on_change, :change!, [twin]],
123
+ [:on_add, :notify_album!, [twin.songs[0]]],
124
+ [:on_add, :reset_song!, [twin.songs[0]]],
125
+ [:on_change, :rehash_name!, []],
126
+ [:on_create, :expire_cache!, []],
127
+ [:on_update, :expire_cache!, []],
128
+ ]
129
+
130
+ context.output.must_equal "changed!notify_album!reset_song!"
131
+ end
132
+ end
133
+
134
+
135
+ class CallbackGroupInheritanceTest < MiniTest::Spec
136
+ class Group < Disposable::Callback::Group
137
+ on_change :change!
138
+ collection :songs do
139
+ on_add :notify_album!
140
+ on_add :reset_song!
141
+ end
142
+ on_change :rehash_name!, property: :title
143
+ property :artist do
144
+ on_change :sing!
145
+ end
146
+ end
147
+
148
+ it do
149
+ Group.hooks.size.must_equal 4
150
+ Group.hooks[0].to_s.must_equal "[:on_change, [:change!]]"
151
+ # Group.hooks[1][1].representer_module.hooks.to_s.must_equal "[[:on_add, [:notify_album!]],[:on_add, [:reset_song!]]]"
152
+ Group.hooks[2].to_s.must_equal "[:on_change, [:rehash_name!, {:property=>:title}]]"
153
+
154
+ Group.representer_class.representable_attrs.get(Group.hooks[3][1]).representer_module.hooks.to_s.must_equal "[[:on_change, [:sing!]]]"
155
+ end
156
+
157
+ class EmptyGroup < Group
158
+ end
159
+
160
+
161
+
162
+ it do
163
+ EmptyGroup.hooks.size.must_equal 4
164
+ # TODO:
165
+ end
166
+
167
+ class EnhancedGroup < Group
168
+ on_change :redo!
169
+ collection :songs do
170
+ on_add :rewind!
171
+ end
172
+ end
173
+
174
+ it do
175
+ Group.hooks.size.must_equal 4
176
+ EnhancedGroup.hooks.size.must_equal 6
177
+ EnhancedGroup.representer_class.representable_attrs.get(EnhancedGroup.hooks[5][1]).representer_module.hooks.to_s.must_equal "[[:on_add, [:rewind!]]]"
178
+ end
179
+
180
+ class EnhancedWithInheritGroup < EnhancedGroup
181
+ collection :songs, inherit: true do # finds first.
182
+ on_add :eat!
183
+ end
184
+ property :artist, inherit: true do
185
+ on_delete :yell!
186
+ end
187
+ end
188
+
189
+ it do
190
+ Group.hooks.size.must_equal 4
191
+ EnhancedGroup.hooks.size.must_equal 6
192
+
193
+ EnhancedGroup.representer_class.representable_attrs.get(EnhancedGroup.hooks[5][1]).representer_module.hooks.to_s.must_equal "[[:on_add, [:rewind!]]]"
194
+ EnhancedWithInheritGroup.hooks.size.must_equal 6
195
+ EnhancedWithInheritGroup.representer_class.representable_attrs.get(EnhancedWithInheritGroup.hooks[1][1]).representer_module.hooks.to_s.must_equal "[[:on_add, [:rewind!]], [:on_add, [:eat!]]]"
196
+ EnhancedWithInheritGroup.representer_class.representable_attrs.get(EnhancedWithInheritGroup.hooks[3][1]).representer_module.hooks.to_s.must_equal "[[:on_change, [:sing!]], [:on_delete, [:yell!]]]"
197
+ end
198
+
199
+ class RemovingInheritGroup < Group
200
+ remove! :on_change, :change!
201
+ collection :songs, inherit: true do # this will not change position
202
+ remove! :on_add, :notify_album!
203
+ end
204
+ end
205
+
206
+ # # puts "@@@@@ #{Group.hooks.object_id.inspect}"
207
+ # # puts "@@@@@ #{EmptyGroup.hooks.object_id.inspect}"
208
+ # puts "@@@@@ Group: #{Group.representer_class.representable_attrs.get(:songs).representer_module.hooks.inspect}"
209
+ # puts "@@@@@ EnhancedGroup: #{EnhancedGroup.representer_class.representable_attrs.get(:songs).representer_module.hooks.inspect}"
210
+ # puts "@@@@@ InheritGroup: #{EnhancedWithInheritGroup.representer_class.representable_attrs.get(:songs).representer_module.hooks.inspect}"
211
+ # puts "@@@@@ RemovingGroup: #{RemovingInheritGroup.representer_class.representable_attrs.get(:songs).representer_module.hooks.inspect}"
212
+ # # puts "@@@@@ #{EnhancedWithInheritGroup.representer_class.representable_attrs.get(:songs).representer_module.hooks.object_id.inspect}"
213
+
214
+ # TODO: object_id tests for all nested representers.
215
+
216
+ it do
217
+ Group.hooks.size.must_equal 4
218
+ RemovingInheritGroup.hooks.size.must_equal 3
219
+ RemovingInheritGroup.representer_class.representable_attrs.get(RemovingInheritGroup.hooks[0][1]).representer_module.hooks.to_s.must_equal "[[:on_add, [:reset_song!]]]"
220
+ RemovingInheritGroup.representer_class.representable_attrs.get(RemovingInheritGroup.hooks[2][1]).representer_module.hooks.to_s.must_equal "[[:on_change, [:sing!]]]"
221
+ end
222
+ end
@@ -0,0 +1,450 @@
1
+ require "test_helper"
2
+ require "disposable/callback"
3
+
4
+ class CallbacksTest < MiniTest::Spec
5
+ before do
6
+ @invokes = []
7
+ end
8
+
9
+ attr_reader :invokes
10
+
11
+ class AlbumTwin < Disposable::Twin
12
+ feature Sync, Save
13
+ feature Persisted, Changed
14
+
15
+ property :name
16
+
17
+ property :artist do
18
+ # on_added
19
+ # on_removed
20
+ property :name
21
+ end
22
+
23
+ collection :songs do
24
+ # after_add: could also be existing user
25
+ # after_remove
26
+ # after_create: this means added+changed?(:persisted): song created and added.
27
+ # after_update
28
+ property :title
29
+ end
30
+ end
31
+
32
+ # - Callbacks don't have before and after. This is up to the caller.
33
+ Callback = Disposable::Callback::Dispatch
34
+ # collection :songs do
35
+ # after_add :song_added! # , context: :operation
36
+ # after_create :notify_album!
37
+ # after_remove :notify_artist!
38
+ # end
39
+
40
+ let (:twin) { AlbumTwin.new(album) }
41
+
42
+ describe "#on_create" do
43
+ let (:album) { Album.new }
44
+
45
+ # after initialization
46
+ it do
47
+ invokes = []
48
+ Callback.new(twin).on_create { |t| invokes << t }
49
+ invokes.must_equal []
50
+ end
51
+
52
+ # save, without any attributes changed.
53
+ it do
54
+ twin.save
55
+
56
+ invokes = []
57
+ Callback.new(twin).on_create { |t| invokes << t }
58
+ invokes.must_equal [twin]
59
+ end
60
+
61
+ # before and after save, with attributes changed
62
+ it do
63
+ # state change, but not persisted, yet.
64
+ twin.name = "Run For Cover"
65
+ invokes = []
66
+ Callback.new(twin).on_create { |t| invokes << t }
67
+ invokes.must_equal []
68
+
69
+ twin.save
70
+
71
+ Callback.new(twin).on_create { |t| invokes << t }
72
+ invokes.must_equal [twin]
73
+ end
74
+
75
+ # for collections.
76
+ it do
77
+ album.songs << song1 = Song.new
78
+ album.songs << Song.create(title: "Run For Cover")
79
+ album.songs << song2 = Song.new
80
+ invokes = []
81
+
82
+ Callback.new(twin.songs).on_create { |t| invokes << t }
83
+ invokes.must_equal []
84
+
85
+ twin.save
86
+
87
+ Callback.new(twin.songs).on_create { |t| invokes << t }
88
+ invokes.must_equal [twin.songs[0], twin.songs[2]]
89
+ end
90
+ end
91
+
92
+ describe "#on_update" do
93
+ let (:album) { Album.new }
94
+
95
+ # after initialization.
96
+ it do
97
+ invokes = []
98
+ Callback.new(twin).on_update { |t| invokes << t }
99
+ invokes.must_equal []
100
+ end
101
+
102
+ # single twin.
103
+ # on_update only works on persisted objects.
104
+ it do
105
+ twin.name = "After The War" # change but not persisted
106
+
107
+ invokes = []
108
+ Callback.new(twin).on_update { |t| invokes << t }
109
+ invokes.must_equal []
110
+
111
+ invokes = []
112
+ twin.save
113
+
114
+ Callback.new(twin).on_update { |t| invokes << t }
115
+ invokes.must_equal []
116
+
117
+
118
+ # now with the persisted album.
119
+ twin = AlbumTwin.new(album) # Album is persisted now.
120
+
121
+ Callback.new(twin).on_update { |t| invokes << t }
122
+ invokes.must_equal []
123
+
124
+ invokes = []
125
+ twin.save
126
+
127
+ # nothing has changed, yet.
128
+ Callback.new(twin).on_update { |t| invokes << t }
129
+ invokes.must_equal []
130
+
131
+ twin.name= "Corridors Of Power"
132
+
133
+ # this will even trigger on_update before saving.
134
+ Callback.new(twin).on_update { |t| invokes << t }
135
+ invokes.must_equal [twin]
136
+
137
+ invokes = []
138
+ twin.save
139
+
140
+ # name changed.
141
+ Callback.new(twin).on_update { |t| invokes << t }
142
+ invokes.must_equal [twin]
143
+ end
144
+
145
+ # for collections.
146
+ it do
147
+ album.songs << song1 = Song.new
148
+ album.songs << Song.create(title: "Run For Cover")
149
+ album.songs << song2 = Song.new
150
+
151
+ invokes = []
152
+ Callback.new(twin.songs).on_update { |t| invokes << t }
153
+ invokes.must_equal []
154
+
155
+ invokes = []
156
+ twin.save
157
+
158
+ # initial save is no update.
159
+ Callback.new(twin.songs).on_update { |t| invokes << t }
160
+ invokes.must_equal []
161
+
162
+
163
+ # now with the persisted album.
164
+ twin = AlbumTwin.new(album) # Album is persisted now.
165
+
166
+ Callback.new(twin.songs).on_update { |t| invokes << t }
167
+ invokes.must_equal []
168
+
169
+ invokes = []
170
+ twin.save
171
+
172
+ # nothing has changed, yet.
173
+ Callback.new(twin.songs).on_update { |t| invokes << t }
174
+ invokes.must_equal []
175
+
176
+ twin.songs[1].title= "After The War"
177
+ twin.songs[2].title= "Run For Cover"
178
+
179
+ # # this will even trigger on_update before saving.
180
+ Callback.new(twin.songs).on_update { |t| invokes << t }
181
+ invokes.must_equal [twin.songs[1], twin.songs[2]]
182
+
183
+ invokes = []
184
+ twin.save
185
+
186
+ Callback.new(twin.songs).on_update { |t| invokes << t }
187
+ invokes.must_equal [twin.songs[1], twin.songs[2]]
188
+ end
189
+ # it do
190
+ # album.songs << song1 = Song.new
191
+ # album.songs << Song.create(title: "Run For Cover")
192
+ # album.songs << song2 = Song.new
193
+ # invokes = []
194
+
195
+ # Callback.new(twin.songs).on_create { |t| invokes << t }
196
+ # invokes.must_equal []
197
+
198
+ # twin.save
199
+
200
+ # Callback.new(twin.songs).on_create { |t| invokes << t }
201
+ # invokes.must_equal [twin.songs[0], twin.songs[2]]
202
+ # end
203
+ end
204
+
205
+
206
+ describe "#on_add" do
207
+ let (:album) { Album.new }
208
+
209
+ # empty collection.
210
+ it do
211
+ invokes = []
212
+ Callback.new(twin.songs).on_add { |t| invokes << t }
213
+ invokes.must_equal []
214
+ end
215
+
216
+ # collection present on initialize are not added.
217
+ it do
218
+ ex_song = Song.create(title: "Run For Cover")
219
+ song = Song.new
220
+ album.songs = [ex_song, song]
221
+
222
+ Callback.new(twin.songs).on_add { |t| invokes << t }
223
+ invokes.must_equal []
224
+ end
225
+
226
+ # items added after initialization are added.
227
+ it do
228
+ ex_song = Song.create(title: "Run For Cover")
229
+ song = Song.new
230
+ album.songs = [ex_song]
231
+
232
+ twin.songs << song
233
+
234
+ Callback.new(twin.songs).on_add { |t| invokes << t }
235
+ invokes.must_equal [twin.songs[1]]
236
+
237
+ twin.save
238
+
239
+ # still shows the added after save.
240
+ invokes = []
241
+ Callback.new(twin.songs).on_add { |t| invokes << t }
242
+ invokes.must_equal [twin.songs[1]]
243
+ end
244
+ end
245
+
246
+ describe "#on_add(:created)" do
247
+ let (:album) { Album.new }
248
+
249
+ # empty collection.
250
+ it do
251
+ invokes = []
252
+ Callback.new(twin.songs).on_add(:created) { |t| invokes << t }
253
+ invokes.must_equal []
254
+ end
255
+
256
+ # collection present on initialize are not added.
257
+ it do
258
+ ex_song = Song.create(title: "Run For Cover")
259
+ song = Song.new
260
+ album.songs = [ex_song, song]
261
+
262
+ Callback.new(twin.songs).on_add(:created) { |t| invokes << t }
263
+ invokes.must_equal []
264
+ end
265
+
266
+ # items added after initialization are added.
267
+ it do
268
+ ex_song = Song.create(title: "Run For Cover")
269
+ song = Song.new
270
+ album.songs = [ex_song]
271
+
272
+ twin.songs << song
273
+ twin.songs << ex_song # already created.
274
+
275
+ Callback.new(twin.songs).on_add(:created) { |t| invokes << t }
276
+ invokes.must_equal []
277
+
278
+ twin.save
279
+
280
+ # still shows the added after save.
281
+ invokes = []
282
+ Callback.new(twin.songs).on_add(:created) { |t| invokes << t }
283
+ invokes.must_equal [twin.songs[1]] # only the created is invoked.
284
+ end
285
+ end
286
+
287
+ describe "#on_delete" do
288
+ let (:album) { Album.new }
289
+
290
+ # empty collection.
291
+ it do
292
+ invokes = []
293
+ Callback.new(twin.songs).on_delete { |t| invokes << t }
294
+ invokes.must_equal []
295
+ end
296
+
297
+ # collection present but nothing deleted.
298
+ it do
299
+ ex_song = Song.create(title: "Run For Cover")
300
+ song = Song.new
301
+ album.songs = [ex_song, song]
302
+
303
+ Callback.new(twin.songs).on_delete { |t| invokes << t }
304
+ invokes.must_equal []
305
+ end
306
+
307
+ # items deleted.
308
+ it do
309
+ ex_song = Song.create(title: "Run For Cover")
310
+ song = Song.new
311
+ album.songs = [ex_song, song]
312
+
313
+ twin.songs.delete(deleted = twin.songs[0])
314
+
315
+ Callback.new(twin.songs).on_delete { |t| invokes << t }
316
+ invokes.must_equal [deleted]
317
+
318
+ twin.save
319
+
320
+ # still shows the deleted after save.
321
+ invokes = []
322
+ Callback.new(twin.songs).on_delete { |t| invokes << t }
323
+ invokes.must_equal [deleted]
324
+ end
325
+ end
326
+
327
+ describe "#on_destroy" do
328
+ let (:album) { Album.new }
329
+
330
+ # empty collection.
331
+ it do
332
+ invokes = []
333
+ Callback.new(twin.songs).on_destroy { |t| invokes << t }
334
+ invokes.must_equal []
335
+ end
336
+
337
+ # collection present but nothing deleted.
338
+ it do
339
+ ex_song = Song.create(title: "Run For Cover")
340
+ song = Song.new
341
+ album.songs = [ex_song, song]
342
+
343
+ Callback.new(twin.songs).on_destroy { |t| invokes << t }
344
+ invokes.must_equal []
345
+ end
346
+
347
+ # items deleted, doesn't trigger on_destroy.
348
+ it do
349
+ ex_song = Song.create(title: "Run For Cover")
350
+ song = Song.new
351
+ album.songs = [ex_song, song]
352
+
353
+ twin.songs.delete(deleted = twin.songs[0])
354
+
355
+ Callback.new(twin.songs).on_destroy { |t| invokes << t }
356
+ invokes.must_equal []
357
+ end
358
+
359
+ # items destroyed.
360
+ it do
361
+ ex_song = Song.create(title: "Run For Cover")
362
+ song = Song.new
363
+ album.songs = [ex_song, song]
364
+
365
+ twin.songs.destroy(deleted = twin.songs[0])
366
+
367
+ Callback.new(twin.songs).on_destroy { |t| invokes << t }
368
+ invokes.must_equal []
369
+
370
+ twin.extend(Disposable::Twin::Collection::Semantics) # now #save will destroy.
371
+ twin.save
372
+
373
+ # still shows the deleted after save.
374
+ invokes = []
375
+ Callback.new(twin.songs).on_destroy { |t| invokes << t }
376
+ invokes.must_equal [deleted]
377
+ end
378
+ end
379
+
380
+
381
+ describe "#on_change" do
382
+ let (:album) { Album.new }
383
+
384
+ # after initialization
385
+ it do
386
+ Callback.new(twin).on_change { |t| invokes << t }
387
+ invokes.must_equal []
388
+ end
389
+
390
+ # save, without any attributes changed. unpersisted before.
391
+ it do
392
+ twin = AlbumTwin.new(Album.create)
393
+
394
+ twin.save
395
+
396
+ Callback.new(twin).on_change { |t| invokes << t }
397
+ invokes.must_equal [] # nothing has changed, not even persisted?.
398
+ end
399
+
400
+ # save, without any attributes changed. persisted before.
401
+ it do
402
+ twin.save
403
+
404
+ Callback.new(twin).on_change { |t| invokes << t }
405
+ invokes.must_equal [twin]
406
+ end
407
+
408
+ # before and after save, with attributes changed
409
+ it do
410
+ # state change, but not persisted, yet.
411
+ twin.name = "Run For Cover"
412
+ invokes = []
413
+ Callback.new(twin).on_change { |t| invokes << t }
414
+ invokes.must_equal [twin]
415
+
416
+ twin.save
417
+
418
+ invokes = []
419
+ Callback.new(twin).on_change { |t| invokes << t }
420
+ invokes.must_equal [twin]
421
+ end
422
+
423
+ # for scalars: on_change(:email).
424
+ it do
425
+ Callback.new(twin).on_change(property: :name) { |t| invokes << t }
426
+ invokes.must_equal []
427
+
428
+ twin.name = "Unforgiven"
429
+
430
+ Callback.new(twin).on_change(property: :name) { |t| invokes << t }
431
+ invokes.must_equal [twin]
432
+ end
433
+
434
+ # for collections.
435
+ # it do
436
+ # album.songs << song1 = Song.new
437
+ # album.songs << Song.create(title: "Run For Cover")
438
+ # album.songs << song2 = Song.new
439
+ # invokes = []
440
+
441
+ # Callback.new(twin.songs).on_change { |t| invokes << t }
442
+ # invokes.must_equal []
443
+
444
+ # twin.save
445
+
446
+ # Callback.new(twin.songs).on_change { |t| invokes << t }
447
+ # invokes.must_equal [twin.songs[0], twin.songs[2]]
448
+ # end
449
+ end
450
+ end