From 75638c576b52715322e4b2425e20f716c13157c1 Mon Sep 17 00:00:00 2001 From: John Barnette Date: Tue, 29 Oct 2013 12:03:54 -0500 Subject: [PATCH] Pull in ActiveSupport::Concern We have quite a few module dependency situations that this can help clarify. --- activesupport/lib/active_support/concern.rb | 134 ++++++++++++++++++++ activesupport/test/concern_test.rb | 98 ++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 activesupport/lib/active_support/concern.rb create mode 100644 activesupport/test/concern_test.rb diff --git a/activesupport/lib/active_support/concern.rb b/activesupport/lib/active_support/concern.rb new file mode 100644 index 0000000000..7d42365851 --- /dev/null +++ b/activesupport/lib/active_support/concern.rb @@ -0,0 +1,134 @@ +module ActiveSupport + # A typical module looks like this: + # + # module M + # def self.included(base) + # base.extend ClassMethods + # base.class_eval do + # scope :disabled, -> { where(disabled: true) } + # end + # end + # + # module ClassMethods + # ... + # end + # end + # + # By using ActiveSupport::Concern the above module could instead be + # written as: + # + # require 'active_support/concern' + # + # module M + # extend ActiveSupport::Concern + # + # included do + # scope :disabled, -> { where(disabled: true) } + # end + # + # module ClassMethods + # ... + # end + # end + # + # Moreover, it gracefully handles module dependencies. Given a +Foo+ module + # and a +Bar+ module which depends on the former, we would typically write the + # following: + # + # module Foo + # def self.included(base) + # base.class_eval do + # def self.method_injected_by_foo + # ... + # end + # end + # end + # end + # + # module Bar + # def self.included(base) + # base.method_injected_by_foo + # end + # end + # + # class Host + # include Foo # We need to include this dependency for Bar + # include Bar # Bar is the module that Host really needs + # end + # + # But why should +Host+ care about +Bar+'s dependencies, namely +Foo+? We + # could try to hide these from +Host+ directly including +Foo+ in +Bar+: + # + # module Bar + # include Foo + # def self.included(base) + # base.method_injected_by_foo + # end + # end + # + # class Host + # include Bar + # end + # + # Unfortunately this won't work, since when +Foo+ is included, its base + # is the +Bar+ module, not the +Host+ class. With ActiveSupport::Concern, + # module dependencies are properly resolved: + # + # require 'active_support/concern' + # + # module Foo + # extend ActiveSupport::Concern + # included do + # def self.method_injected_by_foo + # ... + # end + # end + # end + # + # module Bar + # extend ActiveSupport::Concern + # include Foo + # + # included do + # self.method_injected_by_foo + # end + # end + # + # class Host + # include Bar # works, Bar takes care now of its dependencies + # end + module Concern + class MultipleIncludedBlocks < StandardError #:nodoc: + def initialize + super "Cannot define multiple 'included' blocks for a Concern" + end + end + + def self.extended(base) #:nodoc: + base.instance_variable_set(:@_dependencies, []) + end + + def append_features(base) + if base.instance_variable_defined?(:@_dependencies) + base.instance_variable_get(:@_dependencies) << self + return false + else + return false if base < self + @_dependencies.each { |dep| base.send(:include, dep) } + super + base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods) + base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block) + end + end + + def included(base = nil, &block) + if base.nil? + raise MultipleIncludedBlocks if instance_variable_defined?(:@_included_block) + + @_included_block = block + else + super + end + end + end +end \ No newline at end of file diff --git a/activesupport/test/concern_test.rb b/activesupport/test/concern_test.rb new file mode 100644 index 0000000000..32ac8f4655 --- /dev/null +++ b/activesupport/test/concern_test.rb @@ -0,0 +1,98 @@ +require 'abstract_unit' +require 'active_support/concern' + +class ConcernTest < ActiveSupport::TestCase + module Baz + extend ActiveSupport::Concern + + module ClassMethods + def baz + "baz" + end + + def included_ran=(value) + @@included_ran = value + end + + def included_ran + @@included_ran + end + end + + included do + self.included_ran = true + end + + def baz + "baz" + end + end + + module Bar + extend ActiveSupport::Concern + + include Baz + + def bar + "bar" + end + + def baz + "bar+" + super + end + end + + module Foo + extend ActiveSupport::Concern + + include Bar, Baz + end + + def setup + @klass = Class.new + end + + def test_module_is_included_normally + @klass.send(:include, Baz) + assert_equal "baz", @klass.new.baz + assert @klass.included_modules.include?(ConcernTest::Baz) + end + + def test_class_methods_are_extended + @klass.send(:include, Baz) + assert_equal "baz", @klass.baz + assert_equal ConcernTest::Baz::ClassMethods, (class << @klass; self.included_modules; end)[0] + end + + def test_included_block_is_ran + @klass.send(:include, Baz) + assert_equal true, @klass.included_ran + end + + def test_modules_dependencies_are_met + @klass.send(:include, Bar) + assert_equal "bar", @klass.new.bar + assert_equal "bar+baz", @klass.new.baz + assert_equal "baz", @klass.baz + assert @klass.included_modules.include?(ConcernTest::Bar) + end + + def test_dependencies_with_multiple_modules + @klass.send(:include, 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 + extend ActiveSupport::Concern + + included do + end + + included do + end + end + end + end +end \ No newline at end of file -- GitLab