Created
November 8, 2012 18:45
-
-
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
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
# 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