active_type 0.1.3 → 0.2.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.
@@ -0,0 +1,682 @@
1
+ require 'spec_helper'
2
+
3
+ module NestedAttributesSpec
4
+
5
+ class Record < ActiveRecord::Base
6
+ attr_accessor :fail_on_save, :error
7
+
8
+ before_save :check_fail
9
+
10
+ validate :check_error
11
+
12
+ private
13
+
14
+ def check_fail
15
+ if fail_on_save == true
16
+ false
17
+ end
18
+ end
19
+
20
+ def check_error
21
+ if error.present?
22
+ errors.add(:base, error)
23
+ end
24
+ end
25
+ end
26
+
27
+ end
28
+
29
+ class GlobalRecord < ActiveRecord::Base
30
+ self.table_name = 'records'
31
+ end
32
+
33
+
34
+ describe "ActiveType::Object" do
35
+
36
+ context '.nests_many' do
37
+
38
+ let(:extra_options) { {} }
39
+
40
+ subject do
41
+ extra = extra_options
42
+ Class.new(ActiveType::Object) do
43
+ nests_many :records, extra.merge(:scope => NestedAttributesSpec::Record)
44
+
45
+ def bad(attributes)
46
+ attributes[:persisted_string] =~ /bad/
47
+ end
48
+
49
+ def reject_all
50
+ true
51
+ end
52
+
53
+ end.new
54
+ end
55
+
56
+ def should_assign_and_persist(assign, persist = assign)
57
+ subject.records.map(&:persisted_string).should == assign
58
+ subject.save.should be_true
59
+ NestedAttributesSpec::Record.all.map(&:persisted_string).should =~ persist
60
+ end
61
+
62
+
63
+ context 'with no records assigned' do
64
+
65
+ it 'can save' do
66
+ subject.save.should be_true
67
+ end
68
+
69
+ end
70
+
71
+ context 'assigning nil' do
72
+
73
+ it 'will do nothing' do
74
+ subject.records_attributes = nil
75
+ subject.records.should be_nil
76
+ end
77
+
78
+ end
79
+
80
+ context 'when assigning records without ids' do
81
+
82
+ it 'builds single nested records' do
83
+ subject.records_attributes = { 1 => {:persisted_string => "string"} }
84
+
85
+ should_assign_and_persist(["string"])
86
+ end
87
+
88
+ it 'builds multiple nested records when given a hash of attributes, ordered by key' do
89
+ subject.records_attributes = {
90
+ 3 => {:persisted_string => "string 3"},
91
+ 1 => {:persisted_string => "string 1"},
92
+ 2 => {:persisted_string => "string 2"},
93
+ }
94
+
95
+ should_assign_and_persist(["string 1", "string 2", "string 3"])
96
+ end
97
+
98
+ it 'builds multiple nested records when given an array of attributes' do
99
+ subject.records_attributes = [
100
+ {:persisted_string => "string 1"},
101
+ {:persisted_string => "string 2"},
102
+ {:persisted_string => "string 3"},
103
+ ]
104
+
105
+ should_assign_and_persist(["string 1", "string 2", "string 3"])
106
+ end
107
+
108
+ it 'does not build records that match a :reject_if proc' do
109
+ extra_options.merge!(:reject_if => proc { |attributes| attributes['persisted_string'] =~ /bad/ })
110
+ subject.records_attributes = {
111
+ 1 => {:persisted_string => "good value"},
112
+ 2 => {:persisted_string => "bad value"},
113
+ }
114
+
115
+ should_assign_and_persist(["good value"])
116
+ end
117
+
118
+ it 'does not build records that match a :reject_if method taking attributes' do
119
+ extra_options.merge!(:reject_if => :bad)
120
+ subject.records_attributes = {
121
+ 1 => {:persisted_string => "good value"},
122
+ 2 => {:persisted_string => "bad value"},
123
+ }
124
+
125
+ should_assign_and_persist(["good value"])
126
+ end
127
+
128
+ it 'does not build records that match a :reject_if method taking attributes' do
129
+ extra_options.merge!(:reject_if => :bad)
130
+ subject.records_attributes = {
131
+ 1 => {:persisted_string => "good value"},
132
+ 2 => {:persisted_string => "bad value"},
133
+ }
134
+
135
+ should_assign_and_persist(["good value"])
136
+ end
137
+
138
+ it 'does not build records that match a :reject_if method taking no attributes' do
139
+ extra_options.merge!(:reject_if => :reject_all)
140
+ subject.records_attributes = {
141
+ 1 => {:persisted_string => "good value"},
142
+ 2 => {:persisted_string => "bad value"},
143
+ }
144
+
145
+ should_assign_and_persist([])
146
+ end
147
+
148
+ it 'does not build records that match a :reject_if all_blank' do
149
+ extra_options.merge!(:reject_if => :all_blank)
150
+ subject.records_attributes = {
151
+ 1 => {:persisted_string => "good value"},
152
+ 2 => {},
153
+ }
154
+
155
+ should_assign_and_persist(["good value"])
156
+ end
157
+
158
+ it 'appends to existing records' do
159
+ subject.records = [NestedAttributesSpec::Record.create!(:persisted_string => "existing string")]
160
+ subject.records_attributes = { 1 => {:persisted_string => "new string"} }
161
+
162
+ should_assign_and_persist(["existing string", "new string"])
163
+ end
164
+
165
+ it 'leaves unassigned records alone' do
166
+ NestedAttributesSpec::Record.create!(:persisted_string => "unassigned")
167
+ subject.records_attributes = { 1 => {:persisted_string => "string"} }
168
+
169
+ should_assign_and_persist(["string"], ["unassigned", "string"])
170
+ end
171
+
172
+ it 'does not destroy records on _destroy => trueish by default' do
173
+ existing = NestedAttributesSpec::Record.create!(:persisted_string => 'do not delete this')
174
+
175
+ subject.records_attributes = [
176
+ { :id => existing.id, :_destroy => "true" },
177
+ ]
178
+ should_assign_and_persist(["do not delete this"], ["do not delete this"])
179
+ subject.records.size.should == 1
180
+ end
181
+
182
+ it 'destroys records on _destroy => trueish if allowed' do
183
+ extra_options.merge!(:allow_destroy => true)
184
+ existing = [
185
+ NestedAttributesSpec::Record.create!(:persisted_string => 'delete this'),
186
+ NestedAttributesSpec::Record.create!(:persisted_string => 'delete this'),
187
+ NestedAttributesSpec::Record.create!(:persisted_string => 'delete this'),
188
+ NestedAttributesSpec::Record.create!(:persisted_string => 'keep this'),
189
+ ]
190
+
191
+ subject.records = existing.first(2) # assign some
192
+
193
+ subject.records_attributes = [
194
+ { :id => existing[0].id, :_destroy => "true" },
195
+ { :id => existing[1].id, :_destroy => 1 },
196
+ { :id => existing[2].id, :_destroy => "1" },
197
+ { :id => existing[3].id, :_destroy => "0" },
198
+ ]
199
+ should_assign_and_persist(["delete this", "delete this", "delete this", "keep this"], ["keep this"])
200
+ subject.records.size.should == 1
201
+ end
202
+
203
+ end
204
+
205
+ context 'when assigning records with ids' do
206
+
207
+ it 'updates the record with the id if already assigned' do
208
+ subject.records = [
209
+ NestedAttributesSpec::Record.new(:persisted_string => "existing 1"),
210
+ NestedAttributesSpec::Record.new(:persisted_string => "existing 2"),
211
+ NestedAttributesSpec::Record.new(:persisted_string => "existing 3"),
212
+ ]
213
+ subject.records[0].id = 100
214
+ subject.records[1].id = 101
215
+ subject.records[2].id = 102
216
+
217
+ subject.records_attributes = { 1 => {:id => 101, :persisted_string => "updated"} }
218
+
219
+ should_assign_and_persist(["existing 1", "updated", "existing 3"])
220
+ end
221
+
222
+ it 'does not update records matching a reject_if proc' do
223
+ extra_options.merge!(:reject_if => :bad)
224
+ subject.records = [
225
+ NestedAttributesSpec::Record.new(:persisted_string => "existing 1"),
226
+ NestedAttributesSpec::Record.new(:persisted_string => "existing 2"),
227
+ ]
228
+ subject.records[0].id = 100
229
+ subject.records[1].id = 101
230
+
231
+ subject.records_attributes = [
232
+ {:id => 100, :persisted_string => "good"},
233
+ {:id => 101, :persisted_string => "bad"}
234
+ ]
235
+
236
+ should_assign_and_persist(["good", "existing 2"])
237
+ end
238
+
239
+ it 'fetches the record with the id if not already assigned' do
240
+ record = NestedAttributesSpec::Record.create!(:persisted_string => "existing string 1")
241
+ subject.records = [
242
+ NestedAttributesSpec::Record.new(:persisted_string => "existing string 2"),
243
+ ]
244
+ subject.records[0].id = record.id + 1
245
+
246
+ subject.records_attributes = { 1 => {:id => record.id, :persisted_string => "updated string"} }
247
+
248
+ should_assign_and_persist(["existing string 2", "updated string"])
249
+ end
250
+
251
+ it 'raises an error if the child record does not exist' do
252
+ expect do
253
+ subject.records_attributes = { 1 => {:id => 1, :persisted_string => "updated string"} }
254
+ end.to raise_error(ActiveType::NestedAttributes::RecordNotFound, "could not find a child record with id '1' for 'records'")
255
+ end
256
+
257
+ end
258
+
259
+ context 'save failure' do
260
+
261
+ it 'returns false on #save and does not save the child' do
262
+ # it should also cause a rollback, but that will not work with sqlite3
263
+ subject.records = [
264
+ NestedAttributesSpec::Record.new(:fail_on_save => true),
265
+ ]
266
+
267
+ subject.save.should be_false
268
+ NestedAttributesSpec::Record.count.should == 0
269
+
270
+ # note that other children would be saved and not be rolled back
271
+ # this is also true for regular nested attributes
272
+ end
273
+
274
+ end
275
+
276
+ context 'validations' do
277
+
278
+ describe '#valid?' do
279
+
280
+ it 'is true if there are no records assigned' do
281
+ subject.valid?.should be_true
282
+ end
283
+
284
+ it 'is true if all records are valid' do
285
+ subject.records = [
286
+ NestedAttributesSpec::Record.new,
287
+ NestedAttributesSpec::Record.new,
288
+ ]
289
+
290
+ subject.valid?.should be_true
291
+ end
292
+
293
+ it 'is false if one child has an error' do
294
+ subject.records = [
295
+ NestedAttributesSpec::Record.new,
296
+ NestedAttributesSpec::Record.new(:error => 'some error'),
297
+ ]
298
+
299
+ subject.valid?.should be_false
300
+ end
301
+
302
+ it 'is copies the error to the record' do
303
+ subject.records = [
304
+ NestedAttributesSpec::Record.new,
305
+ NestedAttributesSpec::Record.new(:error => 'some error'),
306
+ ]
307
+
308
+ subject.valid?
309
+ subject.errors["records.base"].should == ['some error']
310
+ end
311
+
312
+ end
313
+
314
+ end
315
+
316
+ end
317
+
318
+
319
+ context '.nests_one' do
320
+
321
+ let(:extra_options) { {} }
322
+
323
+ subject do
324
+ extra = extra_options
325
+ Class.new(ActiveType::Object) do
326
+ nests_one :record, extra.merge(:scope => NestedAttributesSpec::Record)
327
+
328
+ def bad(attributes)
329
+ attributes[:persisted_string] =~ /bad/
330
+ end
331
+ end.new
332
+ end
333
+
334
+ def should_assign_and_persist(assign, persist = assign)
335
+ if assign
336
+ subject.record.should be_present
337
+ subject.record.persisted_string.should == assign
338
+ else
339
+ subject.record.should be_nil
340
+ end
341
+ subject.save.should be_true
342
+ NestedAttributesSpec::Record.all.map(&:persisted_string).should == (persist ? [persist] : [])
343
+ end
344
+
345
+
346
+ context 'with no record assigned' do
347
+
348
+ it 'can save' do
349
+ subject.save.should be_true
350
+ end
351
+
352
+ end
353
+
354
+ context 'assigning nil' do
355
+
356
+ it 'will do nothing' do
357
+ subject.record_attributes = nil
358
+ subject.record.should be_nil
359
+ end
360
+
361
+ end
362
+
363
+ context 'when assigning a records without an id' do
364
+
365
+ it 'builds a nested records' do
366
+ subject.record_attributes = { :persisted_string => "string" }
367
+
368
+ should_assign_and_persist("string")
369
+ end
370
+
371
+ it 'does not build a record that matchs a :reject_if proc' do
372
+ extra_options.merge!(:reject_if => proc { |attributes| attributes['persisted_string'] =~ /bad/ })
373
+ subject.record_attributes = { :persisted_string => "bad" }
374
+
375
+ should_assign_and_persist(nil)
376
+ end
377
+
378
+
379
+ it 'updates an assigned record' do
380
+ subject.record = NestedAttributesSpec::Record.create!(:persisted_string => "existing string")
381
+ subject.record_attributes = { :persisted_string => "new string" }
382
+
383
+ should_assign_and_persist("new string")
384
+ end
385
+
386
+ it 'does not update a record that matchs a :reject_if proc' do
387
+ extra_options.merge!(:reject_if => proc { |attributes| attributes['persisted_string'] =~ /bad/ })
388
+ subject.record = NestedAttributesSpec::Record.create!(:persisted_string => "existing string")
389
+ subject.record_attributes = { :persisted_string => "bad" }
390
+
391
+ should_assign_and_persist("existing string")
392
+ end
393
+
394
+
395
+ end
396
+
397
+ context 'when assigning a records with an id' do
398
+
399
+ let(:record) { record = NestedAttributesSpec::Record.create!(:persisted_string => "existing string") }
400
+
401
+ it 'updates the record if already assigned' do
402
+ subject.record = record
403
+
404
+ subject.record_attributes = { :id => record.id, :persisted_string => "updated string"}
405
+
406
+ should_assign_and_persist("updated string")
407
+ end
408
+
409
+ it 'fetches the record with the id if not already assigned' do
410
+ subject.record_attributes = { :id => record.id, :persisted_string => "updated string" }
411
+
412
+ should_assign_and_persist("updated string")
413
+ end
414
+
415
+ it 'does not destroy records on _destroy => true by default' do
416
+ subject.record_attributes = { :id => record.id, :_destroy => true }
417
+
418
+ should_assign_and_persist("existing string", "existing string")
419
+ end
420
+
421
+ it 'destroys records on _destroy => true if allowed' do
422
+ extra_options.merge!(:allow_destroy => true)
423
+ subject.record_attributes = { :id => record.id, :_destroy => true }
424
+
425
+ should_assign_and_persist("existing string", nil)
426
+ subject.record.should == nil
427
+ end
428
+
429
+ it 'raises an error if the assigned record does not match the id' do
430
+ expect do
431
+ subject.record = NestedAttributesSpec::Record.create!
432
+ subject.record_attributes = { :id => record.id, :persisted_string => "updated string" }
433
+ end.to raise_error(ActiveType::NestedAttributes::AssignmentError, "child record 'record' did not match id '#{record.id}'")
434
+ end
435
+
436
+ it 'raises an error if a record with the id cannot be found' do
437
+ expect do
438
+ subject.record_attributes = { :id => 1, :persisted_string => "updated string" }
439
+ end.to raise_error(ActiveType::NestedAttributes::RecordNotFound, "could not find a child record with id '1' for 'record'")
440
+ end
441
+
442
+ end
443
+
444
+ context 'validations' do
445
+
446
+ describe '#valid?' do
447
+
448
+ it 'is true if there is no record assigned' do
449
+ subject.valid?.should be_true
450
+ end
451
+
452
+ it 'is true if the assigned record is valid' do
453
+ subject.record = NestedAttributesSpec::Record.new
454
+
455
+ subject.valid?.should be_true
456
+ end
457
+
458
+ it 'is false the assigned record has an error' do
459
+ subject.record = NestedAttributesSpec::Record.new(:error => 'some error')
460
+
461
+ subject.valid?.should be_false
462
+ end
463
+
464
+ it 'is copies the error to the record' do
465
+ subject.record = NestedAttributesSpec::Record.new(:error => 'some error')
466
+
467
+ subject.valid?
468
+ subject.errors["record.base"].should == ['some error']
469
+ end
470
+
471
+ end
472
+
473
+ end
474
+
475
+ end
476
+
477
+ context '.nests_one/nests_many' do
478
+
479
+ context 'inheritance' do
480
+
481
+ let(:base_class) do
482
+ Class.new(ActiveType::Object) do
483
+ nests_one :record, :scope => NestedAttributesSpec::Record
484
+ end
485
+ end
486
+
487
+ it 'works across inheritance hierarchy' do
488
+ subject = Class.new(base_class) do
489
+ nests_one :another_record, :scope => NestedAttributesSpec::Record
490
+ end.new
491
+
492
+ subject.record_attributes = { :persisted_string => "string" }
493
+ subject.another_record_attributes = {:persisted_string => "another string"}
494
+
495
+ subject.record.persisted_string.should == "string"
496
+ subject.another_record.persisted_string.should == "another string"
497
+ subject.save.should be_true
498
+ NestedAttributesSpec::Record.all.map(&:persisted_string).should =~ ["string", "another string"]
499
+ end
500
+
501
+ it 'allows overriding of the accessor' do
502
+ subject = Class.new(base_class) do
503
+ def record_attributes=(attributes)
504
+ reached
505
+ super
506
+ end
507
+
508
+ def reached
509
+ end
510
+ end.new
511
+
512
+ subject.should_receive(:reached)
513
+ subject.record_attributes = { :persisted_string => "string" }
514
+
515
+ subject.record.persisted_string.should == "string"
516
+ subject.save.should be_true
517
+ NestedAttributesSpec::Record.all.map(&:persisted_string).should =~ ["string"]
518
+ end
519
+
520
+ end
521
+
522
+ context 'when not giving a scope' do
523
+
524
+ subject do
525
+ Class.new(ActiveType::Object) do
526
+ nests_many :global_records
527
+ nests_one :global_record
528
+ end.new
529
+ end
530
+
531
+ it 'infers the scope from the association name' do
532
+ subject.global_records_attributes = { 1 => { :persisted_string => "string" } }
533
+ subject.global_record_attributes = { :persisted_string => "string" }
534
+
535
+ subject.global_records.first.should be_a(GlobalRecord)
536
+ subject.global_record.should be_a(GlobalRecord)
537
+ end
538
+
539
+ end
540
+
541
+ context 'when giving a scope via a proc' do
542
+
543
+ subject do
544
+ Class.new(ActiveType::Object) do
545
+ nests_many :records, :scope => proc { NestedAttributesSpec::Record.where("persisted_string <> 'invisible'") }
546
+ nests_one :record, :scope => proc { NestedAttributesSpec::Record }
547
+
548
+ attribute :default_value, :string
549
+ nests_many :default_records, :scope => proc { NestedAttributesSpec::Record.where(:persisted_string => default_value) }
550
+ end.new
551
+ end
552
+
553
+ it 'uses the scope' do
554
+ subject.records_attributes = { 1 => { :persisted_string => "string" } }
555
+ subject.record_attributes = { :persisted_string => "string" }
556
+
557
+ subject.records.first.should be_a(NestedAttributesSpec::Record)
558
+ subject.record.should be_a(NestedAttributesSpec::Record)
559
+ end
560
+
561
+ it 'evals the scope lazily in the instance' do
562
+ subject.default_value = "default value"
563
+ subject.default_records_attributes = [{}]
564
+
565
+ subject.default_records.map(&:persisted_string).should == ["default value"]
566
+ end
567
+
568
+ it 'caches the scope' do
569
+ subject.default_value = "default value"
570
+ subject.default_records_attributes = [{}]
571
+ subject.default_value = "another default value"
572
+ subject.default_records_attributes = [{}]
573
+
574
+ subject.default_records.map(&:persisted_string).should == ["default value", "default value"]
575
+ end
576
+
577
+ it 'caches the scope per instance' do
578
+ subject.default_value = "default value"
579
+ subject.default_records_attributes = [{}]
580
+
581
+ another_subject = subject.class.new
582
+ another_subject.default_value = "another default value"
583
+ another_subject.default_records_attributes = [{}]
584
+
585
+ another_subject.default_records.map(&:persisted_string).should == ["another default value"]
586
+ end
587
+
588
+ it 'raises an error if the child record is not found via the scope' do
589
+ record = NestedAttributesSpec::Record.create!(:persisted_string => 'invisible')
590
+
591
+ expect do
592
+ subject.records_attributes = { 1 => { :id => record.id, :persisted_string => "updated string" } }
593
+ end.to raise_error(ActiveType::NestedAttributes::RecordNotFound, "could not find a child record with id '#{record.id}' for 'records'")
594
+ end
595
+ end
596
+
597
+ context 'separate scopes for build and find' do
598
+
599
+ subject do
600
+ find_scope = proc { NestedAttributesSpec::Record.where(:persisted_string => 'findable') }
601
+ build_scope = proc { NestedAttributesSpec::Record.where(:persisted_string => 'buildable') }
602
+ Class.new(ActiveType::Object) do
603
+ nests_many :records, :build_scope => build_scope, :find_scope => find_scope
604
+ nests_one :record, :build_scope => build_scope, :find_scope => find_scope
605
+ end.new
606
+ end
607
+
608
+ it 'nests_many uses the find_scope to find records' do
609
+ record = NestedAttributesSpec::Record.create!(:persisted_string => 'findable')
610
+ hidden_record = NestedAttributesSpec::Record.create!(:persisted_string => 'hidden')
611
+
612
+ expect do
613
+ subject.records_attributes = [{ :id => record.id, :persisted_string => 'updated' }]
614
+ end.to_not raise_error
615
+
616
+ expect do
617
+ subject.records_attributes = [{ :id => hidden_record.id, :persisted_string => 'updated' }]
618
+ end.to raise_error(ActiveType::NestedAttributes::RecordNotFound)
619
+ end
620
+
621
+ it 'nests_many uses the build_scope to find records' do
622
+ subject.records_attributes = [{}]
623
+ subject.records.first.persisted_string.should == 'buildable'
624
+ end
625
+
626
+ it 'nests_one uses the find_scope to find records' do
627
+ record = NestedAttributesSpec::Record.create!(:persisted_string => 'findable')
628
+ hidden_record = NestedAttributesSpec::Record.create!(:persisted_string => 'hidden')
629
+
630
+ expect do
631
+ subject.record_attributes = { :id => record.id, :persisted_string => 'updated' }
632
+ end.to_not raise_error
633
+
634
+ subject.record = nil
635
+ expect do
636
+ subject.record_attributes = { :id => hidden_record.id, :persisted_string => 'updated' }
637
+ end.to raise_error(ActiveType::NestedAttributes::RecordNotFound)
638
+ end
639
+
640
+ it 'nests_one uses the build_scope to find records' do
641
+ subject.record_attributes = {}
642
+ subject.record.persisted_string.should == 'buildable'
643
+ end
644
+
645
+ end
646
+
647
+ context 'defaults' do
648
+
649
+ subject do
650
+ Class.new(ActiveType::Object) do
651
+ nests_many :records, :default => proc { [default_record] }
652
+ nests_one :record, :default => proc { default_record }
653
+
654
+ nests_many :global_records
655
+
656
+ nests_many :other_records, :scope => proc { NestedAttributesSpec::Record }
657
+ nests_one :other_record, :scope => proc { NestedAttributesSpec::Record }
658
+
659
+ nests_many :records_without_default, :default => nil
660
+
661
+ def default_record
662
+ NestedAttributesSpec::Record.new(:persisted_string => "default")
663
+ end
664
+ end.new
665
+ end
666
+
667
+ it 'accepts a :default value' do
668
+ subject.records.map(&:persisted_string).should == ["default"]
669
+ subject.record.persisted_string.should == "default"
670
+ end
671
+
672
+ it 'computes the value lazily' do
673
+ subject.stub :default_record => NestedAttributesSpec::Record.new(:persisted_string => "other default")
674
+ subject.records.map(&:persisted_string).should == ["other default"]
675
+ subject.record.persisted_string.should == "other default"
676
+ end
677
+
678
+ end
679
+
680
+ end
681
+
682
+ end
@@ -284,6 +284,10 @@ describe ActiveType::Object do
284
284
  end
285
285
  end
286
286
 
287
+ describe 'defaults' do
288
+ it_should_behave_like "a class accepting attribute defaults", ActiveType::Object
289
+ end
290
+
287
291
  describe '#belongs_to' do
288
292
  subject { ObjectSpec::ObjectWithBelongsTo.new }
289
293