From ba2bea5e07de7206c7309b1e8da79d70b71dfa8a Mon Sep 17 00:00:00 2001 From: Jason Karns Date: Thu, 12 Sep 2019 11:33:53 -0400 Subject: [PATCH] Concerns learn to be prepended --- activesupport/lib/active_support/concern.rb | 36 +++++++++++ activesupport/test/concern_test.rb | 60 ++++++++++++++++++- .../test/fixtures/concern/some_concern.rb | 4 ++ 3 files changed, 99 insertions(+), 1 deletion(-) diff --git a/activesupport/lib/active_support/concern.rb b/activesupport/lib/active_support/concern.rb index 708c445031..73fd480c54 100644 --- a/activesupport/lib/active_support/concern.rb +++ b/activesupport/lib/active_support/concern.rb @@ -106,6 +106,12 @@ module ActiveSupport end end + class MultiplePrependBlocks < StandardError #:nodoc: + def initialize + super "Cannot define multiple 'prepended' blocks for a Concern" + end + end + def self.extended(base) #:nodoc: base.instance_variable_set(:@_dependencies, []) end @@ -123,6 +129,19 @@ module ActiveSupport end end + def prepend_features(base) #:nodoc: + if base.instance_variable_defined?(:@_dependencies) + base.instance_variable_get(:@_dependencies).unshift self + false + else + return false if base < self + @_dependencies.each { |dep| base.prepend(dep) } + super + base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods) + base.class_eval(&@_prepended_block) if instance_variable_defined?(:@_prepended_block) + end + end + # Evaluate given block in context of base class, # so that you can write class macros here. # When you define more than one +included+ block, it raises an exception. @@ -140,6 +159,23 @@ module ActiveSupport end end + # Evaluate given block in context of base class, + # so that you can write class macros here. + # When you define more than one +prepended+ block, it raises an exception. + def prepended(base = nil, &block) + if base.nil? + if instance_variable_defined?(:@_prepended_block) + if @_prepended_block.source_location != block.source_location + raise MultiplePrependBlocks + end + else + @_prepended_block = block + end + else + super + end + end + # Define class methods from given block. # You can define private class methods as well. # diff --git a/activesupport/test/concern_test.rb b/activesupport/test/concern_test.rb index 28d95514b9..e2e819035b 100644 --- a/activesupport/test/concern_test.rb +++ b/activesupport/test/concern_test.rb @@ -19,12 +19,24 @@ class ConcernTest < ActiveSupport::TestCase def included_ran @included_ran end + + def prepended_ran=(value) + @prepended_ran = value + end + + def prepended_ran + @prepended_ran + end end included do self.included_ran = true end + prepended do + self.prepended_ran = true + end + def baz "baz" end @@ -71,12 +83,24 @@ class ConcernTest < ActiveSupport::TestCase assert_includes @klass.included_modules, ConcernTest::Baz end + def test_module_is_prepended_normally + @klass.prepend(Baz) + assert_equal "baz", @klass.new.baz + assert_includes @klass.included_modules, ConcernTest::Baz + end + def test_class_methods_are_extended @klass.include(Baz) assert_equal "baz", @klass.baz assert_equal ConcernTest::Baz::ClassMethods, (class << @klass; included_modules; end)[0] end + def test_class_methods_are_extended_when_prepended + @klass.prepend(Baz) + assert_equal "baz", @klass.baz + assert_equal ConcernTest::Baz::ClassMethods, (class << @klass; included_modules; end)[0] + end + def test_class_methods_are_extended_only_on_expected_objects ::Object.include(Qux) Object.extend(Qux::ClassMethods) @@ -102,6 +126,21 @@ class ConcernTest < ActiveSupport::TestCase assert_equal true, @klass.included_ran end + def test_included_block_is_not_ran_when_prepended + @klass.prepend(Baz) + assert_nil @klass.included_ran + end + + def test_prepended_block_is_ran + @klass.prepend(Baz) + assert_equal true, @klass.prepended_ran + end + + def test_prepended_block_is_not_ran_when_included + @klass.include(Baz) + assert_nil @klass.prepended_ran + end + def test_modules_dependencies_are_met @klass.include(Bar) assert_equal "bar", @klass.new.bar @@ -115,6 +154,11 @@ class ConcernTest < ActiveSupport::TestCase assert_equal [ConcernTest::Foo, ConcernTest::Bar, ConcernTest::Baz], @klass.included_modules[0..2] end + def test_dependencies_with_multiple_modules_when_prepended + @klass.prepend(Foo) + assert_equal [ConcernTest::Foo, ConcernTest::Bar, ConcernTest::Baz], @klass.included_modules[0..2] + end + def test_raise_on_multiple_included_calls assert_raises(ActiveSupport::Concern::MultipleIncludedBlocks) do Module.new do @@ -129,7 +173,21 @@ class ConcernTest < ActiveSupport::TestCase end end - def test_no_raise_on_same_included_call + def test_raise_on_multiple_prepended_calls + assert_raises(ActiveSupport::Concern::MultiplePrependBlocks) do + Module.new do + extend ActiveSupport::Concern + + prepended do + end + + prepended do + end + end + end + end + + def test_no_raise_on_same_included_or_prepended_call assert_nothing_raised do 2.times do load File.expand_path("../fixtures/concern/some_concern.rb", __FILE__) diff --git a/activesupport/test/fixtures/concern/some_concern.rb b/activesupport/test/fixtures/concern/some_concern.rb index 87f660a81e..6569ba89e0 100644 --- a/activesupport/test/fixtures/concern/some_concern.rb +++ b/activesupport/test/fixtures/concern/some_concern.rb @@ -8,4 +8,8 @@ module SomeConcern included do # shouldn't raise when module is loaded more than once end + + prepended do + # shouldn't raise when module is loaded more than once + end end