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,533 @@
1
+ require 'spec_helper'
2
+
3
+ describe Granite::Form::Model::Associations::EmbedsOne do
4
+ before do
5
+ stub_model(:author) do
6
+ include Granite::Form::Model::Lifecycle
7
+
8
+ attribute :name, String
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
+ embeds_one :author
18
+ end
19
+ end
20
+
21
+ let(:book) { Book.new(title: 'Book') }
22
+ let(:association) { book.association(:author) }
23
+
24
+ let(:existing_book) { Book.instantiate title: 'My Life', author: {'name' => 'Johny'} }
25
+ let(:existing_association) { existing_book.association(:author) }
26
+
27
+ context 'callbacks' do
28
+ before do
29
+ Book.class_eval do
30
+ embeds_one :author, before_add: :before_add, after_add: :after_add
31
+
32
+ def before_add(object)
33
+ callbacks.push([:before_add, object])
34
+ end
35
+
36
+ def after_add(object)
37
+ callbacks.push([:after_add, object])
38
+ end
39
+
40
+ collection :callbacks, Array
41
+ end
42
+ end
43
+ let(:author1) { Author.new(name: 'Author1') }
44
+ let(:author2) { Author.new(name: 'Author2') }
45
+
46
+ specify do
47
+ expect { association.build(name: 'Author1') }
48
+ .to change { book.callbacks }
49
+ .to([[:before_add, author1], [:after_add, author1]])
50
+ end
51
+
52
+ specify do
53
+ expect do
54
+ association.build(name: 'Author1')
55
+ association.build(name: 'Author2')
56
+ end
57
+ .to change { book.callbacks }
58
+ .to([
59
+ [:before_add, author1], [:after_add, author1],
60
+ [:before_add, author2], [:after_add, author2]
61
+ ])
62
+ end
63
+
64
+ specify do
65
+ expect { association.create(name: 'Author1') }
66
+ .to change { book.callbacks }
67
+ .to([[:before_add, author1], [:after_add, author1]])
68
+ end
69
+
70
+ specify do
71
+ expect { association.writer(author1) }
72
+ .to change { book.callbacks }
73
+ .to([[:before_add, author1], [:after_add, author1]])
74
+ end
75
+
76
+ specify do
77
+ expect do
78
+ association.writer(author1)
79
+ association.writer(nil)
80
+ association.writer(author1)
81
+ end
82
+ .to change { book.callbacks }
83
+ .to([
84
+ [:before_add, author1], [:after_add, author1],
85
+ [:before_add, author1], [:after_add, author1]
86
+ ])
87
+ end
88
+
89
+ context 'default' do
90
+ before do
91
+ Book.class_eval do
92
+ embeds_one :author,
93
+ before_add: ->(object) { callbacks.push([:before_add, object]) },
94
+ after_add: ->(object) { callbacks.push([:after_add, object]) },
95
+ default: -> { {name: 'Author1'} }
96
+
97
+ collection :callbacks, Array
98
+ end
99
+ end
100
+
101
+ specify do
102
+ expect { association.writer(author2) }
103
+ .to change { book.callbacks }
104
+ .to([
105
+ [:before_add, author1], [:after_add, author1],
106
+ [:before_add, author2], [:after_add, author2]
107
+ ])
108
+ end
109
+ end
110
+ end
111
+
112
+ describe 'book#association' do
113
+ specify { expect(association).to be_a described_class }
114
+ specify { expect(association).to eq(book.association(:author)) }
115
+ end
116
+
117
+ describe 'author#embedder' do
118
+ let(:author) { Author.new(name: 'Author') }
119
+
120
+ specify { expect(association.build.embedder).to eq(book) }
121
+ specify { expect(association.create.embedder).to eq(book) }
122
+ specify do
123
+ expect { association.writer(author) }
124
+ .to change { author.embedder }.from(nil).to(book)
125
+ end
126
+ specify do
127
+ expect { association.target = author }
128
+ .to change { author.embedder }.from(nil).to(book)
129
+ end
130
+
131
+ context 'default' do
132
+ before do
133
+ Book.class_eval do
134
+ embeds_one :author, default: -> { {name: 'Author1'} }
135
+ end
136
+ end
137
+
138
+ specify { expect(association.target.embedder).to eq(book) }
139
+
140
+ context do
141
+ before do
142
+ Book.class_eval do
143
+ embeds_one :author, default: -> { Author.new(name: 'Author1') }
144
+ end
145
+ end
146
+
147
+ specify { expect(association.target.embedder).to eq(book) }
148
+ end
149
+ end
150
+
151
+ context 'embedding goes before attributes' do
152
+ before do
153
+ Author.class_eval do
154
+ attribute :name, String, normalize: ->(value) { "#{value}#{embedder.title}" }
155
+ end
156
+ end
157
+
158
+ specify { expect(association.build(name: 'Author').name).to eq('AuthorBook') }
159
+ specify { expect(association.create(name: 'Author').name).to eq('AuthorBook') }
160
+ end
161
+ end
162
+
163
+ describe '#build' do
164
+ specify { expect(association.build).to be_a Author }
165
+ specify { expect(association.build).not_to be_persisted }
166
+
167
+ specify do
168
+ expect { association.build(name: 'Fred') }
169
+ .not_to change { book.read_attribute(:author) }
170
+ end
171
+
172
+ specify do
173
+ expect { existing_association.build(name: 'Fred') }
174
+ .not_to change { existing_book.read_attribute(:author) }
175
+ end
176
+ end
177
+
178
+ describe '#create' do
179
+ specify { expect(association.create).to be_a Author }
180
+ specify { expect(association.create).not_to be_persisted }
181
+
182
+ specify { expect(association.create(name: 'Fred')).to be_a Author }
183
+ specify { expect(association.create(name: 'Fred')).to be_persisted }
184
+
185
+ specify do
186
+ expect { association.create }
187
+ .not_to change { book.read_attribute(:author) }
188
+ end
189
+ specify do
190
+ expect { association.create(name: 'Fred') }
191
+ .to change { book.read_attribute(:author) }
192
+ .from(nil).to('name' => 'Fred')
193
+ end
194
+
195
+ specify do
196
+ expect { existing_association.create }
197
+ .not_to change { existing_book.read_attribute(:author) }
198
+ end
199
+ specify do
200
+ expect { existing_association.create(name: 'Fred') }
201
+ .to change { existing_book.read_attribute(:author) }
202
+ .from('name' => 'Johny').to('name' => 'Fred')
203
+ end
204
+ end
205
+
206
+ describe '#create!' do
207
+ specify { expect { association.create! }.to raise_error Granite::Form::ValidationError }
208
+ specify do
209
+ expect { muffle(Granite::Form::ValidationError) { association.create! } }
210
+ .to change { association.target }
211
+ .from(nil).to(an_instance_of(Author))
212
+ end
213
+
214
+ specify { expect(association.create!(name: 'Fred')).to be_a Author }
215
+ specify { expect(association.create!(name: 'Fred')).to be_persisted }
216
+
217
+ specify do
218
+ expect { muffle(Granite::Form::ValidationError) { association.create! } }
219
+ .not_to change { book.read_attribute(:author) }
220
+ end
221
+ specify do
222
+ expect { muffle(Granite::Form::ValidationError) { association.create! } }
223
+ .to change { association.reader.try(:attributes) }
224
+ .from(nil).to('name' => nil)
225
+ end
226
+ specify do
227
+ expect { association.create(name: 'Fred') }
228
+ .to change { book.read_attribute(:author) }
229
+ .from(nil).to('name' => 'Fred')
230
+ end
231
+
232
+ specify do
233
+ expect { muffle(Granite::Form::ValidationError) { existing_association.create! } }
234
+ .not_to change { existing_book.read_attribute(:author) }
235
+ end
236
+ specify do
237
+ expect { muffle(Granite::Form::ValidationError) { existing_association.create! } }
238
+ .to change { existing_association.reader.try(:attributes) }
239
+ .from('name' => 'Johny').to('name' => nil)
240
+ end
241
+ specify do
242
+ expect { existing_association.create!(name: 'Fred') }
243
+ .to change { existing_book.read_attribute(:author) }
244
+ .from('name' => 'Johny').to('name' => 'Fred')
245
+ end
246
+ end
247
+
248
+ describe '#apply_changes' do
249
+ specify do
250
+ association.build
251
+ expect { association.apply_changes }
252
+ .not_to change { association.target.persisted? }.from(false)
253
+ end
254
+ specify do
255
+ association.build(name: 'Fred')
256
+ expect { association.apply_changes }
257
+ .to change { association.target.persisted? }.to(true)
258
+ end
259
+ specify do
260
+ existing_association.target.mark_for_destruction
261
+ expect { existing_association.apply_changes }
262
+ .to change { existing_association.target }.to(nil)
263
+ end
264
+ specify do
265
+ existing_association.target.destroy!
266
+ expect { existing_association.apply_changes }
267
+ .to change { existing_association.target }.to(nil)
268
+ end
269
+ specify do
270
+ existing_association.target.mark_for_destruction
271
+ expect { existing_association.apply_changes }
272
+ .to change { existing_association.destroyed.try(:name) }.from(nil).to('Johny')
273
+ end
274
+ specify do
275
+ existing_association.target.destroy!
276
+ expect { existing_association.apply_changes }
277
+ .to change { existing_association.destroyed.try(:name) }.from(nil).to('Johny')
278
+ end
279
+ end
280
+
281
+ describe '#apply_changes!' do
282
+ specify do
283
+ association.build
284
+ expect { association.apply_changes! }
285
+ .to raise_error Granite::Form::AssociationChangesNotApplied
286
+ end
287
+ specify do
288
+ association.build(name: 'Fred')
289
+ expect { association.apply_changes! }
290
+ .to change { association.target.persisted? }.to(true)
291
+ end
292
+ specify do
293
+ existing_association.target.mark_for_destruction
294
+ expect { existing_association.apply_changes! }
295
+ .to change { existing_association.target }.to(nil)
296
+ end
297
+ specify do
298
+ existing_association.target.destroy!
299
+ expect { existing_association.apply_changes! }
300
+ .to change { existing_association.target }.to(nil)
301
+ end
302
+ end
303
+
304
+ describe '#target' do
305
+ specify { expect(association.target).to be_nil }
306
+ specify { expect(existing_association.target).to eq(existing_book.author) }
307
+ specify { expect { association.build }.to change { association.target }.to(an_instance_of(Author)) }
308
+ end
309
+
310
+ describe '#default' do
311
+ before { Book.embeds_one :author, default: -> { {name: 'Default'} } }
312
+ before do
313
+ Author.class_eval do
314
+ include Granite::Form::Model::Primary
315
+ primary :name
316
+ end
317
+ end
318
+ let(:new_author) { Author.new.tap { |a| a.name = 'Morty' } }
319
+ let(:existing_book) { Book.instantiate title: 'My Life' }
320
+
321
+ specify { expect(association.target.name).to eq('Default') }
322
+ specify { expect(association.target.new_record?).to eq(true) }
323
+ specify { expect { association.replace(new_author) }.to change { association.target.name }.to eq('Morty') }
324
+ specify { expect { association.replace(nil) }.to change { association.target }.to be_nil }
325
+
326
+ specify { expect(existing_association.target).to be_nil }
327
+ specify { expect { existing_association.replace(new_author) }.to change { existing_association.target }.to(an_instance_of(Author)) }
328
+ specify { expect { existing_association.replace(nil) }.not_to change { existing_association.target } }
329
+
330
+ context do
331
+ before { Author.send(:include, Granite::Form::Model::Dirty) }
332
+ specify { expect(association.target).not_to be_changed }
333
+ end
334
+ end
335
+
336
+ describe '#loaded?' do
337
+ let(:new_author) { Author.new(name: 'Morty') }
338
+
339
+ specify { expect(association.loaded?).to eq(false) }
340
+ specify { expect { association.target }.to change { association.loaded? }.to(true) }
341
+ specify { expect { association.build }.to change { association.loaded? }.to(true) }
342
+ specify { expect { association.replace(new_author) }.to change { association.loaded? }.to(true) }
343
+ specify { expect { association.replace(nil) }.to change { association.loaded? }.to(true) }
344
+ specify { expect { existing_association.replace(new_author) }.to change { existing_association.loaded? }.to(true) }
345
+ specify { expect { existing_association.replace(nil) }.to change { existing_association.loaded? }.to(true) }
346
+ end
347
+
348
+ describe '#reload' do
349
+ specify { expect(association.reload).to be_nil }
350
+
351
+ specify { expect(existing_association.reload).to be_a Author }
352
+ specify { expect(existing_association.reload).to be_persisted }
353
+
354
+ context do
355
+ before { association.build(name: 'Fred') }
356
+ specify do
357
+ expect { association.reload }
358
+ .to change { association.reader.try(:attributes) }.from('name' => 'Fred').to(nil)
359
+ end
360
+ end
361
+
362
+ context do
363
+ before { existing_association.build(name: 'Fred') }
364
+ specify do
365
+ expect { existing_association.reload }
366
+ .to change { existing_association.reader.try(:attributes) }
367
+ .from('name' => 'Fred').to('name' => 'Johny')
368
+ end
369
+ end
370
+ end
371
+
372
+ describe '#clear' do
373
+ specify { expect(association.clear).to eq(true) }
374
+ specify { expect { association.clear }.not_to change { association.reader } }
375
+
376
+ specify { expect(existing_association.clear).to eq(true) }
377
+ specify do
378
+ expect { existing_association.clear }
379
+ .to change { existing_association.reader.try(:attributes) }.from('name' => 'Johny').to(nil)
380
+ end
381
+ specify do
382
+ expect { existing_association.clear }
383
+ .to change { existing_book.read_attribute(:author) }.from('name' => 'Johny').to(nil)
384
+ end
385
+
386
+ context do
387
+ before { Author.send(:include, Granite::Form::Model::Callbacks) }
388
+ if ActiveModel.version >= Gem::Version.new('5.0.0')
389
+ before { Author.before_destroy { throw :abort } }
390
+ else
391
+ before { Author.before_destroy { false } }
392
+ end
393
+ specify { expect(existing_association.clear).to eq(false) }
394
+ specify do
395
+ expect { existing_association.clear }
396
+ .not_to change { existing_association.reader }
397
+ end
398
+ specify do
399
+ expect { existing_association.clear }
400
+ .not_to change { existing_book.read_attribute(:author).symbolize_keys }
401
+ end
402
+ end
403
+ end
404
+
405
+ describe '#reader' do
406
+ specify { expect(association.reader).to be_nil }
407
+
408
+ specify { expect(existing_association.reader).to be_a Author }
409
+ specify { expect(existing_association.reader).to be_persisted }
410
+
411
+ context do
412
+ before { association.build }
413
+ specify { expect(association.reader).to be_a Author }
414
+ specify { expect(association.reader).not_to be_persisted }
415
+ specify { expect(association.reader(true)).to be_nil }
416
+ end
417
+
418
+ context do
419
+ before { existing_association.build(name: 'Fred') }
420
+ specify { expect(existing_association.reader.name).to eq('Fred') }
421
+ specify { expect(existing_association.reader(true).name).to eq('Johny') }
422
+ end
423
+ end
424
+
425
+ describe '#writer' do
426
+ let(:new_author) { Author.new(name: 'Morty') }
427
+ let(:invalid_author) { Author.new }
428
+
429
+ context 'new owner' do
430
+ let(:book) do
431
+ Book.new.tap do |book|
432
+ book.send(:mark_persisted!)
433
+ end
434
+ end
435
+
436
+ specify do
437
+ expect { association.writer(nil) }
438
+ .not_to change { book.read_attribute(:author) }
439
+ end
440
+ specify do
441
+ expect { association.writer(new_author) }
442
+ .to change { association.reader.try(:attributes) }.from(nil).to('name' => 'Morty')
443
+ end
444
+ specify do
445
+ expect { association.writer(new_author) }
446
+ .to change { book.read_attribute(:author) }.from(nil).to('name' => 'Morty')
447
+ end
448
+
449
+ specify do
450
+ expect { association.writer(invalid_author) }
451
+ .to raise_error Granite::Form::AssociationChangesNotApplied
452
+ end
453
+ specify do
454
+ expect { muffle(Granite::Form::AssociationChangesNotApplied) { association.writer(invalid_author) } }
455
+ .not_to change { association.reader }
456
+ end
457
+ specify do
458
+ expect { muffle(Granite::Form::AssociationChangesNotApplied) { association.writer(invalid_author) } }
459
+ .not_to change { book.read_attribute(:author) }
460
+ end
461
+ end
462
+
463
+ context 'persisted owner' do
464
+ specify do
465
+ expect { association.writer(stub_model(:dummy).new) }
466
+ .to raise_error Granite::Form::AssociationTypeMismatch
467
+ end
468
+
469
+ specify { expect(association.writer(nil)).to be_nil }
470
+ specify { expect(association.writer(new_author)).to eq(new_author) }
471
+ specify do
472
+ expect { association.writer(nil) }
473
+ .not_to change { book.read_attribute(:author) }
474
+ end
475
+ specify do
476
+ expect { association.writer(new_author) }
477
+ .to change { association.reader.try(:attributes) }.from(nil).to('name' => 'Morty')
478
+ end
479
+ specify do
480
+ expect { association.writer(new_author) }
481
+ .not_to change { book.read_attribute(:author) }
482
+ end
483
+
484
+ specify do
485
+ expect { association.writer(invalid_author) }
486
+ .to change { association.reader.try(:attributes) }.from(nil).to('name' => nil)
487
+ end
488
+ specify do
489
+ expect { association.writer(invalid_author) }
490
+ .not_to change { book.read_attribute(:author) }
491
+ end
492
+
493
+ specify do
494
+ expect { muffle(Granite::Form::AssociationTypeMismatch) { existing_association.writer(stub_model(:dummy).new) } }
495
+ .not_to change { existing_book.read_attribute(:author) }
496
+ end
497
+ specify do
498
+ expect { muffle(Granite::Form::AssociationTypeMismatch) { existing_association.writer(stub_model(:dummy).new) } }
499
+ .not_to change { existing_association.reader }
500
+ end
501
+
502
+ specify { expect(existing_association.writer(nil)).to be_nil }
503
+ specify { expect(existing_association.writer(new_author)).to eq(new_author) }
504
+ specify do
505
+ expect { existing_association.writer(nil) }
506
+ .to change { existing_book.read_attribute(:author) }.from('name' => 'Johny').to(nil)
507
+ end
508
+ specify do
509
+ expect { existing_association.writer(new_author) }
510
+ .to change { existing_association.reader.try(:attributes) }
511
+ .from('name' => 'Johny').to('name' => 'Morty')
512
+ end
513
+ specify do
514
+ expect { existing_association.writer(new_author) }
515
+ .to change { existing_book.read_attribute(:author) }
516
+ .from('name' => 'Johny').to('name' => 'Morty')
517
+ end
518
+
519
+ specify do
520
+ expect { existing_association.writer(invalid_author) }
521
+ .to raise_error Granite::Form::AssociationChangesNotApplied
522
+ end
523
+ specify do
524
+ expect { muffle(Granite::Form::AssociationChangesNotApplied) { existing_association.writer(invalid_author) } }
525
+ .not_to change { existing_association.reader }
526
+ end
527
+ specify do
528
+ expect { muffle(Granite::Form::AssociationChangesNotApplied) { existing_association.writer(invalid_author) } }
529
+ .not_to change { existing_book.read_attribute(:author) }
530
+ end
531
+ end
532
+ end
533
+ end
@@ -0,0 +1,119 @@
1
+ require 'spec_helper'
2
+ require 'shared/nested_attribute_examples'
3
+
4
+ describe Granite::Form::Model::Associations::NestedAttributes do
5
+ context '' do
6
+ before do
7
+ stub_model :user do
8
+ include Granite::Form::Model::Associations
9
+
10
+ attribute :email, String
11
+ embeds_one :profile
12
+ embeds_many :projects
13
+
14
+ accepts_nested_attributes_for :profile, :projects
15
+
16
+ def save
17
+ apply_association_changes!
18
+ end
19
+ end
20
+ end
21
+
22
+ include_examples 'nested attributes'
23
+ end
24
+
25
+ xcontext 'references_one' do
26
+ before do
27
+ stub_class(:author, ActiveRecord::Base)
28
+ stub_class(:user, ActiveRecord::Base)
29
+
30
+ stub_model :book do
31
+ include Granite::Form::Model::Associations
32
+
33
+ references_one :author
34
+ references_many :users
35
+
36
+ accepts_nested_attributes_for :author, :users
37
+ end
38
+ end
39
+
40
+ context 'references_one' do
41
+ let(:book) { Book.new }
42
+
43
+ specify { expect { book.author_attributes = {} }.to change { book.author }.to(an_instance_of(Author)) }
44
+ specify { expect { book.author_attributes = {name: 'Author'} }.to change { book.author.try(:name) }.to('Author') }
45
+ specify { expect { book.author_attributes = {id: 42, name: 'Author'} }.to raise_error Granite::Form::ObjectNotFound }
46
+
47
+ context ':reject_if' do
48
+ context do
49
+ before { Book.accepts_nested_attributes_for :author, reject_if: :all_blank }
50
+ specify { expect { book.author_attributes = {name: ''} }.not_to change { book.author } }
51
+ end
52
+
53
+ context do
54
+ before { Book.accepts_nested_attributes_for :author, reject_if: ->(attributes) { attributes['name'].blank? } }
55
+ specify { expect { book.author_attributes = {name: ''} }.not_to change { book.author } }
56
+ end
57
+ end
58
+
59
+ context 'existing' do
60
+ let(:author) { Author.new(name: 'Author') }
61
+ let(:book) { Book.new author: author }
62
+
63
+ specify { expect { book.author_attributes = {id: 42, name: 'Author'} }.to raise_error Granite::Form::ObjectNotFound }
64
+ specify { expect { book.author_attributes = {id: author.id.to_s, name: 'Author 1'} }.to change { book.author.name }.to('Author 1') }
65
+ specify { expect { book.author_attributes = {name: 'Author 1'} }.to change { book.author.name }.to('Author 1') }
66
+ specify { expect { book.author_attributes = {name: 'Author 1', _destroy: '1'} }.not_to change { book.author.name } }
67
+ specify do
68
+ expect do
69
+ book.author_attributes = {name: 'Author 1', _destroy: '1'}
70
+ book.save { true }
71
+ end.not_to change { book.author.name }
72
+ end
73
+ specify { expect { book.author_attributes = {id: author.id.to_s, name: 'Author 1', _destroy: '1'} }.to change { book.author.name }.to('Author 1') }
74
+ specify do
75
+ expect do
76
+ book.author_attributes = {id: author.id.to_s, name: 'Author 1', _destroy: '1'}
77
+ book.save { true }
78
+ end.to change { book.author.name }.to('Author 1')
79
+ end
80
+
81
+ context ':allow_destroy' do
82
+ before { Book.accepts_nested_attributes_for :author, allow_destroy: true }
83
+
84
+ specify { expect { book.author_attributes = {name: 'Author 1', _destroy: '1'} }.not_to change { book.author.name } }
85
+ specify do
86
+ expect do
87
+ book.author_attributes = {name: 'Author 1', _destroy: '1'}
88
+ book.save { true }
89
+ end.not_to change { book.author.name }
90
+ end
91
+ specify { expect { book.author_attributes = {id: author.id.to_s, name: 'Author 1', _destroy: '1'} }.to change { book.author.name }.to('Author 1') }
92
+ specify do
93
+ expect do
94
+ book.author_attributes = {id: author.id.to_s, name: 'Author 1', _destroy: '1'}
95
+ book.save { true }
96
+ end.to change { book.author }.to(nil)
97
+ end
98
+ end
99
+
100
+ context ':update_only' do
101
+ before { Book.accepts_nested_attributes_for :author, update_only: true }
102
+
103
+ specify do
104
+ expect { book.author_attributes = {id: 42, name: 'Author 1'} }
105
+ .to change { book.author.name }.to('Author 1')
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ context 'references_many' do
112
+ let(:book) { Book.new }
113
+ end
114
+ end
115
+
116
+ describe '#assign_attributes' do
117
+ specify 'invent a good example'
118
+ end
119
+ end
@@ -0,0 +1,58 @@
1
+ require 'spec_helper'
2
+
3
+ describe Granite::Form::Model::Associations::PersistenceAdapters::ActiveRecord do
4
+ before do
5
+ stub_class(:author, ActiveRecord::Base)
6
+ end
7
+
8
+ subject(:adapter) { described_class.new(Author, primary_key, scope_proc) }
9
+ let(:primary_key) { :id }
10
+ let(:scope_proc) { nil }
11
+
12
+ describe '#build' do
13
+ subject { adapter.build(name: name) }
14
+ let(:name) { 'John Doe' }
15
+
16
+ its(:name) { should == name }
17
+ it { is_expected.to be_a Author }
18
+ end
19
+
20
+ describe '#find_one' do
21
+ subject { adapter.find_one(nil, author.id) }
22
+ let(:author) { Author.create }
23
+
24
+ it { should == author }
25
+ end
26
+
27
+ describe '#find_all' do
28
+ subject { adapter.find_all(nil, authors.map(&:id)) }
29
+ let(:authors) { Array.new(2) { Author.create } }
30
+
31
+ it { should == authors }
32
+ end
33
+
34
+ describe '#scope' do
35
+ subject { adapter.scope(owner, source) }
36
+ let(:authors) { ['John Doe', 'Sam Smith', 'John Smith'].map { |name| Author.create(name: name) } }
37
+ let(:source) { authors[0..1].map(&:id) }
38
+ let(:owner) { nil }
39
+
40
+ it { is_expected.to be_a ActiveRecord::Relation }
41
+
42
+ context 'without scope_proc' do
43
+ it { should == Author.where(primary_key => source) }
44
+ end
45
+
46
+ context 'with scope_proc' do
47
+ let(:scope_proc) { -> { where("name LIKE 'John%'") } }
48
+
49
+ its(:to_a) { should == [Author.first] }
50
+ end
51
+ end
52
+
53
+ describe '#primary_key_type' do
54
+ subject { adapter.primary_key_type }
55
+
56
+ it { should == Integer }
57
+ end
58
+ end