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,445 @@
1
+ require 'spec_helper'
2
+
3
+ describe Granite::Form::Model::Associations::ReferencesOne do
4
+ before do
5
+ stub_class(:author, ActiveRecord::Base) do
6
+ validates :name, presence: true
7
+ end
8
+
9
+ stub_model(:book) do
10
+ include Granite::Form::Model::Persistence
11
+ include Granite::Form::Model::Associations
12
+
13
+ attribute :title, String
14
+ references_one :author
15
+ end
16
+ end
17
+
18
+ let(:author) { Author.create!(name: 'Johny') }
19
+ let(:other) { Author.create!(name: 'Other') }
20
+ let(:book) { Book.new }
21
+ let(:association) { book.association(:author) }
22
+
23
+ let(:existing_book) { Book.instantiate title: 'My Life', author_id: author.id }
24
+ let(:existing_association) { existing_book.association(:author) }
25
+
26
+ describe 'book#association' do
27
+ specify { expect(association).to be_a described_class }
28
+ specify { expect(association).to eq(book.association(:author)) }
29
+ end
30
+
31
+ describe 'book#inspect' do
32
+ specify { expect(existing_book.inspect).to eq('#<Book author: #<ReferencesOne #<Author id: 1, name: "Johny">>, title: "My Life", author_id: 1>') }
33
+ end
34
+
35
+ describe '#build' do
36
+ specify { expect(association.build).to be_a Author }
37
+ specify { expect(association.build).not_to be_persisted }
38
+
39
+ specify do
40
+ expect { association.build(name: 'Morty') }
41
+ .not_to change { book.author_id }
42
+ end
43
+ specify do
44
+ expect { association.build(name: 'Morty') }
45
+ .to change { book.author }.from(nil)
46
+ .to(an_instance_of(Author).and(have_attributes(name: 'Morty')))
47
+ end
48
+
49
+ specify do
50
+ expect { existing_association.build(name: 'Morty') }
51
+ .to change { existing_book.author_id }
52
+ .from(author.id).to(nil)
53
+ end
54
+ specify do
55
+ expect { existing_association.build(name: 'Morty') }
56
+ .to change { existing_book.author }.from(author)
57
+ .to(an_instance_of(Author).and(have_attributes(name: 'Morty')))
58
+ end
59
+
60
+ context 'dirty' do
61
+ before do
62
+ Book.include Granite::Form::Model::Dirty
63
+ end
64
+
65
+ specify do
66
+ expect { existing_association.build(name: 'Morty') }
67
+ .to change { existing_book.changes }
68
+ .from({}).to('author_id' => [author.id, nil])
69
+ end
70
+ end
71
+ end
72
+
73
+ describe '#create' do
74
+ specify { expect(association.create).to be_a Author }
75
+ specify { expect(association.create).not_to be_persisted }
76
+
77
+ specify { expect(association.create(name: 'Fred')).to be_a Author }
78
+ specify { expect(association.create(name: 'Fred')).to be_persisted }
79
+
80
+ specify do
81
+ expect { association.create }
82
+ .not_to change { book.author_id }
83
+ end
84
+ specify do
85
+ expect { association.create(name: 'Fred') }
86
+ .to change { book.author_id }
87
+ .from(nil).to(be_a(Integer))
88
+ end
89
+
90
+ specify do
91
+ expect { existing_association.create }
92
+ .to change { existing_book.author_id }
93
+ .from(author.id).to(nil)
94
+ end
95
+ specify do
96
+ expect { existing_association.create(name: 'Fred') }
97
+ .to change { existing_book.author_id }
98
+ .from(author.id).to(be_a(Integer))
99
+ end
100
+
101
+ context 'dirty' do
102
+ before do
103
+ Book.include Granite::Form::Model::Dirty
104
+ end
105
+
106
+ specify do
107
+ expect { existing_association.create(name: 'Fred') }
108
+ .to change { existing_book.changes }
109
+ .from({}).to('author_id' => [author.id, be_a(Integer)])
110
+ end
111
+ end
112
+ end
113
+
114
+ describe '#create!' do
115
+ specify { expect { association.create! }.to raise_error ActiveRecord::RecordInvalid }
116
+ specify do
117
+ expect { muffle(ActiveRecord::RecordInvalid) { association.create! } }
118
+ .to change { association.target }
119
+ .from(nil).to(an_instance_of(Author))
120
+ end
121
+
122
+ specify { expect(association.create!(name: 'Fred')).to be_a Author }
123
+ specify { expect(association.create!(name: 'Fred')).to be_persisted }
124
+
125
+ specify do
126
+ expect { muffle(ActiveRecord::RecordInvalid) { association.create! } }
127
+ .not_to change { book.author_id }
128
+ end
129
+ specify do
130
+ expect { muffle(ActiveRecord::RecordInvalid) { association.create! } }
131
+ .to change { association.reader.try(:attributes).try(:slice, 'name') }
132
+ .from(nil).to('name' => nil)
133
+ end
134
+ specify do
135
+ expect { association.create(name: 'Fred') }
136
+ .to change { book.author_id }
137
+ .from(nil).to(be_a(Integer))
138
+ end
139
+
140
+ specify do
141
+ expect { muffle(ActiveRecord::RecordInvalid) { existing_association.create! } }
142
+ .to change { existing_book.author_id }
143
+ .from(author.id).to(nil)
144
+ end
145
+ specify do
146
+ expect { muffle(ActiveRecord::RecordInvalid) { existing_association.create! } }
147
+ .to change { existing_association.reader.try(:attributes).try(:slice, 'name') }
148
+ .from('name' => 'Johny').to('name' => nil)
149
+ end
150
+ specify do
151
+ expect { existing_association.create!(name: 'Fred') }
152
+ .to change { existing_book.author_id }
153
+ .from(author.id).to(be_a(Integer))
154
+ end
155
+ end
156
+
157
+ context do
158
+ shared_examples 'apply_changes' do |method|
159
+ specify do
160
+ association.build(name: 'Fred')
161
+ expect(association.send(method)).to eq(true)
162
+ end
163
+ specify do
164
+ association.build(name: 'Fred')
165
+ expect { association.send(method) }
166
+ .to change { association.target.persisted? }.to(true)
167
+ end
168
+ specify do
169
+ association.build(name: 'Fred')
170
+ expect { association.send(method) }
171
+ .to change { book.author_id }
172
+ .from(nil).to(be_a(Integer))
173
+ end
174
+ specify do
175
+ existing_association.target.name = 'Fred'
176
+ expect { existing_association.send(method) }
177
+ .not_to change { author.reload.name }
178
+ end
179
+ specify do
180
+ existing_association.target.mark_for_destruction
181
+ expect { existing_association.send(method) }
182
+ .not_to change { existing_association.target.destroyed? }
183
+ end
184
+ specify do
185
+ existing_association.target.mark_for_destruction
186
+ expect { existing_association.send(method) }
187
+ .not_to change { existing_book.author_id }
188
+ end
189
+ specify do
190
+ existing_association.target.destroy!
191
+ expect { existing_association.send(method) }
192
+ .not_to change { existing_association.target.destroyed? }
193
+ end
194
+ specify do
195
+ existing_association.target.destroy!
196
+ expect { existing_association.send(method) }
197
+ .not_to change { existing_book.author_id }
198
+ end
199
+
200
+ context ':autosave' do
201
+ before do
202
+ Book.references_one :author, autosave: true
203
+ end
204
+
205
+ specify do
206
+ association.build(name: 'Fred')
207
+ expect(association.send(method)).to eq(true)
208
+ end
209
+ specify do
210
+ association.build(name: 'Fred')
211
+ expect { association.send(method) }
212
+ .to change { association.target.persisted? }.to(true)
213
+ end
214
+ specify do
215
+ existing_association.target.name = 'Fred'
216
+ expect { existing_association.send(method) }
217
+ .to change { author.reload.name }.from('Johny').to('Fred')
218
+ end
219
+ specify do
220
+ existing_association.target.mark_for_destruction
221
+ expect { existing_association.send(method) }
222
+ .to change { existing_association.target.destroyed? }
223
+ .from(false).to(true)
224
+ end
225
+ specify do
226
+ existing_association.target.mark_for_destruction
227
+ expect { existing_association.send(method) }
228
+ .not_to change { existing_book.author_id }
229
+ .from(author.id)
230
+ end
231
+ specify do
232
+ existing_association.target.destroy!
233
+ expect { existing_association.send(method) }
234
+ .not_to change { existing_association.target.destroyed? }
235
+ .from(true)
236
+ end
237
+ specify do
238
+ existing_association.target.destroy!
239
+ expect { existing_association.send(method) }
240
+ .not_to change { existing_book.author_id }
241
+ .from(author.id)
242
+ end
243
+ end
244
+ end
245
+
246
+ describe '#apply_changes' do
247
+ include_examples 'apply_changes', :apply_changes
248
+
249
+ specify do
250
+ association.build
251
+ expect(association.apply_changes).to eq(false)
252
+ end
253
+ specify do
254
+ association.build
255
+ expect { association.apply_changes }
256
+ .not_to change { association.target.persisted? }.from(false)
257
+ end
258
+
259
+ context ':autosave' do
260
+ before do
261
+ Book.references_one :author, autosave: true
262
+ end
263
+
264
+ specify do
265
+ association.build
266
+ expect(association.apply_changes).to eq(false)
267
+ end
268
+ specify do
269
+ association.build
270
+ expect { association.apply_changes }
271
+ .not_to change { association.target.persisted? }.from(false)
272
+ end
273
+ end
274
+ end
275
+
276
+ describe '#apply_changes!' do
277
+ include_examples 'apply_changes', :apply_changes!
278
+
279
+ specify do
280
+ association.build
281
+ expect { association.apply_changes! }
282
+ .to raise_error(Granite::Form::AssociationChangesNotApplied)
283
+ end
284
+ specify do
285
+ association.build
286
+ expect { muffle(Granite::Form::AssociationChangesNotApplied) { association.apply_changes! } }
287
+ .not_to change { association.target.persisted? }.from(false)
288
+ end
289
+
290
+ context ':autosave' do
291
+ before do
292
+ Book.references_one :author, autosave: true
293
+ end
294
+
295
+ specify do
296
+ association.build
297
+ expect { association.apply_changes! }
298
+ .to raise_error(Granite::Form::AssociationChangesNotApplied)
299
+ end
300
+ specify do
301
+ association.build
302
+ expect { muffle(Granite::Form::AssociationChangesNotApplied) { association.apply_changes! } }
303
+ .not_to change { association.target.persisted? }.from(false)
304
+ end
305
+ end
306
+ end
307
+ end
308
+
309
+ describe '#target' do
310
+ specify { expect(association.target).to be_nil }
311
+ specify { expect(existing_association.target).to eq(existing_book.author) }
312
+ end
313
+
314
+ describe '#loaded?' do
315
+ let(:new_author) { Author.create(name: 'Morty') }
316
+
317
+ specify { expect(association.loaded?).to eq(false) }
318
+ specify { expect { association.target }.to change { association.loaded? }.to(true) }
319
+ specify { expect { association.replace(new_author) }.to change { association.loaded? }.to(true) }
320
+ specify { expect { association.replace(nil) }.to change { association.loaded? }.to(true) }
321
+ specify { expect { existing_association.replace(new_author) }.to change { existing_association.loaded? }.to(true) }
322
+ specify { expect { existing_association.replace(nil) }.to change { existing_association.loaded? }.to(true) }
323
+ end
324
+
325
+ describe '#reload' do
326
+ specify { expect(association.reload).to be_nil }
327
+
328
+ specify { expect(existing_association.reload).to be_a Author }
329
+ specify { expect(existing_association.reload).to be_persisted }
330
+
331
+ context do
332
+ before { existing_association.reader.name = 'New' }
333
+ specify do
334
+ expect { existing_association.reload }
335
+ .to change { existing_association.reader.name }
336
+ .from('New').to('Johny')
337
+ end
338
+ end
339
+ end
340
+
341
+ describe '#reader' do
342
+ specify { expect(association.reader).to be_nil }
343
+
344
+ specify { expect(existing_association.reader).to be_a Author }
345
+ specify { expect(existing_association.reader).to be_persisted }
346
+ end
347
+
348
+ describe '#default' do
349
+ before { Book.references_one :author, default: ->(_book) { author.id } }
350
+ let(:existing_book) { Book.instantiate title: 'My Life' }
351
+
352
+ specify { expect(association.target).to eq(author) }
353
+ specify { expect { association.replace(other) }.to change { association.target }.to(other) }
354
+ specify { expect { association.replace(nil) }.to change { association.target }.to be_nil }
355
+
356
+ specify { expect(existing_association.target).to be_nil }
357
+ specify { expect { existing_association.replace(other) }.to change { existing_association.target }.to(other) }
358
+ specify { expect { existing_association.replace(nil) }.not_to change { existing_association.target } }
359
+ end
360
+
361
+ describe '#writer' do
362
+ context 'new owner' do
363
+ let(:new_author) { Author.new(name: 'Morty') }
364
+
365
+ let(:book) do
366
+ Book.new.tap do |book|
367
+ book.send(:mark_persisted!)
368
+ end
369
+ end
370
+
371
+ specify do
372
+ expect { association.writer(nil) }
373
+ .not_to change { book.author_id }
374
+ end
375
+ specify do
376
+ expect { association.writer(new_author) }
377
+ .to change { muffle(NoMethodError) { association.reader.name } }
378
+ .from(nil).to('Morty')
379
+ end
380
+ specify do
381
+ expect { association.writer(new_author) }
382
+ .not_to change { book.author_id }.from(nil)
383
+ end
384
+ end
385
+
386
+ context 'persisted owner' do
387
+ let(:new_author) { Author.create(name: 'Morty') }
388
+
389
+ specify do
390
+ expect { association.writer(stub_model(:dummy).new) }
391
+ .to raise_error Granite::Form::AssociationTypeMismatch
392
+ end
393
+
394
+ specify { expect(association.writer(nil)).to be_nil }
395
+ specify { expect(association.writer(new_author)).to eq(new_author) }
396
+ specify do
397
+ expect { association.writer(nil) }
398
+ .not_to change { book.read_attribute(:author_id) }
399
+ end
400
+ specify do
401
+ expect { association.writer(new_author) }
402
+ .to change { association.reader.try(:attributes) }.from(nil).to('id' => 1, 'name' => 'Morty')
403
+ end
404
+ specify do
405
+ expect { association.writer(new_author) }
406
+ .to change { book.read_attribute(:author_id) }
407
+ end
408
+
409
+ context do
410
+ before do
411
+ stub_class(:dummy, ActiveRecord::Base) do
412
+ self.table_name = :authors
413
+ end
414
+ end
415
+
416
+ specify do
417
+ expect { muffle(Granite::Form::AssociationTypeMismatch) { existing_association.writer(Dummy.new) } }
418
+ .not_to change { existing_book.read_attribute(:author_id) }
419
+ end
420
+ specify do
421
+ expect { muffle(Granite::Form::AssociationTypeMismatch) { existing_association.writer(Dummy.new) } }
422
+ .not_to change { existing_association.reader }
423
+ end
424
+ end
425
+
426
+ specify { expect(existing_association.writer(nil)).to be_nil }
427
+ specify { expect(existing_association.writer(new_author)).to eq(new_author) }
428
+ specify do
429
+ expect { existing_association.writer(nil) }
430
+ .to change { existing_book.read_attribute(:author_id) }
431
+ .from(author.id).to(nil)
432
+ end
433
+ specify do
434
+ expect { existing_association.writer(new_author) }
435
+ .to change { existing_association.reader.try(:attributes) }
436
+ .from('id' => 1, 'name' => 'Johny').to('id' => 2, 'name' => 'Morty')
437
+ end
438
+ specify do
439
+ expect { existing_association.writer(new_author) }
440
+ .to change { existing_book.read_attribute(:author_id) }
441
+ .from(author.id).to(new_author.id)
442
+ end
443
+ end
444
+ end
445
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+ require 'granite/form/base'
3
+
4
+ describe Granite::Form::Model::Associations::Reflections::EmbedsAny do
5
+ describe '#build' do
6
+ subject { described_class.build(User, User, :projects) {}.klass.new }
7
+
8
+ before do
9
+ stub_model(:project) do
10
+ include Granite::Form::Model::Lifecycle
11
+ attribute :title, String
12
+ end
13
+ stub_model(:user) do
14
+ include Granite::Form::Model::Associations
15
+
16
+ attribute :name, String
17
+ embeds_many :projects
18
+ end
19
+ end
20
+
21
+ it { is_expected.to be_a(Granite::Form::Model) }
22
+ it { is_expected.to be_a(Granite::Form::Model::Primary) }
23
+ it { is_expected.to be_a(Granite::Form::Model::Lifecycle) }
24
+ it { is_expected.to be_a(Granite::Form::Model::Associations) }
25
+
26
+ context 'when Granite::Form.base_concern is defined' do
27
+ before do
28
+ stub_const('MyModule', Module.new)
29
+
30
+ allow(Granite::Form).to receive(:base_concern).and_return(MyModule)
31
+
32
+ stub_model(:user) do
33
+ include Granite::Form::Model::Associations
34
+
35
+ embeds_many :projects
36
+ end
37
+ end
38
+
39
+ it { is_expected.to be_a(MyModule) }
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,145 @@
1
+ require 'spec_helper'
2
+
3
+ describe Granite::Form::Model::Associations::Reflections::EmbedsMany do
4
+ before do
5
+ stub_model(:project) do
6
+ include Granite::Form::Model::Lifecycle
7
+ attribute :title, String
8
+ end
9
+ stub_model(:user) do
10
+ include Granite::Form::Model::Associations
11
+
12
+ attribute :name, String
13
+ embeds_many :projects
14
+ end
15
+ end
16
+ let(:user) { User.new }
17
+
18
+ context ':read, :write' do
19
+ before do
20
+ stub_model(:user) do
21
+ include Granite::Form::Model::Persistence
22
+ include Granite::Form::Model::Associations
23
+
24
+ attribute :name
25
+ embeds_many :projects,
26
+ read: lambda { |reflection, object|
27
+ value = object.read_attribute(reflection.name)
28
+ JSON.parse(value) if value.present?
29
+ },
30
+ write: lambda { |reflection, object, value|
31
+ object.write_attribute(reflection.name, value.to_json)
32
+ }
33
+ end
34
+ end
35
+
36
+ let(:user) { User.instantiate name: 'Rick', projects: [{title: 'Genesis'}].to_json }
37
+ let(:new_project1) { Project.new(title: 'Project 1') }
38
+ let(:new_project2) { Project.new(title: 'Project 2') }
39
+
40
+ specify do
41
+ expect { user.projects.concat([new_project1, new_project2]) }
42
+ .to change { user.read_attribute(:projects) }
43
+ .from([{title: 'Genesis'}].to_json)
44
+ .to([{title: 'Genesis'}, {title: 'Project 1'}, {title: 'Project 2'}].to_json)
45
+ end
46
+ end
47
+
48
+ describe '#projects' do
49
+ specify { expect(user.projects).to eq([]) }
50
+
51
+ describe '#build' do
52
+ let(:project) { Project.new title: 'Project' }
53
+ specify { expect(user.projects.build(title: 'Project')).to eq(project) }
54
+ specify { expect { user.projects.build(title: 'Project') }.to change { user.projects }.from([]).to([project]) }
55
+ end
56
+
57
+ describe '#create' do
58
+ let(:project) { Project.new title: 'Project' }
59
+ specify { expect(user.projects.create(title: 'Project')).to eq(project) }
60
+ specify { expect { user.projects.create(title: 'Project') }.to change { user.projects }.from([]).to([project]) }
61
+ end
62
+
63
+ describe '#create!' do
64
+ let(:project) { Project.new title: 'Project' }
65
+ specify { expect(user.projects.create!(title: 'Project')).to eq(project) }
66
+ specify { expect { user.projects.create!(title: 'Project') }.to change { user.projects }.from([]).to([project]) }
67
+ end
68
+
69
+ describe '#reload' do
70
+ let(:project) { Project.new title: 'Project' }
71
+ before do
72
+ user.update(projects: [project])
73
+ user.apply_association_changes!
74
+ end
75
+ before { user.projects.build }
76
+
77
+ specify { expect(user.projects.count).to eq(2) }
78
+ specify { expect(user.projects.reload).to eq([project]) }
79
+ end
80
+
81
+ describe '#concat' do
82
+ let(:project) { Project.new title: 'Project' }
83
+ specify { expect { user.projects.concat project }.to change { user.projects }.from([]).to([project]) }
84
+ specify { expect { user.projects.concat project, 'string' }.to raise_error Granite::Form::AssociationTypeMismatch }
85
+
86
+ context do
87
+ let(:other) { Project.new title: 'Other' }
88
+ before { user.projects = [other] }
89
+ specify { expect { user.projects.concat project }.to change { user.projects }.from([other]).to([other, project]) }
90
+ end
91
+ end
92
+ end
93
+
94
+ describe '#projects=' do
95
+ let(:project) { Project.new title: 'Project' }
96
+ specify { expect { user.projects = [] }.not_to change { user.projects }.from([]) }
97
+ specify { expect { user.projects = [project] }.to change { user.projects }.from([]).to([project]) }
98
+ specify { expect { user.projects = [project, 'string'] }.to raise_error Granite::Form::AssociationTypeMismatch }
99
+
100
+ context do
101
+ let(:other) { Project.new title: 'Other' }
102
+ before { user.projects = [other] }
103
+ specify { expect { user.projects = [project] }.to change { user.projects }.from([other]).to([project]) }
104
+ specify { expect { user.projects = [] }.to change { user.projects }.from([other]).to([]) }
105
+ end
106
+ end
107
+
108
+ context 'on the fly' do
109
+ context do
110
+ before do
111
+ stub_model(:user) do
112
+ include Granite::Form::Model::Associations
113
+
114
+ attribute :title, String
115
+ embeds_many :projects do
116
+ attribute :title, String
117
+ end
118
+ end
119
+ end
120
+
121
+ specify { expect(User.reflect_on_association(:projects).klass).to eq(User::Project) }
122
+ specify { expect(User.new.projects).to eq([]) }
123
+ specify { expect(User.new.tap { |u| u.projects.create(title: 'Project') }.projects).to be_a(Granite::Form::Model::Associations::Collection::Embedded) }
124
+ specify { expect(User.new.tap { |u| u.projects.create(title: 'Project') }.read_attribute(:projects)).to eq([{'title' => 'Project'}]) }
125
+ end
126
+
127
+ context do
128
+ before do
129
+ stub_model(:user) do
130
+ include Granite::Form::Model::Associations
131
+
132
+ attribute :title, String
133
+ embeds_many :projects, class_name: 'Project' do
134
+ attribute :value, String
135
+ end
136
+ end
137
+ end
138
+
139
+ specify { expect(User.reflect_on_association(:projects).klass).to eq(User::Project) }
140
+ specify { expect(User.new.projects).to eq([]) }
141
+ specify { expect(User.new.tap { |u| u.projects.create(title: 'Project') }.projects).to be_a(Granite::Form::Model::Associations::Collection::Embedded) }
142
+ specify { expect(User.new.tap { |u| u.projects.create(title: 'Project') }.read_attribute(:projects)).to eq([{'title' => 'Project', 'value' => nil}]) }
143
+ end
144
+ end
145
+ end