Skip to content

Instantly share code, notes, and snippets.

@ahoward
Created November 8, 2012 18:45
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ahoward/4040698 to your computer and use it in GitHub Desktop.
Save ahoward/4040698 to your computer and use it in GitHub Desktop.
this is how to build dsls that don't fuck the global namespace
# the entire concept of building a dsl means defining domain terms on an
# object, it's so much simpler to start with the dsl itself being a blank
# slate that simply relays certain methods to a scope
#
class DSL
instance_methods.each do |m|
undef_method m unless m[%r/\A__|\Aobject_id\Z/]
end
def __call__(&block)
Object.instance_method(:instance_eval).bind(self).call(&block)
end
def DSL.scope(scope, &block)
dsl =
Object.instance_method(:tap).bind(allocate).call do |dsl|
dsl.__call__ do
@object = @scope = scope
end
end
dsl.__call__(&block)
end
end
# because then you can do whatever the hell you want, including catching
# mehods defined on the object. you should *never* instance_eval in the
# actual objects for serious dsl's. hook up dsl terms one by one to your
# object... nearly all dsls get this backwards.
#
class ArrayDSL < ::DSL
def push(*args, &block)
@object.push(*args, &block)
ensure
puts "pushed #{ args.inspect } onto #{ @object.class }(#{ @object.inspect }) via the dsl..."
end
def initialize(*args, &block)
@object.clear
@object.push(*args, &block)
end
end
# then we can change thinking from "the dsl of this object" to evaluating a
# set of code with an object as the context/scope
#
ArrayDSL.scope Array.new do
initialize 1,2,3
push 43 #=> pushed [43] onto Array([1, 2, 3, 43]) via the dsl...
end
# so even for really complex dsl's like testing frameworks we need only
# realize that the *test itself* is the scope and build our fancy pants
# methods on the bloody dsl, not every damn object in ObjectSpace...
#
# this is just an example about how nearly any syntax can be contructed
# without polluting Object using the concepts of scope and a proxied blank
# slate. of course this impl is crap - but it shows that it can be easily
# be done.
#
class Spec
class Suite < ::Array
def run
each do |test|
status = test.run
puts "#{ test.name } #=> #{ status }"
end
end
def prefixes
@prefixes ||= []
end
class Name < ::String
def Name.for(*args)
args.join(' ').scan(/\w+/).join('_')
end
def Name.path_for(prefixes, *args)
'/' + [prefixes, Name.for(args)].join('/')
end
end
class Test
attr_accessor :suite
attr_accessor :name
attr_accessor :block
def initialize(suite, name, &block)
@suite = suite
@name = name
@block = block
end
def run
status = DSL.scope(self, &@block)
end
class Value
def initialize(lhs)
@lhs = lhs
end
def should(condition)
condition.call(@lhs)
end
end
class Condition
attr_accessor :type
attr_accessor :rhs
def initialize(type, rhs)
@type = type
@rhs = rhs
end
def call(lhs)
case type.to_s
when /eql/
lhs == rhs ? :success : :failure
else
raise ArgumentError.new(type.inspect)
end
end
end
class DSL < ::DSL
def value(value)
Value.new(value)
end
def eql(value)
Condition.new(:eql, value)
end
end
end
class DSL < ::DSL
def describe(*args, &block)
suite = @object
suite.prefixes.push(Name.for(args))
__call__(&block)
ensure
suite.prefixes.pop
end
def it(*args, &block)
suite = @object
name = Name.path_for(suite.prefixes, args)
test = Test.new(suite, name, &block)
suite.push(test)
end
end
end
def Spec.suite(&block)
suite = Suite.new
Suite::DSL.scope(suite, &block)
suite.run
end
end
# and, even with this hacked together in 10 minutes impl we can easily imagine
# powerful syntaxes that do not hork the global namespace
#
Spec.suite do
describe "something important..." do
it "should use silly english descriptions" do
value( 42 ).should eql 42.0.to_i
end
it "without fubaring every object's namespace..." do
value( 42 ).should eql 'forty-two'
end
end
end
# and here we prove it...
#
BEGIN {
n = 0
ObjectSpace.each_object(Class) do |c|
n += c.methods.size
n += c.instance_methods(false).size
end
puts "BEFORE: #{ n } methods"
puts
}
END {
}
n = 0
ObjectSpace.each_object(Class) do |c|
n += c.methods.size
n += c.instance_methods(false).size
end
puts
puts "AFTER: #{ n } methods"
__END__
BEFORE: 21280 methods
pushed [43] onto Array([1, 2, 3, 43]) via the dsl...
/something_important/should_use_silly_english_descriptions #=> success
/something_important/without_fubaring_every_object_s_namespace #=> failure
AFTER: 22281 methods
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment