Skip to content

Commit 6ee0041

Browse files
committed
Refactor Module#delegate inside ActiveSupport::Delegation
This allow to support some extra private features without exposing them in `Module#delegate`.
1 parent ddc32f5 commit 6ee0041

File tree

6 files changed

+197
-164
lines changed

6 files changed

+197
-164
lines changed

actionview/lib/action_view/template.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -524,12 +524,15 @@ def handle_render_error(view, e)
524524
end
525525
end
526526

527+
RUBY_RESERVED_KEYWORDS = ::ActiveSupport::Delegation::RUBY_RESERVED_KEYWORDS
528+
private_constant :RUBY_RESERVED_KEYWORDS
529+
527530
def locals_code
528531
return "" if strict_locals?
529532

530533
# Only locals with valid variable names get set directly. Others will
531534
# still be available in local_assigns.
532-
locals = @locals - Module::RUBY_RESERVED_KEYWORDS
535+
locals = @locals - RUBY_RESERVED_KEYWORDS
533536

534537
locals = locals.grep(/\A(?![A-Z0-9])(?:[[:alnum:]_]|[^\0-\177])+\z/)
535538

activerecord/lib/active_record/relation/delegation.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def generate_method(method)
7272
MUTEX.synchronize do
7373
return if method_defined?(method)
7474

75-
if /\A[a-zA-Z_]\w*[!?]?\z/.match?(method) && !DELEGATION_RESERVED_METHOD_NAMES.include?(method.to_s)
75+
if /\A[a-zA-Z_]\w*[!?]?\z/.match?(method) && !::ActiveSupport::Delegation::RESERVED_METHOD_NAMES.include?(method.to_s)
7676
module_eval <<-RUBY, __FILE__, __LINE__ + 1
7777
def #{method}(...)
7878
scoping { klass.#{method}(...) }

activesupport/lib/active_support.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ module ActiveSupport
6363
autoload :Callbacks
6464
autoload :Configurable
6565
autoload :Deprecation
66+
autoload :Delegation
6667
autoload :Digest
6768
autoload :ExecutionContext
6869
autoload :Gzip
Lines changed: 21 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,16 @@
11
# frozen_string_literal: true
22

3-
require "set"
4-
53
class Module
6-
# Error generated by +delegate+ when a method is called on +nil+ and +allow_nil+
7-
# option is not used.
8-
class DelegationError < NoMethodError
9-
class << self
10-
def nil_target(method_name, target) # :nodoc:
11-
new("#{method_name} delegated to #{target}, but #{target} is nil")
12-
end
13-
end
14-
end
15-
16-
RUBY_RESERVED_KEYWORDS = %w(__ENCODING__ __LINE__ __FILE__ alias and BEGIN begin break
17-
case class def defined? do else elsif END end ensure false for if in module next nil
18-
not or redo rescue retry return self super then true undef unless until when while yield)
19-
DELEGATION_RESERVED_KEYWORDS = %w(_ arg args block)
20-
DELEGATION_RESERVED_METHOD_NAMES = Set.new(
21-
RUBY_RESERVED_KEYWORDS + DELEGATION_RESERVED_KEYWORDS
22-
).freeze
4+
require "active_support/delegation"
5+
DelegationError = ActiveSupport::DelegationError # :nodoc:
236

247
# Provides a +delegate+ class method to easily expose contained objects'
258
# public methods as your own.
269
#
2710
# ==== Options
2811
# * <tt>:to</tt> - Specifies the target object name as a symbol or string
2912
# * <tt>:prefix</tt> - Prefixes the new method with the target name or a custom prefix
30-
# * <tt>:allow_nil</tt> - If set to true, prevents a +Module::DelegationError+
13+
# * <tt>:allow_nil</tt> - If set to true, prevents a +ActiveSupport::DelegationError+
3114
# from being raised
3215
# * <tt>:private</tt> - If set to true, changes method visibility to private
3316
#
@@ -138,7 +121,7 @@ def nil_target(method_name, target) # :nodoc:
138121
# User.new.age # => 2
139122
#
140123
# If the target is +nil+ and does not respond to the delegated method a
141-
# +Module::DelegationError+ is raised. If you wish to instead return +nil+,
124+
# +ActiveSupport::DelegationError+ is raised. If you wish to instead return +nil+,
142125
# use the <tt>:allow_nil</tt> option.
143126
#
144127
# class User < ActiveRecord::Base
@@ -147,7 +130,7 @@ def nil_target(method_name, target) # :nodoc:
147130
# end
148131
#
149132
# User.new.age
150-
# # => Module::DelegationError: User#age delegated to profile.age, but profile is nil
133+
# # => ActiveSupport::DelegationError: User#age delegated to profile.age, but profile is nil
151134
#
152135
# But if not having a profile yet is fine and should not be an error
153136
# condition:
@@ -175,112 +158,16 @@ def nil_target(method_name, target) # :nodoc:
175158
#
176159
# The target method must be public, otherwise it will raise +NoMethodError+.
177160
def delegate(*methods, to: nil, prefix: nil, allow_nil: nil, private: nil, as: nil)
178-
unless to
179-
raise ArgumentError, "Delegation needs a target. Supply a keyword argument 'to' (e.g. delegate :hello, to: :greeter)."
180-
end
181-
182-
if prefix == true && /^[^a-z_]/.match?(to)
183-
raise ArgumentError, "Can only automatically set the delegation prefix when delegating to a method."
184-
end
185-
186-
method_prefix = \
187-
if prefix
188-
"#{prefix == true ? to : prefix}_"
189-
else
190-
""
191-
end
192-
193-
location = caller_locations(1, 1).first
194-
file, line = location.path, location.lineno
195-
196-
receiver = to.to_s
197-
receiver = "self.#{receiver}" if DELEGATION_RESERVED_METHOD_NAMES.include?(receiver)
198-
199-
explicit_receiver = false
200-
receiver_class = if as
201-
explicit_receiver = true
202-
as
203-
elsif to.is_a?(Module)
204-
to.singleton_class
205-
elsif receiver == "self.class"
206-
singleton_class
207-
end
208-
209-
method_def = []
210-
method_names = []
211-
212-
method_def << "self.private" if private
213-
214-
methods.each do |method|
215-
method_name = prefix ? "#{method_prefix}#{method}" : method
216-
method_names << method_name.to_sym
217-
218-
# Attribute writer methods only accept one argument. Makes sure []=
219-
# methods still accept two arguments.
220-
definition = \
221-
if /[^\]]=\z/.match?(method)
222-
"arg"
223-
else
224-
method_object = if receiver_class
225-
begin
226-
receiver_class.public_instance_method(method)
227-
rescue NameError
228-
raise if explicit_receiver
229-
# Do nothing. Fall back to `"..."`
230-
end
231-
end
232-
233-
if method_object
234-
parameters = method_object.parameters
235-
236-
if parameters.map(&:first).intersect?([:opt, :rest, :keyreq, :key, :keyrest])
237-
"..."
238-
else
239-
defn = parameters.filter_map { |type, arg| arg if type == :req }
240-
defn << "&"
241-
defn.join(", ")
242-
end
243-
else
244-
"..."
245-
end
246-
end
247-
248-
# The following generated method calls the target exactly once, storing
249-
# the returned value in a dummy variable.
250-
#
251-
# Reason is twofold: On one hand doing less calls is in general better.
252-
# On the other hand it could be that the target has side-effects,
253-
# whereas conceptually, from the user point of view, the delegator should
254-
# be doing one call.
255-
if allow_nil
256-
method = method.to_s
257-
258-
method_def <<
259-
"def #{method_name}(#{definition})" <<
260-
" _ = #{receiver}" <<
261-
" if !_.nil? || nil.respond_to?(:#{method})" <<
262-
" _.#{method}(#{definition})" <<
263-
" end" <<
264-
"end"
265-
else
266-
method = method.to_s
267-
method_name = method_name.to_s
268-
269-
method_def <<
270-
"def #{method_name}(#{definition})" <<
271-
" _ = #{receiver}" <<
272-
" _.#{method}(#{definition})" <<
273-
"rescue NoMethodError => e" <<
274-
" if _.nil? && e.name == :#{method}" <<
275-
" raise DelegationError.nil_target(:#{method_name}, :'#{receiver}')" <<
276-
" else" <<
277-
" raise" <<
278-
" end" <<
279-
"end"
280-
end
281-
end
282-
module_eval(method_def.join(";"), file, line)
283-
method_names
161+
::ActiveSupport::Delegation.generate(
162+
self,
163+
methods,
164+
location: caller_locations(1, 1).first,
165+
to: to,
166+
prefix: prefix,
167+
allow_nil: allow_nil,
168+
private: private,
169+
as: as,
170+
)
284171
end
285172

