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,116 @@
1
+ require_relative 'helper/copyable_spec_helper'
2
+
3
+ describe 'copyable:columns' do
4
+ context 'when asked to do a simple column copy' do
5
+ include ActiveSupport::Testing::TimeHelpers
6
+
7
+ before(:each) do
8
+ undefine_copyable_in CopyableCoin
9
+ class CopyableCoin < ActiveRecord::Base
10
+ copyable do
11
+ disable_all_callbacks_and_observers_except_validate
12
+ columns({
13
+ kind: :copy,
14
+ year: :copy,
15
+ })
16
+ associations({
17
+ })
18
+ end
19
+ end
20
+ end
21
+
22
+ it 'should create a new record with matching columns' do
23
+ expect(CopyableCoin.count).to eq(0)
24
+ coin = CopyableCoin.create!(kind: 'cent', year: 1943)
25
+ coin.create_copy!
26
+ expect(CopyableCoin.where(kind: 'cent').count).to eq(2)
27
+ expect(CopyableCoin.where(year: 1943).count).to eq(2)
28
+ end
29
+
30
+ it 'should update timestamp columns' do
31
+ expect(CopyableCoin.count).to eq(0)
32
+ coin = CopyableCoin.create!(kind: 'cent', year: 1943)
33
+ travel_to 2.seconds.from_now do
34
+ coin_copy = coin.create_copy!
35
+ expect(coin_copy.updated_at).to be_present
36
+ expect(coin_copy.created_at).to be_present
37
+ expect(coin_copy.updated_at).not_to eq(coin.updated_at)
38
+ expect(coin_copy.created_at).not_to eq(coin.created_at)
39
+ end
40
+ end
41
+ end
42
+
43
+ context 'when asked to do a column copy with customization' do
44
+ before(:each) do
45
+ undefine_copyable_in CopyableCoin
46
+ class CopyableCoin < ActiveRecord::Base
47
+ copyable do
48
+ disable_all_callbacks_and_observers_except_validate
49
+ columns({
50
+ kind: lambda { |orig| "Copy of #{orig.kind}" },
51
+ year: :copy,
52
+ })
53
+ associations({
54
+ })
55
+ end
56
+ end
57
+ end
58
+ describe '#create_copy!' do
59
+ it 'should create a new record with the customized column' do
60
+ expect(CopyableCoin.count).to eq(0)
61
+ coin = CopyableCoin.create!(kind: 'quarter', year: 1964)
62
+ coin.create_copy!
63
+ expect(CopyableCoin.where(kind: 'quarter').count).to eq(1)
64
+ expect(CopyableCoin.where(kind: 'Copy of quarter').count).to eq(1)
65
+ expect(CopyableCoin.where(year: 1964).count).to eq(2)
66
+ end
67
+ end
68
+ end
69
+
70
+ context 'when asked to do a copy that produces invalid data' do
71
+ before(:each) do
72
+ undefine_copyable_in CopyableCoin
73
+ class CopyableCoin < ActiveRecord::Base
74
+ copyable do
75
+ disable_all_callbacks_and_observers_except_validate
76
+ columns({
77
+ kind: :copy,
78
+ year: lambda { |orig| -444 },
79
+ })
80
+ associations({
81
+ })
82
+ end
83
+ end
84
+ end
85
+ it 'should raise an error if data is not valid' do
86
+ expect(CopyableCoin.count).to eq(0)
87
+ coin = CopyableCoin.create!(kind: 'cent', year: 1982)
88
+ expect {
89
+ coin.create_copy!
90
+ }.to raise_error(ActiveRecord::RecordInvalid)
91
+ end
92
+ end
93
+
94
+ context 'with :do_not_copy advice' do
95
+ before(:each) do
96
+ undefine_copyable_in CopyableCoin
97
+ class CopyableCoin < ActiveRecord::Base
98
+ copyable do
99
+ disable_all_callbacks_and_observers_except_validate
100
+ columns({
101
+ kind: :do_not_copy,
102
+ year: :copy,
103
+ })
104
+ associations({
105
+ })
106
+ end
107
+ end
108
+ end
109
+ it 'should result in a nil value for that column' do
110
+ expect(CopyableCoin.count).to eq(0)
111
+ coin = CopyableCoin.create!(kind: 'cent', year: 1982)
112
+ copied_coin = coin.create_copy!
113
+ expect(copied_coin.kind).to be_nil
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe Copyable do
4
+ it 'has a version number' do
5
+ expect(Copyable::VERSION).not_to be nil
6
+ end
7
+ end
@@ -0,0 +1,136 @@
1
+ require_relative 'helper/copyable_spec_helper'
2
+
3
+ describe 'create_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: :copy,
11
+ year: :copy,
12
+ })
13
+ associations({
14
+ })
15
+ end
16
+ end
17
+ end
18
+
19
+ it 'should exist' do
20
+ coin = CopyableCoin.create!(kind: 'cent', year: 1909)
21
+ expect(coin).to respond_to(:create_copy!)
22
+ end
23
+
24
+ it 'should create a new record' do
25
+ expect(CopyableCoin.count).to eq(0)
26
+ coin = CopyableCoin.create!(kind: 'cent', year: 1909)
27
+ coin.create_copy!
28
+ expect(CopyableCoin.count).to eq(2)
29
+ end
30
+
31
+ it 'should return the new record' do
32
+ expect(CopyableCoin.count).to eq(0)
33
+ coin = CopyableCoin.create!(kind: 'cent', year: 1909)
34
+ coin2 = coin.create_copy!
35
+ expect(coin2).to be_a CopyableCoin
36
+ end
37
+
38
+ it 'should return the new record having an id' do
39
+ expect(CopyableCoin.count).to eq(0)
40
+ coin = CopyableCoin.create!(kind: 'cent', year: 1909)
41
+ coin2 = coin.create_copy!
42
+ expect(coin2.id).not_to be_nil
43
+ end
44
+
45
+ it 'should allow columns to be overridden' do
46
+ expect(CopyableCoin.count).to eq(0)
47
+ coin = CopyableCoin.create!(kind: 'cent', year: 1909)
48
+ coin2 = coin.create_copy!(override: { year: 1959 })
49
+ expect(coin2.kind).to eq('cent')
50
+ expect(coin2.year).to eq(1959)
51
+ end
52
+
53
+ it 'should allow columns to be overridden using strings as column names' do
54
+ expect(CopyableCoin.count).to eq(0)
55
+ coin = CopyableCoin.create!('kind' => 'cent', 'year' => 1909)
56
+ coin2 = coin.create_copy!(override: { 'year' => 1959 })
57
+ expect(coin2.kind).to eq('cent')
58
+ expect(coin2.year).to eq(1959)
59
+ end
60
+
61
+ it 'should allow validations to be skipped' do
62
+ coin = CopyableCoin.create!('kind' => 'cent', 'year' => 1909)
63
+ expect {
64
+ coin.create_copy!(override: { 'year' => 2 })
65
+ }.to raise_error(ActiveRecord::RecordInvalid)
66
+ expect {
67
+ coin.create_copy!(override: { 'year' => 2 }, skip_validations: true)
68
+ }.to_not raise_error
69
+ end
70
+
71
+ it 'should skip validations even in associated objects when skip_validations is true' do
72
+ # set up classes
73
+ undefine_copyable_in CopyablePet
74
+ class CopyablePet < ActiveRecord::Base
75
+ copyable do
76
+ disable_all_callbacks_and_observers_except_validate
77
+ columns({
78
+ name: :copy,
79
+ kind: :copy,
80
+ birth_year: :copy,
81
+ })
82
+ associations({
83
+ copyable_toys: :copy,
84
+ copyable_pet_tag: :do_not_copy,
85
+ copyable_pet_profile: :do_not_copy,
86
+ copyable_pet_foods: :do_not_copy,
87
+ copyable_pet_sitting_patronages: :do_not_copy,
88
+ })
89
+ end
90
+ end
91
+
92
+ undefine_copyable_in CopyableToy
93
+ class CopyableToy < ActiveRecord::Base
94
+ copyable do
95
+ disable_all_callbacks_and_observers_except_validate
96
+ columns({
97
+ name: :copy,
98
+ kind: :copy,
99
+ copyable_pet_id: :copy,
100
+ })
101
+ associations({
102
+ })
103
+ end
104
+ end
105
+
106
+ # set up objects
107
+ fido = CopyablePet.create!(
108
+ name: 'Fido',
109
+ kind: 'canine',
110
+ birth_year: 2005)
111
+
112
+ pillow = CopyableToy.create!(
113
+ name: 'favorite pillow',
114
+ kind: 'soft',
115
+ copyable_pet: fido)
116
+
117
+ # everything should be wonderful
118
+ expect {
119
+ fido.create_copy!
120
+ }.to_not raise_error
121
+
122
+ # now inject some invalid data in an associated object
123
+ pillow.update_column(:name, 'o')
124
+ fido.reload
125
+
126
+ # fail because invalid!
127
+ expect {
128
+ fido.create_copy!
129
+ }.to raise_error(ActiveRecord::RecordInvalid)
130
+
131
+ # oh quit complaning and do it anyway!
132
+ expect {
133
+ fido.create_copy!(skip_validations: true)
134
+ }.to_not raise_error
135
+ end
136
+ end
@@ -0,0 +1,169 @@
1
+ require_relative 'helper/copyable_spec_helper'
2
+
3
+ describe 'complex model hierarchies:' do
4
+ before(:each) do
5
+ undefine_copyable_in CopyableOwner
6
+ class CopyableOwner < ActiveRecord::Base
7
+ copyable do
8
+ disable_all_callbacks_and_observers_except_validate
9
+ columns({
10
+ name: :copy,
11
+ })
12
+ associations({
13
+ copyable_vehicles: :copy,
14
+ copyable_amenities: :copy,
15
+ })
16
+ end
17
+ end
18
+ undefine_copyable_in CopyableVehicle
19
+ class CopyableVehicle < ActiveRecord::Base
20
+ copyable do
21
+ disable_all_callbacks_and_observers_except_validate
22
+ columns({
23
+ name: :copy,
24
+ copyable_owner_id: :copy,
25
+ })
26
+ associations({
27
+ copyable_amenities: :copy,
28
+ })
29
+ end
30
+ end
31
+ undefine_copyable_in CopyableAmenity
32
+ class CopyableAmenity < ActiveRecord::Base
33
+ copyable do
34
+ disable_all_callbacks_and_observers_except_validate
35
+ columns({
36
+ name: :copy,
37
+ copyable_vehicle_id: :copy,
38
+ copyable_owner_id: :copy,
39
+ })
40
+ associations({
41
+ copyable_warranty: :copy,
42
+ })
43
+ end
44
+ end
45
+ undefine_copyable_in CopyableWarranty
46
+ class CopyableWarranty < ActiveRecord::Base
47
+ copyable do
48
+ disable_all_callbacks_and_observers_except_validate
49
+ columns({
50
+ name: :copy,
51
+ copyable_amenity_id: :copy,
52
+ })
53
+ associations({
54
+ })
55
+ end
56
+ end
57
+ end
58
+
59
+ describe 'a tree structure' do
60
+ before(:each) do
61
+ # We are avoiding using owners to avoid the denormalized aspect
62
+ # of the data model and keep this a simple tree.
63
+ @vehicle1 = CopyableVehicle.create!(name: 'Corvette')
64
+
65
+ @amenity1 = CopyableAmenity.create!(name: 'moon roof', copyable_vehicle: @vehicle1)
66
+ @warranty1 = CopyableWarranty.create!(name: 'moon roof warranty', copyable_amenity: @amenity1)
67
+ @amenity2 = CopyableAmenity.create!(name: 'twitter', copyable_vehicle: @vehicle1)
68
+ @warranty2 = CopyableWarranty.create!(name: 'twitter warranty', copyable_amenity: @amenity2)
69
+ @amenity3 = CopyableAmenity.create!(name: 'jazz', copyable_vehicle: @vehicle1)
70
+ @warranty3 = CopyableWarranty.create!(name: 'jazz warranty', copyable_amenity: @amenity3)
71
+ end
72
+
73
+ it 'should copy the entire tree as directed' do
74
+ @vehicle2 = @vehicle1.create_copy!
75
+ expect(CopyableVehicle.count).to eq(2)
76
+ expect(CopyableAmenity.count).to eq(6)
77
+ expect(CopyableWarranty.count).to eq(6)
78
+ expect(@vehicle2.name).to eq('Corvette')
79
+ expect(@vehicle2.copyable_amenities.map(&:name)).to match_array(
80
+ ['moon roof', 'twitter', 'jazz'])
81
+ expect(@vehicle2.copyable_amenities.map(&:copyable_warranty).map(&:name)).to match_array(
82
+ ['moon roof warranty', 'twitter warranty', 'jazz warranty'])
83
+ end
84
+
85
+ it 'should create the expected records if copied multiple times' do
86
+ # this test makes sure the SingleCopyEnforcer isn't too eager
87
+ @vehicle1.create_copy!
88
+ expect(CopyableVehicle.count).to eq(2)
89
+ expect(CopyableAmenity.count).to eq(6)
90
+ expect(CopyableWarranty.count).to eq(6)
91
+ @vehicle1.create_copy!
92
+ expect(CopyableVehicle.count).to eq(3)
93
+ expect(CopyableAmenity.count).to eq(9)
94
+ expect(CopyableWarranty.count).to eq(9)
95
+ @vehicle1.create_copy!
96
+ expect(CopyableVehicle.count).to eq(4)
97
+ expect(CopyableAmenity.count).to eq(12)
98
+ expect(CopyableWarranty.count).to eq(12)
99
+ end
100
+ end
101
+
102
+ describe 'a denormalized structure' do
103
+ context 'having one redundant association' do
104
+ before(:each) do
105
+ @joe = CopyableOwner.create!(name: 'Joe')
106
+ @porsche = CopyableVehicle.create!(name: 'Porsche', copyable_owner: @joe)
107
+ @moon_roof = CopyableAmenity.create!(name: 'moon roof',
108
+ copyable_vehicle: @porsche,
109
+ copyable_owner: @joe)
110
+ end
111
+
112
+ it 'should copy the records without copying any given record more than once' do
113
+ @copy_of_joe = @joe.create_copy!
114
+ expect(CopyableOwner.count).to eq(2)
115
+ expect(CopyableVehicle.count).to eq(2)
116
+ expect(CopyableAmenity.count).to eq(2)
117
+ end
118
+ end
119
+
120
+ context 'with many models, some having redundant associations' do
121
+ before(:each) do
122
+ @joe = CopyableOwner.create!(name: 'Joe')
123
+
124
+ @porsche = CopyableVehicle.create!(name: 'Porsche', copyable_owner: @joe)
125
+ @moon_roof = CopyableAmenity.create!(
126
+ name: 'moon roof',
127
+ copyable_vehicle: @porsche,
128
+ copyable_owner: @joe)
129
+
130
+ @mustang = CopyableVehicle.create!(name: 'Mustang', copyable_owner: @joe)
131
+ @air_conditioning = CopyableAmenity.create!(
132
+ name: 'air conditioning',
133
+ copyable_vehicle: @mustang,
134
+ copyable_owner: @joe)
135
+ @air_conditioning_warranty = CopyableWarranty.create!(
136
+ name: 'air conditioning warranty',
137
+ copyable_amenity: @air_conditioning)
138
+
139
+ @corvette = CopyableVehicle.create!(name: 'Corvette', copyable_owner: @joe)
140
+ @pillows = CopyableAmenity.create!(
141
+ name: 'pillows',
142
+ copyable_vehicle: @corvette,
143
+ copyable_owner: @joe)
144
+ @pillow_warranty = CopyableWarranty.create!(name: 'pillow warranty', copyable_amenity: @pillows)
145
+ @airbags = CopyableAmenity.create!(
146
+ name: 'airbags',
147
+ copyable_vehicle: @corvette,
148
+ copyable_owner: @joe)
149
+ @airbag_warranty = CopyableWarranty.create!(name: 'airbag warranty', copyable_amenity: @airbags)
150
+ @radio = CopyableAmenity.create!(
151
+ name: 'satellite radio',
152
+ copyable_vehicle: @corvette,
153
+ copyable_owner: @joe)
154
+ end
155
+
156
+ it 'should copy the records without copying any given record more than once' do
157
+ @copy_of_joe = @joe.create_copy!
158
+ expect(CopyableOwner.count).to eq(2)
159
+ expect(CopyableVehicle.count).to eq(6)
160
+ expect(CopyableAmenity.count).to eq(10)
161
+ expect(CopyableWarranty.count).to eq(6)
162
+ expect(@copy_of_joe.copyable_vehicles.map(&:name)).to match_array([
163
+ 'Porsche', 'Mustang', 'Corvette'])
164
+ expect(@copy_of_joe.copyable_amenities.map(&:name)).to match_array([
165
+ 'moon roof', 'air conditioning', 'pillows', 'airbags', 'satellite radio'])
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ # copyable needs its own set of tables and ActiveRecord models to test with.
4
+ require_relative 'test_tables'
5
+ Copyable::TestTables.create!
6
+ require_relative 'test_models'
7
+
8
+ # useful for starting off specs with a clean slate
9
+ def undefine_copyable_in(klass)
10
+ klass.instance_eval do
11
+ define_method(:create_copy!) do |*args|
12
+ raise "the create_copy! method has been wiped clean for testing purposes"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,136 @@
1
+ # We need various test models in various configurations to properly test the
2
+ # copying functionality.
3
+
4
+
5
+ #*******************************************************************************
6
+ # For testing basic copying behavior and validation.
7
+ #
8
+
9
+ class CopyableCoin < ActiveRecord::Base
10
+ validates :year, numericality: { greater_than: 1700 }
11
+ end
12
+
13
+
14
+
15
+ #*******************************************************************************
16
+ # For testing observers.
17
+ #
18
+
19
+ class CopyableCar < ActiveRecord::Base
20
+ validates :make, presence: true
21
+ validates :model, presence: true
22
+ validates :year, presence: true
23
+ end
24
+
25
+
26
+
27
+ #*******************************************************************************
28
+ # For testing callbacks.
29
+ #
30
+
31
+ class CopyableTree < ActiveRecord::Base
32
+ after_create :callback1
33
+ before_save { |tree| raise "callback2 called" }
34
+ def callback1
35
+ raise "callback1 called"
36
+ end
37
+ end
38
+
39
+
40
+
41
+ #*******************************************************************************
42
+ # For testing basic associations.
43
+ #
44
+
45
+ class CopyablePet < ActiveRecord::Base
46
+ has_many :copyable_toys
47
+ has_one :copyable_pet_tag
48
+ has_one :copyable_pet_profile
49
+ has_one :copyable_address, through: :copyable_pet_profile
50
+ # Rails 3 expects the join table to be called copyable_pet_foods_copyable_pets
51
+ # Rails 4 expects the join table to be called copyable_pet_foods_pets
52
+ # We explicitly name the join table so that this code works with both Rails 3 and Rails 4.
53
+ has_and_belongs_to_many :copyable_pet_foods, join_table: 'copyable_pet_foods_pets'
54
+ has_many :copyable_pet_sitting_patronages
55
+ has_many :copyable_pet_sitters, through: :copyable_pet_sitting_patronages
56
+ end
57
+
58
+ class CopyableToy < ActiveRecord::Base
59
+ validates :name, length: { minimum: 3 }
60
+ belongs_to :copyable_pet
61
+ end
62
+
63
+ class CopyablePetTag < ActiveRecord::Base
64
+ belongs_to :copyable_pet
65
+ end
66
+
67
+ class CopyablePetFood < ActiveRecord::Base
68
+ # Rails 3 expects the join table to be called copyable_pet_foods_copyable_pets
69
+ # Rails 4 expects the join table to be called copyable_pet_foods_pets
70
+ # We explicitly name the join table so that this code works with both Rails 3 and Rails 4.
71
+ has_and_belongs_to_many :copyable_pets, join_table: 'copyable_pet_foods_pets'
72
+ end
73
+
74
+ class CopyablePetSitter < ActiveRecord::Base
75
+ has_many :copyable_pet_sitting_patronages
76
+ has_many :copyable_pets, through: :copyable_pet_sitting_patronages
77
+ end
78
+
79
+ class CopyablePetSittingPatronage < ActiveRecord::Base
80
+ belongs_to :copyable_pet
81
+ belongs_to :copyable_pet_sitter
82
+ end
83
+
84
+ class CopyablePetProfile < ActiveRecord::Base
85
+ belongs_to :copyable_pet
86
+ has_one :copyable_address
87
+ end
88
+
89
+ class CopyableAddress < ActiveRecord::Base
90
+ belongs_to :copyable_pet_profile
91
+ end
92
+
93
+
94
+
95
+ #*******************************************************************************
96
+ # For testing polymorphic associations and custom-named foreign keys.
97
+ #
98
+
99
+ class CopyablePicture < ActiveRecord::Base
100
+ belongs_to :imageable, polymorphic: true
101
+ belongs_to :copyable_album, foreign_key: 'picture_album_id'
102
+ end
103
+
104
+ class CopyableProduct < ActiveRecord::Base
105
+ has_many :copyable_pictures, as: :imageable
106
+ end
107
+
108
+ class CopyableAlbum < ActiveRecord::Base
109
+ has_many :copyable_pictures, foreign_key: 'picture_album_id'
110
+ end
111
+
112
+
113
+
114
+ #*******************************************************************************
115
+ # For testing denormalized data structures.
116
+ #
117
+
118
+ class CopyableOwner < ActiveRecord::Base
119
+ has_many :copyable_vehicles
120
+ has_many :copyable_amenities # this is not normalized, you wouldn't normally do this
121
+ end
122
+
123
+ class CopyableVehicle < ActiveRecord::Base
124
+ has_many :copyable_amenities
125
+ belongs_to :copyable_owner
126
+ end
127
+
128
+ class CopyableAmenity < ActiveRecord::Base
129
+ belongs_to :copyable_vehicle
130
+ belongs_to :copyable_owner # this is not normalized, you wouldn't normally do this
131
+ has_one :copyable_warranty
132
+ end
133
+
134
+ class CopyableWarranty < ActiveRecord::Base
135
+ belongs_to :copyable_amenity
136
+ end