Skip to content

Commit 95bb5cc

Browse files
authored
Merge pull request rails#50872 from Shopify/delegate-anonymous-block
Improve Active Support delegation
2 parents c7551d0 + 8ec5219 commit 95bb5cc

File tree

10 files changed

+229
-179
lines changed

10 files changed

+229
-179
lines changed

actioncable/lib/action_cable/channel/broadcasting.rb

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,26 @@ def broadcasting_for(model)
2323
serialize_broadcasting([ channel_name, model ])
2424
end
2525

26-
def serialize_broadcasting(object) # :nodoc:
27-
case
28-
when object.is_a?(Array)
29-
object.map { |m| serialize_broadcasting(m) }.join(":")
30-
when object.respond_to?(:to_gid_param)
31-
object.to_gid_param
32-
else
33-
object.to_param
26+
private
27+
def serialize_broadcasting(object) # :nodoc:
28+
case
29+
when object.is_a?(Array)
30+
object.map { |m| serialize_broadcasting(m) }.join(":")
31+
when object.respond_to?(:to_gid_param)
32+
object.to_gid_param
33+
else
34+
object.to_param
35+
end
3436
end
35-
end
3637
end
3738

38-
delegate :broadcasting_for, :broadcast_to, to: :class, as: ClassMethods
39+
def broadcasting_for(model)
40+
self.class.broadcasting_for(model)
41+
end
42+
43+
def broadcast_to(model, message)
44+
self.class.broadcast_to(model, message)
45+
end
3946
end
4047
end
4148
end

actioncable/lib/action_cable/channel/naming.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ def channel_name
1818
end
1919
end
2020

21-
delegate :channel_name, to: :class, as: ClassMethods
21+
def channel_name
22+
self.class.channel_name
23+
end
2224
end
2325
end
2426
end

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 & 162 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:
@@ -174,113 +157,16 @@ def nil_target(method_name, target) # :nodoc:
174157
# Foo.new("Bar").name # raises NoMethodError: undefined method `name'
175158
#
176159
# The target method must be public, otherwise it will raise +NoMethodError+.
177-
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 << "&block"
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
160+
def delegate(*methods, to: nil, prefix: nil, allow_nil: nil, private: nil)
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+
)
284170
end
285171

286172
# When building decorators, a common pattern may emerge:
@@ -322,45 +208,18 @@ def delegate(*methods, to: nil, prefix: nil, allow_nil: nil, private: nil, as: n
322208
# variables, methods, constants, etc.
323209
#
324210
# The delegated method must be public on the target, otherwise it will
325-
# raise +DelegationError+. If you wish to instead return +nil+,
211+
# raise +ActiveSupport::DelegationError+. If you wish to instead return +nil+,
326212
# use the <tt>:allow_nil</tt> option.
327213
#
328214
# The <tt>marshal_dump</tt> and <tt>_dump</tt> methods are exempt from
329215
# delegation due to possible interference when calling
330216
# <tt>Marshal.dump(object)</tt>, should the delegation target method
331217
# of <tt>object</tt> add or remove instance variables.
332218
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
219+
::ActiveSupport::Delegation.generate_method_missing(
220+
self,
221+
target,
222+
allow_nil: allow_nil,
223+
)
365224
end
366225
end

activesupport/lib/active_support/current_attributes.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,8 @@ def attribute(*names, default: nil)
132132
end
133133
end
134134

135-
singleton_class.delegate(*names.flat_map { |name| [name, "#{name}="] }, to: :instance, as: self)
135+
Delegation.generate(singleton_class, names, to: :instance, nilable: false, signature: "")
136+
Delegation.generate(singleton_class, names.map { |n| "#{n}=" }, to: :instance, nilable: false, signature: "value")
136137

137138
self.defaults = defaults.merge(names.index_with { default })
138139
end
@@ -184,7 +185,7 @@ def method_added(name)
184185
return if name == :initialize
185186
return unless public_method_defined?(name)
186187
return if respond_to?(name, true)
187-
singleton_class.delegate(name, to: :instance, as: self)
188+
Delegation.generate(singleton_class, [name], to: :instance, as: self, nilable: false)
188189
end
189190
end
190191

0 commit comments

Comments
 (0)