granite-form 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +13 -0
  3. data/.github/workflows/ci.yml +35 -0
  4. data/.github/workflows/main.yml +29 -0
  5. data/.gitignore +21 -0
  6. data/.rspec +2 -0
  7. data/.rubocop.yml +64 -0
  8. data/.rubocop_todo.yml +48 -0
  9. data/Appraisals +8 -0
  10. data/CHANGELOG.md +73 -0
  11. data/Gemfile +8 -0
  12. data/Guardfile +77 -0
  13. data/LICENSE +22 -0
  14. data/README.md +429 -0
  15. data/Rakefile +6 -0
  16. data/gemfiles/rails.4.2.gemfile +15 -0
  17. data/gemfiles/rails.5.0.gemfile +15 -0
  18. data/gemfiles/rails.5.1.gemfile +15 -0
  19. data/gemfiles/rails.5.2.gemfile +15 -0
  20. data/gemfiles/rails.6.0.gemfile +14 -0
  21. data/gemfiles/rails.6.1.gemfile +14 -0
  22. data/gemfiles/rails.7.0.gemfile +14 -0
  23. data/granite-form.gemspec +31 -0
  24. data/lib/granite/form/active_record/associations.rb +57 -0
  25. data/lib/granite/form/active_record/nested_attributes.rb +20 -0
  26. data/lib/granite/form/base.rb +15 -0
  27. data/lib/granite/form/config.rb +42 -0
  28. data/lib/granite/form/errors.rb +111 -0
  29. data/lib/granite/form/extensions.rb +36 -0
  30. data/lib/granite/form/model/associations/base.rb +97 -0
  31. data/lib/granite/form/model/associations/collection/embedded.rb +14 -0
  32. data/lib/granite/form/model/associations/collection/proxy.rb +35 -0
  33. data/lib/granite/form/model/associations/embeds_any.rb +19 -0
  34. data/lib/granite/form/model/associations/embeds_many.rb +152 -0
  35. data/lib/granite/form/model/associations/embeds_one.rb +112 -0
  36. data/lib/granite/form/model/associations/nested_attributes.rb +215 -0
  37. data/lib/granite/form/model/associations/persistence_adapters/active_record/referenced_proxy.rb +33 -0
  38. data/lib/granite/form/model/associations/persistence_adapters/active_record.rb +68 -0
  39. data/lib/granite/form/model/associations/persistence_adapters/base.rb +55 -0
  40. data/lib/granite/form/model/associations/references_any.rb +43 -0
  41. data/lib/granite/form/model/associations/references_many.rb +113 -0
  42. data/lib/granite/form/model/associations/references_one.rb +88 -0
  43. data/lib/granite/form/model/associations/reflections/base.rb +92 -0
  44. data/lib/granite/form/model/associations/reflections/embeds_any.rb +52 -0
  45. data/lib/granite/form/model/associations/reflections/embeds_many.rb +17 -0
  46. data/lib/granite/form/model/associations/reflections/embeds_one.rb +19 -0
  47. data/lib/granite/form/model/associations/reflections/references_any.rb +65 -0
  48. data/lib/granite/form/model/associations/reflections/references_many.rb +30 -0
  49. data/lib/granite/form/model/associations/reflections/references_one.rb +32 -0
  50. data/lib/granite/form/model/associations/reflections/singular.rb +37 -0
  51. data/lib/granite/form/model/associations/validations.rb +41 -0
  52. data/lib/granite/form/model/associations.rb +120 -0
  53. data/lib/granite/form/model/attributes/attribute.rb +75 -0
  54. data/lib/granite/form/model/attributes/base.rb +134 -0
  55. data/lib/granite/form/model/attributes/collection.rb +19 -0
  56. data/lib/granite/form/model/attributes/dictionary.rb +28 -0
  57. data/lib/granite/form/model/attributes/localized.rb +44 -0
  58. data/lib/granite/form/model/attributes/reference_many.rb +21 -0
  59. data/lib/granite/form/model/attributes/reference_one.rb +52 -0
  60. data/lib/granite/form/model/attributes/reflections/attribute.rb +61 -0
  61. data/lib/granite/form/model/attributes/reflections/base.rb +62 -0
  62. data/lib/granite/form/model/attributes/reflections/collection.rb +12 -0
  63. data/lib/granite/form/model/attributes/reflections/dictionary.rb +15 -0
  64. data/lib/granite/form/model/attributes/reflections/localized.rb +45 -0
  65. data/lib/granite/form/model/attributes/reflections/reference_many.rb +12 -0
  66. data/lib/granite/form/model/attributes/reflections/reference_one.rb +49 -0
  67. data/lib/granite/form/model/attributes/reflections/represents.rb +56 -0
  68. data/lib/granite/form/model/attributes/represents.rb +67 -0
  69. data/lib/granite/form/model/attributes.rb +204 -0
  70. data/lib/granite/form/model/callbacks.rb +72 -0
  71. data/lib/granite/form/model/conventions.rb +40 -0
  72. data/lib/granite/form/model/dirty.rb +84 -0
  73. data/lib/granite/form/model/lifecycle.rb +309 -0
  74. data/lib/granite/form/model/localization.rb +26 -0
  75. data/lib/granite/form/model/persistence.rb +59 -0
  76. data/lib/granite/form/model/primary.rb +59 -0
  77. data/lib/granite/form/model/representation.rb +101 -0
  78. data/lib/granite/form/model/scopes.rb +118 -0
  79. data/lib/granite/form/model/validations/associated.rb +22 -0
  80. data/lib/granite/form/model/validations/nested.rb +56 -0
  81. data/lib/granite/form/model/validations.rb +29 -0
  82. data/lib/granite/form/model.rb +33 -0
  83. data/lib/granite/form/railtie.rb +9 -0
  84. data/lib/granite/form/undefined_class.rb +11 -0
  85. data/lib/granite/form/version.rb +5 -0
  86. data/lib/granite/form.rb +163 -0
  87. data/spec/lib/granite/form/active_record/associations_spec.rb +211 -0
  88. data/spec/lib/granite/form/active_record/nested_attributes_spec.rb +15 -0
  89. data/spec/lib/granite/form/config_spec.rb +66 -0
  90. data/spec/lib/granite/form/model/associations/embeds_many_spec.rb +706 -0
  91. data/spec/lib/granite/form/model/associations/embeds_one_spec.rb +533 -0
  92. data/spec/lib/granite/form/model/associations/nested_attributes_spec.rb +119 -0
  93. data/spec/lib/granite/form/model/associations/persistence_adapters/active_record_spec.rb +58 -0
  94. data/spec/lib/granite/form/model/associations/references_many_spec.rb +572 -0
  95. data/spec/lib/granite/form/model/associations/references_one_spec.rb +445 -0
  96. data/spec/lib/granite/form/model/associations/reflections/embeds_any_spec.rb +42 -0
  97. data/spec/lib/granite/form/model/associations/reflections/embeds_many_spec.rb +145 -0
  98. data/spec/lib/granite/form/model/associations/reflections/embeds_one_spec.rb +117 -0
  99. data/spec/lib/granite/form/model/associations/reflections/references_many_spec.rb +303 -0
  100. data/spec/lib/granite/form/model/associations/reflections/references_one_spec.rb +287 -0
  101. data/spec/lib/granite/form/model/associations/validations_spec.rb +137 -0
  102. data/spec/lib/granite/form/model/associations_spec.rb +198 -0
  103. data/spec/lib/granite/form/model/attributes/attribute_spec.rb +186 -0
  104. data/spec/lib/granite/form/model/attributes/base_spec.rb +97 -0
  105. data/spec/lib/granite/form/model/attributes/collection_spec.rb +72 -0
  106. data/spec/lib/granite/form/model/attributes/dictionary_spec.rb +100 -0
  107. data/spec/lib/granite/form/model/attributes/localized_spec.rb +103 -0
  108. data/spec/lib/granite/form/model/attributes/reflections/attribute_spec.rb +72 -0
  109. data/spec/lib/granite/form/model/attributes/reflections/base_spec.rb +56 -0
  110. data/spec/lib/granite/form/model/attributes/reflections/collection_spec.rb +37 -0
  111. data/spec/lib/granite/form/model/attributes/reflections/dictionary_spec.rb +43 -0
  112. data/spec/lib/granite/form/model/attributes/reflections/localized_spec.rb +37 -0
  113. data/spec/lib/granite/form/model/attributes/reflections/represents_spec.rb +70 -0
  114. data/spec/lib/granite/form/model/attributes/represents_spec.rb +85 -0
  115. data/spec/lib/granite/form/model/attributes_spec.rb +350 -0
  116. data/spec/lib/granite/form/model/callbacks_spec.rb +337 -0
  117. data/spec/lib/granite/form/model/conventions_spec.rb +11 -0
  118. data/spec/lib/granite/form/model/dirty_spec.rb +84 -0
  119. data/spec/lib/granite/form/model/lifecycle_spec.rb +356 -0
  120. data/spec/lib/granite/form/model/persistence_spec.rb +46 -0
  121. data/spec/lib/granite/form/model/primary_spec.rb +84 -0
  122. data/spec/lib/granite/form/model/representation_spec.rb +139 -0
  123. data/spec/lib/granite/form/model/scopes_spec.rb +86 -0
  124. data/spec/lib/granite/form/model/typecasting_spec.rb +193 -0
  125. data/spec/lib/granite/form/model/validations/associated_spec.rb +102 -0
  126. data/spec/lib/granite/form/model/validations/nested_spec.rb +164 -0
  127. data/spec/lib/granite/form/model/validations_spec.rb +31 -0
  128. data/spec/lib/granite/form/model_spec.rb +10 -0
  129. data/spec/lib/granite/form_spec.rb +11 -0
  130. data/spec/shared/nested_attribute_examples.rb +332 -0
  131. data/spec/spec_helper.rb +50 -0
  132. data/spec/support/model_helpers.rb +10 -0
  133. data/spec/support/muffle_helper.rb +7 -0
  134. metadata +403 -0
@@ -0,0 +1,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