Search code examples
rubydesign-patternsdsldesign-principles

Ruby DSL nested constructs


I am using the following code to enforce context of DSL nested constructs. What are the other ways of achieving the same functionality?

def a &block
  p "a"
  def b &block
    p "b"
    def c &block
      p "c"
      instance_eval &block
    end 
    instance_eval &block
    undef :c
  end 
  instance_eval &block 
  undef :b
end 
# Works
a do
  b do
    c do
    end
  end
end

# Doesn't Work 
b do
end
c do
end

Source


Solution

  • You asked about other ways, not the best way. So here's some examples :

    Example A

    class A
      def initialize
        p "a"
      end
    
      def b &block
        B.new.instance_eval &block
      end
    end
    
    class B
      def initialize
        p "b"
      end
    
      def c &block
        C.new.instance_eval &block
      end
    end
    
    class C
      def initialize
        p "c"
      end
    end
    
    def a &block
      A.new.instance_eval &block
    end
    

    Example B

    A bit shorter :

    def a &block
      p "a"
      A.new.instance_eval &block
    end
    
    class A
      def b &block
        p "b"
        B.new.instance_eval &block
      end
    
      class B
        def c &block
          p "c"
          C.new.instance_eval &block
        end
    
        class C
        end
      end
    end
    

    Example C

    If you don't plan to have a d method for an A::B::C object :

    def a &block
      p "a"
      A.new.instance_eval &block
    end
    
    class A
      def b &block
        p "b"
        B.new.instance_eval &block
      end
    
      class B
        def c &block
          p "c"
          instance_eval &block
        end
      end
    end
    

    Example D

    This was a fun one :

    def new_class_and_method(klass_name, next_klass=Object)
      dynamic_klass = Class.new do
        define_method(next_klass.name.downcase){|&block| p next_klass.name.downcase; next_klass.new.instance_eval &block}
      end
      Object.const_set(klass_name, dynamic_klass)
    end
    
    new_class_and_method("A", new_class_and_method("B", new_class_and_method("C")))
    
    def a &block
      p "a"
      A.new.instance_eval &block
    end
    

    Example E

    I dare say this doesn't look half bad:

    def new_method_and_class(x)
      define_method(x) do |&block|
        p x
        self.class.const_get(x.capitalize).new.instance_eval &block
      end
    
      self.const_set(x.capitalize, Class.new)
    end
    
    ["a", "b", "c"].inject(Object){|klass,x| klass.instance_eval{new_method_and_class(x)} }
    

    Example F

    A bit more robust :

    def new_method_and_class(x, parent_klass = Object)
      parent_klass.class_eval do
        define_method(x) do |&block|
          p x
          parent_klass.const_get(x.capitalize).new.instance_eval &block if block
        end
      end
    
      parent_klass.const_set(x.capitalize, Class.new)
    end
    
    ["a", "b", "c"].inject(Object){|klass,x| new_method_and_class(x,klass) }
    

    Explanation

    Example B

    In example B, we first define :

    • an a() method
    • an A class

    both are defined in main, because we want a() to be available directly. a() method doesn't do much expect printing "a" and passing a block to an instance of A.

    Then comes b() method. We don't want it to be available from main, so we define it inside A class. We want to continue with the nested methods, so we define a B class, which is also defined inside A. The B class is actually a A::B class. The A::B#b() method also prints "b", and passes a block to an instance of B.

    We continue with A::B::C inside of A::B, just like we did with A::B and A.

    Example F

    Example F is basically like Example B, but written dynamically.

    In example B, we defined an x method and an X class in every step, with the exact same structure. It should be possible to avoid code repetition with a method called new_method_and_class(x) which uses define_method, const_set and Class.new :

    new_method_and_class("a") # <- Object#a() and A are now defined
    
    a do
      puts self.inspect
    end
    #=> "a"
    #   <A:0x00000000e58bc0>
    

    Now, we want to define a b() method and a B class, but they shouldn't be in main. new_method_and_class("b") wouldn't do. So we pass an extra parameter, called parent_klass, which defaults to Object :

    parent_klass = new_method_and_class("a")
    new_method_and_class("b", parent_klass)
    
    a do 
      b do
        puts self.inspect
      end
    end
    
    # => "a"
    #    "b"
    #    <A::B:0x00000000daf368>
    
    b do
      puts "Not defined"
    end
    
    # => in `<main>': undefined method `b' for main:Object (NoMethodError)
    

    To define the c method, we just add another line :

    parent_klass = new_method_and_class("a")
    parent_klass = new_method_and_class("b", parent_klass)
    parent_klass = new_method_and_class("c", parent_klass)
    

    And so on and so on.

    To avoid code repetition, we can use inject with the parent_klass as accumulator value :

    ["a", "b", "c"].inject(Object){|klass,x| new_method_and_class(x,klass) }
    

    Bonus - Example G

    Here's a modified code from Example F which works with a basic tree structure.

    # http://stackoverflow.com/questions/40641273/ruby-dsl-nested-constructs/40641743#40641743
    def new_method_and_class(x, parent_klass = Object)
      parent_klass.class_eval do
        define_method(x) do |&block|
          p x.to_s
          parent_klass.const_get(x.capitalize).new.instance_eval &block if block
        end
      end
    
      parent_klass.const_set(x.capitalize, Class.new)
    end
    
    def create_dsl(branch,parent_klass = Object)
      case branch
      when Symbol, String
        new_method_and_class(branch,parent_klass)
      when Array
        branch.each do |child|
          create_dsl(child, parent_klass)
        end
      when Hash
        branch.each do |key, value|
          create_dsl(value, new_method_and_class(key,parent_klass))
        end
      end
    end
    
    methods_tree = {
      :a => {
        :b => [
          :c,
          :d
        ],
        :e => :f,
        :g => nil
      }
    }
    
    create_dsl(methods_tree)
    
    a do 
      b do
        c do
          puts self.inspect
        end
    
        d do
        end
      end
    
      e do
        f do
        end
      end
    
      g do
        puts self.inspect
      end
    end
    
    # => 
    #   "a"
    #   "b"
    #   "c"
    #   #<A::B::C:0x0000000243dfa8>
    #   "d"
    #   "e"
    #   "f"
    #   "g"
    #   #<A::G:0x0000000243d918>