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

Ruby Project: DSL Toolkit

This project builds a small toolkit that describes a menu. It collects entries, checks their shape, freezes the result and packs it for later.

Entries arrive through method_missing

The Menu class defines no starter or main method. method_missing stores each known kind as an entry hash, and respond_to_missing? backs the known kinds only.

class Menu
  KINDS = [:starter, :main, :dessert, :drink]
  attr_reader :entries

  def initialize
    @entries = []
  end

  def self.build(&block)
    menu = new
    menu.instance_eval(&block)
    menu.finish
    menu
  end

  def title(text = nil)
    if text
      @title = text
    else
      @title
    end
  end

  def method_missing(name, *args)
    if KINDS.include? name
      @entries << { kind: name, name: args[0], price: args[1] }
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    KINDS.include? name
  end

  def finish
    @entries.each do |entry|
      verdict = case entry
      in { name: String, price: Integer }
        "ok"
      else
        "bad"
      end
      entry[:verdict] = verdict
    end
    @entries.freeze
    freeze
  end
end
menu = Menu.build do
  title "Lunch"
  starter "Soup", 4
  main "Pasta", 9
end
puts menu.title
puts menu.entries.length
first = menu.entries.first
puts first[:kind]
puts first[:name]
puts menu.respond_to? :dessert

Pattern matching checks each entry

A hash pattern asks for a string name and a whole-number price. The entry with a string price fails the pattern and earns the bad verdict.

entries = [
  { kind: :main, name: "Pasta", price: 9 },
  { kind: :main, name: "Salad", price: "7" }
]
entries.each do |entry|
  verdict = case entry
  in { name: String, price: Integer }
    "ok"
  else
    "bad"
  end
  puts "#{entry[:name]}: #{verdict}"
end

Freeze, pack and restore

finish freezes the built menu, so frozen? answers true. Marshal then packs the menu into a string, and Marshal.load restores it whole.

class Menu
  KINDS = [:starter, :main, :dessert, :drink]
  attr_reader :entries

  def initialize
    @entries = []
  end

  def self.build(&block)
    menu = new
    menu.instance_eval(&block)
    menu.finish
    menu
  end

  def title(text = nil)
    if text
      @title = text
    else
      @title
    end
  end

  def method_missing(name, *args)
    if KINDS.include? name
      @entries << { kind: name, name: args[0], price: args[1] }
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    KINDS.include? name
  end

  def finish
    @entries.each do |entry|
      verdict = case entry
      in { name: String, price: Integer }
        "ok"
      else
        "bad"
      end
      entry[:verdict] = verdict
    end
    @entries.freeze
    freeze
  end
end
menu = Menu.build do
  title "Lunch"
  starter "Soup", 4
  main "Pasta", 9
end
puts menu.frozen?
packed = Marshal.dump menu
restored = Marshal.load packed
puts restored.title
puts restored.entries.length

Example

class Menu
  KINDS = [:starter, :main, :dessert, :drink]
  attr_reader :entries

  def initialize
    @entries = []
  end

  def self.build(&block)
    menu = new
    menu.instance_eval(&block)
    menu.finish
    menu
  end

  def title(text = nil)
    if text
      @title = text
    else
      @title
    end
  end

  def method_missing(name, *args)
    if KINDS.include? name
      @entries << { kind: name, name: args[0], price: args[1] }
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    KINDS.include? name
  end

  def finish
    @entries.each do |entry|
      verdict = case entry
      in { name: String, price: Integer }
        "ok"
      else
        "bad"
      end
      entry[:verdict] = verdict
    end
    @entries.freeze
    freeze
  end
end
menu = Menu.build do
  title "Lunch"
  starter "Soup", 4
  main "Pasta", 9
  main "Salad", 7
  dessert "Cake", "5"
end
puts menu.title
menu.entries.each do |entry|
  puts "#{entry[:kind]}: #{entry[:name]} costs #{entry[:price]} (#{entry[:verdict]})"
end
puts "Entries: #{menu.entries.length}"
puts "Frozen: #{menu.frozen?}"
packed = Marshal.dump menu
restored = Marshal.load packed
puts "Restored: #{restored.title} with #{restored.entries.length} entries"

Key points

  • The builder runs its block with instance_eval, so entries read as bare calls.
  • method_missing turns each dish kind into a stored entry.
  • case/in pattern matching checks the shape of every entry.
  • finish freezes the menu and its entries before anyone reads them.
  • Marshal packs the whole menu into one string.
  • Marshal.load restores the packed menu with its title and entries.

In this example, the block describes a lunch menu with bare calls. method_missing collects each dish kind into an entry, and the pattern match marks the cake entry bad because its price is a string. The report prints every entry, and the packed copy restores with all four entries.

Try it Yourself

Edit the code, then press Run.

Running is simulated

Exercise

Which step checks that every entry has the right shape?