Created
December 28, 2010 23:15
-
-
Save bmarini/757889 to your computer and use it in GitHub Desktop.
Example of how to build a DSL
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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