aboutsummaryrefslogtreecommitdiffstats
path: root/activesupport/lib/active_support/core_ext/module/delegation.rb
diff options
context:
space:
mode:
Diffstat (limited to 'activesupport/lib/active_support/core_ext/module/delegation.rb')
-rw-r--r--activesupport/lib/active_support/core_ext/module/delegation.rb97
1 files changed, 53 insertions, 44 deletions
diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb
index 3f6e8bd26c..4310df3024 100644
--- a/activesupport/lib/active_support/core_ext/module/delegation.rb
+++ b/activesupport/lib/active_support/core_ext/module/delegation.rb
@@ -1,15 +1,19 @@
-require 'set'
+# frozen_string_literal: true
+
+require "set"
+require "active_support/core_ext/regexp"
class Module
# Error generated by +delegate+ when a method is called on +nil+ and +allow_nil+
# option is not used.
class DelegationError < NoMethodError; end
+ RUBY_RESERVED_KEYWORDS = %w(alias and BEGIN begin break case class def defined? do
+ else elsif END end ensure false for if in module next nil not or redo rescue retry
+ return self super then true undef unless until when while yield)
+ DELEGATION_RESERVED_KEYWORDS = %w(_ arg args block)
DELEGATION_RESERVED_METHOD_NAMES = Set.new(
- %w(_ arg args alias and BEGIN begin block break case class def defined? do
- else elsif END end ensure false for if in module next nil not or redo
- rescue retry return self super then true undef unless until when while
- yield)
+ RUBY_RESERVED_KEYWORDS + DELEGATION_RESERVED_KEYWORDS
).freeze
# Provides a +delegate+ class method to easily expose contained objects'
@@ -18,7 +22,8 @@ class Module
# ==== Options
# * <tt>:to</tt> - Specifies the target object
# * <tt>:prefix</tt> - Prefixes the new method with the target name or a custom prefix
- # * <tt>:allow_nil</tt> - if set to true, prevents a +NoMethodError+ from being raised
+ # * <tt>:allow_nil</tt> - if set to true, prevents a +Module::DelegationError+
+ # from being raised
#
# The macro receives one or more method names (specified as symbols or
# strings) and the name of the target object via the <tt>:to</tt> option
@@ -110,18 +115,16 @@ class Module
# invoice.customer_address # => 'Vimmersvej 13'
#
# If the target is +nil+ and does not respond to the delegated method a
- # +NoMethodError+ is raised, as with any other value. Sometimes, however, it
- # makes sense to be robust to that situation and that is the purpose of the
- # <tt>:allow_nil</tt> option: If the target is not +nil+, or it is and
- # responds to the method, everything works as usual. But if it is +nil+ and
- # does not respond to the delegated method, +nil+ is returned.
+ # +Module::DelegationError+ is raised. If you wish to instead return +nil+,
+ # use the <tt>:allow_nil</tt> option.
#
# class User < ActiveRecord::Base
# has_one :profile
# delegate :age, to: :profile
# end
#
- # User.new.age # raises NoMethodError: undefined method `age'
+ # User.new.age
+ # # => Module::DelegationError: User#age delegated to profile.age, but profile is nil
#
# But if not having a profile yet is fine and should not be an error
# condition:
@@ -150,18 +153,18 @@ class Module
# The target method must be public, otherwise it will raise +NoMethodError+.
def delegate(*methods, to: nil, prefix: nil, allow_nil: nil)
unless to
- raise ArgumentError, 'Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, to: :greeter).'
+ raise ArgumentError, "Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, to: :greeter)."
end
- if prefix == true && to =~ /^[^a-z_]/
- raise ArgumentError, 'Can only automatically set the delegation prefix when delegating to a method.'
+ if prefix == true && /^[^a-z_]/.match?(to)
+ raise ArgumentError, "Can only automatically set the delegation prefix when delegating to a method."
end
method_prefix = \
if prefix
"#{prefix == true ? to : prefix}_"
else
- ''
+ ""
end
location = caller_locations(1, 1).first
@@ -170,10 +173,10 @@ class Module
to = to.to_s
to = "self.#{to}" if DELEGATION_RESERVED_METHOD_NAMES.include?(to)
- methods.each do |method|
+ methods.map do |method|
# Attribute writer methods only accept one argument. Makes sure []=
# methods still accept two arguments.
- definition = (method =~ /[^\]]=$/) ? 'arg' : '*args, &block'
+ definition = /[^\]]=$/.match?(method) ? "arg" : "*args, &block"
# The following generated method calls the target exactly once, storing
# the returned value in a dummy variable.
@@ -190,7 +193,7 @@ class Module
" _.#{method}(#{definition})",
"end",
"end"
- ].join ';'
+ ].join ";"
else
exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")
@@ -205,7 +208,7 @@ class Module
" raise",
" end",
"end"
- ].join ';'
+ ].join ";"
end
module_eval(method_def, file, line)
@@ -215,62 +218,68 @@ class Module
# When building decorators, a common pattern may emerge:
#
# class Partition
- # def initialize(first_event)
- # @events = [ first_event ]
+ # def initialize(event)
+ # @event = event
# end
#
- # def people
- # if @events.first.detail.people.any?
- # @events.collect { |e| Array(e.detail.people) }.flatten.uniq
- # else
- # @events.collect(&:creator).uniq
- # end
+ # def person
+ # @event.detail.person || @event.creator
# end
#
# private
# def respond_to_missing?(name, include_private = false)
- # @events.respond_to?(name, include_private)
+ # @event.respond_to?(name, include_private)
# end
#
# def method_missing(method, *args, &block)
- # @events.send(method, *args, &block)
+ # @event.send(method, *args, &block)
# end
# end
#
- # With `Module#delegate_missing_to`, the above is condensed to:
+ # With <tt>Module#delegate_missing_to</tt>, the above is condensed to:
#
# class Partition
- # delegate_missing_to :@events
+ # delegate_missing_to :@event
#
- # def initialize(first_event)
- # @events = [ first_event ]
+ # def initialize(event)
+ # @event = event
# end
#
- # def people
- # if @events.first.detail.people.any?
- # @events.collect { |e| Array(e.detail.people) }.flatten.uniq
- # else
- # @events.collect(&:creator).uniq
- # end
+ # def person
+ # @event.detail.person || @event.creator
# end
# end
#
- # The target can be anything callable withing the object. E.g. instance
- # variables, methods, constants ant the likes.
+ # The target can be anything callable within the object, e.g. instance
+ # variables, methods, constants, etc.
+ #
+ # The delegated method must be public on the target, otherwise it will
+ # raise +NoMethodError+.
def delegate_missing_to(target)
target = target.to_s
target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target)
module_eval <<-RUBY, __FILE__, __LINE__ + 1
def respond_to_missing?(name, include_private = false)
- #{target}.respond_to?(name, include_private)
+ # It may look like an oversight, but we deliberately do not pass
+ # +include_private+, because they do not get delegated.
+
+ #{target}.respond_to?(name) || super
end
def method_missing(method, *args, &block)
if #{target}.respond_to?(method)
#{target}.public_send(method, *args, &block)
else
- super
+ begin
+ super
+ rescue NoMethodError
+ if #{target}.nil?
+ raise DelegationError, "\#{method} delegated to #{target}, but #{target} is nil"
+ else
+ raise
+ end
+ end
end
end
RUBY