copyable 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +23 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +3 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +264 -0
  8. data/Rakefile +7 -0
  9. data/copyable.gemspec +28 -0
  10. data/lib/copyable.rb +32 -0
  11. data/lib/copyable/config.rb +19 -0
  12. data/lib/copyable/copy_registry.rb +50 -0
  13. data/lib/copyable/copyable_extension.rb +118 -0
  14. data/lib/copyable/declarations/after_copy.rb +15 -0
  15. data/lib/copyable/declarations/associations.rb +116 -0
  16. data/lib/copyable/declarations/columns.rb +34 -0
  17. data/lib/copyable/declarations/declaration.rb +15 -0
  18. data/lib/copyable/declarations/declarations.rb +14 -0
  19. data/lib/copyable/declarations/disable_all_callbacks_and_observers_except_validate.rb +9 -0
  20. data/lib/copyable/declarations/main.rb +32 -0
  21. data/lib/copyable/exceptions.rb +10 -0
  22. data/lib/copyable/model_hooks.rb +40 -0
  23. data/lib/copyable/option_checker.rb +23 -0
  24. data/lib/copyable/railtie.rb +9 -0
  25. data/lib/copyable/saver.rb +19 -0
  26. data/lib/copyable/single_copy_enforcer.rb +68 -0
  27. data/lib/copyable/syntax_checking/association_checker.rb +41 -0
  28. data/lib/copyable/syntax_checking/column_checker.rb +43 -0
  29. data/lib/copyable/syntax_checking/completeness_checker.rb +27 -0
  30. data/lib/copyable/syntax_checking/declaration_checker.rb +26 -0
  31. data/lib/copyable/syntax_checking/declaration_stubber.rb +18 -0
  32. data/lib/copyable/syntax_checking/syntax_checker.rb +15 -0
  33. data/lib/copyable/version.rb +3 -0
  34. data/lib/tasks/copyable.rake +55 -0
  35. data/spec/config_spec.rb +132 -0
  36. data/spec/copy_registry_spec.rb +55 -0
  37. data/spec/copyable_after_copy_spec.rb +28 -0
  38. data/spec/copyable_associations_spec.rb +366 -0
  39. data/spec/copyable_columns_spec.rb +116 -0
  40. data/spec/copyable_spec.rb +7 -0
  41. data/spec/create_copy_spec.rb +136 -0
  42. data/spec/deep_structure_copy_spec.rb +169 -0
  43. data/spec/helper/copyable_spec_helper.rb +15 -0
  44. data/spec/helper/test_models.rb +136 -0
  45. data/spec/helper/test_tables.rb +135 -0
  46. data/spec/model_hooks_spec.rb +66 -0
  47. data/spec/spec_helper.rb +29 -0
  48. data/spec/stress_test_spec.rb +261 -0
  49. data/spec/syntax_checking/association_checker_spec.rb +80 -0
  50. data/spec/syntax_checking/column_checker_spec.rb +49 -0
  51. data/spec/syntax_checking/declaration_checker_spec.rb +58 -0
  52. data/spec/syntax_checking_spec.rb +258 -0
  53. data/spec/transaction_spec.rb +78 -0
  54. metadata +200 -0
