From b16c36e688970df2f96f793a759365b248b582ad Mon Sep 17 00:00:00 2001
From: Jeremy Kemper <jeremy@bitsweat.net>
Date: Sun, 23 Feb 2014 12:06:18 -0700
Subject: Introduce Concern#class_methods and Kernel#concern

---
 activesupport/CHANGELOG.md                         | 28 +++++++++++
 activesupport/lib/active_support/concern.rb        | 10 +++-
 .../lib/active_support/core_ext/kernel.rb          |  3 +-
 .../lib/active_support/core_ext/kernel/concern.rb  | 10 ++++
 activesupport/test/concern_test.rb                 | 10 +++-
 activesupport/test/core_ext/kernel/concern_test.rb | 12 +++++
 .../test/core_ext/module/concerning_test.rb        | 58 ++++++++++++++++------
 7 files changed, 113 insertions(+), 18 deletions(-)
 create mode 100644 activesupport/lib/active_support/core_ext/kernel/concern.rb
 create mode 100644 activesupport/test/core_ext/kernel/concern_test.rb

diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md
index 713bb3c1e2..b3e8d1d2bd 100644
--- a/activesupport/CHANGELOG.md
+++ b/activesupport/CHANGELOG.md
@@ -1,3 +1,31 @@
+*   Introduce `Concern#class_methods` as a sleek alternative to clunky
+    `module ClassMethods`. Add `Kernel#concern` to define at the toplevel
+    without chunky `module Foo; extend ActiveSupport::Concern` boilerplate.
+
+        # app/models/concerns/authentication.rb
+        concern :Authentication do
+          included do
+            after_create :generate_private_key
+          end
+
+          class_methods do
+            def authenticate(credentials)
+              # ...
+            end
+          end
+
+          def generate_private_key
+            # ...
+          end
+        end
+
+        # app/models/user.rb
+        class User < ActiveRecord::Base
+          include Authentication
+        end
+
+    *Jeremy Kemper*
+
 *   Added `Object#present_in` to simplify value whitelisting.
 
     Before:
diff --git a/activesupport/lib/active_support/concern.rb b/activesupport/lib/active_support/concern.rb
index b796d01dfd..9d5cee54e3 100644
--- a/activesupport/lib/active_support/concern.rb
+++ b/activesupport/lib/active_support/concern.rb
@@ -26,7 +26,7 @@ module ActiveSupport
   #       scope :disabled, -> { where(disabled: true) }
   #     end
   #
-  #     module ClassMethods
+  #     class_methods do
   #       ...
   #     end
   #   end
@@ -130,5 +130,13 @@ module ActiveSupport
         super
       end
     end
+
+    def class_methods(&class_methods_module_definition)
+      mod = const_defined?(:ClassMethods) ?
+        const_get(:ClassMethods) :
+        const_set(:ClassMethods, Module.new)
+
+      mod.module_eval(&class_methods_module_definition)
+    end
   end
 end
diff --git a/activesupport/lib/active_support/core_ext/kernel.rb b/activesupport/lib/active_support/core_ext/kernel.rb
index 0275f4c037..aa19aed43b 100644
--- a/activesupport/lib/active_support/core_ext/kernel.rb
+++ b/activesupport/lib/active_support/core_ext/kernel.rb
@@ -1,4 +1,5 @@
-require 'active_support/core_ext/kernel/reporting'
 require 'active_support/core_ext/kernel/agnostics'
+require 'active_support/core_ext/kernel/concern'
 require 'active_support/core_ext/kernel/debugger'
+require 'active_support/core_ext/kernel/reporting'
 require 'active_support/core_ext/kernel/singleton_class'
diff --git a/activesupport/lib/active_support/core_ext/kernel/concern.rb b/activesupport/lib/active_support/core_ext/kernel/concern.rb
new file mode 100644
index 0000000000..c200a78d36
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/kernel/concern.rb
@@ -0,0 +1,10 @@
+require 'active_support/core_ext/module/concerning'
+
+module Kernel
+  # A shortcut to define a toplevel concern, not within a module.
+  #
+  # See ActiveSupport::CoreExt::Module::Concerning for more.
+  def concern(topic, &module_definition)
+    Object.concern topic, &module_definition
+  end
+end
diff --git a/activesupport/test/concern_test.rb b/activesupport/test/concern_test.rb
index a74ee880b2..60bd8a06aa 100644
--- a/activesupport/test/concern_test.rb
+++ b/activesupport/test/concern_test.rb
@@ -5,7 +5,7 @@ class ConcernTest < ActiveSupport::TestCase
   module Baz
     extend ActiveSupport::Concern
 
-    module ClassMethods
+    class_methods do
       def baz
         "baz"
       end
@@ -33,6 +33,12 @@ class ConcernTest < ActiveSupport::TestCase
 
     include Baz
 
+    module ClassMethods
+      def baz
+        "bar's baz + " + super
+      end
+    end
+
     def bar
       "bar"
     end
