Skip to content

Commit ba2bea5

Browse files
jasonkarnskaspth
authored andcommitted
Concerns learn to be prepended
1 parent 6e2ca1a commit ba2bea5

File tree

3 files changed

+99
-1
lines changed

3 files changed

+99
-1
lines changed

activesupport/lib/active_support/concern.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@ def initialize
106106
end
107107
end
108108

109+
class MultiplePrependBlocks < StandardError #:nodoc:
110+
def initialize
111+
super "Cannot define multiple 'prepended' blocks for a Concern"
112+
end
113+
end
114+
109115
def self.extended(base) #:nodoc:
110116
base.instance_variable_set(:@_dependencies, [])
111117
end
@@ -123,6 +129,19 @@ def append_features(base) #:nodoc:
123129
end
124130
end
125131

132+
def prepend_features(base) #:nodoc:
133+
if base.instance_variable_defined?(:@_dependencies)
134+
base.instance_variable_get(:@_dependencies).unshift self
135+
false
136+
else
137+
return false if base < self
138+
@_dependencies.each { |dep| base.prepend(dep) }
139+
super
140+
base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
141+
base.class_eval(&@_prepended_block) if instance_variable_defined?(:@_prepended_block)
142+
end
143+
end
144+
126145
# Evaluate given block in context of base class,
127146
# so that you can write class macros here.
128147
# When you define more than one +included+ block, it raises an exception.
@@ -140,6 +159,23 @@ def included(base = nil, &block)
140159
end
141160
end
142161

162+
# Evaluate given block in context of base class,
163+
# so that you can write class macros here.
164+
# When you define more than one +prepended+ block, it raises an exception.
165+
def prepended(base = nil, &block)
166+
if base.nil?
167+
if instance_variable_defined?(:@_prepended_block)
168+
if @_prepended_block.source_location != block.source_location
169+
raise MultiplePrependBlocks
170+
end
171+
else
172+
@_prepended_block = block
173+
end
174+
else
175+
super
176+
end
177+
end
178+
143179
# Define class methods from given block.
144180
# You can define private class methods as well.
145181
#

activesupport/test/concern_test.rb

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,24 @@ def included_ran=(value)
1919
def included_ran
2020
@included_ran
2121
end
22+
23+
def prepended_ran=(value)
24+
@prepended_ran = value
25+
end
26+
27+
def prepended_ran
28+
@prepended_ran
29+
end
2230
end
2331

2432
included do
2533
self.included_ran = true
2634
end
2735

36+
prepended do
37+
self.prepended_ran = true
38+
end
39+
2840
def baz
2941
"baz"
3042
end
@@ -71,12 +83,24 @@ def test_module_is_included_normally
7183
assert_includes @klass.included_modules, ConcernTest::Baz
7284
end
7385

86+
def test_module_is_prepended_normally
87+
@klass.prepend(Baz)
88+
assert_equal "baz", @klass.new.baz
89+
assert_includes @klass.included_modules, ConcernTest::Baz
90+
end
91+
7492
def test_class_methods_are_extended
7593
@klass.include(Baz)
7694
assert_equal "baz", @klass.baz
7795
assert_equal ConcernTest::Baz::ClassMethods, (class << @klass; included_modules; end)[0]
7896
end
7997

98+
def test_class_methods_are_extended_when_prepended
99+
@klass.prepend(Baz)
100+
assert_equal "baz", @klass.baz
101+
assert_equal ConcernTest::Baz::ClassMethods, (class << @klass; included_modules; end)[0]
102+
end
103+
80104
def test_class_methods_are_extended_only_on_expected_objects
81105
::Object.include(Qux)
82106
Object.extend(Qux::ClassMethods)
@@ -102,6 +126,21 @@ def test_included_block_is_ran
102126
assert_equal true, @klass.included_ran
103127
end
104128

129+
def test_included_block_is_not_ran_when_prepended
130+
@klass.prepend(Baz)
131+
assert_nil @klass.included_ran
132+
end
133+
134+
def test_prepended_block_is_ran
135+
@klass.prepend(Baz)
136+
assert_equal true, @klass.prepended_ran
137+
end
138+
139+
def test_prepended_block_is_not_ran_when_included
140+
@klass.include(Baz)
141+
assert_nil @klass.prepended_ran
142+
end
143+
105144
def test_modules_dependencies_are_met
106145
@klass.include(Bar)
107146
assert_equal "bar", @klass.new.bar
@@ -115,6 +154,11 @@ def test_dependencies_with_multiple_modules
115154
assert_equal [ConcernTest::Foo, ConcernTest::Bar, ConcernTest::Baz], @klass.included_modules[0..2]
116155
end
117156

157+
def test_dependencies_with_multiple_modules_when_prepended
158+
@klass.prepend(Foo)
159+
assert_equal [ConcernTest::Foo, ConcernTest::Bar, ConcernTest::Baz], @klass.included_modules[0..2]
160+
end
161+
118162
def test_raise_on_multiple_included_calls
119163
assert_raises(ActiveSupport::Concern::MultipleIncludedBlocks) do
120164
Module.new do
@@ -129,7 +173,21 @@ def test_raise_on_multiple_included_calls
129173
end
130174
end
131175

132-
def test_no_raise_on_same_included_call
176+
def test_raise_on_multiple_prepended_calls
177+
assert_raises(ActiveSupport::Concern::MultiplePrependBlocks) do
178+
Module.new do
179+
extend ActiveSupport::Concern
180+
181+
prepended do
182+
end
183+
184+
prepended do
185+
end
186+
end
187+
end
188+
end
189+
190+
def test_no_raise_on_same_included_or_prepended_call
133191
assert_nothing_raised do
134192
2.times do
135193
load File.expand_path("../fixtures/concern/some_concern.rb", __FILE__)

activesupport/test/fixtures/concern/some_concern.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,8 @@ module SomeConcern
88
included do
99
# shouldn't raise when module is loaded more than once
1010
end
11+
12+
prepended do
13+
# shouldn't raise when module is loaded more than once
14+
end
1115
end

0 commit comments

Comments
 (0)