Class: DslBlock

Inherits:
Object
  • Object
show all
Defined in:
lib/dsl_block.rb,
lib/dsl_block/version.rb,
lib/dsl_block/executor.rb

Overview

DslBlock is a base class for defining a Domain Specific Language. Subclasses of DslBlock define the desired dsl. These methods become available to ruby code running in the context of the subclass. The block execution is automatically isolated to prevent the called block from accessing instance methods unless specifically designated as callable. DslBlocks can be nested and parent blocks can allow their methods to be exposed to child block.

Example

# Define three DslBlocks each with at least one command in each block
class Foo < DslBlock
  commands :show_foo
  def show_foo(x)
    "Mr. T says you are a foo times #{x.to_i}"
  end
end

class Bar < DslBlock
  commands :show_bar
  def show_bar(x)
    "Ordering #{x.to_i} Shirley Temples from the bar"
  end
end

class Baz < DslBlock
  commands :show_baz
  def show_baz(x)
    "Baz spaz #{x.inspect}"
  end
end

# Connect the blocks to each other so they can be easily nested
Baz.add_command_to(Bar)
Bar.add_command_to(Foo, :propagate => true) # Let Bar blocks also respond to foo methods
Foo.add_command_to(self)

# Use the new DSL
foo do
  self.inspect       # => #<Foo:0x007fdbd52b54e0 @block=#<Proc:0x007fdbd52b5530@/home/fhall/wonderland/alice.rb:29>, @parent=nil>
  x = 10/10
  show_foo x         # => Mr. T says you are a foo times 1

  bar do
    x *= 2
    show_bar x       # => Ordering 2 Shirley Temples from the bar

    x += 1
    show_foo x       # => Mr. T says you are a foo times 3

    baz do

      x *= 4
      x /= 3
      show_baz x     # => Baz spaz 4

      begin
        x += 1
        show_bar x   # => NameError
      rescue NameError
        'No bar for us'
      end
    end

  end

end

Defined Under Namespace

Modules: Version Classes: Executor

Constant Summary collapse

VERSION =

2.0.0

'2.0.0'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*args, &block) ⇒ DslBlock

Create a new DslBlock instance.

block

Required block of code that will be executed when yield is called on the new DslBlock instance.

Options:

:parent

Optional parent DslBlock or Object that is providing additional commands to the block. (default: nil)

:block

Optional method of passing in a block.

Raises:

  • (ArgumentError)


116
117
118
119
120
121
# File 'lib/dsl_block.rb', line 116

def initialize(*args, &block)
  options = args.extract_options!
  @block = block_given? ? block : options[:block]
  raise ArgumentError, 'block must be provided' unless @block
  @parent = options[:parent]
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name, *args, &block) ⇒ Object



161
162
163
164
165
166
167
# File 'lib/dsl_block.rb', line 161

def method_missing(name, *args, &block)
  if @parent && @parent.respond_to?(name)
    @parent.send(name, *args, &block)
  else
    super
  end
end

Instance Attribute Details

#blockObject

Block of code that will be executed upon yield.



75
76
77
# File 'lib/dsl_block.rb', line 75

def block
  @block
end

#parentObject

Parent object providing additional commands to the block.



73
74
75
# File 'lib/dsl_block.rb', line 73

def parent
  @parent
end

Class Method Details

.add_command_to(destination, options = {}) ⇒ Object

This is a convenience command that allows this DslBlock to inject itself as a method into another DslBlock or Object. If the parent is also a DslBlock, the new method will automatically be added to the available commands.

Params:

destination

The object to receive the new method

options

A hash of options configuring the command

Options:

:propagate

Allow methods in the destination to be called by the block. (default: false)

:command_name

The name of the method to be created or nil to use the default which is based off of the class name. (default: nil)



95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/dsl_block.rb', line 95

def self.add_command_to(destination, options={})
  # Determine the name of the method to create
  command_name = (options[:command_name] || name).to_s.underscore.to_sym
  # Save a reference to our self so we will have something to call in a bit when self will refer to someone else.
  this_class = self
  # Define the command in the destination.
  destination.send(:define_method, command_name) do |&block|
    # Create a new instance of our self with the callers 'self' passed in as an optional parent.
    # Immediately after initialization, yield the block.
    this_class.new(:parent => options[:propagate] ? self : nil, &block).yield
  end
  # Add the new command to the parent if it is a DslBlock.
  destination.commands << command_name if destination.is_a?(Class) && destination < DslBlock
end

.commands(*args) ⇒ Object

With no arguments, returns an array of command names that this DslBlock makes available to blocks either directly or indirectly. With arguments, adds new names to the array of command names, then returns the new array.



79
80
81
82
83
# File 'lib/dsl_block.rb', line 79

def self.commands(*args)
  @commands ||= []
  @commands = (@commands + args.map(&:to_sym)).uniq
  @commands
end

Instance Method Details

#_commandsObject

This is the entire list of commands that this instance makes available to the block of code to be run. It is a combination of three distinct sources.

  1. The class’s declared commands

  2. If there is a parent of this DslBock instance…

    • The parents declared commands if it is a DslBlock

    • The parents public_methods if it is any other type of object

  3. Kernel.methods

This method is prefixed with an underscore in an attempt to avoid collisions with commands in the given block.



132
133
134
135
136
137
138
139
140
141
142
# File 'lib/dsl_block.rb', line 132

def _commands
  cmds = self.class.commands.dup
  if @parent
    if @parent.is_a?(DslBlock)
      cmds += @parent._commands
    else
      cmds +=  @parent.public_methods
    end
  end
  (cmds + Kernel.methods).uniq
end

#respond_to_missing?(method, include_all) ⇒ Boolean

:nodoc:



157
158
159
# File 'lib/dsl_block.rb', line 157

def respond_to_missing?(method, include_all)
  @parent && @parent.respond_to?(method, include_all) || super
end

#yieldObject

Yield the block given.



145
146
147
148
149
150
151
152
153
154
# File 'lib/dsl_block.rb', line 145

def yield
  begin
    # Evaluate the block in an executor to provide isolation
    # and prevent accidental interference with ourselves.
    Executor.new(self).instance_eval(&@block)
  rescue Exception => e
    e.set_backtrace(caller.select { |x| !x.include?(__FILE__)})
    raise e
  end
end