Skip to content

Commit a8a5ec4

Browse files
Merge pull request rails#44665 from jonathanhefner/active_model-attribute-type-decoration
Support Active Model attribute type decoration
2 parents 53438e9 + 31bb0b4 commit a8a5ec4

File tree

2 files changed

+135
-24
lines changed

2 files changed

+135
-24
lines changed

activemodel/lib/active_model/attribute_registration.rb

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,27 @@ module AttributeRegistration # :nodoc:
1010

1111
module ClassMethods # :nodoc:
1212
def attribute(name, type = nil, default: (no_default = true), **options)
13+
name = resolve_attribute_name(name)
1314
type = resolve_type_name(type, **options) if type.is_a?(Symbol)
1415

15-
pending = pending_attribute(name)
16-
pending.type = type if type
17-
pending.default = default unless no_default
16+
pending_attribute_modifications << PendingType.new(name, type) if type || no_default
17+
pending_attribute_modifications << PendingDefault.new(name, default) unless no_default
18+
19+
reset_default_attributes
20+
end
21+
22+
def decorate_attributes(names = nil, &decorator) # :nodoc:
23+
names = names&.map { |name| resolve_attribute_name(name) }
24+
25+
pending_attribute_modifications << PendingDecorator.new(names, decorator)
1826

1927
reset_default_attributes
2028
end
2129

2230
def _default_attributes # :nodoc:
23-
@default_attributes ||= build_default_attributes
31+
@default_attributes ||= AttributeSet.new({}).tap do |attribute_set|
32+
apply_pending_attribute_modifications(attribute_set)
33+
end
2434
end
2535

2636
def attribute_types # :nodoc:
@@ -30,33 +40,39 @@ def attribute_types # :nodoc:
3040
end
3141

3242
private
33-
class PendingAttribute # :nodoc:
34-
attr_accessor :type, :default
35-
36-
def apply_to(attribute)
37-
attribute = attribute.with_type(type || attribute.type)
38-
attribute = attribute.with_user_default(default) if defined?(@default)
39-
attribute
43+
PendingType = Struct.new(:name, :type) do # :nodoc:
44+
def apply_to(attribute_set)
45+
attribute = attribute_set[name]
46+
attribute_set[name] = attribute.with_type(type || attribute.type)
4047
end
4148
end
4249

43-
def pending_attribute(name)
44-
@pending_attributes ||= {}
45-
@pending_attributes[resolve_attribute_name(name)] ||= PendingAttribute.new
50+
PendingDefault = Struct.new(:name, :default) do # :nodoc:
51+
def apply_to(attribute_set)
52+
attribute_set[name] = attribute_set[name].with_user_default(default)
53+
end
4654
end
4755

48-
def apply_pending_attributes(attribute_set)
49-
superclass.send(__method__, attribute_set) if superclass.respond_to?(__method__, true)
50-
51-
defined?(@pending_attributes) && @pending_attributes.each do |name, pending|
52-
attribute_set[name] = pending.apply_to(attribute_set[name])
56+
PendingDecorator = Struct.new(:names, :decorator) do # :nodoc:
57+
def apply_to(attribute_set)
58+
(names || attribute_set.keys).each do |name|
59+
attribute = attribute_set[name]
60+
type = decorator.call(name, attribute.type)
61+
attribute_set[name] = attribute.with_type(type) if type
62+
end
5363
end
64+
end
5465

55-
attribute_set
66+
def pending_attribute_modifications
67+
@pending_attribute_modifications ||= []
5668
end
5769

58-
def build_default_attributes
59-
apply_pending_attributes(AttributeSet.new({}))
70+
def apply_pending_attribute_modifications(attribute_set)
71+
superclass.send(__method__, attribute_set) if superclass.respond_to?(__method__, true)
72+
73+
pending_attribute_modifications.each do |modification|
74+
modification.apply_to(attribute_set)
75+
end
6076
end
6177

6278
def reset_default_attributes

activemodel/test/cases/attribute_registration_test.rb

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,20 @@ module ActiveModel
66
class AttributeRegistrationTest < ActiveModel::TestCase
77
MyType = Class.new(Type::Value)
88
Type.register(MyType.name.to_sym, MyType)
9+
910
TYPE_1 = MyType.new(precision: 1)
1011
TYPE_2 = MyType.new(precision: 2)
1112

