module Blockenspiel

Blockenspiel

The Blockenspiel module provides a namespace for Blockenspiel, as well as the main entry point method “invoke”.

Constants

VERSION

Current gem version, as a Versionomy::Value if the versionomy library is available, or as a frozen string if not.

VERSION_STRING

Current gem version, as a frozen string.

Public Class Methods

invoke(*args_, &builder_block_) click to toggle source

Invoke a given DSL

This is the entry point for Blockenspiel. Call this function to invoke a set of DSL code provided by the user of your API.

For example, if you want users of your API to be able to do this:

call_dsl do
  foo(1)
  bar(2)
end

Then you should implement call_dsl like this:

def call_dsl(&block)
  my_dsl = create_block_implementation
  Blockenspiel.invoke(block, my_dsl)
  do_something_with(my_dsl)
end

In the above, create_block_implementation is a placeholder that returns an instance of your DSL methods class. This class includes the Blockenspiel::DSL module and defines the DSL methods foo and bar. See Blockenspiel::DSLSetupMethods for a set of tools you can use in your DSL methods class for creating a DSL.

Usage patterns

The invoke method has a number of forms, depending on whether the API user's DSL code is provided as a block or a string, and depending on whether the DSL methods are specified statically using a DSL class or dynamically using a block.

Blockenspiel.invoke(user_block, my_dsl, opts)

This form takes the user's code as a block, and the DSL itself as an object with DSL methods. The opts hash is optional and provides a set of arguments as described below under “Block DSL options”.

Blockenspiel.invoke(user_block, opts) { ... }

This form takes the user's code as a block, while the DSL itself is specified in the given block, as described below under “Dynamic target generation”. The opts hash is optional and provides a set of arguments as described below under “Block DSL options”.

Blockenspiel.invoke(user_string, my_dsl, opts)

This form takes the user's code as a string, and the DSL itself as an object with DSL methods. The opts hash is optional and provides a set of arguments as described below under “String DSL options”.

Blockenspiel.invoke(user_string, opts) { ... }

This form takes the user's code as a block, while the DSL itself is specified in the given block, as described below under “Dynamic target generation”. The opts hash is optional and provides a set of arguments as described below under “String DSL options”.

Blockenspiel.invoke(my_dsl, opts)

This form reads the user's code from a file, and takes the DSL itself as an object with DSL methods. The opts hash is required and provides a set of arguments as described below under “String DSL options”. The :file option is required.

Blockenspiel.invoke(opts) { ... }

This form reads the user's code from a file, while the DSL itself is specified in the given block, as described below under “Dynamic target generation”. The opts hash is required and provides a set of arguments as described below under “String DSL options”. The :file option is required.

Block DSL options

When a user provides DSL code using a block, you simply pass that block as the first parameter to ::invoke. Normally, Blockenspiel will first check the block's arity to see whether it takes a parameter. If so, it will pass the given target to the block. If the block takes no parameter, and the given target is an instance of a class with DSL capability, the DSL methods are made available on the caller's self object so they may be called without a block parameter.

Following are the options understood by Blockenspiel when providing code using a block:

:parameterless

If set to false, disables parameterless blocks and always attempts to pass a parameter to the block. Otherwise, you may set it to one of three behaviors for parameterless blocks: :mixin (the default), :instance, and :proxy. See below for detailed descriptions of these behaviors. This option key is also available as :behavior.

:parameter

If set to false, disables blocks with parameters, and always attempts to use parameterless blocks. Default is true, enabling parameter mode.

The following values control the precise behavior of parameterless blocks. These are values for the :parameterless option.

:proxy

This is the default behavior for parameterless blocks. This behavior changes self to a proxy object created by applying the DSL methods to an empty object, whose method_missing points back at the block's context. This behavior is a compromise between instance and mixin. As with instance, self is changed, so the caller loses access to its own instance variables. However, the caller's own methods should still be available since any methods not handled by the DSL are delegated back to the caller. Also, as with mixin, the target object's instance variables are not available (and thus cannot be clobbered) in the block, and the transformations specified by dsl_method directives are honored.

:instance

This behavior changes self directly to the target object using instance_eval. Thus, the caller loses access to its own helper methods and instance variables, and instead gains access to the target object's instance variables. The target object's methods are not modified: this behavior does not apply any DSL method changes specified using dsl_method directives.

:mixin

This behavior is not available on all ruby platforms. DSL methods from the target are temporarily overlayed on the caller's self object, but self still points to the same object. Thus the helper methods and instance variables from the caller's closure remain available. The DSL methods are removed when the block completes.

