aboutsummaryrefslogtreecommitdiffstats
path: root/activesupport/lib/active_support/concern.rb
diff options
context:
space:
mode:
Diffstat (limited to 'activesupport/lib/active_support/concern.rb')
-rw-r--r--activesupport/lib/active_support/concern.rb148
1 files changed, 148 insertions, 0 deletions
diff --git a/activesupport/lib/active_support/concern.rb b/activesupport/lib/active_support/concern.rb
new file mode 100644
index 0000000000..5d356a0ab6
--- /dev/null
+++ b/activesupport/lib/active_support/concern.rb
@@ -0,0 +1,148 @@
+# frozen_string_literal: true
+
+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 <tt>ActiveSupport::Concern</tt> 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
+ #
+ # class_methods do
+ # ...
+ # 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 <tt>base</tt>
+ # is the +Bar+ module, not the +Host+ class. With <tt>ActiveSupport::Concern</tt>,
+ # 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 # It works, now Bar takes care 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
+ false
+ else
+ return false if base < self
+ @_dependencies.each { |dep| base.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?
+ if instance_variable_defined?(:@_included_block)
+ if @_included_block.source_location != block.source_location
+ raise MultipleIncludedBlocks
+ end
+ else
+ @_included_block = block
+ end
+ else
+ super
+ end
+ end
+
+ def class_methods(&class_methods_module_definition)
+ mod = const_defined?(:ClassMethods, false) ?
+ const_get(:ClassMethods) :
+ const_set(:ClassMethods, Module.new)
+
+ mod.module_eval(&class_methods_module_definition)
+ end
+ end
+end