copyable 0.0.1

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 (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