String DSL options

When a user provides DSL code using a string (either directly or via a file), Blockenspiel always treats it as a “parameterless” invocation, since there is no way to “pass a parameter” to a string. Thus, the two options recognized for block DSLs, :parameterless, and :parameter, are meaningless and ignored. However, the following new options are recognized:

:file

The value of this option should be a string indicating the path to the file from which the user's DSL code is coming. It is passed as the “file” parameter to eval; that is, it is included in the stack trace should an exception be thrown out of the DSL. If no code string is provided directly, this option is required and must be set to the path of the file from which to load the code.

:line

This option is passed as the “line” parameter to eval; that is, it indicates the starting line number for the code string, and is used to compute line numbers for the stack trace should an exception be thrown out of the DSL. This option is optional and defaults to 1.

:behavior

Controls how the DSL is called. Recognized values are :proxy (the default) and :instance. See below for detailed descriptions of these behaviors. Note that :mixin is not allowed in this case because its behavior would be indistinguishable from the proxy behavior.

The following values are recognized for the :behavior option:

:proxy

This behavior changes self to a proxy object created by applying the DSL methods to an empty object. Thus, the code in the DSL string does not have access to the target object's internal instance variables or private methods. Furthermore, the transformations specified by dsl_method directives are honored. This is the default behavior.

:instance

This behavior actually changes self to the target object using instance_eval. Thus, the code in the DSL string gains access to the target object's instance variables and private methods. Also, the target object's methods are not modified: this behavior does not apply any DSL method changes specified using dsl_method directives.

Dynamic target generation

It is also possible to dynamically generate a target object by passing a block to this method. This is probably best illustrated by example:

Blockenspiel.invoke(block) do
  add_method(:set_foo) do |value|
    my_foo = value
  end
  add_method(:set_things_from_block) do |value, &blk|
    my_foo = value
    my_bar = blk.call
  end
end

The above is roughly equivalent to invoking Blockenspiel with an instance of this target class:

class MyFooTarget
  include Blockenspiel::DSL
  def set_foo(value)
    set_my_foo_from(value)
  end
  def set_things_from_block(value)
    set_my_foo_from(value)
    set_my_bar_from(yield)
  end
end

Blockenspiel.invoke(block, MyFooTarget.new)

The obvious advantage of using dynamic object generation is that you are creating methods using closures, which provides the opportunity to, for example, modify closure local variables such as my_foo. This is more difficult to do when you create a target class since its methods do not have access to outside data. Hence, in the above example, we hand-waved, assuming the existence of some method called “set_my_foo_from”.

The disadvantage is performance. If you dynamically generate a target object, it involves parsing and creating a new class whenever it is invoked. Thus, it is recommended that you use this technique for calls that are not used repeatedly, such as one-time configuration.

See the Blockenspiel::Builder class for more details on add_method.

(And yes, you guessed it: this API is a DSL block, and is itself implemented using Blockenspiel.)

# File lib/blockenspiel/impl.rb, line 267
def self.invoke(*args_, &builder_block_)
  # This method itself is responsible for parsing the args to invoke,
  # and handling the dynamic target generation. It then passes control
  # to one of the _invoke_with_* methods.

  # The arguments.
  block_ = nil
  eval_str_ = nil
  target_ = nil
  opts_ = {}

  # Get the code
  case args_.first
  when ::String
    eval_str_ = args_.shift
  when ::Proc
    block_ = args_.shift
  end

  # Get the target, performing dynamic target generation if requested
  if builder_block_
    builder_ = ::Blockenspiel::Builder.new
    invoke(builder_block_, builder_)
    target_ = builder_._create_target
    args_.shift if args_.first.nil?
  else
    target_ = args_.shift
    unless target_
      raise ::ArgumentError, "No DSL target provided"
    end
  end

  # Get the options hash
  if args_.first.kind_of?(::Hash)
    opts_ = args_.shift
  end
  if args_.size > 0
    raise ::ArgumentError, "Unexpected arguments"
  end

  # Invoke
  if block_
    _invoke_with_block(block_, target_, opts_)
  else
    _invoke_with_string(eval_str_, target_, opts_)
  end
end
mixin_available?() click to toggle source

Determine whether the mixin strategy is available

Returns true if the mixin strategy is available on the current ruby platform. This will be false for most platforms.

# File lib/blockenspiel/impl.rb, line 48
def self.mixin_available?
  !::Blockenspiel::Unmixer.const_defined?(:UNIMPLEMENTED)
end