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 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, whosemethod_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 bydsl_method
directives are honored. :instance
-
This behavior changes
self
directly to the target object usinginstance_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 usingdsl_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, butself
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 bydsl_method
directives are honored. This is the default behavior. :instance
-
This behavior actually changes
self
to the target object usinginstance_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 usingdsl_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