Skip to content

Commit a6cf649

Browse files
committed
Merge pull request rails#37174
Closes rails#37174
2 parents 5bd29af + 54ecc90 commit a6cf649

File tree

6 files changed

+187
-5
lines changed

6 files changed

+187
-5
lines changed

activesupport/CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,23 @@
1+
* Support `prepend` with `ActiveSupport::Concern`.
2+
3+
Allows a module with `extend ActiveSupport::Concern` to be prepended.
4+
5+
module Imposter
6+
extend ActiveSupport::Concern
7+
8+
# Same as `included`, except only run when prepended.
9+
prepended do
10+
end
11+
end
12+
13+
class Person
14+
prepend Imposter
15+
end
16+
17+
Concerning is also updated: `concerning :Imposter, prepend: true do`
18+
19+
*Jason Karns*
20+
121
* Deprecate using `Range#include?` method to check the inclusion of a value
222
in a date time range. It is recommended to use `Range#cover?` method
323
instead of `Range#include?` to check the inclusion of a value

activesupport/lib/active_support/concern.rb

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,27 @@ module ActiveSupport
9999
# class Host
100100
# include Bar # It works, now Bar takes care of its dependencies
101101
# end
102+
#
103+
# === Prepending concerns
104+
#
105+
# Just like `include`, concerns also support `prepend` with a corresponding
106+
# `prepended do` callback. `module ClassMethods` or `class_methods do` are
107+
# still extended.
108+
#
109+
# `prepend` is also used for any dependencies.
102110
module Concern
103111
class MultipleIncludedBlocks < StandardError #:nodoc:
104112
def initialize
105113
super "Cannot define multiple 'included' blocks for a Concern"
106114
end
107115
end
108116

117+
class MultiplePrependBlocks < StandardError #:nodoc:
118+
def initialize
119+
super "Cannot define multiple 'prepended' blocks for a Concern"
120+
end
121+
end
122+
109123
def self.extended(base) #:nodoc:
110124
base.instance_variable_set(:@_dependencies, [])
111125
end
@@ -123,6 +137,19 @@ def append_features(base) #:nodoc:
123137
end
124138
end
125139

140+
def prepend_features(base) #:nodoc:
141+
if base.instance_variable_defined?(:@_dependencies)
142+
base.instance_variable_get(:@_dependencies).unshift self
143+
false
144+
else
145+
return false if base < self
146+
@_dependencies.each { |dep| base.prepend(dep) }
147+
super
148+
base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
149+
base.class_eval(&@_prepended_block) if instance_variable_defined?(:@_prepended_block)
150+
end
151+
end
152+
126153
# Evaluate given block in context of base class,
127154
# so that you can write class macros here.
128155
# When you define more than one +included+ block, it raises an exception.
@@ -140,6 +167,23 @@ def included(base = nil, &block)
140167
end
141168
end
142169

170+
# Evaluate given block in context of base class,
171+
# so that you can write class macros here.
172+
# When you define more than one +prepended+ block, it raises an exception.
173+
def prepended(base = nil, &block)
174+
if base.nil?
175+
if instance_variable_defined?(:@_prepended_block)
176+
if @_prepended_block.source_location != block.source_location
177+
raise MultiplePrependBlocks
178+
end
179+
else
180+
@_prepended_block = block
181+
end
182+
else
183+
super
184+
end
185+
end
186+
143187
# Define class methods from given block.
144188
# You can define private class methods as well.
145189
#

activesupport/lib/active_support/core_ext/module/concerning.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,16 @@ class Module
104104
# * grok the behavior of our class in one glance,
105105
# * clean up monolithic junk-drawer classes by separating their concerns, and
106106
# * stop leaning on protected/private for crude "this is internal stuff" modularity.
107+
#
108+
# === Prepending `concerning`
109+
#
110+
# `concerning` supports a `prepend: true` argument which will `prepend` the
111+
# concern instead of using `include` for it.
107112
module Concerning
108113
# Define a new concern and mix it in.
109-
def concerning(topic, &block)
110-
include concern(topic, &block)
114+
def concerning(topic, prepend: false, &block)
115+
method = prepend ? :prepend : :include
116+
__send__(method, concern(topic, &block))
111117
end
112118

