granite-form 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 (134) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +13 -0
  3. data/.github/workflows/ci.yml +35 -0
  4. data/.github/workflows/main.yml +29 -0
  5. data/.gitignore +21 -0
  6. data/.rspec +2 -0
  7. data/.rubocop.yml +64 -0
  8. data/.rubocop_todo.yml +48 -0
  9. data/Appraisals +8 -0
  10. data/CHANGELOG.md +73 -0
  11. data/Gemfile +8 -0
  12. data/Guardfile +77 -0
  13. data/LICENSE +22 -0
  14. data/README.md +429 -0
  15. data/Rakefile +6 -0
  16. data/gemfiles/rails.4.2.gemfile +15 -0
  17. data/gemfiles/rails.5.0.gemfile +15 -0
  18. data/gemfiles/rails.5.1.gemfile +15 -0
  19. data/gemfiles/rails.5.2.gemfile +15 -0
  20. data/gemfiles/rails.6.0.gemfile +14 -0
  21. data/gemfiles/rails.6.1.gemfile +14 -0
  22. data/gemfiles/rails.7.0.gemfile +14 -0
  23. data/granite-form.gemspec +31 -0
  24. data/lib/granite/form/active_record/associations.rb +57 -0
  25. data/lib/granite/form/active_record/nested_attributes.rb +20 -0
  26. data/lib/granite/form/base.rb +15 -0
  27. data/lib/granite/form/config.rb +42 -0
  28. data/lib/granite/form/errors.rb +111 -0
  29. data/lib/granite/form/extensions.rb +36 -0
  30. data/lib/granite/form/model/associations/base.rb +97 -0
  31. data/lib/granite/form/model/associations/collection/embedded.rb +14 -0
  32. data/lib/granite/form/model/associations/collection/proxy.rb +35 -0
  33. data/lib/granite/form/model/associations/embeds_any.rb +19 -0
  34. data/lib/granite/form/model/associations/embeds_many.rb +152 -0
  35. data/lib/granite/form/model/associations/embeds_one.rb +112 -0
  36. data/lib/granite/form/model/associations/nested_attributes.rb +215 -0
  37. data/lib/granite/form/model/associations/persistence_adapters/active_record/referenced_proxy.rb +33 -0
  38. data/lib/granite/form/model/associations/persistence_adapters/active_record.rb +68 -0
  39. data/lib/granite/form/model/associations/persistence_adapters/base.rb +55 -0
  40. data/lib/granite/form/model/associations/references_any.rb +43 -0
  41. data/lib/granite/form/model/associations/references_many.rb +113 -0
  42. data/lib/granite/form/model/associations/references_one.rb +88 -0
  43. data/lib/granite/form/model/associations/reflections/base.rb +92 -0
  44. data/lib/granite/form/model/associations/reflections/embeds_any.rb +52 -0
  45. data/lib/granite/form/model/associations/reflections/embeds_many.rb +17 -0
  46. data/lib/granite/form/model/associations/reflections/embeds_one.rb +19 -0
  47. data/lib/granite/form/model/associations/reflections/references_any.rb +65 -0
  48. data/lib/granite/form/model/associations/reflections/references_many.rb +30 -0
  49. data/lib/granite/form/model/associations/reflections/references_one.rb +32 -0
  50. data/lib/granite/form/model/associations/reflections/singular.rb +37 -0
  51. data/lib/granite/form/model/associations/validations.rb +41 -0
  52. data/lib/granite/form/model/associations.rb +120 -0
  53. data/lib/granite/form/model/attributes/attribute.rb +75 -0
  54. data/lib/granite/form/model/attributes/base.rb +134 -0
  55. data/lib/granite/form/model/attributes/collection.rb +19 -0
  56. data/lib/granite/form/model/attributes/dictionary.rb +28 -0
  57. data/lib/granite/form/model/attributes/localized.rb +44 -0
  58. data/lib/granite/form/model/attributes/reference_many.rb +21 -0
  59. data/lib/granite/form/model/attributes/reference_one.rb +52 -0
  60. data/lib/granite/form/model/attributes/reflections/attribute.rb +61 -0
  61. data/lib/granite/form/model/attributes/reflections/base.rb +62 -0
  62. data/lib/granite/form/model/attributes/reflections/collection.rb +12 -0
  63. data/lib/granite/form/model/attributes/reflections/dictionary.rb +15 -0
  64. data/lib/granite/form/model/attributes/reflections/localized.rb +45 -0
  65. data/lib/granite/form/model/attributes/reflections/reference_many.rb +12 -0
  66. data/lib/granite/form/model/attributes/reflections/reference_one.rb +49 -0
  67. data/lib/granite/form/model/attributes/reflections/represents.rb +56 -0
  68. data/lib/granite/form/model/attributes/represents.rb +67 -0
  69. data/lib/granite/form/model/attributes.rb +204 -0
  70. data/lib/granite/form/model/callbacks.rb +72 -0
  71. data/lib/granite/form/model/conventions.rb +40 -0
  72. data/lib/granite/form/model/dirty.rb +84 -0
  73. data/lib/granite/form/model/lifecycle.rb +309 -0
  74. data/lib/granite/form/model/localization.rb +26 -0
  75. data/lib/granite/form/model/persistence.rb +59 -0
  76. data/lib/granite/form/model/primary.rb +59 -0
  77. data/lib/granite/form/model/representation.rb +101 -0
  78. data/lib/granite/form/model/scopes.rb +118 -0
  79. data/lib/granite/form/model/validations/associated.rb +22 -0
  80. data/lib/granite/form/model/validations/nested.rb +56 -0
  81. data/lib/granite/form/model/validations.rb +29 -0
  82. data/lib/granite/form/model.rb +33 -0
  83. data/lib/granite/form/railtie.rb +9 -0
  84. data/lib/granite/form/undefined_class.rb +11 -0
  85. data/lib/granite/form/version.rb +5 -0
  86. data/lib/granite/form.rb +163 -0
  87. data/spec/lib/granite/form/active_record/associations_spec.rb +211 -0
  88. data/spec/lib/granite/form/active_record/nested_attributes_spec.rb +15 -0
  89. data/spec/lib/granite/form/config_spec.rb +66 -0
  90. data/spec/lib/granite/form/model/associations/embeds_many_spec.rb +706 -0
  91. data/spec/lib/granite/form/model/associations/embeds_one_spec.rb +533 -0
  92. data/spec/lib/granite/form/model/associations/nested_attributes_spec.rb +119 -0
  93. data/spec/lib/granite/form/model/associations/persistence_adapters/active_record_spec.rb +58 -0
  94. data/spec/lib/granite/form/model/associations/references_many_spec.rb +572 -0
  95. data/spec/lib/granite/form/model/associations/references_one_spec.rb +445 -0
  96. data/spec/lib/granite/form/model/associations/reflections/embeds_any_spec.rb +42 -0
  97. data/spec/lib/granite/form/model/associations/reflections/embeds_many_spec.rb +145 -0
  98. data/spec/lib/granite/form/model/associations/reflections/embeds_one_spec.rb +117 -0
  99. data/spec/lib/granite/form/model/associations/reflections/references_many_spec.rb +303 -0
  100. data/spec/lib/granite/form/model/associations/reflections/references_one_spec.rb +287 -0
  101. data/spec/lib/granite/form/model/associations/validations_spec.rb +137 -0
  102. data/spec/lib/granite/form/model/associations_spec.rb +198 -0
  103. data/spec/lib/granite/form/model/attributes/attribute_spec.rb +186 -0
  104. data/spec/lib/granite/form/model/attributes/base_spec.rb +97 -0
  105. data/spec/lib/granite/form/model/attributes/collection_spec.rb +72 -0
  106. data/spec/lib/granite/form/model/attributes/dictionary_spec.rb +100 -0
  107. data/spec/lib/granite/form/model/attributes/localized_spec.rb +103 -0
  108. data/spec/lib/granite/form/model/attributes/reflections/attribute_spec.rb +72 -0
  109. data/spec/lib/granite/form/model/attributes/reflections/base_spec.rb +56 -0
  110. data/spec/lib/granite/form/model/attributes/reflections/collection_spec.rb +37 -0
  111. data/spec/lib/granite/form/model/attributes/reflections/dictionary_spec.rb +43 -0
  112. data/spec/lib/granite/form/model/attributes/reflections/localized_spec.rb +37 -0
  113. data/spec/lib/granite/form/model/attributes/reflections/represents_spec.rb +70 -0
  114. data/spec/lib/granite/form/model/attributes/represents_spec.rb +85 -0
  115. data/spec/lib/granite/form/model/attributes_spec.rb +350 -0
  116. data/spec/lib/granite/form/model/callbacks_spec.rb +337 -0
  117. data/spec/lib/granite/form/model/conventions_spec.rb +11 -0
  118. data/spec/lib/granite/form/model/dirty_spec.rb +84 -0
  119. data/spec/lib/granite/form/model/lifecycle_spec.rb +356 -0
  120. data/spec/lib/granite/form/model/persistence_spec.rb +46 -0
  121. data/spec/lib/granite/form/model/primary_spec.rb +84 -0
  122. data/spec/lib/granite/form/model/representation_spec.rb +139 -0
  123. data/spec/lib/granite/form/model/scopes_spec.rb +86 -0
  124. data/spec/lib/granite/form/model/typecasting_spec.rb +193 -0
  125. data/spec/lib/granite/form/model/validations/associated_spec.rb +102 -0
  126. data/spec/lib/granite/form/model/validations/nested_spec.rb +164 -0
  127. data/spec/lib/granite/form/model/validations_spec.rb +31 -0
  128. data/spec/lib/granite/form/model_spec.rb +10 -0
  129. data/spec/lib/granite/form_spec.rb +11 -0
  130. data/spec/shared/nested_attribute_examples.rb +332 -0
  131. data/spec/spec_helper.rb +50 -0
  132. data/spec/support/model_helpers.rb +10 -0
  133. data/spec/support/muffle_helper.rb +7 -0
  134. metadata +403 -0
