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