Skip to content

Commit

Permalink
Support Active Model attribute type decoration
Browse files Browse the repository at this point in the history
This adds support for decorating Active Model attribute types, similar
to Active Record, but with the `decorate_attribute` API from rails#41262.
For now, this is kept as a private API.

Co-authored-by: Ryuta Kamizono <kamipo@gmail.com>
  • Loading branch information
jonathanhefner and kamipo committed Mar 12, 2022
1 parent 608cbfa commit 5bbd2d6
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 2 deletions.
26 changes: 24 additions & 2 deletions activemodel/lib/active_model/attribute_registration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ def attribute(name, type = nil, default: (no_default = true), **options)
reset_default_attributes
end

def decorate_attribute(attribute, decorator, default: (no_default = true), **options)
pending = pending_attribute(attribute)
pending.decorate { |type| resolve_type_name(decorator, **options, subtype: type) }
pending.default = default unless no_default

reset_default_attributes
end

def _default_attributes # :nodoc:
@default_attributes ||= build_default_attributes
end
Expand All @@ -31,10 +39,24 @@ def attribute_types # :nodoc:

private
class PendingAttribute # :nodoc:
attr_accessor :type, :default
attr_accessor :default, :decorator

def type=(type)
self.decorator = nil
@type = type
end
attr_reader :type

def decorate(&decorator)
if type
self.type = decorator.call(type)
else
self.decorator = [self.decorator, decorator].compact.reduce(&:>>)
end
end

def apply_to(attribute)
attribute = attribute.with_type(type || attribute.type)
attribute = attribute.with_type(type || decorator&.call(attribute.type) || attribute.type)
attribute = attribute.with_user_default(default) if defined?(@default)
attribute
end
Expand Down
69 changes: 69 additions & 0 deletions activemodel/test/cases/attribute_registration_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,20 @@
module ActiveModel
class AttributeRegistrationTest < ActiveModel::TestCase
MyType = Class.new(Type::Value)

MyDecorator = DelegateClass(Type::Value) do
alias :subtype :__getobj__
attr_reader :my_option

def initialize(subtype:, my_option: nil)
super(subtype)
@my_option = my_option
end
end

Type.register(MyType.name.to_sym, MyType)
Type.register(MyDecorator.name.to_sym, MyDecorator)

TYPE_1 = MyType.new(precision: 1)
TYPE_2 = MyType.new(precision: 2)

Expand Down Expand Up @@ -52,6 +65,52 @@ class AttributeRegistrationTest < ActiveModel::TestCase
assert_not_predicate attributes["bar"], :came_from_user?
end

test "types can be decorated" do
attributes = default_attributes_for do
attribute :foo, TYPE_1
decorate_attribute :foo, MyDecorator.name.to_sym, my_option: 123
end

assert_instance_of MyDecorator, attributes["foo"].type
assert_same TYPE_1, attributes["foo"].type.subtype
assert_equal 123, attributes["foo"].type.my_option
end

test "default value can be specified when decorating a type" do
attributes = default_attributes_for do
attribute :foo
decorate_attribute :foo, MyDecorator.name.to_sym, default: 321
end

assert_equal 321, attributes["foo"].value
end

test "type decorators can be stacked" do
attributes = default_attributes_for do
attribute :foo, TYPE_1
decorate_attribute :foo, MyDecorator.name.to_sym, my_option: 123
decorate_attribute :foo, MyDecorator.name.to_sym, my_option: 456
end

assert_instance_of MyDecorator, attributes["foo"].type
assert_equal 456, attributes["foo"].type.my_option

assert_instance_of MyDecorator, attributes["foo"].type.subtype
assert_equal 123, attributes["foo"].type.subtype.my_option

assert_same TYPE_1, attributes["foo"].type.subtype.subtype
end

test "re-registering an attribute overrides previous type decorators" do
attributes = default_attributes_for do
attribute :foo, TYPE_1
decorate_attribute :foo, MyDecorator.name.to_sym
attribute :foo, TYPE_1
end

assert_same TYPE_1, attributes["foo"].type
end

test "attribute_types reflects registered attribute types" do
klass = class_with { attribute :foo, TYPE_1 }
assert_same TYPE_1, klass.attribute_types["foo"]
Expand All @@ -78,6 +137,7 @@ class AttributeRegistrationTest < ActiveModel::TestCase
test "attributes are inherited" do
parent = class_with do
attribute :foo, TYPE_1, default: 123
decorate_attribute :foo, MyDecorator.name.to_sym
end

child = Class.new(parent)
Expand Down Expand Up @@ -137,6 +197,15 @@ class AttributeRegistrationTest < ActiveModel::TestCase
assert_nil parent._default_attributes["bar"].value
end

test "superclass attribute types can be decorated" do
parent = class_with { attribute :foo, TYPE_1 }
child = class_with(parent) { decorate_attribute :foo, MyDecorator.name.to_sym }

assert_instance_of MyDecorator, child._default_attributes["foo"].type
assert_same TYPE_1, child._default_attributes["foo"].type.subtype
assert_same TYPE_1, parent._default_attributes["foo"].type
end

private
def class_with(base_class = nil, &block)
Class.new(*base_class) do
Expand Down

0 comments on commit 5bbd2d6

Please sign in to comment.