@@ -0,0 +1,706 @@
1
+ require 'spec_helper'
2
+
3
+ describe Granite::Form::Model::Associations::EmbedsMany do
4
+ before do
5
+ stub_model(:dummy) do
6
+ include Granite::Form::Model::Associations
7
+ end
8
+
9
+ stub_model(:project) do
10
+ include Granite::Form::Model::Lifecycle
11
+
12
+ attribute :title, String
13
+ validates :title, presence: true
14
+ end
15
+ stub_model(:user) do
16
+ include Granite::Form::Model::Persistence
17
+ include Granite::Form::Model::Associations
18
+
19
+ attribute :name, String
20
+ embeds_many :projects
21
+ end
22
+ end
23
+
24
+ let(:user) { User.new(name: 'User') }
25
+ let(:association) { user.association(:projects) }
26
+
27
+ let(:existing_user) { User.instantiate name: 'Rick', projects: [{title: 'Genesis'}] }
28
+ let(:existing_association) { existing_user.association(:projects) }
29
+
30
+ context 'performers' do
31
+ let(:user) { User.new(projects: [Project.new(title: 'Project 1')]) }
32
+
33
+ specify do
34
+ p2 = user.projects.build(title: 'Project 2')
35
+ p3 = user.projects.build(title: 'Project 3')
36
+ p4 = user.projects.create(title: 'Project 4')
37
+ expect(user.read_attribute(:projects)).to eq([{'title' => 'Project 4'}])
38
+ p2.save!
39
+ expect(user.read_attribute(:projects)).to eq([{'title' => 'Project 2'}, {'title' => 'Project 4'}])
40
+ p2.destroy!.destroy!
41
+ expect(user.read_attribute(:projects)).to eq([{'title' => 'Project 4'}])
42
+ user.projects.create(title: 'Project 5')
43
+ expect(user.read_attribute(:projects)).to eq([{'title' => 'Project 4'}, {'title' => 'Project 5'}])
44
+ p3.destroy!
45
+ user.projects.first.destroy!
46
+ expect(user.read_attribute(:projects)).to eq([{'title' => 'Project 4'}, {'title' => 'Project 5'}])
47
+ p4.destroy!.save!
48
+ expect(user.read_attribute(:projects)).to eq([{'title' => 'Project 4'}, {'title' => 'Project 5'}])
49
+ expect(user.projects.count).to eq(5)
50
+ user.projects.map(&:save!)
51
+ expect(user.read_attribute(:projects)).to eq([
52
+ {'title' => 'Project 1'}, {'title' => 'Project 2'}, {'title' => 'Project 3'},
53
+ {'title' => 'Project 4'}, {'title' => 'Project 5'}
54
+ ])
55
+ user.projects.map(&:destroy!)
56
+ expect(user.read_attribute(:projects)).to eq([])
57
+ user.projects.first(2).map(&:save!)
58
+ expect(user.read_attribute(:projects)).to eq([{'title' => 'Project 1'}, {'title' => 'Project 2'}])
59
+ expect(user.projects.reload.count).to eq(2)
60
+ p3 = user.projects.create!(title: 'Project 3')
61
+ expect(user.read_attribute(:projects)).to eq([
62
+ {'title' => 'Project 1'}, {'title' => 'Project 2'}, {'title' => 'Project 3'}
63
+ ])
64
+ p3.destroy!
65
+ expect(user.read_attribute(:projects)).to eq([{'title' => 'Project 1'}, {'title' => 'Project 2'}])
66
+ user.projects.create!(title: 'Project 4')
67
+ expect(user.read_attribute(:projects)).to eq([
68
+ {'title' => 'Project 1'}, {'title' => 'Project 2'}, {'title' => 'Project 4'}
69
+ ])
70
+ end
71
+ end
72
+
73
+ context 'callbacks' do
74
+ before do
75
+ User.class_eval do
76
+ embeds_many :projects,
77
+ before_add: ->(object) { callbacks.push([:before_add, object]) },
78
+ after_add: ->(object) { callbacks.push([:after_add, object]) }
79
+
80
+ collection :callbacks, Array
81
+ end
82
+ end
83
+ let(:project1) { Project.new(title: 'Project1') }
84
+ let(:project2) { Project.new(title: 'Project2') }
85
+
86
+ specify do
87
+ expect { association.build(title: 'Project1') }
88
+ .to change { user.callbacks }
89
+ .to([[:before_add, project1], [:after_add, project1]])
90
+ end
91
+
92
+ specify do
93
+ expect do
94
+ association.build(title: 'Project1')
95
+ association.build(title: 'Project2')
96
+ end
97
+ .to change { user.callbacks }
98
+ .to([
99
+ [:before_add, project1], [:after_add, project1],
100
+ [:before_add, project2], [:after_add, project2]
101
+ ])
102
+ end
103
+
104
+ specify do
105
+ expect { association.create(title: 'Project1') }
106
+ .to change { user.callbacks }
107
+ .to([[:before_add, project1], [:after_add, project1]])
108
+ end
109
+
110
+ specify do
111
+ expect do
112
+ association.create(title: 'Project1')
113
+ association.create(title: 'Project2')
114
+ end
115
+ .to change { user.callbacks }
116
+ .to([
117
+ [:before_add, project1], [:after_add, project1],
118
+ [:before_add, project2], [:after_add, project2]
119
+ ])
120
+ end
121
+
122
+ specify do
123
+ expect { association.concat(project1, project2) }
124
+ .to change { user.callbacks }
125
+ .to([
126
+ [:before_add, project1], [:after_add, project1],
127
+ [:before_add, project2], [:after_add, project2]
128
+ ])
129
+ end
130
+
131
+ specify do
132
+ expect do
133
+ association.concat(project1)
134
+ association.concat(project2)
135
+ end
136
+ .to change { user.callbacks }
137
+ .to([
138
+ [:before_add, project1], [:after_add, project1],
139
+ [:before_add, project2], [:after_add, project2]
140
+ ])
141
+ end
142
+
143
+ specify do
144
+ expect { association.writer([project2, project1]) }
145
+ .to change { user.callbacks }
146
+ .to([
147
+ [:before_add, project2], [:after_add, project2],
148
+ [:before_add, project1], [:after_add, project1]
149
+ ])
150
+ end
151
+
152
+ specify do
153
+ expect do
154
+ association.writer([project1])
155
+ association.writer([])
156
+ association.writer([project2])
157
+ end
158
+ .to change { user.callbacks }
159
+ .to([
160
+ [:before_add, project1], [:after_add, project1],
161
+ [:before_add, project2], [:after_add, project2]
162
+ ])
163
+ end
164
+
165
+ context 'default' do
166
+ before do
167
+ User.class_eval do
168
+ embeds_many :projects,
169
+ before_add: ->(owner, object) { owner.callbacks.push([:before_add, object]) },
170
+ after_add: ->(owner, object) { owner.callbacks.push([:after_add, object]) },
171
+ default: -> { {title: 'Project1'} }
172
+
173
+ collection :callbacks, Array
174
+ end
175
+ end
176
+
177
+ specify do
178
+ expect { association.concat(project2) }
179
+ .to change { user.callbacks }
180
+ .to([
181
+ [:before_add, project2], [:after_add, project2],
182
+ [:before_add, project1], [:after_add, project1]
183
+ ])
184
+ end
185
+ end
186
+ end
187
+
188
+ describe 'user#association' do
189
+ specify { expect(association).to be_a described_class }
190
+ specify { expect(association).to eq(user.association(:projects)) }
191
+ end
192
+
193
+ describe 'project#embedder' do
194
+ let(:project) { Project.new(title: 'Project') }
195
+
196
+ specify { expect(association.build.embedder).to eq(user) }
197
+ specify { expect(association.create.embedder).to eq(user) }
198
+ specify do
199
+ expect { association.writer([project]) }
200
+ .to change { project.embedder }.from(nil).to(user)
201
+ end
202
+ specify do
203
+ expect { association.concat(project) }
204
+ .to change { project.embedder }.from(nil).to(user)
205
+ end
206
+ specify do
207
+ expect { association.target = [project] }
208
+ .to change { project.embedder }.from(nil).to(user)
209
+ end
210
+
211
+ context 'default' do
212
+ before do
213
+ User.class_eval do
214
+ embeds_many :projects, default: -> { {title: 'Project1'} }
215
+ end
216
+ end
217
+
218
+ specify { expect(association.target.first.embedder).to eq(user) }
219
+
220
+ context do
221
+ before do
222
+ User.class_eval do
223
+ embeds_many :projects, default: -> { Project.new(title: 'Project1') }
224
+ end
225
+ end
226
+
227
+ specify { expect(association.target.first.embedder).to eq(user) }
228
+ end
229
+ end
230
+
231
+ context 'embedding goes before attributes' do
232
+ before do
233
+ Project.class_eval do
234
+ attribute :title, String, normalize: ->(value) { "#{value}#{embedder.name}" }
235
+ end
236
+ end
237
+
238
+ specify { expect(association.build(title: 'Project').title).to eq('ProjectUser') }
239
+ specify { expect(association.create(title: 'Project').title).to eq('ProjectUser') }
240
+ end
241
+ end
242
+
243
+ describe '#build' do
244
+ specify { expect(association.build).to be_a Project }
245
+ specify { expect(association.build).not_to be_persisted }
246
+
247
+ specify do
248
+ expect { association.build(title: 'Swordfish') }
249
+ .not_to change { user.read_attribute(:projects) }
250
+ end
251
+ specify do
252
+ expect { association.build(title: 'Swordfish') }
253
+ .to change { association.reader.map(&:attributes) }
254
+ .from([]).to([{'title' => 'Swordfish'}])
255
+ end
256
+
257
+ specify do
258
+ expect { existing_association.build(title: 'Swordfish') }
259
+ .not_to change { existing_user.read_attribute(:projects) }
260
+ end
261
+ specify do
262
+ expect { existing_association.build(title: 'Swordfish') }
263
+ .to change { existing_association.reader.map(&:attributes) }
264
+ .from([{'title' => 'Genesis'}]).to([{'title' => 'Genesis'}, {'title' => 'Swordfish'}])
265
+ end
266
+ end
267
+
268
+ describe '#create' do
269
+ specify { expect(association.create).to be_a Project }
270
+ specify { expect(association.create).not_to be_persisted }
271
+
272
+ specify { expect(association.create(title: 'Swordfish')).to be_a Project }
273
+ specify { expect(association.create(title: 'Swordfish')).to be_persisted }
274
+
275
+ specify do
276
+ expect { association.create }
277
+ .not_to change { user.read_attribute(:projects) }
278
+ end
279
+ specify do
280
+ expect { association.create(title: 'Swordfish') }
281
+ .to change { user.read_attribute(:projects) }
282
+ .from(nil).to([{'title' => 'Swordfish'}])
283
+ end
284
+ specify do
285
+ expect { association.create(title: 'Swordfish') }
286
+ .to change { association.reader.map(&:attributes) }
287
+ .from([]).to([{'title' => 'Swordfish'}])
288
+ end
289
+
290
+ specify do
291
+ expect { existing_association.create }
292
+ .not_to change { existing_user.read_attribute(:projects) }
293
+ end
294
+ specify do
295
+ expect { existing_association.create(title: 'Swordfish') }
296
+ .to change { existing_user.read_attribute(:projects) }
297
+ .from([{title: 'Genesis'}]).to([{title: 'Genesis'}, {'title' => 'Swordfish'}])
298
+ end
299
+ specify do
300
+ expect { existing_association.create(title: 'Swordfish') }
301
+ .to change { existing_association.reader.map(&:attributes) }
302
+ .from([{'title' => 'Genesis'}]).to([{'title' => 'Genesis'}, {'title' => 'Swordfish'}])
303
+ end
304
+ end
305
+
306
+ describe '#create!' do
307
+ specify { expect { association.create! }.to raise_error Granite::Form::ValidationError }
308
+
309
+ specify { expect(association.create!(title: 'Swordfish')).to be_a Project }
310
+ specify { expect(association.create!(title: 'Swordfish')).to be_persisted }
311
+
312
+ specify do
313
+ expect { muffle(Granite::Form::ValidationError) { association.create! } }
314
+ .not_to change { user.read_attribute(:projects) }
315
+ end
316
+ specify do
317
+ expect { muffle(Granite::Form::ValidationError) { association.create! } }
318
+ .to change { association.reader.map(&:attributes) }.from([]).to([{'title' => nil}])
319
+ end
320
+ specify do
321
+ expect { association.create!(title: 'Swordfish') }
322
+ .to change { user.read_attribute(:projects) }.from(nil).to([{'title' => 'Swordfish'}])
323
+ end
324
+ specify do
325
+ expect { association.create!(title: 'Swordfish') }
326
+ .to change { association.reader.map(&:attributes) }
327
+ .from([]).to([{'title' => 'Swordfish'}])
328
+ end
329
+
330
+ specify do
331
+ expect { muffle(Granite::Form::ValidationError) { existing_association.create! } }
332
+ .not_to change { existing_user.read_attribute(:projects) }
333
+ end
334
+ specify do
335
+ expect { muffle(Granite::Form::ValidationError) { existing_association.create! } }
336
+ .to change { existing_association.reader.map(&:attributes) }
337
+ .from([{'title' => 'Genesis'}]).to([{'title' => 'Genesis'}, {'title' => nil}])
338
+ end
339
+ specify do
340
+ expect { existing_association.create!(title: 'Swordfish') }
341
+ .to change { existing_user.read_attribute(:projects) }
342
+ .from([{title: 'Genesis'}]).to([{title: 'Genesis'}, {'title' => 'Swordfish'}])
343
+ end
344
+ specify do
345
+ expect { existing_association.create!(title: 'Swordfish') }
346
+ .to change { existing_association.reader.map(&:attributes) }
347
+ .from([{'title' => 'Genesis'}]).to([{'title' => 'Genesis'}, {'title' => 'Swordfish'}])
348
+ end
349
+ end
350
+
351
+ describe '#apply_changes' do
352
+ specify do
353
+ association.build
354
+ expect { association.apply_changes }
355
+ .not_to change { association.target.map(&:persisted?) }
356
+ .from([false])
357
+ end
358
+ specify do
359
+ association.build(title: 'Genesis')
360
+ expect { association.apply_changes }
361
+ .to change { association.target.map(&:persisted?) }
362
+ .from([false]).to([true])
363
+ end
364
+ specify do
365
+ existing_association.target.first.mark_for_destruction
366
+ existing_association.build(title: 'Swordfish')
367
+ expect { existing_association.apply_changes }
368
+ .to change { existing_association.target.map(&:title) }
369
+ .to(['Swordfish'])
370
+ end
371
+ specify do
372
+ existing_association.target.first.mark_for_destruction
373
+ existing_association.build(title: 'Swordfish')
374
+ expect { existing_association.apply_changes }
375
+ .to change { existing_association.target.map(&:persisted?) }
376
+ .from([true, false]).to([true])
377
+ end
378
+ specify do
379
+ existing_association.target.first.mark_for_destruction
380
+ existing_association.build(title: 'Swordfish')
381
+ expect { existing_association.apply_changes }
382
+ .to change { existing_association.destroyed.map(&:title) }
383
+ .from([]).to(['Genesis'])
384
+ end
385
+ specify do
386
+ existing_association.target.first.destroy!
387
+ existing_association.build(title: 'Swordfish')
388
+ expect { existing_association.apply_changes }
389
+ .to change { existing_association.target.map(&:title) }
390
+ .to(['Swordfish'])
391
+ end
392
+ specify do
393
+ existing_association.target.first.destroy!
394
+ existing_association.build(title: 'Swordfish')
395
+ expect { existing_association.apply_changes }
396
+ .to change { existing_association.destroyed.map(&:title) }
397
+ .from([]).to(['Genesis'])
398
+ end
399
+ end
400
+
401
+ describe '#apply_changes!' do
402
+ specify do
403
+ association.build
404
+ expect { association.apply_changes! }
405
+ .to raise_error Granite::Form::AssociationChangesNotApplied
406
+ end
407
+ specify do
408
+ association.build(title: 'Genesis')
409
+ expect { association.apply_changes! }
410
+ .to change { association.target.map(&:persisted?) }
411
+ .to([true])
412
+ end
413
+ specify do
414
+ existing_association.target.first.mark_for_destruction
415
+ existing_association.build(title: 'Swordfish')
416
+ expect { existing_association.apply_changes! }
417
+ .to change { existing_association.target.map(&:title) }
418
+ .to(['Swordfish'])
419
+ end
420
+ specify do
421
+ existing_association.target.first.mark_for_destruction
422
+ existing_association.build(title: 'Swordfish')
423
+ expect { existing_association.apply_changes! }
424
+ .to change { existing_association.target.map(&:persisted?) }
425
+ .from([true, false]).to([true])
426
+ end
427
+ specify do
428
+ existing_association.target.first.destroy!
429
+ existing_association.build(title: 'Swordfish')
430
+ expect { existing_association.apply_changes! }
431
+ .to change { existing_association.target.map(&:title) }
432
+ .to(['Swordfish'])
433
+ end
434
+ end
435
+
436
+ describe '#target' do
437
+ specify { expect(association.target).to eq([]) }
438
+ specify { expect(existing_association.target).to eq(existing_user.projects) }
439
+ specify { expect { association.build }.to change { association.target.count }.to(1) }
440
+ end
441
+
442
+ describe '#default' do
443
+ before { User.embeds_many :projects, default: -> { {title: 'Default'} } }
444
+ before do
445
+ Project.class_eval do
446
+ include Granite::Form::Model::Primary
447
+ primary :title
448
+ end
449
+ end
450
+ let(:new_project) { Project.new.tap { |p| p.title = 'Project' } }
451
+ let(:existing_user) { User.instantiate name: 'Rick' }
452
+
453
+ specify { expect(association.target.map(&:title)).to eq(['Default']) }
454
+ specify { expect(association.target.map(&:new_record?)).to eq([true]) }
455
+ specify { expect { association.replace([new_project]) }.to change { association.target.map(&:title) }.to eq(['Project']) }
456
+ specify { expect { association.replace([]) }.to change { association.target }.to([]) }
457
+
458
+ specify { expect(existing_association.target).to eq([]) }
459
+ specify { expect { existing_association.replace([new_project]) }.to change { existing_association.target.map(&:title) }.to(['Project']) }
460
+ specify { expect { existing_association.replace([]) }.not_to change { existing_association.target } }
461
+
462
+ context do
463
+ before { Project.send(:include, Granite::Form::Model::Dirty) }
464
+ specify { expect(association.target.any?(&:changed?)).to eq(false) }
465
+ end
466
+ end
467
+
468
+ describe '#loaded?' do
469
+ specify { expect(association.loaded?).to eq(false) }
470
+ specify { expect { association.target }.to change { association.loaded? }.to(true) }
471
+ specify { expect { association.build }.to change { association.loaded? }.to(true) }
472
+ specify { expect { association.replace([]) }.to change { association.loaded? }.to(true) }
473
+ specify { expect { existing_association.replace([]) }.to change { existing_association.loaded? }.to(true) }
474
+ end
475
+
476
+ describe '#reload' do
477
+ specify { expect(association.reload).to eq([]) }
478
+
479
+ specify { expect(existing_association.reload).to eq(existing_user.projects) }
480
+
481
+ context do
482
+ before { association.build(title: 'Swordfish') }
483
+ specify do
484
+ expect { association.reload }
485
+ .to change { association.reader.map(&:attributes) }.from([{'title' => 'Swordfish'}]).to([])
486
+ end
487
+ end
488
+
489
+ context do
490
+ before { existing_association.build(title: 'Swordfish') }
491
+ specify do
492
+ expect { existing_association.reload }
493
+ .to change { existing_association.reader.map(&:attributes) }
494
+ .from([{'title' => 'Genesis'}, {'title' => 'Swordfish'}]).to([{'title' => 'Genesis'}])
495
+ end
496
+ end
497
+ end
498
+
499
+ describe '#clear' do
500
+ specify { expect(association.clear).to eq(true) }
501
+ specify { expect { association.clear }.not_to change { association.reader } }
502
+
503
+ specify { expect(existing_association.clear).to eq(true) }
504
+ specify do
505
+ expect { existing_association.clear }
506
+ .to change { existing_association.reader.map(&:attributes) }.from([{'title' => 'Genesis'}]).to([])
507
+ end
508
+ specify do
509
+ expect { existing_association.clear }
510
+ .to change { existing_user.read_attribute(:projects) }.from([{title: 'Genesis'}]).to([])
511
+ end
512
+
513
+ context do
514
+ let(:existing_user) { User.instantiate name: 'Rick', projects: [{title: 'Genesis'}, {title: 'Swordfish'}] }
515
+ before { Project.send(:include, Granite::Form::Model::Callbacks) }
516
+ if ActiveModel.version >= Gem::Version.new('5.0.0')
517
+ before { Project.before_destroy { throw :abort } }
518
+ else
519
+ before { Project.before_destroy { false } }
520
+ end
521
+
522
+ specify { expect(existing_association.clear).to eq(false) }
523
+ specify do
524
+ expect { existing_association.clear }
525
+ .not_to change { existing_association.reader }
526
+ end
527
+ specify do
528
+ expect { existing_association.clear }
529
+ .not_to change { existing_user.read_attribute(:projects) }
530
+ end
531
+ end
532
+ end
533
+
534
+ describe '#reader' do
535
+ specify { expect(association.reader).to eq([]) }
536
+
537
+ specify { expect(existing_association.reader.first).to be_a Project }
538
+ specify { expect(existing_association.reader.first).to be_persisted }
539
+
540
+ context do
541
+ before { association.build }
542
+ specify { expect(association.reader.last).to be_a Project }
543
+ specify { expect(association.reader.last).not_to be_persisted }
544
+ specify { expect(association.reader.size).to eq(1) }
545
+ specify { expect(association.reader(true)).to eq([]) }
546
+ end
547
+
548
+ context do
549
+ before { existing_association.build(title: 'Swordfish') }
550
+ specify { expect(existing_association.reader.size).to eq(2) }
551
+ specify { expect(existing_association.reader.last.title).to eq('Swordfish') }
552
+ specify { expect(existing_association.reader(true).size).to eq(1) }
553
+ specify { expect(existing_association.reader(true).last.title).to eq('Genesis') }
554
+ end
555
+ end
556
+
557
+ describe '#writer' do
558
+ let(:new_project1) { Project.new(title: 'Project 1') }
559
+ let(:new_project2) { Project.new(title: 'Project 2') }
560
+ let(:invalid_project) { Project.new }
561
+
562
+ specify do
563
+ expect { association.writer([Dummy.new]) }
564
+ .to raise_error Granite::Form::AssociationTypeMismatch
565
+ end
566
+
567
+ specify { expect { association.writer(nil) }.to raise_error NoMethodError }
568
+ specify { expect { association.writer(new_project1) }.to raise_error NoMethodError }
569
+ specify { expect(association.writer([])).to eq([]) }
570
+
571
+ specify { expect(association.writer([new_project1])).to eq([new_project1]) }
572
+ specify do
573
+ expect { association.writer([new_project1]) }
574
+ .to change { association.reader.map(&:attributes) }.from([]).to([{'title' => 'Project 1'}])
575
+ end
576
+ specify do
577
+ expect { association.writer([new_project1]) }
578
+ .not_to change { user.read_attribute(:projects) }
579
+ end
580
+
581
+ specify do
582
+ expect { existing_association.writer([new_project1, invalid_project]) }
583
+ .to raise_error Granite::Form::AssociationChangesNotApplied
584
+ end
585
+ specify do
586
+ expect { muffle(Granite::Form::AssociationChangesNotApplied) { existing_association.writer([new_project1, invalid_project]) } }
587
+ .not_to change { existing_user.read_attribute(:projects) }
588
+ end
589
+ specify do
590
+ expect { muffle(Granite::Form::AssociationChangesNotApplied) { existing_association.writer([new_project1, invalid_project]) } }
591
+ .not_to change { existing_association.reader }
592
+ end
593
+
594
+ specify do
595
+ expect { existing_association.writer([new_project1, Dummy.new, new_project2]) }
596
+ .to raise_error Granite::Form::AssociationTypeMismatch
597
+ end
598
+ specify do
599
+ expect { muffle(Granite::Form::AssociationTypeMismatch) { existing_association.writer([new_project1, Dummy.new, new_project2]) } }
600
+ .not_to change { existing_user.read_attribute(:projects) }
601
+ end
602
+ specify do
603
+ expect { muffle(Granite::Form::AssociationTypeMismatch) { existing_association.writer([new_project1, Dummy.new, new_project2]) } }
604
+ .not_to change { existing_association.reader }
605
+ end
606
+
607
+ specify { expect { existing_association.writer(nil) }.to raise_error NoMethodError }
608
+ specify do
609
+ expect { muffle(NoMethodError) { existing_association.writer(nil) } }
610
+ .not_to change { existing_user.read_attribute(:projects) }
611
+ end
612
+ specify do
613
+ expect { muffle(NoMethodError) { existing_association.writer(nil) } }
614
+ .not_to change { existing_association.reader }
615
+ end
616
+
617
+ specify { expect(existing_association.writer([])).to eq([]) }
618
+ specify do
619
+ expect { existing_association.writer([]) }
620
+ .to change { existing_user.read_attribute(:projects) }.to([])
621
+ end
622
+ specify do
623
+ expect { existing_association.writer([]) }
624
+ .to change { existing_association.reader }.to([])
625
+ end
626
+
627
+ specify { expect(existing_association.writer([new_project1, new_project2])).to eq([new_project1, new_project2]) }
628
+ specify do
629
+ expect { existing_association.writer([new_project1, new_project2]) }
630
+ .to change { existing_association.reader.map(&:attributes) }
631
+ .from([{'title' => 'Genesis'}]).to([{'title' => 'Project 1'}, {'title' => 'Project 2'}])
632
+ end
633
+ specify do
634
+ expect { existing_association.writer([new_project1, new_project2]) }
635
+ .to change { existing_user.read_attribute(:projects) }
636
+ .from([{title: 'Genesis'}]).to([{'title' => 'Project 1'}, {'title' => 'Project 2'}])
637
+ end
638
+ end
639
+
640
+ describe '#concat' do
641
+ let(:new_project1) { Project.new(title: 'Project 1') }
642
+ let(:new_project2) { Project.new(title: 'Project 2') }
643
+ let(:invalid_project) { Project.new }
644
+
645
+ specify do
646
+ expect { association.concat(Dummy.new) }
647
+ .to raise_error Granite::Form::AssociationTypeMismatch
648
+ end
649
+
650
+ specify { expect { association.concat(nil) }.to raise_error Granite::Form::AssociationTypeMismatch }
651
+ specify { expect(association.concat([])).to eq([]) }
652
+ specify { expect(existing_association.concat([])).to eq(existing_user.projects) }
653
+ specify { expect(existing_association.concat).to eq(existing_user.projects) }
654
+
655
+ specify { expect(association.concat(new_project1)).to eq([new_project1]) }
656
+ specify do
657
+ expect { association.concat(new_project1) }
658
+ .to change { association.reader.map(&:attributes) }.from([]).to([{'title' => 'Project 1'}])
659
+ end
660
+ specify do
661
+ expect { association.concat(new_project1) }
662
+ .not_to change { user.read_attribute(:projects) }
663
+ end
664
+
665
+ specify { expect(existing_association.concat(new_project1, invalid_project)).to eq(false) }
666
+ specify do
667
+ expect { existing_association.concat(new_project1, invalid_project) }
668
+ .to change { existing_user.read_attribute(:projects) }
669
+ .from([{title: 'Genesis'}]).to([{'title' => 'Genesis'}, {'title' => 'Project 1'}])
670
+ end
671
+ specify do
672
+ expect { existing_association.concat(new_project1, invalid_project) }
673
+ .to change { existing_association.reader.map(&:attributes) }
674
+ .from([{'title' => 'Genesis'}]).to([{'title' => 'Genesis'}, {'title' => 'Project 1'}, {'title' => nil}])
675
+ end
676
+
677
+ specify do
678
+ expect { existing_association.concat(new_project1, Dummy.new, new_project2) }
679
+ .to raise_error Granite::Form::AssociationTypeMismatch
680
+ end
681
+ specify do
682
+ expect { muffle(Granite::Form::AssociationTypeMismatch) { existing_association.concat(new_project1, Dummy.new, new_project2) } }
683
+ .not_to change { existing_user.read_attribute(:projects) }
684
+ end
685
+ specify do
686
+ expect { muffle(Granite::Form::AssociationTypeMismatch) { existing_association.concat(new_project1, Dummy.new, new_project2) } }
687
+ .to change { existing_association.reader.map(&:attributes) }
688
+ .from([{'title' => 'Genesis'}]).to([{'title' => 'Genesis'}, {'title' => 'Project 1'}])
689
+ end
690
+
691
+ specify do
692
+ expect(existing_association.concat(new_project1, new_project2))
693
+ .to eq([existing_user.projects.first, new_project1, new_project2])
694
+ end
695
+ specify do
696
+ expect { existing_association.concat([new_project1, new_project2]) }
697
+ .to change { existing_association.reader.map(&:attributes) }
698
+ .from([{'title' => 'Genesis'}]).to([{'title' => 'Genesis'}, {'title' => 'Project 1'}, {'title' => 'Project 2'}])
699
+ end
700
+ specify do
701
+ expect { existing_association.concat([new_project1, new_project2]) }
702
+ .to change { existing_user.read_attribute(:projects) }
703
+ .from([{title: 'Genesis'}]).to([{'title' => 'Genesis'}, {'title' => 'Project 1'}, {'title' => 'Project 2'}])
704
+ end
705
+ end
706
+ end