46

I have a DSL in Ruby that works like so:

desc 'list all todos'
command :list do |c|
  c.desc 'show todos in long form'
  c.switch :l
  c.action do |global,option,args|
    # some code that's not relevant to this question
  end
end

desc 'make a new todo'
command :new do |c|
  # etc.
end

A fellow developer suggested I enhance my DSL to not require passing c to the command block, and thus not require the c. for all the methods inside; presumably, he implied I could make the following code work the same:

desc 'list all todos'
command :list do
  desc 'show todos in long form'
  switch :l
  action do |global,option,args|
    # some code that's not relevant to this question
  end
end

desc 'make a new todo'
command :new do
  # etc.
end

The code for command looks something like

def command(*names)
  command = make_command_object(..)
  yield command                                                                                                                      
end

I tried several things and was unable to get it to work; I couldn't figure out how to change the context/binding of the code inside the command block to be different than the default.

Any ideas on if this is possible and how I might do it?

davetron5000
  • 24,123
  • 11
  • 70
  • 98

4 Answers4

37

Paste this code:

  def evaluate(&block)
    @self_before_instance_eval = eval "self", block.binding
    instance_eval &block
  end

  def method_missing(method, *args, &block)
    @self_before_instance_eval.send method, *args, &block
  end

For more information, refer to this really good article here

Jatin Ganhotra
  • 6,825
  • 6
  • 48
  • 71
  • is evaluate special? The linked article doesn't indicate it as such. My code, in the definition of `command`, does a `yield`. Are you saying that I should put the &block in my method sig, and then instance_eval that block instead of yield? (updating question with this info) – davetron5000 May 02 '11 at 00:39
14

Maybe

def command(*names, &blk)
  command = make_command_object(..)
  command.instance_eval(&blk)
end

can evaluate the block in the context of command object.

Sony Santos
  • 5,435
  • 30
  • 41
6
class CommandDSL
  def self.call(&blk)
    # Create a new CommandDSL instance, and instance_eval the block to it
    instance = new
    instance.instance_eval(&blk)
    # Now return all of the set instance variables as a Hash
    instance.instance_variables.inject({}) { |result_hash, instance_variable|
      result_hash[instance_variable] = instance.instance_variable_get(instance_variable)
      result_hash # Gotta have the block return the result_hash
    }
  end

  def desc(str); @desc = str; end
  def switch(sym); @switch = sym; end
  def action(&blk); @action = blk; end
end

def command(name, &blk)
  values_set_within_dsl = CommandDSL.call(&blk)

  # INSERT CODE HERE
  p name
  p values_set_within_dsl 
end

command :list do
  desc 'show todos in long form'
  switch :l
  action do |global,option,args|
    # some code that's not relevant to this question
  end
end

Will print:

:list
{:@desc=>"show todos in long form", :@switch=>:l, :@action=>#<Proc:0x2392830@C:/Users/Ryguy/Desktop/tesdt.rb:38>}
RyanScottLewis
  • 13,396
  • 16
  • 56
  • 84
2

I wrote a class that handles this exact issue, and deals with things like @instance_variable access, nesting, and so forth. Here's the write-up from another question:

Block call in Ruby on Rails

Community
  • 1
  • 1
Irongaze.com
  • 1,639
  • 16
  • 22