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,572 @@
1
+ require 'spec_helper'
2
+
3
+ describe Granite::Form::Model::Associations::ReferencesMany do
4
+ before do
5
+ stub_model(:dummy)
6
+ stub_class(:author, ActiveRecord::Base) do
7
+ scope :name_starts_with_a, -> { where('name LIKE "a%"') }
8
+
9
+ validates :name, presence: true
10
+ end
11
+
12
+ stub_model(:book) do
13
+ include Granite::Form::Model::Persistence
14
+ include Granite::Form::Model::Associations
15
+
16
+ attribute :title, String
17
+ references_many :authors
18
+ end
19
+ end
20
+
21
+ let(:author) { Author.create!(name: 'Rick') }
22
+ let(:other) { Author.create!(name: 'Ben') }
23
+
24
+ let(:book) { Book.new }
25
+ let(:association) { book.association(:authors) }
26
+
27
+ let(:existing_book) { Book.instantiate title: 'Genesis', author_ids: [author.id] }
28
+ let(:existing_association) { existing_book.association(:authors) }
29
+
30
+ describe 'book#association' do
31
+ specify { expect(association).to be_a described_class }
32
+ specify { expect(association).to eq(book.association(:authors)) }
33
+ end
34
+
35
+ describe 'book#inspect' do
36
+ specify { expect(existing_book.inspect).to eq('#<Book authors: #<ReferencesMany [#<Author id: 1, name: "Rick">]>, title: "Genesis", author_ids: [1]>') }
37
+ end
38
+
39
+ describe '#build' do
40
+ specify { expect(association.build).to be_a Author }
41
+ specify { expect(association.build).not_to be_persisted }
42
+
43
+ specify do
44
+ expect { association.build(name: 'Morty') }
45
+ .to change { book.author_ids }
46
+ .from([]).to([nil])
47
+ end
48
+ specify do
49
+ expect { association.build(name: 'Morty') }
50
+ .to change { association.reader }.from([])
51
+ .to([an_instance_of(Author).and(have_attributes(name: 'Morty'))])
52
+ end
53
+
54
+ specify do
55
+ expect { existing_association.build(name: 'Morty') }
56
+ .to change { existing_book.author_ids }
57
+ .from([author.id]).to([author.id, nil])
58
+ end
59
+ specify do
60
+ expect { existing_association.build(name: 'Morty') }
61
+ .to change { existing_association.reader }.from([author])
62
+ .to([author, an_instance_of(Author).and(have_attributes(name: 'Morty'))])
63
+ end
64
+
65
+ context 'dirty' do
66
+ before do
67
+ Book.include Granite::Form::Model::Dirty
68
+ end
69
+
70
+ specify do
71
+ expect { existing_association.build(name: 'Morty') }
72
+ .to change { existing_book.changes }
73
+ .from({}).to('author_ids' => [[author.id], [author.id, nil]])
74
+ end
75
+ end
76
+ end
77
+
78
+ describe '#create' do
79
+ specify { expect(association.create).to be_a Author }
80
+ specify { expect(association.create).not_to be_persisted }
81
+
82
+ specify { expect(association.create(name: 'Morty')).to be_a Author }
83
+ specify { expect(association.create(name: 'Morty')).to be_persisted }
84
+
85
+ specify do
86
+ expect { association.create }
87
+ .to change { book.author_ids }
88
+ .from([]).to([nil])
89
+ end
90
+ specify do
91
+ expect { association.create }
92
+ .to change { association.target }
93
+ .from([]).to([an_instance_of(Author).and(be_new_record)])
94
+ end
95
+
96
+ specify do
97
+ expect { association.create(name: 'Morty') }
98
+ .to change { book.author_ids }
99
+ .from([]).to([be_a(Integer)])
100
+ end
101
+ specify do
102
+ expect { association.create(name: 'Morty') }
103
+ .to change { association.target }.from([])
104
+ .to([an_instance_of(Author)
105
+ .and(have_attributes(name: 'Morty'))
106
+ .and(be_persisted)])
107
+ end
108
+
109
+ specify do
110
+ expect { existing_association.create }
111
+ .to change { existing_book.author_ids }
112
+ .from([author.id]).to([author.id, nil])
113
+ end
114
+ specify do
115
+ expect { existing_association.create }
116
+ .to change { existing_association.reader }.from([author])
117
+ .to([author, an_instance_of(Author).and(be_new_record)])
118
+ end
119
+
120
+ specify do
121
+ expect { existing_association.create(name: 'Morty') }
122
+ .to change { existing_book.author_ids }
123
+ .from([author.id]).to([author.id, be_a(Integer)])
124
+ end
125
+ specify do
126
+ expect { existing_association.create(name: 'Morty') }
127
+ .to change { existing_association.reader }.from([author])
128
+ .to([author, an_instance_of(Author)
129
+ .and(have_attributes(name: 'Morty'))
130
+ .and(be_persisted)])
131
+ end
132
+
133
+ context 'dirty' do
134
+ before do
135
+ Book.include Granite::Form::Model::Dirty
136
+ end
137
+
138
+ specify do
139
+ expect { existing_association.create(name: 'Morty') }
140
+ .to change { existing_book.changes }
141
+ .from({}).to('author_ids' => [[author.id], [author.id, be_a(Integer)]])
142
+ end
143
+ end
144
+ end
145
+
146
+ describe '#create!' do
147
+ specify { expect { association.create! }.to raise_error ActiveRecord::RecordInvalid }
148
+
149
+ specify { expect(association.create!(name: 'Morty')).to be_a Author }
150
+ specify { expect(association.create!(name: 'Morty')).to be_persisted }
151
+
152
+ specify do
153
+ expect { muffle(ActiveRecord::RecordInvalid) { association.create! } }
154
+ .to change { book.author_ids }
155
+ .from([]).to([nil])
156
+ end
157
+ specify do
158
+ expect { muffle(ActiveRecord::RecordInvalid) { association.create! } }
159
+ .to change { association.target }
160
+ .from([]).to([an_instance_of(Author).and(be_new_record)])
161
+ end
162
+
163
+ specify do
164
+ expect { association.create!(name: 'Morty') }
165
+ .to change { book.author_ids }
166
+ .from([]).to([be_a(Integer)])
167
+ end
168
+ specify do
169
+ expect { association.create!(name: 'Morty') }
170
+ .to change { association.target }.from([])
171
+ .to([an_instance_of(Author)
172
+ .and(have_attributes(name: 'Morty'))
173
+ .and(be_persisted)])
174
+ end
175
+
176
+ specify do
177
+ expect { muffle(ActiveRecord::RecordInvalid) { existing_association.create! } }
178
+ .to change { existing_book.author_ids }
179
+ .from([author.id]).to([author.id, nil])
180
+ end
181
+ specify do
182
+ expect { muffle(ActiveRecord::RecordInvalid) { existing_association.create! } }
183
+ .to change { existing_association.reader }.from([author])
184
+ .to([author, an_instance_of(Author).and(be_new_record)])
185
+ end
186
+
187
+ specify do
188
+ expect { existing_association.create!(name: 'Morty') }
189
+ .to change { existing_book.author_ids }
190
+ .from([author.id]).to([author.id, be_a(Integer)])
191
+ end
192
+ specify do
193
+ expect { existing_association.create!(name: 'Morty') }
194
+ .to change { existing_association.reader }.from([author])
195
+ .to([author, an_instance_of(Author)
196
+ .and(have_attributes(name: 'Morty'))
197
+ .and(be_persisted)])
198
+ end
199
+ end
200
+
201
+ describe '#apply_changes' do
202
+ specify do
203
+ association.build
204
+ expect(association.apply_changes).to eq(false)
205
+ end
206
+ specify do
207
+ association.build
208
+ expect { association.apply_changes }
209
+ .not_to change { association.target.map(&:persisted?) }
210
+ .from([false])
211
+ end
212
+ specify do
213
+ association.build(name: 'Rick')
214
+ expect(association.apply_changes).to eq(true)
215
+ end
216
+ specify do
217
+ association.build(name: 'Rick')
218
+ expect { association.apply_changes }
219
+ .to change { association.target.map(&:persisted?) }
220
+ .from([false]).to([true])
221
+ end
222
+ specify do
223
+ association.build(name: 'Rick')
224
+ expect { association.apply_changes }
225
+ .to change { book.author_ids }
226
+ .from([nil]).to([be_a(Integer)])
227
+ end
228
+ specify do
229
+ existing_association.target.first.name = 'Morty'
230
+ expect { existing_association.apply_changes }
231
+ .not_to change { author.reload.name }
232
+ end
233
+ specify do
234
+ existing_association.target.first.mark_for_destruction
235
+ existing_association.build(name: 'Morty')
236
+ expect { existing_association.apply_changes }
237
+ .to change { existing_book.author_ids }
238
+ .from([author.id, nil]).to([author.id, be_a(Integer)])
239
+ end
240
+ specify do
241
+ existing_association.target.first.mark_for_destruction
242
+ existing_association.build(name: 'Morty')
243
+ expect { existing_association.apply_changes }
244
+ .to change { existing_association.target.map(&:persisted?) }
245
+ .from([true, false]).to([true, true])
246
+ end
247
+ specify do
248
+ existing_association.target.first.destroy!
249
+ existing_association.build(name: 'Morty')
250
+ expect { existing_association.apply_changes }
251
+ .to change { existing_book.author_ids }
252
+ .from([author.id, nil]).to([author.id, be_a(Integer)])
253
+ end
254
+ specify do
255
+ existing_association.target.first.destroy!
256
+ existing_association.build(name: 'Morty')
257
+ expect { existing_association.apply_changes }
258
+ .to change { existing_association.target.map(&:persisted?) }
259
+ .from([false, false]).to([false, true])
260
+ end
261
+
262
+ context ':autosave' do
263
+ before do
264
+ Book.references_many :authors, autosave: true
265
+ end
266
+
267
+ specify do
268
+ association.build
269
+ expect(association.apply_changes).to eq(false)
270
+ end
271
+ specify do
272
+ association.build
273
+ expect { association.apply_changes }
274
+ .not_to change { association.target.map(&:persisted?) }
275
+ .from([false])
276
+ end
277
+ specify do
278
+ association.build(name: 'Rick')
279
+ expect(association.apply_changes).to eq(true)
280
+ end
281
+ specify do
282
+ association.build(name: 'Rick')
283
+ expect { association.apply_changes }
284
+ .to change { association.target.map(&:persisted?) }
285
+ .from([false]).to([true])
286
+ end
287
+ specify do
288
+ association.build(name: 'Rick')
289
+ expect { association.apply_changes }
290
+ .to change { book.author_ids }
291
+ .from([nil]).to([be_a(Integer)])
292
+ end
293
+ specify do
294
+ existing_association.target.first.name = 'Morty'
295
+ expect { existing_association.apply_changes }
296
+ .to change { author.reload.name }
297
+ .from('Rick').to('Morty')
298
+ end
299
+ specify do
300
+ existing_association.target.first.mark_for_destruction
301
+ existing_association.build(name: 'Morty')
302
+ expect { existing_association.apply_changes }
303
+ .to change { existing_book.author_ids }
304
+ .from([author.id, nil]).to([author.id, be_a(Integer)])
305
+ end
306
+ specify do
307
+ existing_association.target.first.mark_for_destruction
308
+ existing_association.build(name: 'Morty')
309
+ expect { existing_association.apply_changes }
310
+ .to change { existing_association.target.map(&:persisted?) }
311
+ .from([true, false]).to([false, true])
312
+ end
313
+ specify do
314
+ existing_association.target.first.destroy!
315
+ existing_association.build(name: 'Morty')
316
+ expect { existing_association.apply_changes }
317
+ .to change { existing_book.author_ids }
318
+ .from([author.id, nil]).to([author.id, be_a(Integer)])
319
+ end
320
+ specify do
321
+ existing_association.target.first.destroy!
322
+ existing_association.build(name: 'Morty')
323
+ expect { existing_association.apply_changes }
324
+ .to change { existing_association.target.map(&:persisted?) }
325
+ .from([false, false]).to([false, true])
326
+ end
327
+ end
328
+ end
329
+
330
+ describe '#apply_changes!' do
331
+ specify do
332
+ association.build
333
+ expect { association.apply_changes! }
334
+ .to raise_error(Granite::Form::AssociationChangesNotApplied)
335
+ end
336
+ specify do
337
+ association.build
338
+ expect { muffle(Granite::Form::AssociationChangesNotApplied) { association.apply_changes! } }
339
+ .not_to change { association.target.map(&:persisted?) }
340
+ .from([false])
341
+ end
342
+
343
+ context ':autosave' do
344
+ before do
345
+ Book.references_many :authors, autosave: true
346
+ end
347
+
348
+ specify do
349
+ association.build
350
+ expect { association.apply_changes! }
351
+ .to raise_error(Granite::Form::AssociationChangesNotApplied)
352
+ end
353
+ specify do
354
+ association.build
355
+ expect { muffle(Granite::Form::AssociationChangesNotApplied) { association.apply_changes! } }
356
+ .not_to change { association.target.map(&:persisted?) }
357
+ .from([false])
358
+ end
359
+ end
360
+ end
361
+
362
+ describe '#scope' do
363
+ specify { expect(association.scope).to be_a ActiveRecord::Relation }
364
+ specify { expect(association.scope).to respond_to(:where) }
365
+ specify { expect(association.scope).to respond_to(:name_starts_with_a) }
366
+ end
367
+
368
+ describe '#target' do
369
+ specify { expect(association.target).to eq([]) }
370
+ specify { expect(existing_association.target).to eq(existing_book.authors) }
371
+ specify { expect { association.concat author }.to change { association.target.count }.to(1) }
372
+ end
373
+
374
+ describe '#default' do
375
+ before { Book.references_many :authors, default: ->(_book) { author.id } }
376
+ let(:existing_book) { Book.instantiate title: 'Genesis' }
377
+
378
+ specify { expect(association.target).to eq([author]) }
379
+ specify { expect { association.replace([other]) }.to change { association.target }.to([other]) }
380
+ specify { expect { association.replace([]) }.to change { association.target }.to eq([]) }
381
+
382
+ specify { expect(existing_association.target).to eq([]) }
383
+ specify { expect { existing_association.replace([other]) }.to change { existing_association.target }.to([other]) }
384
+ specify { expect { existing_association.replace([]) }.not_to change { existing_association.target } }
385
+ end
386
+
387
+ describe '#loaded?' do
388
+ specify { expect(association.loaded?).to eq(false) }
389
+ specify { expect { association.target }.to change { association.loaded? }.to(true) }
390
+ specify { expect { association.replace([]) }.to change { association.loaded? }.to(true) }
391
+ specify { expect { existing_association.replace([]) }.to change { existing_association.loaded? }.to(true) }
392
+ end
393
+
394
+ describe '#reload' do
395
+ specify { expect(association.reload).to eq([]) }
396
+
397
+ specify { expect(existing_association.reload).to eq(existing_book.authors) }
398
+
399
+ context do
400
+ before { existing_association.reader.last.name = 'Conan' }
401
+ specify do
402
+ expect { existing_association.reload }
403
+ .to change { existing_association.reader.map(&:name) }
404
+ .from(['Conan']).to(['Rick'])
405
+ end
406
+ end
407
+ end
408
+
409
+ describe '#reader' do
410
+ specify { expect(association.reader).to eq([]) }
411
+ specify { expect(association.reader).to be_a Granite::Form::Model::Associations::PersistenceAdapters::ActiveRecord::ReferencedProxy }
412
+
413
+ specify { expect(existing_association.reader.first).to be_a Author }
414
+ specify { expect(existing_association.reader.first).to be_persisted }
415
+
416
+ context do
417
+ before { association.concat author }
418
+ specify { expect(association.reader.last).to be_a Author }
419
+ specify { expect(association.reader.size).to eq(1) }
420
+ specify { expect(association.reader(true)).to eq([author]) }
421
+ end
422
+
423
+ context do
424
+ before { existing_association.concat other }
425
+ specify { expect(existing_association.reader.size).to eq(2) }
426
+ specify { expect(existing_association.reader.last.name).to eq('Ben') }
427
+ specify { expect(existing_association.reader(true).size).to eq(2) }
428
+ specify { expect(existing_association.reader(true).last.name).to eq('Ben') }
429
+ end
430
+
431
+ context 'proxy missing method delection' do
432
+ specify { expect(existing_association.reader).to respond_to(:where) }
433
+ specify { expect(existing_association.reader).to respond_to(:name_starts_with_a) }
434
+ end
435
+ end
436
+
437
+ describe '#writer' do
438
+ let(:new_author1) { Author.create!(name: 'John') }
439
+ let(:new_author2) { Author.create!(name: 'Adam') }
440
+ let(:new_author3) { Author.new(name: 'Jane') }
441
+
442
+ specify do
443
+ expect { association.writer([Dummy.new]) }
444
+ .to raise_error Granite::Form::AssociationTypeMismatch
445
+ end
446
+
447
+ specify { expect { association.writer(nil) }.to raise_error NoMethodError }
448
+ specify { expect { association.writer(new_author1) }.to raise_error NoMethodError }
449
+ specify { expect(association.writer([])).to eq([]) }
450
+
451
+ specify { expect(association.writer([new_author1])).to eq([new_author1]) }
452
+ specify do
453
+ expect { association.writer([new_author1]) }
454
+ .to change { association.reader.map(&:name) }.from([]).to(['John'])
455
+ end
456
+ specify do
457
+ expect { association.writer([new_author1]) }
458
+ .to change { book.read_attribute(:author_ids) }
459
+ .from([]).to([new_author1.id])
460
+ end
461
+
462
+ specify do
463
+ expect { existing_association.writer([new_author1, Dummy.new, new_author2]) }
464
+ .to raise_error Granite::Form::AssociationTypeMismatch
465
+ end
466
+ specify do
467
+ expect { muffle(Granite::Form::AssociationTypeMismatch) { existing_association.writer([new_author1, Dummy.new, new_author2]) } }
468
+ .not_to change { existing_book.read_attribute(:author_ids) }
469
+ end
470
+ specify do
471
+ expect { muffle(Granite::Form::AssociationTypeMismatch) { existing_association.writer([new_author1, Dummy.new, new_author2]) } }
472
+ .not_to change { existing_association.reader }
473
+ end
474
+
475
+ specify { expect { existing_association.writer(nil) }.to raise_error NoMethodError }
476
+ specify do
477
+ expect { muffle(NoMethodError) { existing_association.writer(nil) } }
478
+ .not_to change { existing_book.read_attribute(:author_ids) }
479
+ end
480
+ specify do
481
+ expect { muffle(NoMethodError) { existing_association.writer(nil) } }
482
+ .not_to change { existing_association.reader }
483
+ end
484
+
485
+ specify { expect(existing_association.writer([])).to eq([]) }
486
+ specify do
487
+ expect { existing_association.writer([]) }
488
+ .to change { existing_book.read_attribute(:author_ids) }.to([])
489
+ end
490
+ specify do
491
+ expect { existing_association.writer([]) }
492
+ .to change { existing_association.reader }.from([author]).to([])
493
+ end
494
+
495
+ specify { expect(existing_association.writer([new_author1, new_author2])).to eq([new_author1, new_author2]) }
496
+ specify do
497
+ expect { existing_association.writer([new_author1, new_author2]) }
498
+ .to change { existing_association.reader.map(&:name) }
499
+ .from(['Rick']).to(%w[John Adam])
500
+ end
501
+ specify do
502
+ expect { existing_association.writer([new_author1, new_author2]) }
503
+ .to change { existing_book.read_attribute(:author_ids) }
504
+ .from([author.id]).to([new_author1.id, new_author2.id])
505
+ end
506
+
507
+ specify do
508
+ expect { existing_association.writer([new_author3]) }
509
+ .to change { existing_association.target }.from([author]).to([new_author3])
510
+ end
511
+ specify do
512
+ expect { existing_association.writer([new_author3]) }
513
+ .to change { existing_book.read_attribute(:author_ids) }
514
+ .from([author.id]).to([nil])
515
+ end
516
+ end
517
+
518
+ describe '#concat' do
519
+ let(:new_author1) { Author.create!(name: 'John') }
520
+ let(:new_author2) { Author.create!(name: 'Adam') }
521
+
522
+ specify do
523
+ expect { association.concat(Dummy.new) }
524
+ .to raise_error Granite::Form::AssociationTypeMismatch
525
+ end
526
+
527
+ specify { expect { association.concat(nil) }.to raise_error Granite::Form::AssociationTypeMismatch }
528
+ specify { expect(association.concat([])).to eq([]) }
529
+ specify { expect(existing_association.concat([])).to eq(existing_book.authors) }
530
+ specify { expect(existing_association.concat).to eq(existing_book.authors) }
531
+
532
+ specify { expect(association.concat(new_author1)).to eq([new_author1]) }
533
+ specify do
534
+ expect { association.concat(new_author1) }
535
+ .to change { association.reader.map(&:name) }.from([]).to(['John'])
536
+ end
537
+ specify do
538
+ expect { association.concat(new_author1) }
539
+ .to change { book.read_attribute(:author_ids) }.from([]).to([1])
540
+ end
541
+
542
+ specify do
543
+ expect { existing_association.concat(new_author1, Dummy.new, new_author2) }
544
+ .to raise_error Granite::Form::AssociationTypeMismatch
545
+ end
546
+ specify do
547
+ expect { muffle(Granite::Form::AssociationTypeMismatch) { existing_association.concat(new_author1, Dummy.new, new_author2) } }
548
+ .to change { existing_book.read_attribute(:author_ids) }
549
+ .from([author.id]).to([author.id, new_author1.id])
550
+ end
551
+ specify do
552
+ expect { muffle(Granite::Form::AssociationTypeMismatch) { existing_association.concat(new_author1, Dummy.new, new_author2) } }
553
+ .to change { existing_association.reader.map(&:name) }
554
+ .from(['Rick']).to(%w[Rick John])
555
+ end
556
+
557
+ specify do
558
+ expect(existing_association.concat(new_author1, new_author2))
559
+ .to eq([author, new_author1, new_author2])
560
+ end
561
+ specify do
562
+ expect { existing_association.concat([new_author1, new_author2]) }
563
+ .to change { existing_association.reader.map(&:name) }
564
+ .from(['Rick']).to(%w[Rick John Adam])
565
+ end
566
+ specify do
567
+ expect { existing_association.concat([new_author1, new_author2]) }
568
+ .to change { existing_book.read_attribute(:author_ids) }
569
+ .from([author.id]).to([author.id, new_author1.id, new_author2.id])
570
+ end
571
+ end
572
+ end