Class: Module
- Inherits:
-
Object
- Object
- Module
- Defined in:
- lib/memoize_delegate/delegation.rb
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)
- DELEGATION_RESERVED_KEYWORDS =
%w(_ arg args block)
- DELEGATION_RESERVED_METHOD_NAMES =
Set.new( RUBY_RESERVED_KEYWORDS + DELEGATION_RESERVED_KEYWORDS ).freeze
Instance Method Summary collapse
-
#memoize_delegate(*methods, to: nil, prefix: nil, allow_nil: nil) ⇒ Object
Provides a
memoize_delegate
class method to easily expose contained objects’ public methods as your own.
Instance Method Details
#memoize_delegate(*methods, to: nil, prefix: nil, allow_nil: nil) ⇒ Object
Provides a memoize_delegate
class method to easily expose contained objects’ public methods as your own.
Options
-
:to
- Specifies the target object -
:prefix
- Prefixes the new method with the target name or a custom prefix -
:allow_nil
- if set to true, prevents aNoMethodError
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 :to
option (also a symbol or string).
Delegation is particularly useful with Active Record associations:
class Greeter < ActiveRecord::Base
def hello
'hello'
end
def goodbye
'goodbye'
end
end
class Foo < ActiveRecord::Base
belongs_to :greeter
memoize_delegate :hello, to: :greeter
end
Foo.new.hello # => "hello"
Foo.new.goodbye # => NoMethodError: undefined method `goodbye' for #<Foo:0x1af30c>
Multiple memoize_delegates to the same target are allowed:
class Foo < ActiveRecord::Base
belongs_to :greeter
memoize_delegate :hello, :goodbye, to: :greeter
end
Foo.new.goodbye # => "goodbye"
Methods can be memoize_delegated to instance variables, class variables, or constants by providing them as a symbols:
class Foo
CONSTANT_ARRAY = [0,1,2,3]
@@class_array = [4,5,6,7]
def initialize
@instance_array = [8,9,10,11]
end
memoize_delegate :sum, to: :CONSTANT_ARRAY
memoize_delegate :min, to: :@@class_array
memoize_delegate :max, to: :@instance_array
end
Foo.new.sum # => 6
Foo.new.min # => 4
Foo.new.max # => 11
It’s also possible to memoize_delegate a method to the class by using :class
:
class Foo
def self.hello
"world"
end
memoize_delegate :hello, to: :class
end
Foo.new.hello # => "world"
Delegates can optionally be prefixed using the :prefix
option. If the value is true
, the memoize_delegate methods are prefixed with the name of the object being memoize_delegated to.
Person = Struct.new(:name, :address)
class Invoice < Struct.new(:client)
memoize_delegate :name, :address, to: :client, prefix: true
end
john_doe = Person.new('John Doe', 'Vimmersvej 13')
invoice = Invoice.new(john_doe)
invoice.client_name # => "John Doe"
invoice.client_address # => "Vimmersvej 13"
It is also possible to supply a custom prefix.
class Invoice < Struct.new(:client)
memoize_delegate :name, :address, to: :client, prefix: :customer
end
invoice = Invoice.new(john_doe)
invoice.customer_name # => 'John Doe'
invoice.customer_address # => 'Vimmersvej 13'
If the target is nil
and does not respond to the memoize_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 :allow_nil
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 memoize_delegated method, nil
is returned.
class User < ActiveRecord::Base
has_one :profile
memoize_delegate :age, to: :profile
end
User.new.age # raises NoMethodError: undefined method `age'
But if not having a profile yet is fine and should not be an error condition:
class User < ActiveRecord::Base
has_one :profile
memoize_delegate :age, to: :profile, allow_nil: true
end
User.new.age # nil
Note that if the target is not nil
then the call is attempted regardless of the :allow_nil
option, and thus an exception is still raised if said object does not respond to the method:
class Foo
def initialize()
@bar =
end
memoize_delegate :name, to: :@bar, allow_nil: true
end
Foo.new("Bar").name # raises NoMethodError: undefined method `name'
The target method must be public, otherwise it will raise NoMethodError
.
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 |
# File 'lib/memoize_delegate/delegation.rb', line 153 def memoize_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. memoize_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.' 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) methods.each do |method| # Attribute writer methods only accept one argument. Makes sure []= # methods still accept two arguments. definition = (method =~ /[^\]]=$/) ? 'arg' : '*args, &block' # The following generated method calls the target exactly once, storing # the returned value in a dummy variable. # # Reason is twofold: On one hand doing less calls is in general better. # On the other hand it could be that the target has side-effects, # whereas conceptually, from the user point of view, the delegator should # be doing one call. if allow_nil method_def = [ "def #{method_prefix}#{method}(#{definition})", "_ = #{to}", "if !_.nil? || nil.respond_to?(:#{method})", " if instance_variable_get('@_memoize_delegate_#{to}_#{method}')", " instance_variable_get('@_memoize_delegate_#{to}_#{method}')", " else", " instance_variable_set('@_memoize_delegate_#{to}_#{method}', _.#{method}(#{definition}))", " end", "end", "end" ].join ';' else exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} memoize_delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}") method_def = [ "def #{method_prefix}#{method}(#{definition})", " _ = #{to}", " if instance_variable_get('@_memoize_delegate_#{to}_#{method}')", " instance_variable_get('@_memoize_delegate_#{to}_#{method}')", " else", " instance_variable_set('@_memoize_delegate_#{to}_#{method}', _.#{method}(#{definition}))", " end", "rescue NoMethodError => e", " if _.nil? && e.name == :#{method}", " #{exception}", " else", " raise", " end", "end" ].join ';' end module_eval(method_def, file, line) end end |