Summary
For a plain ActiveModel object (include ActiveModel::Attributes + extend Enumerize, not ActiveRecord), enumerized attribute readers return the raw assigned value instead of an Enumerize::Value on Rails 8.1. Value-level predicate methods then raise NoMethodError.
Environment
|
|
| enumerize |
2.8.1 and current master (4b1c918) |
| Rails |
8.1.3 (broken) · 8.0.5, 7.2.3.1 (unaffected) |
| Ruby |
3.3.6 |
Reproduction
Self-contained, using only activemodel + enumerize:
require "active_model"
require "enumerize"
class Filter
include ActiveModel::Model
include ActiveModel::Attributes
extend Enumerize
enumerize :mode, in: %i[default full_text], default: :default
end
f = Filter.new(mode: "full_text")
f.mode.class # Rails <= 8.0: Enumerize::Value ; Rails 8.1: String
f.mode.full_text? # Rails 8.1: NoMethodError: undefined method `full_text?' for an instance of String
Root cause
Enumerize::ActiveModelAttributesSupport::Type (a subclass of ActiveModel::Type::Value) overrides deserialize but not cast. ActiveRecord reads go through deserialize (DB value → wrapped), so ActiveRecord is unaffected. Plain ActiveModel::Attributes assignment resolves reads through cast, which returns the raw value.
It historically "worked" only because enumerize's own reader module (from define_methods!) re-wraps via find_value(super) and used to sit above the ActiveModel-generated attribute-methods module in the ancestor chain. Rails 8.1 reordered attribute-method module inclusion so the generated reader now outranks enumerize's module — exposing that cast never wrapped.
The manifestation depends on declaration order:
- Declaring a plain
attribute :x before enumerize keeps enumerize's module on top, so the bug is hidden (this is why the existing ActiveModelUser test fixture, which declares attribute :name, :string first, still passes on Rails 8.1).
- Declaring
enumerize first (or only enumerized attributes, as above) exposes it.
Confirmed via MRO: the reader's owner is the ActiveModel-generated module, sitting above Enumerize::Module in .ancestors.
Fix
Define Type#cast to wrap values (returning invalid single values unchanged so inclusion validation still rejects them), keeping deserialize intact. This makes correctness independent of ancestor-chain ordering. I have verified it green on Rails 8.1.3 / 8.0.5 / 7.2.3.1.
This looks like the same problem that #476 already targets. I've reproduced and validated that approach. Also added model-level (predicate) regression tests + a CHANGELOG entry in persona-id#3.
Related
Summary
For a plain
ActiveModelobject (include ActiveModel::Attributes+extend Enumerize, not ActiveRecord), enumerized attribute readers return the raw assigned value instead of anEnumerize::Valueon Rails 8.1. Value-level predicate methods then raiseNoMethodError.Environment
master(4b1c918)Reproduction
Self-contained, using only
activemodel+enumerize:Root cause
Enumerize::ActiveModelAttributesSupport::Type(a subclass ofActiveModel::Type::Value) overridesdeserializebut notcast. ActiveRecord reads go throughdeserialize(DB value → wrapped), so ActiveRecord is unaffected. PlainActiveModel::Attributesassignment resolves reads throughcast, which returns the raw value.It historically "worked" only because enumerize's own reader module (from
define_methods!) re-wraps viafind_value(super)and used to sit above the ActiveModel-generated attribute-methods module in the ancestor chain. Rails 8.1 reordered attribute-method module inclusion so the generated reader now outranks enumerize's module — exposing thatcastnever wrapped.The manifestation depends on declaration order:
attribute :xbeforeenumerizekeeps enumerize's module on top, so the bug is hidden (this is why the existingActiveModelUsertest fixture, which declaresattribute :name, :stringfirst, still passes on Rails 8.1).enumerizefirst (or only enumerized attributes, as above) exposes it.Confirmed via MRO: the reader's owner is the ActiveModel-generated module, sitting above
Enumerize::Modulein.ancestors.Fix
Define
Type#castto wrap values (returning invalid single values unchanged so inclusion validation still rejects them), keepingdeserializeintact. This makes correctness independent of ancestor-chain ordering. I have verified it green on Rails 8.1.3 / 8.0.5 / 7.2.3.1.This looks like the same problem that #476 already targets. I've reproduced and validated that approach. Also added model-level (predicate) regression tests + a CHANGELOG entry in persona-id#3.
Related