Skip to content

Commit 2dabea7

Browse files
committed
[D-326] Store accessor support for assignable_values
1 parent 6f848bf commit 2dabea7

File tree

5 files changed

+239
-7
lines changed

5 files changed

+239
-7
lines changed

.github/workflows/test.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
runs-on: ubuntu-20.04
1313
services:
1414
mysql:
15-
image: mysql:5.6
15+
image: mysql:5.7
1616
env:
1717
MYSQL_ROOT_PASSWORD: password
1818
ports:

lib/assignable_values/active_record/restriction/scalar_attribute.rb

+24-5
Original file line numberDiff line numberDiff line change
@@ -119,15 +119,34 @@ def previously_saved_value(record)
119119
end
120120

121121
def value_was(record)
122-
if record.respond_to?(:attribute_in_database)
123-
record.attribute_in_database(:"#{property}") # Rails >= 5.1
124-
else
125-
record.send(value_was_method) # Rails <= 5.0
122+
if record.respond_to?(:attribute_in_database) # Rails >= 5.1
123+
if store_accessor_attribute?
124+
accessor = record.attribute_in_database(:"#{store_identifier}").with_indifferent_access
125+
accessor[property]
126+
else
127+
record.attribute_in_database(:"#{property}")
128+
end
129+
else # Rails <= 5.0
130+
result = record.send(value_was_method)
131+
result = result.with_indifferent_access[property] if store_accessor_attribute?
132+
result
126133
end
127134
end
128135

129136
def value_was_method
130-
:"#{property}_was"
137+
if store_accessor_attribute?
138+
:"#{store_identifier}_was"
139+
else
140+
:"#{property}_was"
141+
end
142+
end
143+
144+
def store_accessor_attribute?
145+
store_identifier.present?
146+
end
147+
148+
def store_identifier
149+
@model.stored_attributes.find { |_, attrs| attrs.include?(property.to_sym) }&.first
131150
end
132151

133152
end

spec/assignable_values/active_record_spec.rb

+206-1
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ def save_without_validation(record)
261261

262262
end
263263

264-
context 'if the :allow_blank option is set to a lambda ' do
264+
context 'if the :allow_blank option is set to a lambda' do
265265

266266
before :each do
267267
@klass = Song.disposable_copy do
@@ -370,6 +370,211 @@ def save_without_validation(record)
370370

371371
end
372372