13+
MyDecorator = DelegateClass(Type::Value) do
14+
attr_reader :name
15+
alias :cast_type :__getobj__
16+
17+
def initialize(name, cast_type)
18+
super(cast_type)
19+
@name = name
20+
end
21+
end
22+
1223
test "attributes can be registered" do
1324
attributes = default_attributes_for { attribute :foo, TYPE_1 }
1425
assert_same TYPE_1, attributes["foo"].type
@@ -52,12 +63,12 @@ class AttributeRegistrationTest < ActiveModel::TestCase
5263
assert_not_predicate attributes["bar"], :came_from_user?
5364
end
5465

55-
test "attribute_types reflects registered attribute types" do
66+
test "::attribute_types reflects registered attribute types" do
5667
klass = class_with { attribute :foo, TYPE_1 }
5768
assert_same TYPE_1, klass.attribute_types["foo"]
5869
end
5970

60-
test "attribute_types returns the default type when key is missing" do
71+
test "::attribute_types returns the default type when key is missing" do
6172
klass = class_with { attribute :foo, TYPE_1 }
6273
assert_equal Type::Value.new, klass.attribute_types["bar"]
6374
end
@@ -137,6 +148,90 @@ class AttributeRegistrationTest < ActiveModel::TestCase
137148
assert_nil parent._default_attributes["bar"].value
138149
end
139150

151+
test "::decorate_attributes decorates specified attributes" do
152+
attributes = default_attributes_for do
153+
attribute :foo, TYPE_1
154+
attribute :bar, TYPE_2
155+
attribute :qux, TYPE_2
156+
decorate_attributes([:foo, :bar]) { |name, type| MyDecorator.new(name, type) }
157+
end
158+
159+
assert_instance_of MyDecorator, attributes["foo"].type
160+
assert_equal "foo", attributes["foo"].type.name
161+
assert_same TYPE_1, attributes["foo"].type.cast_type
162+
163+
assert_instance_of MyDecorator, attributes["bar"].type
164+
assert_equal "bar", attributes["bar"].type.name
165+
assert_same TYPE_2, attributes["bar"].type.cast_type
166+
167+
assert_same TYPE_2, attributes["qux"].type
168+
end
169+
170+
test "::decorate_attributes decorates all attributes when none are specified" do
171+
attributes = default_attributes_for do
172+
attribute :foo, TYPE_1
173+
attribute :bar, TYPE_2
174+
decorate_attributes { |name, type| MyDecorator.new(name, type) }
175+
end
176+
177+
assert_same TYPE_1, attributes["foo"].type.cast_type
178+
assert_same TYPE_2, attributes["bar"].type.cast_type
179+
end
180+
181+
test "::decorate_attributes supports conditional decoration" do
182+
attributes = default_attributes_for do
183+
attribute :foo, TYPE_1
184+
attribute :bar, TYPE_2
185+
decorate_attributes { |name, type| MyDecorator.new(name, type) if name.match?(/oo/) }
186+
end
187+
188+
assert_same TYPE_1, attributes["foo"].type.cast_type
189+
assert_same TYPE_2, attributes["bar"].type
190+
end
191+
192+
test "::decorate_attributes stacks decorators" do
193+
attributes = default_attributes_for do
194+
attribute :foo, TYPE_1
195+
decorate_attributes { |name, type| MyDecorator.new("#{name}1", type) }
196+
decorate_attributes { |name, type| MyDecorator.new("#{name}2", type) }
197+
end
198+
199+
assert_instance_of MyDecorator, attributes["foo"].type
200+
assert_equal "foo2", attributes["foo"].type.name
201+
202+
assert_instance_of MyDecorator, attributes["foo"].type.cast_type
203+
assert_equal "foo1", attributes["foo"].type.cast_type.name
204+
205+
assert_same TYPE_1, attributes["foo"].type.cast_type.cast_type
206+
end
207+
208+
test "superclass attribute types can be decorated" do
209+
parent = class_with do
210+
attribute :foo, TYPE_1
211+
end
212+
213+
child = class_with(parent) do
214+
decorate_attributes { |name, type| MyDecorator.new(name, type) }
215+
end
216+
217+
assert_instance_of MyDecorator, child._default_attributes["foo"].type
218+
assert_same TYPE_1, child._default_attributes["foo"].type.cast_type
219+
assert_same TYPE_1, parent._default_attributes["foo"].type
220+
end
221+
222+
test "re-registering an attribute overrides previous decorators" do
223+
parent = class_with do
224+
attribute :foo, TYPE_1
225+
decorate_attributes { |name, type| MyDecorator.new(name, type) }
226+
end
227+
228+
child = class_with(parent) do
229+
attribute :foo, TYPE_1
230+
end
231+
232+
assert_same TYPE_1, child._default_attributes["foo"].type
233+
end
234+
140235
private
141236
def class_with(base_class = nil, &block)
142237
Class.new(*base_class) do

0 commit comments

Comments
 (0)