286173
# When building decorators, a common pattern may emerge:
@@ -322,45 +209,18 @@ def delegate(*methods, to: nil, prefix: nil, allow_nil: nil, private: nil, as: n
322209
# variables, methods, constants, etc.
323210
#
324211
# The delegated method must be public on the target, otherwise it will
325-
# raise +DelegationError+. If you wish to instead return +nil+,
212+
# raise +ActiveSupport::DelegationError+. If you wish to instead return +nil+,
326213
# use the <tt>:allow_nil</tt> option.
327214
#
328215
# The <tt>marshal_dump</tt> and <tt>_dump</tt> methods are exempt from
329216
# delegation due to possible interference when calling
330217
# <tt>Marshal.dump(object)</tt>, should the delegation target method
331218
# of <tt>object</tt> add or remove instance variables.
332219
def delegate_missing_to(target, allow_nil: nil)
333-
target = target.to_s
334-
target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target)
335-
336-
module_eval <<-RUBY, __FILE__, __LINE__ + 1
337-
def respond_to_missing?(name, include_private = false)
338-
# It may look like an oversight, but we deliberately do not pass
339-
# +include_private+, because they do not get delegated.
340-
341-
return false if name == :marshal_dump || name == :_dump
342-
#{target}.respond_to?(name) || super
343-
end
344-
345-
def method_missing(method, ...)
346-
if #{target}.respond_to?(method)
347-
#{target}.public_send(method, ...)
348-
else
349-
begin
350-
super
351-
rescue NoMethodError
352-
if #{target}.nil?
353-
if #{allow_nil == true}
354-
nil
355-
else
356-
raise DelegationError.nil_target(method, :'#{target}')
357-
end
358-
else
359-
raise
360-
end
361-
end
362-
end
363-
end
364-
RUBY
220+
::ActiveSupport::Delegation.generate_method_missing(
221+
self,
222+
target,
223+
allow_nil: allow_nil,
224+
)
365225
end
366226
end

0 commit comments

Comments
 (0)