373+
context 'when validating scalar attributes from a store_accessor' do
374+
375+
context 'without options' do
376+
377+
before :each do
378+
@klass = Song.disposable_copy do
379+
store :metadata, accessors: [:format], coder: JSON
380+
381+
assignable_values_for :format do
382+
%w[mp3 wav]
383+
end
384+
end
385+
end
386+
387+
it 'should validate that the attribute is allowed' do
388+
@klass.new(:format => 'mp3').should be_valid
389+
@klass.new(:format => 'disallowed value').should_not be_valid
390+
end
391+
392+
it 'should use the same error message as validates_inclusion_of' do
393+
record = @klass.new(:format => 'disallowed value')
394+
record.valid?
395+
errors = record.errors[:format]
396+
error = errors.respond_to?(:first) ? errors.first : errors # the return value sometimes was a string, sometimes an Array in Rails
397+
error.should == I18n.t('errors.messages.inclusion')
398+
error.should == 'is not included in the list'
399+
end
400+
401+
it 'should not allow nil for the attribute value' do
402+
@klass.new(:format => nil).should_not be_valid
403+
end
404+
405+
it 'should allow a previously saved value even if that value is no longer allowed' do
406+
record = @klass.create!(:format => 'mp3')
407+
408+
record.update_column(:metadata, { 'format' => 'pretend previously valid value' }) # update without validations for the sake of this test
409+
record.reload.should be_valid
410+
end
411+
412+
it 'should allow a previously saved, blank format value even if that value is no longer allowed' do
413+
record = @klass.create!(:format => 'mp3')
414+
415+
record.update_column(:metadata, { 'format' => nil }) # update without validations for the sake of this test
416+
record.reload.should be_valid
417+
end
418+
419+
it 'should not allow nil (the "previous value") if the record was never saved' do
420+
record = @klass.new(:format => nil)
421+
# Show that nil is not assignable, even though `record.genre_was` is nil.
422+
record.should_not be_valid
423+
end
424+
425+
it 'should generate a method returning the humanized value' do
426+
song = @klass.new(:format => 'mp3')
427+
song.humanized_format.should == 'MP3-Codec'
428+
end
429+
430+
it 'should generate a method returning the humanized value, which is nil when the value is blank' do
431+
song = @klass.new
432+
song.format = nil
433+
song.humanized_format.should be_nil
434+
song.format = ''
435+
song.humanized_format.should be_nil
436+
end
437+
438+
it 'should generate an instance method to retrieve the humanization of any given value' do
439+
song = @klass.new(:format => 'mp3')
440+
song.humanized_format('wav').should == 'WAV-Codec'
441+
end
442+
443+
it 'should generate a class method to retrieve the humanization of any given value' do
444+
@klass.humanized_format('wav').should == 'WAV-Codec'
445+
end
446+
447+
context 'for multiple: true' do
448+
before :each do
449+
@klass = Song.disposable_copy do
450+
store :metadata, accessors: [:instruments], coder: JSON
451+
452+
assignable_values_for :instruments, multiple: true do
453+
%w[piano drums guitar]
454+
end
455+
end
456+
end
457+
458+
it 'should raise when trying to humanize a value without an argument' do
459+
song = @klass.new
460+
proc { song.humanized_instrument }.should raise_error(ArgumentError)
461+
end
462+
463+
it 'should generate an instance method to retrieve the humanization of any given value' do
464+
song = @klass.new(:instruments => 'drums')
465+
song.humanized_instrument('piano').should == 'Piano'
466+
end
467+
468+
it 'should generate a class method to retrieve the humanization of any given value' do
469+
@klass.humanized_instrument('piano').should == 'Piano'
470+
end
471+
472+
it 'should generate an instance method to retrieve the humanizations of all current values' do
473+
song = @klass.new
474+
song.instruments = nil
475+
song.humanized_instruments.should == nil
476+
song.instruments = []
477+
song.humanized_instruments.should == []
478+
song.instruments = ['piano', 'drums']
479+
song.humanized_instruments.should == ['Piano', 'Drums']
480+
end
481+
end
482+
end
483+
484+
context 'if the :allow_blank option is set to true' do
485+
486+
before :each do
487+
@klass = Song.disposable_copy do
488+
489+
store :metadata, accessors: [:format], coder: JSON
490+
491+
assignable_values_for :format, :allow_blank => true do
492+
%w[mp3 wav]
493+
end
494+
end
495+
end
496+
497+
it 'should allow nil for the attribute value' do
498+
@klass.new(:format => nil).should be_valid
499+
end
500+
501+
it 'should allow an empty string as value' do
502+
@klass.new(:format => '').should be_valid
503+
end
504+
505+
end
506+
507+
context 'if the :allow_blank option is set to a symbol that refers to an instance method' do
508+
509+
before :each do
510+
@klass = Song.disposable_copy do
511+
512+
store :metadata, accessors: [:format], coder: JSON
513+
514+
attr_accessor :format_may_be_blank
515+
516+
assignable_values_for :format, :allow_blank => :format_may_be_blank do
517+
%w[mp3 wav]
518+
end
519+
520+
end
521+
end
522+
523+
it 'should call that method to determine if a blank value is allowed' do
524+
@klass.new(:format => '', :format_may_be_blank => true).should be_valid
525+
@klass.new(:format => '', :format_may_be_blank => false).should_not be_valid
526+
end
527+
528+
end
529+
530+
context 'if the :allow_blank option is set to a lambda' do
531+
532+
before :each do
533+
@klass = Song.disposable_copy do
534+
535+
store :metadata, accessors: [:format], coder: JSON
536+
537+
attr_accessor :format_may_be_blank
538+
539+
assignable_values_for :format, :allow_blank => lambda { format_may_be_blank } do
540+
%w[mp3 wav]
541+
end
542+
543+
end
544+
end
545+
546+
it 'should evaluate that lambda in the record context to determine if a blank value is allowed' do
547+
@klass.new(:format => '', :format_may_be_blank => true).should be_valid
548+
@klass.new(:format => '', :format_may_be_blank => false).should_not be_valid
549+
end
550+
551+
end
552+
553+
context 'if the :message option is set to a string' do
554+
555+
before :each do
556+
@klass = Song.disposable_copy do
557+
558+
store :metadata, accessors: [:format], coder: JSON
559+
560+
assignable_values_for :format, :message => 'should be something different' do
561+
%w[mp3 wav]
562+
end
563+
end
564+
end
565+
566+
it 'should use this string as a custom error message' do
567+
record = @klass.new(:format => 'disallowed value')
568+
record.valid?
569+
errors = record.errors[:format]
570+
error = errors.respond_to?(:first) ? errors.first : errors # the return value sometimes was a string, sometimes an Array in Rails
571+
error.should == 'should be something different'
572+
end
573+
574+
end
575+
576+
end
577+
373578
context 'when validating belongs_to associations' do
374579

375580
it 'should validate that the association is allowed' do

spec/support/database.rb

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
t.integer :year
1212
t.integer :duration
1313
t.string :multi_genres, :array => true
14+
t.json :metadata
1415
end
1516

1617
create_table :vinyl_recordings do |t|

spec/support/i18n.yml

+7
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,16 @@ en:
99

1010
assignable_values:
1111
song:
12+
format:
13+
mp3: 'MP3-Codec'
14+
wav: 'WAV-Codec'
1215
genre:
1316
pop: 'Pop music'
1417
rock: 'Rock music'
18+
instruments:
19+
drums: 'Drums'
20+
guitar: 'Guitar'
21+
piano: 'Piano'
1522
multi_genres:
1623
pop: 'Pop music'
1724
rock: 'Rock music'

0 commit comments

Comments
 (0)