๐ŸŽ“ A1Learner
๐Ÿ”
0 of 28 lessons completed

Ruby DSL with Method Missing

A settings builder can accept names nobody defined in advance. method_missing catches each bare name and turns it into a stored setting.

Any bare name becomes a setting

The Settings class defines no theme or size method. method_missing stores each name in a hash, and a later bare call reads the value back.

class Settings
  def initialize
    @values = {}
  end

  def method_missing(name, *args)
    if args.length == 1
      @values[name] = args[0]
    elsif args.empty?
      @values[name]
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    @values.key?(name)
  end

  def to_h
    @values
  end
end
settings = Settings.new
settings.theme "dark"
settings.size "large"
puts settings.theme
puts settings.size

One argument sets, none reads

The argument count splits setters from readers. Reading a name that was never set answers nil, and a call with two arguments falls through to super.

class Settings
  def initialize
    @values = {}
  end

  def method_missing(name, *args)
    if args.length == 1
      @values[name] = args[0]
    elsif args.empty?
      @values[name]
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    @values.key?(name)
  end

  def to_h
    @values
  end
end
settings = Settings.new
settings.theme "dark"
p settings.color
begin
  settings.theme "dark", "blue"
rescue => e
  puts e.class
end

respond_to? tells the truth

respond_to_missing? checks the stored keys, so respond_to? follows the settings as they appear. The method call then builds a real method for a stored name.

class Settings
  def initialize
    @values = {}
  end

  def method_missing(name, *args)
    if args.length == 1
      @values[name] = args[0]
    elsif args.empty?
      @values[name]
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    @values.key?(name)
  end

  def to_h
    @values
  end
end
settings = Settings.new
puts settings.respond_to? :theme
settings.theme "dark"
puts settings.respond_to? :theme
found = settings.method :theme
puts found.call

Example

class Settings
  def initialize
    @values = {}
  end

  def method_missing(name, *args)
    if args.length == 1
      @values[name] = args[0]
    elsif args.empty?
      @values[name]
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    @values.key?(name)
  end

  def to_h
    @values
  end
end
settings = Settings.new
settings.theme "dark"
settings.font "mono"
settings.size 14
p settings.to_h
puts settings.font
puts settings.size

Key points

  • method_missing receives every bare name the builder does not define.
  • A call with one argument stores that value under the name.
  • A call with no arguments reads the value stored under the name.
  • A call with more arguments falls through to super and fails.
  • respond_to_missing? turns true for a name once its setting exists.

In this example, three bare calls store three settings in the builder. The p line shows the whole hash of stored values. The last two lines read two settings back by name.

Try it Yourself

Edit the code, then press Run.

Running is simulated

Exercise

What decides whether a bare call stores a setting or reads one?