#from http://weblog.raganwald.com/2007/03/approach-to-composing-domain-specific.html #!/usr/bin/env ruby # # Created by Reginald Braithwaite on 2007-03-11. # Copyright (c) 2007. All rights reserved. # # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE # Version 2, December 2004 # # Copyright (C) 2004 Sam Hocevar # 22 rue de Plaisance, 75014 Paris, France # Everyone is permitted to copy and distribute verbatim or modified # copies of this license document, and changing it is allowed as long # as the name is changed. # # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE # TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION # # 0. You just DO WHAT THE FUCK YOU WANT TO. # A Domain Specific Language is used to introduce a new scope with an embedded set of methods. # # The idea is to avoid polluting the global namespace. Instead of adding methods to Kernel, we # add methods to a new DomainSpecificLanguage, and then we can evaluate code with the new language # using the #eval class method or using the #with method added to Kernel. # # For a similar approach, see http://www.infoq.com/articles/eval-options-in-ruby class DomainSpecificLanguage # See http://whytheluckystiff.net/articles/seeingMetaclassesClearly.html def metaclass; class << self; self; end; end def meta_eval &blk; metaclass.instance_eval &blk; end # Adds methods to a metaclass def meta_def name, &blk meta_eval { define_method name, &blk } end # See http://onestepback.org/index.cgi/Tech/Ruby/RubyBindings.rdoc class ReadOnlyReference def initialize(var_name, vars) @getter = eval "lambda { #{var_name} }", vars end def value @getter.call end end attr_reader :outer_binding, :outer_self # instances of a DomainSpecificLanguage are created each time # a block is evaluated with that language. The instance is # intialized with the block's binding. def initialize(given_binding) @outer_binding = given_binding @outer_self = ReadOnlyReference.new(:self, given_binding) end # some jiggery-pokery to access methods defined in the block's # scope, because when the block is evaluated with the DomainSpecificLanguage, # we use #instance_eval to set self to the DomainSpecificLanguage # instance. def method_missing(symbol, *args, &block) if args.empty? r = ReadOnlyReference.new(symbol, outer_binding) meta_def(symbol) { r.value } r.value else outer_self.value.send symbol, *args, &block end end class << self # Evaluates a block in the context of a new DomainSpecificlanguage # instance. def eval &block new(block.binding).instance_eval(&block) end end end # We open Kernel and add just one method for introducing DomainSpecificLanguages module Kernel # Evaluate a block with a DomainSpecificLanguage def with dsl_class, &block dsl_class.eval(&block) end end # Let is a DomainSpecificLanguage that actually creates DomainSpecificLanguages. # # Let works a lot like let in Scheme. Your provide a hash of names and value # expressions. The value expressions are evaluated in the outer scope, and then we have # a little domain specific language wher ethe inner scope has the names all bound to the # values. E.g. # # with Let do # let :x => 100, :y => 50 do # print "#{x + y} should equal fifty" # end # end # # # Within the Let DomainSpecificLanguage, you can declare multiple let statements and nest # them as you please. # # One important limitation: you cannot bind a value to a name that is already a local variable. class Let < DomainSpecificLanguage class Scope < DomainSpecificLanguage # initializes a Scope. In addition to the outer binding, we also pass in the # hash of names and values. Note the check to ensure we are not trying to # override a lcoal variable. def initialize given_binding, let_clauses = {} let_clauses.each do |symbol, value| var_name = symbol.to_s raise ArgumentError.new("Cannot override local #{var_name}") if eval("local_variables", given_binding).detect { |local| local == var_name } meta_eval { attr_accessor(var_name) } send "#{var_name}=", value end super(given_binding) end end # Define a new Scope: you're really defining a new DomainSpecificLanguage def let let_clauses = {}, &block Scope.new(block.binding, let_clauses).instance_eval(&block) end class << self # If you just want a one-off # def eval let_clauses = {}, &block # Scope.new(block.binding, let_clauses).instance_eval(&block) # end end end # A DomainSpecificDelegator is a DSL that delegates methods to a class or object. # The main use is to separate the mechanics of scoping from the methods of a utility # class. class DomainSpecificDelegator < DomainSpecificLanguage class << self # insert one or more #delegate_to calls in the class definition, giving a receiver # and a hash. Each hash pair is of the form verb => method where verb is the # name you will use in the DSL and method is the method in the receiver that will handle # it. def delegate_to receiver, method_hash @@delegations ||= {} method_hash.each { |verb, method_name| @@delegations[verb.to_s] = [receiver, method_name.to_s] } end end def method_missing symbol, *args, &block receiver, method_name = *@@delegations[symbol.to_s] if receiver receiver.send method_name, *args, &block else super(symbol, *args, &block) end end end #!/usr/bin/env ruby # # Created by Reginald Braithwaite on 2007-03-11. # Copyright (c) 2007. All rights reserved. # # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE # Version 2, December 2004 # # Copyright (C) 2004 Sam Hocevar # 22 rue de Plaisance, 75014 Paris, France # Everyone is permitted to copy and distribute verbatim or modified # copies of this license document, and changing it is allowed as long # as the name is changed. # # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE # TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION # # 0. You just DO WHAT THE FUCK YOU WANT TO. require "test/unit" require "dsl" class DefinesVariable < DomainSpecificLanguage def y 5 end end class DefinesMethodWithBlock < DomainSpecificLanguage def say something something end end class TestDomainSpecificLanguage < Test::Unit::TestCase def test_simple_read x = 100 assert_equal(500, DomainSpecificLanguage.eval { x * 5 }) end def test_define_reader x = 100 assert_equal(500, DefinesVariable.eval { x * y }) end def test_define_method assert_nothing_raised(Exception) { DefinesMethodWithBlock.eval { say 'hello' } } end def test_uses_lambda double = lambda { |x| x * 2 } assert_equal(8, DomainSpecificLanguage.eval { double.call(4) }) end def square x x * x end def test_uses_method assert_equal(16, DomainSpecificLanguage.eval { square(4) }) end end class DomainSpecificDelegatorTestHelper < DomainSpecificDelegator delegate_to "hello", :say => :to_s end class TestDomainSpecificDelegator < Test::Unit::TestCase def test_say with DomainSpecificDelegatorTestHelper do assert_equal('hello', say) end end def world 'world' end def test_outer_scope_method with DomainSpecificDelegatorTestHelper do assert_equal('hello world', "#{say} #{world}") end end end class TestLet < Test::Unit::TestCase def test_cannot_try_to_override_locals with Let do local_var = :snafu assert_raise(ArgumentError) do let :local_var => :fubar do fail(); end end end end def test_scoping with Let do let :x => 1, :y => 2 do assert_equal(1, x) assert_equal(2, y) end end end def test_access_to_outer_locals with Let do x = 1 let :y => 2 do assert_equal(3, x + y) end end end def z -1 end def test_access_to_outer_methods with Let do let :x => 1 do assert_equal(0, x + z) end end end def test_nesting with Let do let :x => 0, :y => 1 do assert_equal(1, x + y) let :x => 2 do assert_equal(3, x + y) end assert_equal(0, x) end end end def test_assignment with Let do let :x => 0, :y => 1 do assert_equal(1, x + y) let :x => 2 do assert_equal(3, x + y) x = 5 assert_equal(5, x) assert_equal(2, x()) # assignment creates a local that shadows the method. self.x = 7 assert_equal(5, x) assert_equal(7, x()) # you can hack around the read only nature, but it's not pretty end assert_equal(0, x) end end end def test_let_and_block_parameters with Let do let :x => nil do (1..10).each { |x| assert !x.nil? } assert_nil(x) end end end attr_accessor :w def test_outer_assigment w = 5 with Let do let :x => 5 do assert_equal(25, w * x) w = 7 assert_equal(35, w * x) end end assert_equal(7, w) end end