granite-form 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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