Skip to content

Instantly share code, notes, and snippets.

@bmarini
Created December 28, 2010 23:15
Show Gist options
  • Save bmarini/757889 to your computer and use it in GitHub Desktop.
Save bmarini/757889 to your computer and use it in GitHub Desktop.
Example of how to build a DSL
# Goal: Demonstrate the building blocks for creating a DSL in ruby, based on
# bundler's Gemfile DSL implementation
module Remote
# Responsible for evaluating the specification. This will transform the
# specification into meaningful data structures that can be acted on.
class Dsl
# Given a string of dsl code, create a new instance of dsl and evaluate
# the code within the context of the instance.
def self.evaluate(specification)
builder = new
builder.instance_eval(specification)
builder.to_definition
end
# Setup any variables needed for data composed by the dsl. Usually means
# hashes, stacks, and arrays.
def initialize
@servers = {}
@server_names = []
@commands = []
end
# Pass important data over to a new class that is responsible for acting
# on the data
def to_definition
Definition.new(@commands)
end
# DSL's API
# The public methods of this class (with exception of #to_definition) make
# up the api for this dsl. Two common cases:
#
# 1) Normally a method will take some parameters and push data onto an
# array or into a hash.
#
# For example, `server` adds a server definition to the `@servers` hash
#
# 2) If a method takes a block, usually that means you want to push some
# data onto a stack, evaluate the block (within the context of the main
# dsl object or perhaps a new object) and afterwards pop data off the same
# stack.
#
# For example, `commands` takes an array of server aliases and a block. It
# will push the server aliases onto a stack, evaluate the block, and pop
# the aliases back off. In this case we will use the stack of aliases as
# the servers that commands defined in the block will execute on.
# Defines a server alias
def server(name, definition)
@servers[name] = definition
end
# Defines a list of commands to be run on a list of servers
def commands(*server_names)
@server_names.concat(server_names)
yield
ensure
server_names.each { @server_names.pop }
end
# Defines a command to run. Assumes called from within a `commands` block,
# otherwise it won't run on any servers
def run(command)
servers = @server_names.map { |s| @servers[s] }
@commands << Command.new(command, servers)
end
end
# Responsible for running a command on a list of servers
class Command < Struct.new(:command, :servers)
def run
servers.each do |server|
system "ssh %s %s" % [server, command]
end
end
end
# Responsible for housing and acting on the data structures created by the
# dsl spec
class Definition < Struct.new(:commands)
def self.build(specfile)
Dsl.evaluate( File.read(specfile) )
end
def execute
commands.each { |c| c.run }
end
end
end
if $0 == __FILE__
require 'test/unit'
class TestDsl < Test::Unit::TestCase
def test_dsl
definition = Remote::Dsl.evaluate(DATA.read)
assert_equal 1, definition.commands.size
assert_equal ['user@domain1.tld', 'user@domain2.tld'],
definition.commands.first.servers
assert_equal 'echo $PATH', definition.commands.first.command
end
end
end
__END__
server :alias1, "user@domain1.tld"
server :alias2, "user@domain2.tld"
commands :alias1, :alias2 do
run 'echo $PATH'
end
# Usage:
#
# routes = RouteSet.new
# routes.draw do
# match "/home", :to => "home#index"
# end
class RouteSet
def draw(&block)
mapper = Mapper.new(self)
mapper.instance_exec(&block)
end
end
class Mapper
def initialize(set)
@set = set
@scope = {}
end
def match(path, options=nil)
mapping = Mapping.new(@set, @scope, path, options || {}).to_route
@set.add_route(*mapping)
self
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment