I'm Mo
I'm Mo

Reputation: 131

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

Upvotes: 3

Views: 359

Answers (1)

Eric Duminil
Eric Duminil

Reputation: 54223

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>

Upvotes: 3

Related Questions