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? :dessertPattern 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}"
endFreeze, 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.lengthExample
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?