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.
- checksums.yaml +4 -4
- data/.travis.yml +2 -5
- data/CHANGES.md +4 -0
- data/Gemfile +1 -1
- data/README.md +154 -1
- data/database.sqlite3 +0 -0
- data/disposable.gemspec +7 -7
- data/gemfiles/Gemfile.rails-3.0.lock +10 -8
- data/gemfiles/Gemfile.rails-3.2.lock +9 -7
- data/gemfiles/Gemfile.rails-4.0.lock +9 -7
- data/gemfiles/Gemfile.rails-4.1.lock +10 -8
- data/lib/disposable.rb +6 -7
- data/lib/disposable/callback.rb +174 -0
- data/lib/disposable/composition.rb +21 -58
- data/lib/disposable/expose.rb +49 -0
- data/lib/disposable/twin.rb +85 -38
- data/lib/disposable/twin/builder.rb +12 -30
- data/lib/disposable/twin/changed.rb +50 -0
- data/lib/disposable/twin/collection.rb +95 -0
- data/lib/disposable/twin/composition.rb +43 -15
- data/lib/disposable/twin/option.rb +1 -1
- data/lib/disposable/twin/persisted.rb +20 -0
- data/lib/disposable/twin/property_processor.rb +29 -0
- data/lib/disposable/twin/representer.rb +42 -14
- data/lib/disposable/twin/save.rb +19 -34
- data/lib/disposable/twin/schema.rb +31 -0
- data/lib/disposable/twin/setup.rb +38 -0
- data/lib/disposable/twin/sync.rb +114 -0
- data/lib/disposable/version.rb +1 -1
- data/test/api_semantics_test.rb +263 -0
- data/test/callback_group_test.rb +222 -0
- data/test/callbacks_test.rb +450 -0
- data/test/example.rb +40 -0
- data/test/expose_test.rb +92 -0
- data/test/persisted_test.rb +101 -0
- data/test/test_helper.rb +64 -0
- data/test/twin/benchmarking.rb +33 -0
- data/test/twin/builder_test.rb +32 -0
- data/test/twin/changed_test.rb +108 -0
- data/test/twin/collection_test.rb +223 -0
- data/test/twin/composition_test.rb +56 -25
- data/test/twin/expose_test.rb +73 -0
- data/test/twin/feature_test.rb +61 -0
- data/test/twin/from_test.rb +37 -0
- data/test/twin/inherit_test.rb +57 -0
- data/test/twin/option_test.rb +27 -0
- data/test/twin/readable_test.rb +57 -0
- data/test/twin/save_test.rb +192 -0
- data/test/twin/schema_test.rb +69 -0
- data/test/twin/setup_test.rb +139 -0
- data/test/twin/skip_unchanged_test.rb +64 -0
- data/test/twin/struct_test.rb +168 -0
- data/test/twin/sync_option_test.rb +228 -0
- data/test/twin/sync_test.rb +128 -0
- data/test/twin/twin_test.rb +49 -128
- data/test/twin/writeable_test.rb +56 -0
- metadata +106 -20
- data/STUFF +0 -4
- data/lib/disposable/twin/finders.rb +0 -29
- data/lib/disposable/twin/new.rb +0 -30
- data/lib/disposable/twin/save_.rb +0 -21
- 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
|