Building DSLs
25.1 Example: Configuration DSL
class ServerConfig
attr_reader :settings
def self.configure(&block)
config = new
config.instance_eval(&block)
config
end
def initialize = @settings = { routes: [] }
def host(value) = @settings[:host] = value
def port(value) = @settings[:port] = value
def route(path, to:) = @settings[:routes] << { path:, to: }
def ssl(&block)
ssl_config = SslConfig.new
ssl_config.instance_eval(&block)
@settings[:ssl] = ssl_config.to_h
end
end
class SslConfig
def initialize = @opts = {}
def cert(path) = @opts[:cert] = path
def key(path) = @opts[:key] = path
def to_h = @opts
end
config = ServerConfig.configure do
host "0.0.0.0"
port 8443
route "/users", to: "users#index"
route "/health", to: "health#show"
ssl do
cert "/etc/ssl/cert.pem"
key "/etc/ssl/key.pem"
end
end
pp config.settings25.2 Example: HTML Builder with method_missing
class HTML
def initialize = @out = +""
def self.render(&) = new.tap { it.instance_eval(&) }.to_s
def method_missing(tag, content = nil, **attrs, &block)
attr_str = attrs.map { |k, v| %( #{k}="#{v}") }.join
@out << "<#{tag}#{attr_str}>"
block ? instance_eval(&block) : @out << content.to_s
@out << "</#{tag}>"
end
def respond_to_missing?(*) = true
def to_s = @out
end
puts HTML.render {
div(class: "card") do
h1 "Title"
p_ "Paragraph" # p is a Kernel method, so use p_ or define it explicitly
end
}25.3 DSL Techniques
instance_eval (clean syntax, but the block loses access to the outer self) vs yielding a builder object (config { |c| c.host "x" }, which keeps the outer context: used by Bundler and Rails config).
Class macros (has_many, validates, attribute) via class methods plus define_method.
Optional parentheses, keyword arguments, blocks, and method_missing make Ruby a natural DSL host: RSpec, Rake, Rails routes, Sinatra, Gemfile, Capybara, Chef/Puppet recipes, Vagrantfile, Fastlane.