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 Blockenspiel.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.
- :mixin
- This is the default behavior. DSL
methods from the target are temporarily overlayed on the caller‘s
self object, but self still points to the same object, so
the helper methods and instance variables from the caller‘s closure
remain available. The DSL methods are
removed when the block completes.
- :instance
- This behavior actually changes self 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.
- :proxy
- 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.
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.)