Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

### bug fix

* Fixed `Enumerize::ActiveModelAttributesSupport::Type` to wrap values in `#cast` so enumerized readers on plain `ActiveModel::Attributes` objects return an `Enumerize::Value` (and value predicates keep working) on Rails 8.1. Rails 8.1 reordered attribute-method module inclusion so the generated reader can outrank enumerize's module; assignment reads go through `#cast`, which previously returned the raw value. See #482.
* Fixed `Enumerize::ActiveModelAttributesSupport::Type#deserialize` to properly handle arrays for `multiple: true` attributes. Previously, deserializing an array would return `nil` instead of the enumerated values. This bug only affected ActiveModel::Attributes usage (not ActiveRecord) and was exposed when used with gems like store_model v2.0.0+ that call `deserialize` during load.

### enchancements
Expand Down
61 changes: 61 additions & 0 deletions test/activemodel_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ class InterestsRequiredActiveModelUser < ActiveModelUser
validates :interests, presence: true
end

# No plain `attribute` is declared before `enumerize` here. On Rails 8.1 the
# ActiveModel-generated attribute-method module then outranks enumerize's own
# reader module in the ancestor chain, so reads resolve through the type's
# #cast. This model reproduces the raw-value regression that #cast fixes.
class CastActiveModelUser
include ActiveModel::Model
include ActiveModel::Attributes
extend Enumerize

enumerize :sex, :in => %w[male female]
enumerize :role, :in => %w[admin user], :default => 'user'
end

let(:model) { ActiveModelUser }

it 'initialize value' do
Expand Down Expand Up @@ -235,6 +248,54 @@ class InterestsRequiredActiveModelUser < ActiveModelUser
expect(result).must_be_nil
end
end

describe 'Type#cast (model reader)' do
# Regression coverage for the Rails 8.1 attribute-method module ordering
# change: when no plain `attribute` precedes `enumerize`, the generated
# reader outranks enumerize's module, so reads go through #cast and must
# still return an Enumerize::Value. Without the #cast override the first
# two examples fail on Rails 8.1 (raw String/Symbol instead of a value).
it 'wraps a user-assigned value so predicate methods work' do
user = CastActiveModelUser.new(sex: 'male')
expect(user.sex).must_be_instance_of Enumerize::Value
expect(user.sex.male?).must_equal true
expect(user.sex.female?).must_equal false
end

it 'wraps a value assigned after initialization' do
user = CastActiveModelUser.new
user.sex = :female
expect(user.sex).must_be_instance_of Enumerize::Value
expect(user.sex.female?).must_equal true
end

it 'wraps the default value' do
user = CastActiveModelUser.new
expect(user.role).must_be_instance_of Enumerize::Value
expect(user.role.user?).must_equal true
end

it 'returns nil when nil is assigned' do
user = CastActiveModelUser.new(sex: 'male')
user.sex = nil
expect(user.sex).must_be_nil
end

it 'keeps an invalid value assignable so inclusion validation still rejects it' do
# The reader's return for an invalid value is ordering-dependent (nil when
# enumerize's writer wins on Rails <= 8.0, the raw value when the generated
# reader wins on Rails 8.1); the portable guarantee is that validation fails.
user = CastActiveModelUser.new(sex: 'invalid')
expect(user).wont_be :valid?
expect(user.errors[:sex]).wont_be :empty?
end

it 'leaves multiple: true attributes as an unmangled Enumerize::Set' do
user = ActiveModelUser.new(interests: [:music, :programming])
expect(user.interests).must_be_instance_of Enumerize::Set
expect(user.interests.to_a).must_equal %w[music programming]
end
end
end

else
Expand Down