Module: Blockenspiel

Defined in:
lib/blockenspiel.rb,
lib/blockenspiel/impl.rb,
lib/blockenspiel/errors.rb,
lib/blockenspiel/builder.rb,
lib/blockenspiel/version.rb,
lib/blockenspiel/dsl_setup.rb,
lib/blockenspiel/versionomy.rb,
lib/blockenspiel/unmixer_rubinius.rb,
lib/blockenspiel/unmixer_unimplemented.rb

Overview

Blockenspiel

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

Defined Under Namespace

Modules: DSL, DSLSetupMethods, Unmixer Classes: Base, BlockParameterError, BlockenspielError, Builder, DSLMissingError, ProxyDelegator

Constant Summary collapse

NO_VALUE =

:stopdoc:

::Object.new
VERSION_STRING =

Current gem version, as a frozen string.

::File.read(::File.dirname(__FILE__)+'/../../Version').strip.freeze
VERSION =

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

defined?(::Versionomy) ? ::Versionomy.parse(VERSION_STRING, :standard) : VERSION_STRING

Class Method Summary collapse

Class Method Details

._execute_dsl(use_mixin_method_, block_, eval_str_, target_, file_, line_) ⇒ Object

This is the "meat" of Blockenspiel, implementing both the proxy and mixin methods.



417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
# File 'lib/blockenspiel/impl.rb', line 417

def self._execute_dsl(use_mixin_method_, block_, eval_str_, target_, file_, line_)  # :nodoc:
  # Get the module of dsl methods
  mod_ = target_.class._get_blockenspiel_module rescue nil
  unless mod_
    raise ::Blockenspiel::DSLMissingError, "Given DSL target does not include Blockenspiel::DSL"
  end

  # Get the block's calling context object
  context_object_ = block_ ? ::Kernel.eval('self', block_.binding) : nil

  if use_mixin_method_

    # Create hash keys
    mixin_count_key_ = [context_object_.object_id, mod_.object_id]
    target_stack_key_ = _current_context_id(context_object_)

    # Store the target for inheriting.
    # We maintain a target call stack per thread.
    target_stack_ = @_target_stacks[target_stack_key_] ||= []
    target_stack_.push(target_)

    # Mix this module into the object, if required.
    # This ensures that we keep track of the number of requests to
    # mix this module in, from nested blocks and possibly multiple threads.
    @_mutex.synchronize do
      count_ = @_mixin_counts[mixin_count_key_]
      if count_
        @_mixin_counts[mixin_count_key_] = count_ + 1
      else
        @_mixin_counts[mixin_count_key_] = 1
        context_object_.extend(mod_)
      end
    end

    begin

      # Now call the block
      return block_.call

    ensure

      # Clean up the target stack
      target_stack_.pop
      @_target_stacks.delete(target_stack_key_) if target_stack_.size == 0

      # Remove the mixin from the object, if required.
      @_mutex.synchronize do
        count_ = @_mixin_counts[mixin_count_key_]
        if count_ == 1
          @_mixin_counts.delete(mixin_count_key_)
          ::Blockenspiel::Unmixer.unmix(context_object_, mod_)
        else
          @_mixin_counts[mixin_count_key_] = count_ - 1
        end
      end

    end

  else

    # Create proxy object
    proxy_ = ::Blockenspiel::ProxyDelegator.new(context_object_)
    proxy_.extend(mod_)

    # Store the target object so the dispatcher can get it
    target_stack_key_ = _current_context_id(proxy_)
    @_target_stacks[target_stack_key_] = [target_]

    begin

      # Evaluate with the proxy as self
      if block_
        return proxy_.instance_eval(&block_)
      else
        return proxy_.instance_eval(eval_str_, file_, line_)
      end

    ensure

      # Clean up the dispatcher information
      @_target_stacks.delete(target_stack_key_)

    end

  end
end

._invoke_with_block(block_, target_, opts_) ⇒ Object

Invoke when the DSL user provides code as a block. We read the given options hash, handle a few special cases, and then pass control to the _execute method.



