Skip to content

Commit

Permalink
[D-326] Store accessor support for assignable_values
Browse files Browse the repository at this point in the history
  • Loading branch information
begerdom committed Dec 5, 2024
1 parent 7c1a557 commit 8ce4ac4
Show file tree
Hide file tree
Showing 8 changed files with 272 additions and 12 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-20.04
services:
mysql:
image: mysql:5.6
image: mysql:5.7
env:
MYSQL_ROOT_PASSWORD: password
ports:
Expand Down
1 change: 1 addition & 0 deletions lib/assignable_values.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
require 'assignable_values/active_record/restriction/base'
require 'assignable_values/active_record/restriction/belongs_to_association'
require 'assignable_values/active_record/restriction/scalar_attribute'
require 'assignable_values/active_record/restriction/store_accessor_attribute'
require 'assignable_values/humanized_value'
require 'assignable_values/humanizable_string'
17 changes: 16 additions & 1 deletion lib/assignable_values/active_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ module ActiveRecord
private

def assignable_values_for(property, options = {}, &values)
restriction_type = belongs_to_association?(property) ? Restriction::BelongsToAssociation : Restriction::ScalarAttribute
restriction_type = if belongs_to_association?(property)
Restriction::BelongsToAssociation
elsif store_accessor_attribute?(property)
Restriction::StoreAccessorAttribute
else
Restriction::ScalarAttribute
end

restriction_type.new(self, property, options, &values)
end

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

def store_accessor_attribute?(property)
store_identifier_of(property).present?
end

def store_identifier_of(property)
stored_attributes.find { |_, attrs| attrs.include?(property.to_sym) }&.first
end

end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,10 @@ def decorate_values(values, klass)
end

def has_previously_saved_value?(record)
if record.respond_to?(:attribute_in_database)
!record.new_record? # Rails >= 5.1
else
!record.new_record? && record.respond_to?(value_was_method) # Rails <= 5.0
if record.respond_to?(:attribute_in_database) # Rails >= 5.1
!record.new_record?
else # Rails <= 5.0
!record.new_record? && record.respond_to?(value_was_method)
end
end

Expand All @@ -119,10 +119,10 @@ def previously_saved_value(record)
end

def value_was(record)
if record.respond_to?(:attribute_in_database)
record.attribute_in_database(:"#{property}") # Rails >= 5.1
else
record.send(value_was_method) # Rails <= 5.0
if record.respond_to?(:attribute_in_database) # Rails >= 5.1
record.attribute_in_database(:"#{property}")
else # Rails <= 5.0
record.send(value_was_method)
end
end

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module AssignableValues
module ActiveRecord
module Restriction
class StoreAccessorAttribute < ScalarAttribute

private

def store_identifier
@model.stored_attributes.find { |_, attrs| attrs.include?(property.to_sym) }&.first
end

def value_was_method
:"#{store_identifier}_was"
end

def value_was(record)
accessor = if record.respond_to?(:attribute_in_database) # Rails >= 5.1
record.attribute_in_database(:"#{store_identifier}")
else # Rails <= 5.0
record.send(value_was_method)
end

accessor.with_indifferent_access[property]
end

end
end
end
end
211 changes: 209 additions & 2 deletions spec/assignable_values/active_record_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,6 @@ def save_without_validation(record)
errors = record.errors[:genre]
error = errors.respond_to?(:first) ? errors.first : errors # the return value sometimes was a string, sometimes an Array in Rails
error.should == I18n.t('errors.messages.inclusion')
error.should == 'is not included in the list'
end

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

end

context 'if the :allow_blank option is set to a lambda ' do
context 'if the :allow_blank option is set to a lambda' do

before :each do
@klass = Song.disposable_copy do
Expand Down Expand Up @@ -370,6 +369,214 @@ def save_without_validation(record)

end

context 'when validating scalar attributes from a store_accessor' do

context 'without options' do

before :each do
@klass = Song.disposable_copy do
store :metadata, accessors: [:format], coder: JSON

assignable_values_for :format do
%w[mp3 wav]
end
end
end

it 'should validate that the attribute is allowed' do
@klass.new(:format => 'mp3').should be_valid
@klass.new(:format => 'disallowed value').should_not be_valid
end

it 'should use the same error message as validates_inclusion_of' do
record = @klass.new(:format => 'disallowed value')
record.valid?
errors = record.errors[:format]
error = errors.respond_to?(:first) ? errors.first : errors # the return value sometimes was a string, sometimes an Array in Rails
error.should == I18n.t('errors.messages.inclusion')
end

it 'should not allow nil for the attribute value' do
@klass.new(:format => nil).should_not be_valid
end

it 'should allow a previously saved value even if that value is no longer allowed' do
record = @klass.create!(:format => 'mp3')

record.update_column(:metadata, { 'format' => 'pretend previously valid value' }) # update without validations for the sake of this test
record.reload.should be_valid
end

it 'should allow a previously saved, blank format value even if that value is no longer allowed' do
record = @klass.create!(:format => 'mp3')

