diff --git a/lib/enumerize/activerecord.rb b/lib/enumerize/activerecord.rb index 068b280..d52af33 100644 --- a/lib/enumerize/activerecord.rb +++ b/lib/enumerize/activerecord.rb @@ -115,10 +115,19 @@ def initialize(attr, subtype) end def serialize(value) - v = @attr.find_value(value) - return value unless v + # First try to find the enumerize value directly + enumerize_value = @attr.find_value(value) + + # If not found and we have a subtype, delegate to it for transformation + # (e.g., normalization) and try again + if !enumerize_value && @subtype + casted_value = @subtype.cast(value) + enumerize_value = @attr.find_value(casted_value) + end - v.value + return value unless enumerize_value + + enumerize_value.value end def cast(value) @@ -127,10 +136,15 @@ def cast(value) # First try to find the enumerize value directly enumerize_value = @attr.find_value(value) - return enumerize_value if enumerize_value - # If not found, delegate to subtype then try to find value - @attr.find_value(@subtype.cast(value)) + # If not found and we have a subtype, delegate to it for transformation + # (e.g., normalization) and try again + if !enumerize_value && @subtype + casted_value = @subtype.cast(value) + enumerize_value = @attr.find_value(casted_value) + end + + enumerize_value end def as_json(options = nil) diff --git a/lib/enumerize/attribute.rb b/lib/enumerize/attribute.rb index b22bb7c..d8ed57d 100644 --- a/lib/enumerize/attribute.rb +++ b/lib/enumerize/attribute.rb @@ -88,6 +88,15 @@ def respond_to_missing?(method, include_private=false) @value_hash.include?(method.to_s) || super end + # Defines getter and setter methods for the enumerated attribute. + # + # The setter method behavior: + # - Valid enum values: Stores the underlying value + # - Empty strings: Converts to nil (historically consistent behavior, now explicitly maintained) + # - Invalid values: Passes through to allow Rails processing (e.g., normalizes, type casting) + # + # Note: Empty string to nil conversion was implicit in previous versions but is now + # explicitly implemented to ensure consistent behavior across all ORMs. def define_methods!(mod) mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{name} @@ -105,20 +114,27 @@ def #{name} end def #{name}=(new_value) - allowed_value_or_nil = self.class.enumerized_attributes[:#{name}].find_value(new_value) - allowed_value_or_nil = allowed_value_or_nil.value unless allowed_value_or_nil.nil? + allowed_value = self.class.enumerized_attributes[:#{name}].find_value(new_value) + + value_to_assign = if allowed_value + allowed_value.value + elsif new_value == '' + nil + else + new_value + end if defined?(super) - super allowed_value_or_nil + super value_to_assign elsif respond_to?(:write_attribute, true) - write_attribute '#{name}', allowed_value_or_nil + write_attribute '#{name}', value_to_assign else - @#{name} = allowed_value_or_nil + @#{name} = value_to_assign end _enumerized_values_for_validation['#{name}'] = new_value.nil? ? nil : new_value.to_s - allowed_value_or_nil + value_to_assign end def #{name}_text diff --git a/test/activerecord_test.rb b/test/activerecord_test.rb index 5fdc213..76ecc03 100644 --- a/test/activerecord_test.rb +++ b/test/activerecord_test.rb @@ -75,6 +75,7 @@ t.string :foo t.boolean :newsletter_subscribed, default: true t.json :store_accessor_store_with_no_defaults + t.string :locale end create_table :documents do |t| @@ -114,6 +115,12 @@ class User < ActiveRecord::Base enumerize :language, :in => [:en, :jp] enumerize :country_code, :in => [:us, :ca] + # normalizes is available in Rails 7.1+ + if ActiveRecord.gem_version >= Gem::Version.new("7.1") + normalizes :locale, with: ->(value) { value.downcase.strip.presence } + end + enumerize :locale, :in => %i[de en pl] + serialize :interests, type: Array enumerize :interests, :in => [:music, :sports, :dancing, :programming], :multiple => true @@ -808,13 +815,13 @@ class AdminUser < User users = User.all.to_a expect(users.size).must_equal 3 - + expect(users[0].sex).must_equal 'male' expect(users[0].status).must_equal 'active' - + expect(users[1].sex).must_equal 'female' expect(users[1].status).must_equal 'blocked' - + expect(users[2].sex).must_equal 'male' expect(users[2].status).must_equal 'blocked' end @@ -846,14 +853,28 @@ class AdminUser < User users = User.order(:id).to_a expect(users.size).must_equal 3 - + expect(users[0].sex).must_equal 'male' expect(users[0].status).must_equal 'active' - + expect(users[1].sex).must_equal 'female' expect(users[1].status).must_equal 'blocked' - + expect(users[2].sex).must_equal 'female' expect(users[2].status).must_equal 'blocked' end + + # normalizes is available in Rails 7.1+ + if ActiveRecord.gem_version >= Gem::Version.new("7.1") + it 'supports AR#normalizes class methods' do + User.delete_all + User.create!(locale: 'de') + expect(User.exists?(locale: ' DE ')).must_equal true + end + + it 'supports AR#normalizes instance methods' do + user = User.new(locale: ' DE ') + expect(user.locale).must_equal 'de' + end + end end