Metaprogramming

24.1 Dynamic Method Calls

obj.send(:private_method, arg)        # calls any method (ignores visibility)
obj.public_send(:name)                # respects visibility (preferred)
obj.respond_to?(:name)
method_name = "upcase"
"hi".public_send(method_name)

24.2 Defining Methods Dynamically

class Product
  STATUSES = %i[draft published archived]

  STATUSES.each do |status|
    define_method("#{status}?") { @status == status }
    define_method("#{status}!") { @status = status; self }
  end

  def self.attribute(name, default: nil)
    define_method(name) { instance_variable_get("@#{name}") || default }
    define_method("#{name}=") { |value| instance_variable_set("@#{name}", value) }
  end

  attribute :title, default: "Untitled"
  attribute :price, default: 0
end

p = Product.new
p.published!.published?   # true
p.title                   # "Untitled"

24.3 method_missing and respond_to_missing?

Always pair method_missing with respond_to_missing?. Consider defining real methods on first use for performance ("ghost methods" → defined methods).

class Config
  def initialize(data = {}) = @data = data

  def method_missing(name, *args)
    key = name.to_s
    if key.end_with?("=")
      @data[key.chomp("=").to_sym] = args.first
    elsif @data.key?(name)
      @data[name]
    else
      super                               # important: fall back to NoMethodError
    end
  end

  def respond_to_missing?(name, include_private = false)
    name.to_s.end_with?("=") || @data.key?(name) || super
  end
end

c = Config.new(host: "localhost")
c.host; c.port = 80; c.respond_to?(:host)

24.4 class_eval, instance_eval, instance_exec

String.class_eval do                  # adds instance methods (like reopening the class)
  def vowels = count("aeiou")
end

obj = Object.new
obj.instance_eval { @secret = 42 }    # self = obj: access internals
obj.instance_variable_get(:@secret)

class Builder
  def self.build(&block) = new.tap { it.instance_eval(&block) }   # DSL foundation
  def name(value) = @name = value
end
Builder.build { name "widget" }

callback = proc { |x| "#{x} from #{self.class}" }
"str".instance_exec(5, &callback)     # pass arguments + change self

24.5 Introspection and Reflection

klass = Object.const_get("BankAccount")       # string → class
klass.instance_methods(false)
klass.private_instance_methods(false)
klass.method_defined?(:deposit)
obj.instance_variables
obj.instance_variable_set(:@balance, 100)
Module.const_defined?(:Foo)
ObjectSpace.each_object(Class).select { it < StandardError }.first(5)
Class.new(StandardError) { def message = "custom" }     # anonymous class
Object.const_set(:DynamicError, Class.new(StandardError))
method(:puts).owner                            # Kernel

24.6 Code Evaluation

eval("1 + 2")                         # avoid with untrusted input (code injection!)
binding.local_variables
TOPLEVEL_BINDING.eval("self")