From aae6356cdf4e18dedd151d46804b9e31278b9bce Mon Sep 17 00:00:00 2001 From: Dominic Beger Date: Thu, 7 Nov 2024 19:07:46 +0100 Subject: [PATCH] [D-326] Store accessor support for assignable_values --- .github/workflows/test.yml | 2 +- .../restriction/scalar_attribute.rb | 29 ++- spec/assignable_values/active_record_spec.rb | 207 +++++++++++++++++- spec/support/database.rb | 1 + spec/support/i18n.yml | 7 + 5 files changed, 239 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8783200..fb45421 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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: diff --git a/lib/assignable_values/active_record/restriction/scalar_attribute.rb b/lib/assignable_values/active_record/restriction/scalar_attribute.rb index 602cc02..fe03c57 100644 --- a/lib/assignable_values/active_record/restriction/scalar_attribute.rb +++ b/lib/assignable_values/active_record/restriction/scalar_attribute.rb @@ -119,15 +119,34 @@ 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 + if store_accessor_attribute? + accessor = record.attribute_in_database(:"#{store_identifier}").with_indifferent_access + accessor[property] + else + record.attribute_in_database(:"#{property}") + end + else # Rails <= 5.0 + result = record.send(value_was_method).with_indifferent_access + result = result[property] if store_accessor_attribute? + result end end def value_was_method - :"#{property}_was" + if store_accessor_attribute? + :"#{store_identifier}_was" + else + :"#{property}_was" + end + end + + def store_accessor_attribute? + store_identifier.present? + end + + def store_identifier + @model.stored_attributes.find { |_, attrs| attrs.include?(property.to_sym) }&.first end end diff --git a/spec/assignable_values/active_record_spec.rb b/spec/assignable_values/active_record_spec.rb index 01aa4e7..e80bbe1 100644 --- a/spec/assignable_values/active_record_spec.rb +++ b/spec/assignable_values/active_record_spec.rb @@ -261,7 +261,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 @@ -370,6 +370,211 @@ 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') + error.should == 'is not included in the list' + 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) + # Show that nil is not assignable, even though `record.genre_was` is 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 '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 diff --git a/spec/support/database.rb b/spec/support/database.rb index 21e3a08..098a2fa 100644 --- a/spec/support/database.rb +++ b/spec/support/database.rb @@ -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| diff --git a/spec/support/i18n.yml b/spec/support/i18n.yml index 14085a1..2274e3b 100644 --- a/spec/support/i18n.yml +++ b/spec/support/i18n.yml @@ -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'