From 46eb8600feca444c5f66a936cf0f5cb973c34fde Mon Sep 17 00:00:00 2001 From: Shinji Nakamatsu <19329+snaka@users.noreply.github.com> Date: Sat, 13 Sep 2025 11:13:14 +0900 Subject: [PATCH 1/2] test: add ActiveRecord normalizes method compatibility tests Add test cases to verify that enumerized attributes work correctly with Rails 7.1+ normalizes method for data normalization. --- test/activerecord_test.rb | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) 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 From d5d9faff8b506cc1cc3fe836eebf10cbd11b36de Mon Sep 17 00:00:00 2001 From: Shinji Nakamatsu <19329+snaka@users.noreply.github.com> Date: Mon, 22 Sep 2025 17:23:54 +0900 Subject: [PATCH 2/2] fix: normalize empty strings to nil for consistency across ORMs Fix issue where empty strings were not being stored as nil in Sequel and other ORMs. This ensures consistent behavior across all ORMs by converting empty strings to nil, matching ActiveRecord's behavior. - Convert empty strings to nil in attribute setter - Fix failing SequelTest for empty string assignment - Ensure consistent behavior between ActiveRecord and Sequel --- lib/enumerize/activerecord.rb | 26 ++++++++++++++++++++------ lib/enumerize/attribute.rb | 28 ++++++++++++++++++++++------ 2 files changed, 42 insertions(+), 12 deletions(-) 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