113119
# A low-cruft shortcut to define a concern.

activesupport/test/concern_test.rb

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,30 @@ def baz
1313
end
1414

1515
def included_ran=(value)
16-
@@included_ran = value
16+
@included_ran = value
1717
end
1818

1919
def included_ran
20-
@@included_ran
20+
@included_ran
21+
end
22+
23+
def prepended_ran=(value)
24+
@prepended_ran = value
25+
end
26+
27+
def prepended_ran
28+
@prepended_ran
2129
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/core_ext/module/concerning_test.rb

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,21 @@ class ModuleConcerningTest < ActiveSupport::TestCase
77
def test_concerning_declares_a_concern_and_includes_it_immediately
88
klass = Class.new { concerning(:Foo) { } }
99
assert_includes klass.ancestors, klass::Foo, klass.ancestors.inspect
10+
11+
klass = Class.new { concerning(:Foo, prepend: true) { } }
12+
assert_includes klass.ancestors, klass::Foo, klass.ancestors.inspect
13+
end
14+
15+
def test_concerning_can_prepend_concern
16+
klass = Class.new do
17+
def hi; "self"; end
18+
19+
concerning(:Foo, prepend: true) do
20+
def hi; "hello, #{super}"; end
21+
end
22+
end
23+
24+
assert_equal "hello, self", klass.new.hi
1025
end
1126
end
1227

@@ -15,6 +30,7 @@ def test_concern_creates_a_module_extended_with_active_support_concern
1530
klass = Class.new do
1631
concern :Baz do
1732
included { @foo = 1 }
33+
prepended { @foo = 2 }
1834
def should_be_public; end
1935
end
2036
end
@@ -30,6 +46,9 @@ def should_be_public; end
3046

3147
# Calls included hook
3248
assert_equal 1, Class.new { include klass::Baz }.instance_variable_get("@foo")
49+
50+
# Calls prepended hook
51+
assert_equal 2, Class.new { prepend klass::Baz }.instance_variable_get("@foo")
3352
end
3453

3554
class Foo
@@ -52,6 +71,26 @@ def nicer_dsl; end
5271
def doesnt_clobber; end
5372
end
5473
end
74+
75+
concerning :Baz, prepend:true do
76+
module ClassMethods
77+
def will_be_orphaned_also; end
78+
end
79+
80+
const_set :ClassMethods, Module.new {
81+
def hacked_on_also; end
82+
}
83+
84+
# Doesn't overwrite existing ClassMethods module.
85+
class_methods do
86+
def nicer_dsl_also; end
87+
end
88+
89+
# Doesn't overwrite previous class_methods definitions.
90+
class_methods do
91+
def doesnt_clobber_also; end
92+
end
93+
end
5594
end
5695

5796
def test_using_class_methods_blocks_instead_of_ClassMethods_module
@@ -64,4 +103,15 @@ def test_using_class_methods_blocks_instead_of_ClassMethods_module
64103
assert Foo.const_defined?(:ClassMethods)
65104
assert Foo::ClassMethods.method_defined?(:will_be_orphaned)
66105
end
106+
107+
def test_using_class_methods_blocks_instead_of_ClassMethods_module_prepend
108+
assert_not_respond_to Foo, :will_be_orphaned_also
109+
assert_respond_to Foo, :hacked_on_also
110+
assert_respond_to Foo, :nicer_dsl_also
111+
assert_respond_to Foo, :doesnt_clobber_also
112+
113+
# Orphan in Foo::ClassMethods, not Bar::ClassMethods.
114+
assert Foo.const_defined?(:ClassMethods)
115+
assert Foo::ClassMethods.method_defined?(:will_be_orphaned_also)
116+
end
67117
end

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)