Skip to content
Permalink
Browse files

Introduce Concern#class_methods and Kernel#concern

  • Loading branch information
jeremy committed Feb 23, 2014
1 parent 96759cf commit b16c36e688970df2f96f793a759365b248b582ad
@@ -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:
@@ -26,7 +26,7 @@ module ActiveSupport
# scope :disabled, -> { where(disabled: true) }
# end
#
# module ClassMethods
# class_methods do
# ...
# end
# end
@@ -130,5 +130,13 @@ def included(base = nil, &block)
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
@@ -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'
@@ -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
@@ -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 @@ module Bar

include Baz

module ClassMethods
def baz
"bar's baz + " + super
end
end

def bar
"bar"
end
@@ -73,7 +79,7 @@ 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_equal "bar's baz + baz", @klass.baz
assert @klass.included_modules.include?(ConcernTest::Bar)
end

@@ -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
@@ -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

5 comments on commit b16c36e

@et

This comment has been minimized.

Copy link

@et et replied Feb 23, 2014

Was it really that clunky? This seems to be reintroducing "magic" back to rails.
Not to complain, just wondering if there was a sound miscomprehension of module Foo; extend ActiveSupport::Concern

@sobrinho

This comment has been minimized.

Copy link
Contributor

@sobrinho sobrinho replied Feb 24, 2014

I'm really against this, there is no clunky with the current way.

Changing this:

# app/models/concerns/authentication.rb
module Authentication
  extend ActiveSupport::Concern

  included do
    after_create :generate_private_key
  end

  module ClassMethods
    def authenticate(credentials)
      # ...
    end
  end

  def generate_private_key
    # ...
  end
end

# app/models/user.rb
class User < ActiveRecord::Base
  include Authentication
end

To this:

# 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

Creates magic without any benefit.

@arthurnn

This comment has been minimized.

Copy link
Member

@arthurnn arthurnn replied Feb 24, 2014

@sobrinho you still can do the old style.

@sobrinho

This comment has been minimized.

Copy link
Contributor

@sobrinho sobrinho replied Feb 24, 2014

@arthurnn sure, but from the time that something like this enters on core it becomes a standard.

@fxn

This comment has been minimized.

Copy link
Member

@fxn fxn replied May 25, 2014

included and module ClassMethods are "magic" disguised as ordinary Ruby. These macros make clear you are doing something special. In addition, in my view, the code looks much simpler and the intention is clear.

PS: Rails has no "magic", if you see a concern macro you go to the documentation and see what it deterministically does.

Please sign in to comment.
You can’t perform that action at this time.