record.update_column(:metadata, { 'format' => nil }) # update without validations for the sake of this test
record.reload.should be_valid
end

it 'should not allow nil (the "previous value") if the record was never saved' do
record = @klass.new(:format => nil)
record.should_not be_valid
end

it 'should generate a method returning the humanized value' do
song = @klass.new(:format => 'mp3')
song.humanized_format.should == 'MP3-Codec'
end

it 'should generate a method returning the humanized value, which is nil when the value is blank' do
song = @klass.new
song.format = nil
song.humanized_format.should be_nil
song.format = ''
song.humanized_format.should be_nil
end

it 'should generate an instance method to retrieve the humanization of any given value' do
song = @klass.new(:format => 'mp3')
song.humanized_format('wav').should == 'WAV-Codec'
end

it 'should generate a class method to retrieve the humanization of any given value' do
@klass.humanized_format('wav').should == 'WAV-Codec'
end

context 'for multiple: true' do
before :each do
@klass = Song.disposable_copy do
store :metadata, accessors: [:instruments], coder: JSON

assignable_values_for :instruments, multiple: true do
%w[piano drums guitar]
end
end
end

it 'allows multiple assignments' do
song = @klass.new(:instruments => %w[guitar drums])
song.should be_valid
end

it 'should raise when trying to humanize a value without an argument' do
song = @klass.new
proc { song.humanized_instrument }.should raise_error(ArgumentError)
end

it 'should generate an instance method to retrieve the humanization of any given value' do
song = @klass.new(:instruments => 'drums')
song.humanized_instrument('piano').should == 'Piano'
end

it 'should generate a class method to retrieve the humanization of any given value' do
@klass.humanized_instrument('piano').should == 'Piano'
end

it 'should generate an instance method to retrieve the humanizations of all current values' do
song = @klass.new
song.instruments = nil
song.humanized_instruments.should == nil
song.instruments = []
song.humanized_instruments.should == []
song.instruments = ['piano', 'drums']
song.humanized_instruments.should == ['Piano', 'Drums']
end
end
end

context 'if the :allow_blank option is set to true' do

before :each do
@klass = Song.disposable_copy do

store :metadata, accessors: [:format], coder: JSON

assignable_values_for :format, :allow_blank => true do
%w[mp3 wav]
end
end
end

it 'should allow nil for the attribute value' do
@klass.new(:format => nil).should be_valid
end

it 'should allow an empty string as value' do
@klass.new(:format => '').should be_valid
end

end

context 'if the :allow_blank option is set to a symbol that refers to an instance method' do

before :each do
@klass = Song.disposable_copy do

store :metadata, accessors: [:format], coder: JSON

attr_accessor :format_may_be_blank

assignable_values_for :format, :allow_blank => :format_may_be_blank do
%w[mp3 wav]
end

end
end

it 'should call that method to determine if a blank value is allowed' do
@klass.new(:format => '', :format_may_be_blank => true).should be_valid
@klass.new(:format => '', :format_may_be_blank => false).should_not be_valid
end

end

context 'if the :allow_blank option is set to a lambda' do

before :each do
@klass = Song.disposable_copy do

store :metadata, accessors: [:format], coder: JSON

attr_accessor :format_may_be_blank

assignable_values_for :format, :allow_blank => lambda { format_may_be_blank } do
%w[mp3 wav]
end

end
end

it 'should evaluate that lambda in the record context to determine if a blank value is allowed' do
@klass.new(:format => '', :format_may_be_blank => true).should be_valid
@klass.new(:format => '', :format_may_be_blank => false).should_not be_valid
end

end

context 'if the :message option is set to a string' do

before :each do
@klass = Song.disposable_copy do

store :metadata, accessors: [:format], coder: JSON

assignable_values_for :format, :message => 'should be something different' do
%w[mp3 wav]
end
end
end

it 'should use this string as a custom error message' do
record = @klass.new(:format => 'disallowed value')
record.valid?
errors = record.errors[:format]
error = errors.respond_to?(:first) ? errors.first : errors # the return value sometimes was a string, sometimes an Array in Rails
error.should == 'should be something different'
end

end

end

context 'when validating belongs_to associations' do

it 'should validate that the association is allowed' do
Expand Down
1 change: 1 addition & 0 deletions spec/support/database.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
t.integer :year
t.integer :duration
t.string :multi_genres, :array => true
t.json :metadata
end

create_table :vinyl_recordings do |t|
Expand Down
7 changes: 7 additions & 0 deletions spec/support/i18n.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,16 @@ en:

assignable_values:
song:
format:
mp3: 'MP3-Codec'
wav: 'WAV-Codec'
genre:
pop: 'Pop music'
rock: 'Rock music'
instruments:
drums: 'Drums'
guitar: 'Guitar'
piano: 'Piano'
multi_genres:
pop: 'Pop music'
rock: 'Rock music'
Expand Down

0 comments on commit 8ce4ac4

Please sign in to comment.