@@ -73,7 +79,7 @@ class ConcernTest < ActiveSupport::TestCase
     @klass.send(:include, Bar)
     assert_equal "bar", @klass.new.bar
     assert_equal "bar+baz", @klass.new.baz
-    assert_equal "baz", @klass.baz
+    assert_equal "bar's baz + baz", @klass.baz
     assert @klass.included_modules.include?(ConcernTest::Bar)
   end
 
diff --git a/activesupport/test/core_ext/kernel/concern_test.rb b/activesupport/test/core_ext/kernel/concern_test.rb
new file mode 100644
index 0000000000..9b1fdda3b0
--- /dev/null
+++ b/activesupport/test/core_ext/kernel/concern_test.rb
@@ -0,0 +1,12 @@
+require 'abstract_unit'
+require 'active_support/core_ext/kernel/concern'
+
+class KernelConcernTest < ActiveSupport::TestCase
+  def test_may_be_defined_at_toplevel
+    mod = ::TOPLEVEL_BINDING.eval 'concern(:ToplevelConcern) { }'
+    assert_equal mod, ::ToplevelConcern
+    assert_kind_of ActiveSupport::Concern, ::ToplevelConcern
+    assert !Object.ancestors.include?(::ToplevelConcern), mod.ancestors.inspect
+    Object.send :remove_const, :ToplevelConcern
+  end
+end
diff --git a/activesupport/test/core_ext/module/concerning_test.rb b/activesupport/test/core_ext/module/concerning_test.rb
index c6863b24a4..07d860b71c 100644
--- a/activesupport/test/core_ext/module/concerning_test.rb
+++ b/activesupport/test/core_ext/module/concerning_test.rb
@@ -1,35 +1,65 @@
 require 'abstract_unit'
 require 'active_support/core_ext/module/concerning'
 
-class ConcerningTest < ActiveSupport::TestCase
-  def test_concern_shortcut_creates_a_module_but_doesnt_include_it
-    mod = Module.new { concern(:Foo) { } }
-    assert_kind_of Module, mod::Foo
-    assert mod::Foo.respond_to?(:included)
-    assert !mod.ancestors.include?(mod::Foo), mod.ancestors.inspect
+class ModuleConcerningTest < ActiveSupport::TestCase
+  def test_concerning_declares_a_concern_and_includes_it_immediately
+    klass = Class.new { concerning(:Foo) { } }
+    assert klass.ancestors.include?(klass::Foo), klass.ancestors.inspect
   end
+end
 
+class ModuleConcernTest < ActiveSupport::TestCase
   def test_concern_creates_a_module_extended_with_active_support_concern
     klass = Class.new do
-      concern :Foo do
+      concern :Baz do
         included { @foo = 1 }
         def should_be_public; end
       end
     end
 
     # Declares a concern but doesn't include it
-    assert_kind_of Module, klass::Foo
-    assert !klass.ancestors.include?(klass::Foo), klass.ancestors.inspect
+    assert klass.const_defined?(:Baz, false)
+    assert !ModuleConcernTest.const_defined?(:Baz)
+    assert_kind_of ActiveSupport::Concern, klass::Baz
+    assert !klass.ancestors.include?(klass::Baz), klass.ancestors.inspect
 
     # Public method visibility by default
-    assert klass::Foo.public_instance_methods.map(&:to_s).include?('should_be_public')
+    assert klass::Baz.public_instance_methods.map(&:to_s).include?('should_be_public')
 
     # Calls included hook
-    assert_equal 1, Class.new { include klass::Foo }.instance_variable_get('@foo')
+    assert_equal 1, Class.new { include klass::Baz }.instance_variable_get('@foo')
   end
 
-  def test_concerning_declares_a_concern_and_includes_it_immediately
-    klass = Class.new { concerning(:Foo) { } }
-    assert klass.ancestors.include?(klass::Foo), klass.ancestors.inspect
+  class Foo
+    concerning :Bar do
+      module ClassMethods
+        def will_be_orphaned; end
+      end
+
+      const_set :ClassMethods, Module.new {
+        def hacked_on; end
+      }
+
+      # Doesn't overwrite existing ClassMethods module.
+      class_methods do
+        def nicer_dsl; end
+      end
+
+      # Doesn't overwrite previous class_methods definitions.
+      class_methods do
+        def doesnt_clobber; end
+      end
+    end
+  end
+
+  def test_using_class_methods_blocks_instead_of_ClassMethods_module
+    assert !Foo.respond_to?(:will_be_orphaned)
+    assert Foo.respond_to?(:hacked_on)
+    assert Foo.respond_to?(:nicer_dsl)
+    assert Foo.respond_to?(:doesnt_clobber)
+
+    # Orphan in Foo::ClassMethods, not Bar::ClassMethods.
+    assert Foo.const_defined?(:ClassMethods)
+    assert Foo::ClassMethods.method_defined?(:will_be_orphaned)
   end
 end
-- 
cgit v1.2.3