Skip to content

Commit 6d51b00

Browse files
committed
Merge pull request rails#35969 from shioyama/avoid_anonymous_module_inclusion_in_acceptance_validator
Avoid LazilyDefinedAttributes being included multiple times in class, and remove methods after first use
1 parent d5b8944 commit 6d51b00

File tree

2 files changed

+61
-24
lines changed

2 files changed

+61
-24
lines changed

activemodel/lib/active_model/validations/acceptance.rb

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,53 +17,66 @@ def validate_each(record, attribute, value)
1717
private
1818

1919
def setup!(klass)
20-
klass.include(LazilyDefineAttributes.new(AttributeDefinition.new(attributes)))
20+
klass.include(LazilyDefineAttributes.new(klass)) unless klass.ancestors.any?(LazilyDefineAttributes)
2121
end
2222

2323
def acceptable_option?(value)
2424
Array(options[:accept]).include?(value)
2525
end
2626

2727
class LazilyDefineAttributes < Module
28-
def initialize(attribute_definition)
28+
attr_reader :removed
29+
30+
def initialize(klass)
31+
@klass = klass
32+
@removed = false
33+
mod = self
34+
2935
define_method(:respond_to_missing?) do |method_name, include_private = false|
30-
super(method_name, include_private) || attribute_definition.matches?(method_name)
36+
mod.define_attributes
37+
super(method_name, include_private) || mod.matches?(method_name)
3138
end
3239

3340
define_method(:method_missing) do |method_name, *args, &block|
34-
if attribute_definition.matches?(method_name)
35-
attribute_definition.define_on(self.class)
41+
mod.define_attributes
42+
if mod.matches?(method_name)
3643
send(method_name, *args, &block)
3744
else
3845
super(method_name, *args, &block)
3946
end
4047
end
4148
end
42-
end
43-
44-
class AttributeDefinition
45-
def initialize(attributes)
46-
@attributes = attributes.map(&:to_s)
47-
end
4849

4950
def matches?(method_name)
50-
attr_name = convert_to_reader_name(method_name)
51-
attributes.include?(attr_name)
51+
attr_name = method_name.to_s.chomp("=")
52+
attributes.any? { |name| name.to_s == attr_name }
5253
end
5354

54-
def define_on(klass)
55+
def define_attributes
5556
attr_readers = attributes.reject { |name| klass.attribute_method?(name) }
5657
attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") }
57-
klass.define_attribute_methods
58-
klass.attr_reader(*attr_readers)
59-
klass.attr_writer(*attr_writers)
58+
59+
remove_methods
60+
61+
attr_reader(*attr_readers)
62+
attr_writer(*attr_writers)
63+
end
64+
65+
def self.===(other)
66+
super && !other.removed
6067
end
6168

6269
private
63-
attr_reader :attributes
70+
attr_reader :klass
71+
72+
def attributes
73+
@attributes ||= klass.validators.grep(AcceptanceValidator).flat_map(&:attributes).uniq
74+
end
6475

65-
def convert_to_reader_name(method_name)
66-
method_name.to_s.chomp("=")
76+
def remove_methods
77+
remove_method :respond_to_missing?
78+
remove_method :method_missing
79+
@removed = true
6780
end
6881
end
6982
end

activemodel/test/cases/validations/acceptance_validation_test.rb

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,35 @@ def test_validates_acceptance_of_true
9191
assert_predicate klass.new(terms_of_service: true), :valid?
9292
end
9393

94-
private
94+
def test_lazy_attribute_module_included_only_once
95+
klass = define_test_class(Topic)
96+
assert_difference -> { klass.ancestors.count }, 1 do
97+
2.times { klass.validates_acceptance_of(:something_to_accept) }
98+
2.times { klass.validates_acceptance_of(:something_else_to_accept) }
99+
end
100+
end
101+
102+
def test_lazy_attributes_module_included_again_if_needed
103+
klass = define_test_class(Topic)
104+
assert_difference -> { klass.ancestors.count }, 1 do
105+
klass.validates_acceptance_of(:something_to_accept)
106+
end
107+
topic = klass.new
108+
topic.something_to_accept
109+
assert_difference -> { klass.ancestors.count }, 1 do
110+
klass.validates_acceptance_of(:something_else_to_accept)
111+
end
112+
assert topic.respond_to?(:something_else_to_accept)
113+
end
95114

96-
# Acceptance validator includes anonymous module into class, which cannot
97-
# be cleared, so to avoid multiple inclusions we use a named subclass which
98-
# we can remove in teardown.
115+
def test_lazy_attributes_respond_to?
116+
klass = define_test_class(Topic)
117+
klass.validates_acceptance_of(:terms_of_service)
118+
topic = klass.new
119+
assert topic.respond_to?(:terms_of_service)
120+
end
121+
122+
private
99123
def define_test_class(parent)
100124
self.class.const_set(:TestClass, Class.new(parent))
101125
end

0 commit comments

Comments
 (0)