@@ -0,0 +1,55 @@
1
+ require_relative 'helper/copyable_spec_helper'
2
+
3
+ describe Copyable::CopyRegistry do
4
+ before(:all) do
5
+ DummyModelPerson = Struct.new(:id, :name)
6
+ DummyModelPlace = Struct.new(:id, :name)
7
+ end
8
+
9
+ before(:each) do
10
+ Copyable::CopyRegistry.clear
11
+ @bob = DummyModelPerson.new(10, "Bob")
12
+ @new_bob = DummyModelPerson.new(11, "Copy of Bob")
13
+ @fred = DummyModelPerson.new(15, "Fred")
14
+ @new_fred = DummyModelPerson.new(16, "Copy of Fred")
15
+ @winnipeg = DummyModelPlace.new(89, "Winnipeg")
16
+ @new_winnipeg = DummyModelPlace.new(90, "Copy of Winnipeg")
17
+ @yellowknife = DummyModelPlace.new(10, "Yellowknife")
18
+ @new_yellowknife = DummyModelPlace.new(11, "Copy of Yellowknife")
19
+ end
20
+
21
+ it 'should know whether a model has already been copied' do
22
+ registry = Copyable::CopyRegistry
23
+ registry.register(@bob, @new_bob)
24
+ registry.register(@fred, @new_fred)
25
+ registry.register(@yellowknife, @new_yellowknife)
26
+ # using record option
27
+ expect(registry.already_copied?(record: @bob)).to be_truthy
28
+ expect(registry.already_copied?(record: @fred)).to be_truthy
29
+ expect(registry.already_copied?(record: @winnipeg)).to be_falsey
30
+ expect(registry.already_copied?(record: @yellowknife)).to be_truthy
31
+ # using id and class options
32
+ expect(registry.already_copied?(id: 10, class: DummyModelPerson)).to be_truthy
33
+ expect(registry.already_copied?(id: 15, class: DummyModelPerson)).to be_truthy
34
+ expect(registry.already_copied?(id: 89, class: DummyModelPlace)).to be_falsey
35
+ expect(registry.already_copied?(id: 10, class: DummyModelPlace)).to be_truthy
36
+ end
37
+
38
+ it 'should provide the copy of a model that has already been copied' do
39
+ registry = Copyable::CopyRegistry
40
+ registry.register(@bob, @new_bob)
41
+ registry.register(@fred, @new_fred)
42
+ registry.register(@winnipeg, @new_winnipeg)
43
+ registry.register(@yellowknife, @new_yellowknife)
44
+ # using record option
45
+ expect(registry.fetch_copy(record: @bob)).to eq(@new_bob)
46
+ expect(registry.fetch_copy(record: @fred)).to eq(@new_fred)
47
+ expect(registry.fetch_copy(record: @winnipeg)).to eq(@new_winnipeg)
48
+ expect(registry.fetch_copy(record: @yellowknife)).to eq(@new_yellowknife)
49
+ # using id and class options
50
+ expect(registry.fetch_copy(id: 10, class: DummyModelPerson)).to eq(@new_bob)
51
+ expect(registry.fetch_copy(id: 15, class: DummyModelPerson)).to eq(@new_fred)
52
+ expect(registry.fetch_copy(id: 89, class: DummyModelPlace)).to eq(@new_winnipeg)
53
+ expect(registry.fetch_copy(id: 10, class: DummyModelPlace)).to eq(@new_yellowknife)
54
+ end
55
+ end
@@ -0,0 +1,28 @@
1
+ require_relative 'helper/copyable_spec_helper'
2
+
3
+ describe 'copyable:after_copy' do
4
+ before(:each) do
5
+ undefine_copyable_in CopyableCoin
6
+ class CopyableCoin < ActiveRecord::Base
7
+ copyable do
8
+ disable_all_callbacks_and_observers_except_validate
9
+ columns({
10
+ kind: lambda { |orig| "Copy of #{orig.kind}" },
11
+ year: :copy,
12
+ })
13
+ associations({
14
+ })
15
+ after_copy do |original_model, new_model|
16
+ raise "#{original_model.kind} #{new_model.kind}"
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ it 'should execute the after_copy block after copying' do
23
+ coin = CopyableCoin.create!(kind: 'nickel', year: 1883)
24
+ expect {
25
+ coin.create_copy!
26
+ }.to raise_error(RuntimeError, "nickel Copy of nickel")
27
+ end
28
+ end
@@ -0,0 +1,366 @@
1
+ require_relative 'helper/copyable_spec_helper'
2
+
3
+ describe 'copyable:associations' do
4
+
5
+ context 'copying a has many relationship' do
6
+ before(:each) do
7
+ undefine_copyable_in CopyablePetSitter
8
+ class CopyablePetSitter < ActiveRecord::Base
9
+ copyable do
10
+ disable_all_callbacks_and_observers_except_validate
11
+ columns({
12
+ name: :copy,
13
+ })
14
+ associations({
15
+ copyable_pet_sitting_patronages: :copy,
16
+ })
17
+ end
18
+ end
19
+ undefine_copyable_in CopyablePetSittingPatronage
20
+ class CopyablePetSittingPatronage < ActiveRecord::Base
21
+ copyable do
22
+ disable_all_callbacks_and_observers_except_validate
23
+ columns({
24
+ copyable_pet_id: :copy,
25
+ copyable_pet_sitter_id: :copy,
26
+ })
27
+ associations({
28
+ })
29
+ end
30
+ end
31
+ end
32
+ it 'should produce copies of the associated records' do
33
+ pet1 = CopyablePet.create!(name: 'Pet1', kind: 'cat', birth_year: 2010)
34
+ pet2 = CopyablePet.create!(name: 'Pet2', kind: 'cat', birth_year: 2011)
35
+ pet3 = CopyablePet.create!(name: 'Pet3', kind: 'cat', birth_year: 2012)
36
+ sitter1 = CopyablePetSitter.create!(name: 'Sitter1')
37
+ sitter2 = CopyablePetSitter.create!(name: 'Sitter2')
38
+ sitter3 = CopyablePetSitter.create!(name: 'Sitter3')
39
+ sitter4 = CopyablePetSitter.create!(name: 'Sitter4')
40
+ patronage1 = CopyablePetSittingPatronage.create!(
41
+ copyable_pet_id: pet1.id,
42
+ copyable_pet_sitter_id: sitter2.id)
43
+ patronage2 = CopyablePetSittingPatronage.create!(
44
+ copyable_pet_id: pet2.id,
45
+ copyable_pet_sitter_id: sitter2.id)
46
+ patronage3 = CopyablePetSittingPatronage.create!(
47
+ copyable_pet_id: pet3.id,
48
+ copyable_pet_sitter_id: sitter4.id)
49
+ sitter2.reload
50
+ expect(sitter2.copyable_pet_sitting_patronages.size).to eq(2)
51
+ copied_sitter = sitter2.create_copy!
52
+ expect(copied_sitter.copyable_pet_sitting_patronages.size).to eq(2)
53
+ expect(copied_sitter.copyable_pets.map(&:name)).to match_array(['Pet1', 'Pet2'])
54
+ end
55
+ end
56
+
57
+ context 'do not copy' do
58
+ before(:each) do
59
+ undefine_copyable_in CopyablePetSitter
60
+ class CopyablePetSitter < ActiveRecord::Base
61
+ copyable do
62
+ disable_all_callbacks_and_observers_except_validate
63
+ columns({
64
+ name: :copy,
65
+ })
66
+ associations({
67
+ copyable_pet_sitting_patronages: :do_not_copy,
68
+ })
69
+ end
70
+ end
71
+ undefine_copyable_in CopyablePetSittingPatronage
72
+ class CopyablePetSittingPatronage < ActiveRecord::Base
73
+ copyable do
74
+ disable_all_callbacks_and_observers_except_validate
75
+ columns({
76
+ copyable_pet_id: :copy,
77
+ copyable_pet_sitter_id: :copy,
78
+ })
79
+ associations({
80
+ })
81
+ end
82
+ end
83
+ end
84
+ it 'should not produce copies of the associated records' do
85
+ pet1 = CopyablePet.create!(name: 'Pet1', kind: 'cat', birth_year: 2010)
86
+ pet2 = CopyablePet.create!(name: 'Pet2', kind: 'cat', birth_year: 2011)
87
+ pet3 = CopyablePet.create!(name: 'Pet3', kind: 'cat', birth_year: 2012)
88
+ sitter1 = CopyablePetSitter.create!(name: 'Sitter1')
89
+ sitter2 = CopyablePetSitter.create!(name: 'Sitter2')
90
+ sitter3 = CopyablePetSitter.create!(name: 'Sitter3')
91
+ sitter4 = CopyablePetSitter.create!(name: 'Sitter4')
92
+ patronage1 = CopyablePetSittingPatronage.create!(
93
+ copyable_pet_id: pet1.id,
94
+ copyable_pet_sitter_id: sitter2.id)
95
+ patronage2 = CopyablePetSittingPatronage.create!(
96
+ copyable_pet_id: pet2.id,
97
+ copyable_pet_sitter_id: sitter2.id)
98
+ patronage3 = CopyablePetSittingPatronage.create!(
99
+ copyable_pet_id: pet3.id,
100
+ copyable_pet_sitter_id: sitter4.id)
101
+ sitter2.reload
102
+ expect(sitter2.copyable_pet_sitting_patronages.size).to eq(2)
103
+ expect(CopyablePetSittingPatronage.count).to eq(3)
104
+ copied_sitter = sitter2.create_copy!
105
+ expect(copied_sitter.copyable_pet_sitting_patronages.size).to eq(0)
106
+ expect(CopyablePetSittingPatronage.count).to eq(3)
107
+ end
108
+ end
109
+
110
+ context 'copying a has one relationship' do
111
+ before(:each) do
112
+ undefine_copyable_in CopyablePetProfile
113
+ class CopyablePetProfile < ActiveRecord::Base
114
+ copyable do
115
+ disable_all_callbacks_and_observers_except_validate
116
+ columns({
117
+ description: :copy,
118
+ nickname: :copy,
119
+ copyable_pet_id: lambda { |orig| 177777 }, # random bogus CopyablePet id since we don't need a real CopyablePet record for this test
120
+ })
121
+ associations({
122
+ copyable_address: :copy,
123
+ })
124
+ end
125
+ end
126
+ undefine_copyable_in CopyableAddress
127
+ class CopyableAddress < ActiveRecord::Base
128
+ copyable do
129
+ disable_all_callbacks_and_observers_except_validate
130
+ columns({
131
+ address1: :copy,
132
+ address2: :copy,
133
+ city: :copy,
134
+ state: :copy,
135
+ copyable_pet_profile_id: :copy,
136
+ })
137
+ associations({
138
+ })
139
+ end
140
+ end
141
+ end
142
+
143
+ it 'should produce a copy of the associated record' do
144
+ profile1 = CopyablePetProfile.create!(description: 'Prof1',
145
+ copyable_pet_id: 177777)
146
+ profile2 = CopyablePetProfile.create!(description: 'Prof2',
147
+ copyable_pet_id: 188888)
148
+ profile3 = CopyablePetProfile.create!(description: 'Prof3',
149
+ copyable_pet_id: 199999)
150
+ address1 = CopyableAddress.create!(address1: 'Add1',
151
+ city: 'Cambridge',
152
+ state: 'MA',
153
+ copyable_pet_profile_id: profile1.id)
154
+ address2 = CopyableAddress.create!(address1: 'Add2',
155
+ city: 'Boston',
156
+ state: 'MA',
157
+ copyable_pet_profile_id: profile2.id)
158
+ address3 = CopyableAddress.create!(address1: 'Add3',
159
+ city: 'Somerville',
160
+ state: 'MA',
161
+ copyable_pet_profile_id: profile3.id)
162
+ expect(CopyablePetProfile.count).to eq(3)
163
+ expect(CopyableAddress.count).to eq(3)
164
+ copied_profile = profile1.create_copy!
165
+ expect(CopyablePetProfile.count).to eq(4)
166
+ expect(CopyableAddress.count).to eq(4)
167
+ expect(copied_profile.copyable_address.address1).to eq('Add1')
168
+ expect(copied_profile.copyable_address.city).to eq('Cambridge')
169
+ end
170
+
171
+ it 'should not copy the associated record if it is nil' do
172
+ profile1 = CopyablePetProfile.create!(description: 'Prof1',
173
+ copyable_pet_id: 56565656) # bogus id
174
+ expect(CopyablePetProfile.count).to eq(1)
175
+ expect(CopyableAddress.count).to eq(0)
176
+ copied_profile = profile1.create_copy!
177
+ expect(CopyablePetProfile.count).to eq(2)
178
+ expect(CopyableAddress.count).to eq(0)
179
+ expect(copied_profile.copyable_address).to be_nil
180
+ end
181
+ end
182
+
183
+ context 'copying a has and belongs to many relationship' do
184
+ before(:each) do
185
+ undefine_copyable_in CopyablePetFood
186
+ class CopyablePetFood < ActiveRecord::Base
187
+ copyable do
188
+ disable_all_callbacks_and_observers_except_validate
189
+ columns({
190
+ name: :copy,
191
+ })
192
+ associations({
193
+ copyable_pets: :copy_only_habtm_join_records,
194
+ })
195
+ end
196
+ end
197
+ end
198
+ it 'should produce a copy of the associations but not the records' do
199
+ pet1 = CopyablePet.create!(name: 'Pet1', kind: 'cat', birth_year: 2010)
200
+ pet2 = CopyablePet.create!(name: 'Pet2', kind: 'cat', birth_year: 2011)
201
+ pet3 = CopyablePet.create!(name: 'Pet3', kind: 'cat', birth_year: 2012)
202
+ food1 = CopyablePetFood.create!(name: 'Food1')
203
+ food2 = CopyablePetFood.create!(name: 'Food2')
204
+ food3 = CopyablePetFood.create!(name: 'Food3')
205
+ food4 = CopyablePetFood.create!(name: 'Food4')
206
+ food1.copyable_pets << pet1
207
+ food1.copyable_pets << pet2
208
+ food3.copyable_pets << pet3
209
+ copied_food1 = food1.create_copy!
210
+ copied_food2 = food2.create_copy!
211
+ copied_food3 = food3.create_copy!
212
+ expect(copied_food1.copyable_pets.map(&:name)).to match_array(['Pet1', 'Pet2'])
213
+ expect(copied_food2.copyable_pets.map(&:name)).to match_array([])
214
+ expect(copied_food3.copyable_pets.map(&:name)).to match_array(['Pet3'])
215
+ expect(pet1.copyable_pet_foods.size).to eq(2)
216
+ expect(CopyablePet.count).to eq(3)
217
+ end
218
+ end
219
+
220
+ context 'copying a polymorphic has many' do
221
+ before(:each) do
222
+ undefine_copyable_in CopyableProduct
223
+ class CopyableProduct < ActiveRecord::Base
224
+ copyable do
225
+ disable_all_callbacks_and_observers_except_validate
226
+ columns({
227
+ name: :copy,
228
+ })
229
+ associations({
230
+ copyable_pictures: :copy,
231
+ })
232
+ end
233
+ end
234
+ undefine_copyable_in CopyablePicture
235
+ class CopyablePicture < ActiveRecord::Base
236
+ copyable do
237
+ disable_all_callbacks_and_observers_except_validate
238
+ columns({
239
+ name: :copy,
240
+ imageable_id: :copy,
241
+ imageable_type: :copy,
242
+ picture_album_id: :copy,
243
+ })
244
+ associations({
245
+ })
246
+ end
247
+ end
248
+ end
249
+ it 'should produce a copy of the associations' do
250
+ product1 = CopyableProduct.create!(name: 'camera')
251
+ picture1 = CopyablePicture.create!(name: 'photo1')
252
+ picture2 = CopyablePicture.create!(name: 'photo2')
253
+ product1.copyable_pictures << picture1
254
+ product1.copyable_pictures << picture2
255
+ expect(CopyableProduct.count).to eq(1)
256
+ expect(CopyablePicture.count).to eq(2)
257
+ copied_product = product1.create_copy!
258
+ expect(CopyableProduct.count).to eq(2)
259
+ expect(CopyablePicture.count).to eq(4)
260
+ expect(copied_product.copyable_pictures.size).to eq(2)
261
+ expect(copied_product.copyable_pictures.map(&:id)).not_to match_array(product1.copyable_pictures.map(&:id))
262
+ end
263
+ end
264
+
265
+ context 'with a custom foreign key' do
266
+ before(:each) do
267
+ undefine_copyable_in CopyablePicture
268
+ class CopyablePicture < ActiveRecord::Base
269
+ copyable do
270
+ disable_all_callbacks_and_observers_except_validate
271
+ columns({
272
+ name: :copy,
273
+ imageable_id: :copy,
274
+ imageable_type: :copy,
275
+ picture_album_id: :copy,
276
+ })
277
+ associations({
278
+ })
279
+ end
280
+ end
281
+ undefine_copyable_in CopyableAlbum
282
+ class CopyableAlbum < ActiveRecord::Base
283
+ copyable do
284
+ disable_all_callbacks_and_observers_except_validate
285
+ columns({
286
+ name: :copy,
287
+ })
288
+ associations({
289
+ copyable_pictures: :copy,
290
+ })
291
+ end
292
+ end
293
+ end
294
+
295
+ it 'should produce copies of the associated records without an error' do
296
+ album = CopyableAlbum.create!(name: 'an album')
297
+ picture1 = CopyablePicture.create!(name: 'photo1')
298
+ picture2 = CopyablePicture.create!(name: 'photo2', copyable_album: album)
299
+ picture3 = CopyablePicture.create!(name: 'photo3', copyable_album: album)
300
+ expect(CopyableAlbum.count).to eq(1)
301
+ expect(CopyablePicture.count).to eq(3)
302
+ copied_album = album.create_copy!
303
+ expect(CopyableAlbum.count).to eq(2)
304
+ expect(CopyablePicture.count).to eq(5)
305
+ expect(copied_album.copyable_pictures.map(&:name)).to match_array(['photo2', 'photo3'])
306
+ end
307
+
308
+ it 'should update polymorphic belongs_to associations correctly' do
309
+ # This tests how the update_other_belongs_to_associations method handles
310
+ # polymorphic assocations. Even though the steps of this example may be
311
+ # similar (or exactly the same) as another example, the behavior that this
312
+ # example describes is different and is a significant part of the
313
+ # correctness of the software.
314
+ album = CopyableAlbum.create!(name: 'an album')
315
+ picture1 = CopyablePicture.create!(name: 'photo1')
316
+ picture2 = CopyablePicture.create!(name: 'photo2', copyable_album: album)
317
+ picture3 = CopyablePicture.create!(name: 'photo3', copyable_album: album)
318
+ expect(CopyableAlbum.count).to eq(1)
319
+ expect(CopyablePicture.count).to eq(3)
320
+ copied_album = album.create_copy!
321
+ expect(CopyableAlbum.count).to eq(2)
322
+ expect(CopyablePicture.count).to eq(5)
323
+ expect(copied_album.copyable_pictures.map(&:name)).to match_array(['photo2', 'photo3'])
324
+ end
325
+ end
326
+
327
+ context 'copying a has many relationship with a missing copyable declaration' do
328
+ before(:each) do
329
+ undefine_copyable_in CopyablePetSitter
330
+ class CopyablePetSitter < ActiveRecord::Base
331
+ copyable do
332
+ disable_all_callbacks_and_observers_except_validate
333
+ columns({
334
+ name: :copy,
335
+ })
336
+ associations({
337
+ copyable_pet_sitting_patronages: :copy,
338
+ })
339
+ end
340
+ end
341
+ undefine_copyable_in CopyablePetSittingPatronage
342
+ class CopyablePetSittingPatronage < ActiveRecord::Base
343
+ undef create_copy!
344
+ # MISSING copyable do
345
+ # disable_all_callbacks_and_observers_except_validate
346
+ # columns({
347
+ # copyable_pet_id: :copy,
348
+ # copyable_pet_sitter_id: :copy,
349
+ # })
350
+ # associations({
351
+ # })
352
+ # end
353
+ end
354
+ end
355
+ it 'should raise an informative error' do
356
+ pet1 = CopyablePet.create!(name: 'Pet1', kind: 'cat', birth_year: 2010)
357
+ sitter1 = CopyablePetSitter.create!(name: 'Sitter1')
358
+ patronage1 = CopyablePetSittingPatronage.create!(
359
+ copyable_pet_id: pet1.id,
360
+ copyable_pet_sitter_id: sitter1.id)
361
+ expect {
362
+ sitter1.create_copy!
363
+ }.to raise_error(Copyable::CopyableError)
364
+ end
365
+ end
366
+ end