Skip to content

Commit 8ce4ac4

Browse files
committed
[D-326] Store accessor support for assignable_values
1 parent 7c1a557 commit 8ce4ac4

File tree

8 files changed

+272
-12
lines changed

8 files changed

+272
-12
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.rb

+1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
require 'assignable_values/active_record/restriction/base'
66
require 'assignable_values/active_record/restriction/belongs_to_association'
77
require 'assignable_values/active_record/restriction/scalar_attribute'
8+
require 'assignable_values/active_record/restriction/store_accessor_attribute'
89
require 'assignable_values/humanized_value'
910
require 'assignable_values/humanizable_string'

lib/assignable_values/active_record.rb

+16-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@ module ActiveRecord
44
private
55

66
def assignable_values_for(property, options = {}, &values)
7-
restriction_type = belongs_to_association?(property) ? Restriction::BelongsToAssociation : Restriction::ScalarAttribute
7+
restriction_type = if belongs_to_association?(property)
8+
Restriction::BelongsToAssociation
9+
elsif store_accessor_attribute?(property)
10+
Restriction::StoreAccessorAttribute
11+
else
12+
Restriction::ScalarAttribute
13+
end
14+
815
restriction_type.new(self, property, options, &values)
916
end
1017

@@ -13,6 +20,14 @@ def belongs_to_association?(property)
1320
reflection && reflection.macro == :belongs_to
1421
end
1522

23+
def store_accessor_attribute?(property)
24+
store_identifier_of(property).present?
25+
end
26+
27+
def store_identifier_of(property)
28+
stored_attributes.find { |_, attrs| attrs.include?(property.to_sym) }&.first
29+
end
30+
1631
end
1732
end
1833

lib/assignable_values/active_record/restriction/scalar_attribute.rb

+8-8
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,10 @@ def decorate_values(values, klass)
107107
end
108108

109109
def has_previously_saved_value?(record)
110-
if record.respond_to?(:attribute_in_database)
111-
!record.new_record? # Rails >= 5.1
112-
else
113-
!record.new_record? && record.respond_to?(value_was_method) # Rails <= 5.0
110+
if record.respond_to?(:attribute_in_database) # Rails >= 5.1
111+
!record.new_record?
112+
else # Rails <= 5.0
113+
!record.new_record? && record.respond_to?(value_was_method)
114114
end
115115
end
116116

@@ -119,10 +119,10 @@ 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+
record.attribute_in_database(:"#{property}")
124+
else # Rails <= 5.0
125+
record.send(value_was_method)
126126
end
127127
end
128128

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
module AssignableValues
2+
module ActiveRecord
3+
module Restriction
4+
class StoreAccessorAttribute < ScalarAttribute
5+
6+
private
7+
8+
def store_identifier
9+
@model.stored_attributes.find { |_, attrs| attrs.include?(property.to_sym) }&.first
10+
end
11+
12+
def value_was_method
13+
:"#{store_identifier}_was"
14+
end
15+
16+
def value_was(record)
17+
accessor = if record.respond_to?(:attribute_in_database) # Rails >= 5.1
18+
record.attribute_in_database(:"#{store_identifier}")
19+
else # Rails <= 5.0
20+
record.send(value_was_method)
21+
end
22+
23+
accessor.with_indifferent_access[property]
24+
end
25+
26+
end
27+
end
28+
end
29+
end

spec/assignable_values/active_record_spec.rb

+209-2
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,6 @@ def save_without_validation(record)
146146
errors = record.errors[:genre]
147147
error = errors.respond_to?(:first) ? errors.first : errors # the return value sometimes was a string, sometimes an Array in Rails
148148
error.should == I18n.t('errors.messages.inclusion')
149-
error.should == 'is not included in the list'
150149
end
151150

152151
it 'should not allow nil for the attribute value' do
@@ -261,7 +260,7 @@ def save_without_validation(record)
261260

262261
end
263262

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

266265
before :each do
267266
@klass = Song.disposable_copy do
@@ -370,6 +369,214 @@ def save_without_validation(record)
370369

371370
end
372371

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

375582
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)