350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
# File 'lib/blockenspiel/impl.rb', line 350

def self._invoke_with_block(block_, target_, opts_)  # :nodoc:
  # Read options
  parameter_ = opts_[:parameter]
  parameterless_ = opts_.include?(:behavior) ? opts_[:behavior] : opts_[:parameterless]

  # Handle no-target behavior
  if parameter_ == false && parameterless_ == false
    if block_.arity != 0 && block_.arity != -1
      raise ::Blockenspiel::BlockParameterError, "Block should not take parameters"
    end
    return block_.call
  end

  # Handle parametered block case
  if parameter_ != false && block_.arity == 1 || parameterless_ == false
    if block_.arity != 1
      raise ::Blockenspiel::BlockParameterError, "Block should take exactly one parameter"
    end
    return block_.call(target_)
  end

  # Check arity for parameterless case
  if block_.arity != 0 && block_.arity != -1
    raise ::Blockenspiel::BlockParameterError, "Block should not take parameters"
  end

  # Handle instance-eval behavior
  if parameterless_ == :instance
    return target_.instance_eval(&block_)
  end

  # Execute the DSL
  _execute_dsl(parameterless_ == :mixin, block_, nil, target_, nil, nil)
end

._invoke_with_string(eval_str_, target_, opts_) ⇒ Object

Invoke when the DSL user provides code as a string or file. We open and read the file if need be, and then pass control to the _execute method.



320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
# File 'lib/blockenspiel/impl.rb', line 320

def self._invoke_with_string(eval_str_, target_, opts_)  # :nodoc:
  # Read options
  file_ = opts_[:file]
  line_ = opts_[:line] || 1

  # Read file if no string provided directly
  unless eval_str_
    if file_
      eval_str_ = ::File.read(file_)
    else
      raise ::ArgumentError, "No code or file provided."
    end
  else
    file_ ||= "(String passed to Blockenspiel)"
  end

  # Handle instance-eval behavior
  if opts_[:behavior] == :instance
    return target_.instance_eval(eval_str_, file_, line_)
  end

  # Execute the DSL using the proxy method.
  _execute_dsl(false, nil, eval_str_, target_, file_, line_)
end

._proxy_dispatch(proxy_, name_, params_, block_) ⇒ Object

This implements the proxy fall-back behavior. We look up the context object, and call the given method on that object.



527
528
529
530
531
532
533
534
# File 'lib/blockenspiel/impl.rb', line 527

def self._proxy_dispatch(proxy_, name_, params_, block_)  # :nodoc:
  delegate_ = proxy_.instance_variable_get(:@_blockenspiel_delegate)
  if delegate_
    delegate_.send(name_, *params_, &block_)
  else
    raise ::NoMethodError, "undefined method `#{name_}' in DSL"
  end
end

._target_dispatch(object_, name_, params_, block_) ⇒ Object

This implements the mapping between DSL module methods and target object methods. We look up the current target object based on the current thread. Then we attempt to call the given method on that object. If we can't find an appropriate method to call, return the special value NO_VALUE.



510
511
512
513
514
515
516
517
518
519
520
521
# File 'lib/blockenspiel/impl.rb', line 510

def self._target_dispatch(object_, name_, params_, block_)  # :nodoc:
  target_stack_ = @_target_stacks[_current_context_id(object_)]
  return ::Blockenspiel::NO_VALUE unless target_stack_
  target_stack_.reverse_each do |target_|
    target_class_ = target_.class
    delegate_ = target_class_._get_blockenspiel_delegate(name_)
    if delegate_ && target_class_.public_method_defined?(delegate_)
      return target_.send(delegate_, *params_, &block_)
    end
  end
  return ::Blockenspiel::NO_VALUE
end

.invoke(*args_, &builder_block_) ⇒ Object

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.

[: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.)



267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# 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?Boolean

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.



48
49
50
# File 'lib/blockenspiel/impl.rb', line 48

def self.mixin_available?
  !::Blockenspiel::Unmixer.const_defined?(:UNIMPLEMENTED)
end