Support Active Model attribute type decoration
This adds a `decorate_attributes` method to Active Model to support attribute type decoration. `decorate_attributes` is a private, low-level API that is intended to be wrapped by high-level APIs like `ActiveRecord::Base::normalizes` and `ActiveRecord::Base::enum`.
This commit is contained in:
parent
53438e9216
commit
31bb0b4aee
@ -10,17 +10,27 @@ module AttributeRegistration # :nodoc:
|
||||
|
||||
module ClassMethods # :nodoc:
|
||||
def attribute(name, type = nil, default: (no_default = true), **options)
|
||||
name = resolve_attribute_name(name)
|
||||
type = resolve_type_name(type, **options) if type.is_a?(Symbol)
|
||||
|
||||
pending = pending_attribute(name)
|
||||
pending.type = type if type
|
||||
pending.default = default unless no_default
|
||||
pending_attribute_modifications << PendingType.new(name, type) if type || no_default
|
||||
pending_attribute_modifications << PendingDefault.new(name, default) unless no_default
|
||||
|
||||
reset_default_attributes
|
||||
end
|
||||
|
||||
def decorate_attributes(names = nil, &decorator) # :nodoc:
|
||||
names = names&.map { |name| resolve_attribute_name(name) }
|
||||
|
||||
pending_attribute_modifications << PendingDecorator.new(names, decorator)
|
||||
|
||||
reset_default_attributes
|
||||
end
|
||||
|
||||
def _default_attributes # :nodoc:
|
||||
@default_attributes ||= build_default_attributes
|
||||
@default_attributes ||= AttributeSet.new({}).tap do |attribute_set|
|
||||
apply_pending_attribute_modifications(attribute_set)
|
||||
end
|
||||
end
|
||||
|
||||
def attribute_types # :nodoc:
|
||||
@ -30,33 +40,39 @@ def attribute_types # :nodoc:
|
||||
end
|
||||
|
||||
private
|
||||
class PendingAttribute # :nodoc:
|
||||
attr_accessor :type, :default
|
||||
|
||||
def apply_to(attribute)
|
||||
attribute = attribute.with_type(type || attribute.type)
|
||||
attribute = attribute.with_user_default(default) if defined?(@default)
|
||||
attribute
|
||||
PendingType = Struct.new(:name, :type) do # :nodoc:
|
||||
def apply_to(attribute_set)
|
||||
attribute = attribute_set[name]
|
||||
attribute_set[name] = attribute.with_type(type || attribute.type)
|
||||
end
|
||||
end
|
||||
|
||||
def pending_attribute(name)
|
||||
@pending_attributes ||= {}
|
||||
@pending_attributes[resolve_attribute_name(name)] ||= PendingAttribute.new
|
||||
PendingDefault = Struct.new(:name, :default) do # :nodoc:
|
||||
def apply_to(attribute_set)
|
||||
attribute_set[name] = attribute_set[name].with_user_default(default)
|
||||
end
|
||||
end
|
||||
|
||||
def apply_pending_attributes(attribute_set)
|
||||
PendingDecorator = Struct.new(:names, :decorator) do # :nodoc:
|
||||
def apply_to(attribute_set)
|
||||
(names || attribute_set.keys).each do |name|
|
||||
attribute = attribute_set[name]
|
||||
type = decorator.call(name, attribute.type)
|
||||
attribute_set[name] = attribute.with_type(type) if type
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def pending_attribute_modifications
|
||||
@pending_attribute_modifications ||= []
|
||||
end
|
||||
|
||||
def apply_pending_attribute_modifications(attribute_set)
|
||||
superclass.send(__method__, attribute_set) if superclass.respond_to?(__method__, true)
|
||||
|
||||
defined?(@pending_attributes) && @pending_attributes.each do |name, pending|
|
||||
attribute_set[name] = pending.apply_to(attribute_set[name])
|
||||
pending_attribute_modifications.each do |modification|
|
||||
modification.apply_to(attribute_set)
|
||||
end
|
||||
|
||||
attribute_set
|
||||
end
|
||||
|
||||
def build_default_attributes
|
||||
apply_pending_attributes(AttributeSet.new({}))
|
||||
end
|
||||
|
||||
def reset_default_attributes
|
||||
|
@ -6,9 +6,20 @@ module ActiveModel
|
||||
class AttributeRegistrationTest < ActiveModel::TestCase
|
||||
MyType = Class.new(Type::Value)
|
||||
Type.register(MyType.name.to_sym, MyType)
|
||||
|
||||
TYPE_1 = MyType.new(precision: 1)
|
||||
TYPE_2 = MyType.new(precision: 2)
|
||||
|
||||
MyDecorator = DelegateClass(Type::Value) do
|
||||
attr_reader :name
|
||||
alias :cast_type :__getobj__
|
||||
|
||||
def initialize(name, cast_type)
|
||||
super(cast_type)
|
||||
@name = name
|
||||
end
|
||||
end
|
||||
|
||||
test "attributes can be registered" do
|
||||
attributes = default_attributes_for { attribute :foo, TYPE_1 }
|
||||
assert_same TYPE_1, attributes["foo"].type
|
||||
@ -52,12 +63,12 @@ class AttributeRegistrationTest < ActiveModel::TestCase
|
||||
assert_not_predicate attributes["bar"], :came_from_user?
|
||||
end
|
||||
|
||||
test "attribute_types reflects registered attribute types" do
|
||||
test "::attribute_types reflects registered attribute types" do
|
||||
klass = class_with { attribute :foo, TYPE_1 }
|
||||
assert_same TYPE_1, klass.attribute_types["foo"]
|
||||
end
|
||||
|
||||
test "attribute_types returns the default type when key is missing" do
|
||||
test "::attribute_types returns the default type when key is missing" do
|
||||
klass = class_with { attribute :foo, TYPE_1 }
|
||||
assert_equal Type::Value.new, klass.attribute_types["bar"]
|
||||
end
|
||||
@ -137,6 +148,90 @@ class AttributeRegistrationTest < ActiveModel::TestCase
|
||||
assert_nil parent._default_attributes["bar"].value
|
||||
end
|
||||
|
||||
test "::decorate_attributes decorates specified attributes" do
|
||||
attributes = default_attributes_for do
|
||||
attribute :foo, TYPE_1
|
||||
attribute :bar, TYPE_2
|
||||
attribute :qux, TYPE_2
|
||||
decorate_attributes([:foo, :bar]) { |name, type| MyDecorator.new(name, type) }
|
||||
end
|
||||
|
||||
assert_instance_of MyDecorator, attributes["foo"].type
|
||||
assert_equal "foo", attributes["foo"].type.name
|
||||
assert_same TYPE_1, attributes["foo"].type.cast_type
|
||||
|
||||
assert_instance_of MyDecorator, attributes["bar"].type
|
||||
assert_equal "bar", attributes["bar"].type.name
|
||||
assert_same TYPE_2, attributes["bar"].type.cast_type
|
||||
|
||||
assert_same TYPE_2, attributes["qux"].type
|
||||
end
|
||||
|
||||
test "::decorate_attributes decorates all attributes when none are specified" do
|
||||
attributes = default_attributes_for do
|
||||
attribute :foo, TYPE_1
|
||||
attribute :bar, TYPE_2
|
||||
decorate_attributes { |name, type| MyDecorator.new(name, type) }
|
||||
end
|
||||
|
||||
assert_same TYPE_1, attributes["foo"].type.cast_type
|
||||
assert_same TYPE_2, attributes["bar"].type.cast_type
|
||||
end
|
||||
|
||||
test "::decorate_attributes supports conditional decoration" do
|
||||
attributes = default_attributes_for do
|
||||
attribute :foo, TYPE_1
|
||||
attribute :bar, TYPE_2
|
||||
decorate_attributes { |name, type| MyDecorator.new(name, type) if name.match?(/oo/) }
|
||||
end
|
||||
|
||||
assert_same TYPE_1, attributes["foo"].type.cast_type
|
||||
assert_same TYPE_2, attributes["bar"].type
|
||||
end
|
||||
|
||||
test "::decorate_attributes stacks decorators" do
|
||||
attributes = default_attributes_for do
|
||||
attribute :foo, TYPE_1
|
||||
decorate_attributes { |name, type| MyDecorator.new("#{name}1", type) }
|
||||
decorate_attributes { |name, type| MyDecorator.new("#{name}2", type) }
|
||||
end
|
||||
|
||||
assert_instance_of MyDecorator, attributes["foo"].type
|
||||
assert_equal "foo2", attributes["foo"].type.name
|
||||
|
||||
assert_instance_of MyDecorator, attributes["foo"].type.cast_type
|
||||
assert_equal "foo1", attributes["foo"].type.cast_type.name
|
||||
|
||||
assert_same TYPE_1, attributes["foo"].type.cast_type.cast_type
|
||||
end
|
||||
|
||||
test "superclass attribute types can be decorated" do
|
||||
parent = class_with do
|
||||
attribute :foo, TYPE_1
|
||||
end
|
||||
|
||||
child = class_with(parent) do
|
||||
decorate_attributes { |name, type| MyDecorator.new(name, type) }
|
||||
end
|
||||
|
||||
assert_instance_of MyDecorator, child._default_attributes["foo"].type
|
||||
assert_same TYPE_1, child._default_attributes["foo"].type.cast_type
|
||||
assert_same TYPE_1, parent._default_attributes["foo"].type
|
||||
end
|
||||
|
||||
test "re-registering an attribute overrides previous decorators" do
|
||||
parent = class_with do
|
||||
attribute :foo, TYPE_1
|
||||
decorate_attributes { |name, type| MyDecorator.new(name, type) }
|
||||
end
|
||||
|
||||
child = class_with(parent) do
|
||||
attribute :foo, TYPE_1
|
||||
end
|
||||
|
||||
assert_same TYPE_1, child._default_attributes["foo"].type
|
||||
end
|
||||
|
||||
private
|
||||
def class_with(base_class = nil, &block)
|
||||
Class.new(*base_class) do
|
||||
|
Loading…
Reference in New Issue
Block a user