Module: Delegates

Defined in:
lib/delegates.rb

Overview

Provides a #delegate class method to define methods whose calls are delegated to nested objects

Examples:

“‘ruby class Greeter

def hello
  'hello'
end

def goodbye
  'goodbye'
end

end

class Foo

def greeter
  Greeter.new
end

extend Delegates
delegate :hello, to: :greeter

end

Foo.new.hello # => “hello” Foo.new.goodbye # => NoMethodError: undefined method ‘goodbye’ for #<Foo:0x1af30c> “‘

Methods can be delegated to instance variables, class variables, or constants by providing them as a symbols:

“‘ruby class Foo

CONSTANT_ARRAY = [0,1,2,3]
@@class_array  = [4,5,6,7]

def initialize
  @instance_array = [8,9,10,11]
end
delegate :sum, to: :CONSTANT_ARRAY
delegate :min, to: :@@class_array
delegate :max, to: :@instance_array

end

Foo.new.sum # => 6 Foo.new.min # => 4 Foo.new.max # => 11 “‘

See #delegate docs for available params.

The target method must be public, otherwise it will raise ‘NoMethodError`.

Defined Under Namespace

Classes: DelegationError

Constant Summary collapse

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].freeze
DELEGATION_RESERVED_KEYWORDS =
%w[_ arg args block].freeze
DELEGATION_RESERVED_METHOD_NAMES =
Set.new(
  RUBY_RESERVED_KEYWORDS + DELEGATION_RESERVED_KEYWORDS
).freeze
ALLOW_NIL_PATTERN =
<<~RUBY
  def %{method_name}(%{definition})
    _ = %{to}
    if !_.nil? || nil.respond_to?(:%{method})
      _.%{method}(%{definition})
    end
  end
RUBY
NO_NIL_PATTERN =
<<~RUBY
  def %{method_name}(%{definition})
    _ = %{to}
    _.%{method}(%{definition})
  rescue NoMethodError => e
    if _.nil? && e.name == :%{method}
      raise DelegationError, "%{module}#%{method_name} delegated to %{to}.%{method}, but %{to} is nil: \#{self.inspect}"
    else
      raise
    end
  end
RUBY

Instance Method Summary collapse

Instance Method Details

#delegate(*methods, to:, prefix: nil, allow_nil: false, private: false) ⇒ Array<Symbol>

Defines methods specified in ‘methods` to delegate their calls to `to`.

Parameters:

  • methods (Array<String, Symbol>)

    List of method names to delegate

  • to (String, Symbol)

    Specifies the target object name, could be anything callable from inside the class, e.g. ‘:some_attribute`, `:@any_instance_variable`, `’chain.of.calls`

  • prefix (true, String, Symbol) (defaults to: nil)

    If ‘true`, prefixes new method with target method, e.g. `delegate :name, to: :customer, prefix: true` produces method named `customer_name`; if set to a string/symbol, uses it as a custom prefix, e.g. `delegate :name, to: :customer, prefix: ’cs’‘ produces method named `cs_name`

  • allow_nil (true, false) (defaults to: false)

    If set to ‘true`, then returns `nil` when delegated object is `nil`; otherwise (default) raises `Delegates::DelegationError`

  • private (true, false) (defaults to: false)

    If set to ‘true`, changes method visibility to private

Returns:

  • (Array<Symbol>)

    Array of method names defined



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/delegates.rb', line 119

def delegate(*methods, to:, prefix: nil, allow_nil: false, private: false)
  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
  file, line = location.path, location.lineno

  to = to.to_s
  to = "self.#{to}" if DELEGATION_RESERVED_METHOD_NAMES.include?(to)

  method_defs = []
  method_names = []

  methods.map(&:to_s).map do |method|
    method_name = "#{method_prefix}#{method}"
    method_names << method_name.to_sym

    # Attribute writer methods only accept one argument. Makes sure []=
    # methods still accept two arguments.
    definition = if method.match?(/[^\]]=$/)
                   'arg'
                 elsif RUBY_VERSION >= '2.7'
                   '...'
                 else
                   '*args, &block'
                 end

    method_defs <<
      if allow_nil
        ALLOW_NIL_PATTERN % {
          method_name: method_name,
          definition: definition,
          method: method,
          to: to
        }
      else
        NO_NIL_PATTERN % {
          module: self,
          method_name: method_name,
          definition: definition,
          method: method,
          to: to
        }
      end
  end
  module_eval(method_defs.join(';').gsub(/ *\n */m, ';'), file, line)
  private(*method_names) if private
  method_names
end