blockenspiel-0.5.0/0000755000004100000410000000000012652620702014220 5ustar www-datawww-datablockenspiel-0.5.0/History.rdoc0000644000004100000410000001203312652620702016531 0ustar www-datawww-data=== 0.5.0 / 2016-01-07 * Fixed an issue with the proxy strategy, where if a block spawns blocks that live longer than it, the sub-blocks lost their context. * Changed the default strategy to proxy due to semantic issues with mixin and difficulty supporting it. * Dropped support for the mixin strategy on MRI because Ruby 2.3.0 broke it and I don't have the bandwidth to find a remedy. * Updated the Rakefile, tests, and general infrastructure to play better with modern Rubies. * Dropped support for Ruby 1.8, because who still uses 1.8??? === 0.4.6 / (never actually released) * Compatibility with the signature change to reset_method_cache in recent builds of Rubinius 2.0. * The gemspec no longer includes the timestamp in the version, so that bundler can pull from github. (Reported by corneverbruggen) * The Rakefile is now compatible with Ruby 2.0 and RubyGems 2.0. === 0.4.5 / 2012-06-27 * The 0.4.4 build was missing the JRuby unmixer. Fixed. === 0.4.4 / 2012-06-27 * Under JRuby 1.9 mode, if two threads mixed into the same object, the calls sometimes went to the wrong place. Fixed. * The VERSION constant behaved very oddly under Rubinius 1.9 mode. Fixed. * Eliminate some warnings. * Integrate with Travis CI. === 0.4.3 / 2011-06-22 * MRI C extension experienced a compile error under the current MRI head (e.g. 1.9.3). Fixed. * Rake-based build of the MRI C extension failed under newer rakes. Fixed. * Eliminated some Rakefile deprecation warnings under newer rakes. === 0.4.2 / 2011-06-02 * Fixed an unmixer compatibility issue with Rubinius > 1.2.x. (Thanks to @meh for the fix.) * Recent versions of Rubinius raised exceptions involving Fiber. Fixed. * Workaround for a JRuby NullPointerException (JRUBY-5842). * Integrated JRuby platform gem back into main gem. * A .gemspec file is now available for gem building and bundler git integration. * Some cleanup of the Rakefile and tests. === 0.4.1 / 2010-06-23 * Support for rubinius 1.0. === 0.4.0 / 2010-06-21 * Implemented string- and file-based DSLs (in addition to block-based). * Correctly handle separate active DSLs in different fibers within the same thread, when fibers are avaialble. * Updated ruby runtime dependencies to reflect what I'm actually testing. * Organized the source a little better, and fixed some Rakefile quirks. === 0.3.3 / 2010-05-24 * Some Rakefile fixes to match RDoc and Ruby 1.9 changes. * Minor documentation updates. === 0.3.2 / 2009-11-17 * Modules included in a DSL-ized class now have their methods included in the DSL. * Raise a more informative error (for now) when trying to include Blockenspiel::DSL in a module. At some point, we'll support this usage, once I figure out the right semantics for it. === 0.3.1 / 2009-11-08 * Blockenspiel#invoke can now take its options hash as the second argument (instead of the third) when using dynamic target generation, since the second argument is otherwise unused in this case. * Now defines Blockenspiel::VERSION, as a versionomy object if the versionomy library is available, or as a version string if not. === 0.3.0 / 2009-11-04 * dsl_attr_writer and dsl_attr_accessor convenience methods are available for creating DSL-friendly attributes. * Dynamic DSL methods can now take real block arguments, if supported by the Ruby interpreter. * Shortened README.rdoc and renamed the longer version to Blockenspiel.rdoc. * Some documentation updates. === 0.2.2 / 2009-10-28 * Support for gemcutter hosting in the build/release scripts. * Some clarifications to constant scopes internal in the code. * A few documentation updates. * Minor changes to the Implementing DSL Blocks paper to deal with Why's disappearance. === 0.2.1 / 2009-04-16 * Now compatible with Ruby 1.9. * Now compatible with JRuby 1.2. * No longer requires the mixology gem. * Building no longer requires hoe. === 0.2.0 / 2009-04-15 * Earlier build of 0.2.1 that had some problems with JRuby. === 0.1.1 / 2008-11-06 * Added ability to pass the block as the first parameter in the dynamic DSL builder API; cleaned up the API a little * Minor fixes to Implementing DSL Blocks paper * Some updates to rdocs * More test coverage === 0.1.0 / 2008-10-29 * Alpha release, opened for public feedback * Tightened constraints on block parameters * Added some test cases for threads and parameter constraints * Revisions to the Implementing DSL Blocks paper === 0.0.4 / 2008-10-24 * Improvements to the logic for choosing behaviors * Added exception classes and provided better error handling * Actually added the behavior test case to the gem manifest... * Documentation revisions * Revisions to the Implementing DSL Blocks paper === 0.0.3 / 2008-10-23 * Added :proxy behavior for parameterless blocks * Removed option to turn off inheriting, since the semantics are somewhat ill-defined and inconsistent. All parameterless blocks now exhibit the inheriting behavior. * Added tests for the different behavior settings. === 0.0.2 / 2008-10-21 * Cleaned up some of the documentation * Revisions to the Implementing DSL Blocks paper === 0.0.1 / 2008-10-20 * Initial test release blockenspiel-0.5.0/README.rdoc0000644000004100000410000001251012652620702016025 0ustar www-datawww-data== Blockenspiel Blockenspiel is a helper library designed to make it easy to implement DSL blocks. It is designed to be comprehensive and robust, supporting most common usage patterns, and working correctly in the presence of nested blocks and multithreading. === Summary Blockenspiel is a helper library providing several different strategies for implementing DSL blocks. It supports both DSLs that take a block parameter and those that do not. For example: # Call DSL block with parameter configure_me do |config| config.add_foo(1) config.add_bar(2) end # Call DSL block without parameter configure_me do add_foo(3) add_bar(4) end To support the above usage, you can do this: # Implement DSL block methods class ConfigMethods include Blockenspiel::DSL def add_foo(value) # do something end def add_bar(value) # do something end end # Implement configure_me method def configure_me(&block) Blockenspiel.invoke(block, ConfigMethods.new) end By default, Blockenspiel uses a "delegation" technique (to my knowledge first proposed by Dan Manges) to support parameterless blocks while mitigating some of the issues with instance_eval. It supports nested blocks and multithreaded access, and provides a variety of tools for handling the typical issues you may encounter when writing DSLs. On some ruby platforms, Blockenspiel also supports a mixin technique (proposed by Why The Lucky Stiff). For more detailed usage and examples, see {Blockenspiel.rdoc}[link:Blockenspiel\_rdoc.html]. For an extended analysis of different ways to implement DSL blocks, see {ImplementingDSLblocks.rdoc}[link:ImplementingDSLblocks\_rdoc.html]. === Requirements * Ruby 1.9.3 or later, JRuby 1.5 or later, or Rubinius 1.0 or later. === Installation gem install blockenspiel === Known issues and to-do items * Implementing wildcard DSL methods using method_missing doesn't work. I haven't yet decided on the right semantics for this case, or whether it is even a reasonable feature at all. * Including Blockenspiel::DSL in a module (rather than a class) is not supported, but this could appear in a future release. * Find a way to implement mixin behavior reliably on MRI. === Development and support Documentation is available at http://dazuma.github.com/blockenspiel/rdoc Source code is hosted on Github at http://github.com/dazuma/blockenspiel Contributions are welcome. Fork the project on Github. Build status: {}[http://travis-ci.org/dazuma/blockenspiel] Report bugs on Github issues at http://github.org/dazuma/blockenspiel/issues Contact the author at dazuma at gmail dot com. === Author / Credits Blockenspiel is written by Daniel Azuma (http://www.daniel-azuma.com/). The mixin implementation is based on a concept by the late Why The Lucky Stiff, documented in his 6 October 2008 blog posting entitled "Mixing Our Way Out Of Instance Eval?". The original link has disappeared along with its author, but you may find copies or mirrors out there. The unmixer code is based on {Mixology}[http://rubyforge.org/projects/mixology], version by Patrick Farley, anonymous z, Dan Manges, and Clint Bishop. The JRuby code is adapted from Mixology 0.1, and has been stripped down and modified to support JRuby >= 1.2. The Rubinius code was adapted from unreleased code in the Mixology source tree and modified to support Rubinius 1.0. I know Mixology 0.2 is now available, but its Rubinius support is not active, and I'd rather keep the unmixer bundled with Blockenspiel for now to reduce dependencies. Earlier versions of Blockenspiel also included a C extension, adapted from Mixology, to support mixins for MRI, but this code has been disabled due to issues with newer versions of Ruby. The dsl_attr_writer and dsl_attr_accessor feature came from a suggestion by Luis Lavena. === License Copyright 2008 Daniel Azuma. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder, nor the names of any other contributors to this software, may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. blockenspiel-0.5.0/Version0000644000004100000410000000000612652620702015564 0ustar www-datawww-data0.5.0 blockenspiel-0.5.0/lib/0000755000004100000410000000000012652620702014766 5ustar www-datawww-datablockenspiel-0.5.0/lib/blockenspiel/0000755000004100000410000000000012652620702017440 5ustar www-datawww-datablockenspiel-0.5.0/lib/blockenspiel/errors.rb0000644000004100000410000000466112652620702021310 0ustar www-datawww-data# ----------------------------------------------------------------------------- # # Blockenspiel error classes # # ----------------------------------------------------------------------------- # Copyright 2008 Daniel Azuma # # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # * Neither the name of the copyright holder, nor the names of any other # contributors to this software, may be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # ----------------------------------------------------------------------------- ; module Blockenspiel # Base exception for all exceptions raised by Blockenspiel class BlockenspielError < ::RuntimeError end # This exception is rasied when attempting to use the :proxy or # :mixin parameterless behavior with a target that does not have # the DSL module included. It is an error made by the DSL implementor. class DSLMissingError < ::Blockenspiel::BlockenspielError end # This exception is raised when the block provided does not take the # expected number of parameters. It is an error made by the caller. class BlockParameterError < ::Blockenspiel::BlockenspielError end end blockenspiel-0.5.0/lib/blockenspiel/versionomy.rb0000644000004100000410000000412312652620702022177 0ustar www-datawww-data# ----------------------------------------------------------------------------- # # Blockenspiel version # # ----------------------------------------------------------------------------- # Copyright 2008 Daniel Azuma # # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # * Neither the name of the copyright holder, nor the names of any other # contributors to this software, may be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # ----------------------------------------------------------------------------- ; begin require 'versionomy' rescue ::LoadError end module Blockenspiel # Current gem version, as a Versionomy::Value if the versionomy library # is available, or as a frozen string if not. VERSION = defined?(::Versionomy) ? ::Versionomy.parse(VERSION_STRING, :standard) : VERSION_STRING end blockenspiel-0.5.0/lib/blockenspiel/unmixer_rubinius.rb0000644000004100000410000000652312652620702023402 0ustar www-datawww-data# ----------------------------------------------------------------------------- # # Blockenspiel unmixer for Rubinius # # ----------------------------------------------------------------------------- # Copyright 2011 Daniel Azuma # # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # * Neither the name of the copyright holder, nor the names of any other # contributors to this software, may be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # ----------------------------------------------------------------------------- ; module Blockenspiel # :stopdoc: module Unmixer @old_metaclass = ''.respond_to?(:metaclass) # Unmix a module from an object in Rubinius. # # This implementation is based on unreleased code from the Mixology # source, written by Dan Manges. # See http://github.com/dan-manges/mixology # # It has been stripped down and modified for compatibility with the # Rubinius 1.0 release. def self.unmix(obj_, mod_) # :nodoc: last_super_ = @old_metaclass ? obj_.metaclass : obj_.singleton_class this_super_ = last_super_.direct_superclass while this_super_ if (this_super_ == mod_ || this_super_.respond_to?(:module) && this_super_.module == mod_) _reset_method_cache(obj_) last_super_.superclass = this_super_.direct_superclass _reset_method_cache(obj_) return else last_super_ = this_super_ this_super_ = this_super_.direct_superclass end end nil end if ::Rubinius::VM.method(:reset_method_cache).arity == 1 # Older versions of Rubinius def self._reset_method_cache(obj_) # :nodoc: obj_.methods.each do |name_| ::Rubinius::VM.reset_method_cache(name_.to_sym) end end else # Newer versions of Rubinius def self._reset_method_cache(obj_) # :nodoc: klass_ = obj_.class obj_.methods.each do |name_| ::Rubinius::VM.reset_method_cache(klass_, name_.to_sym) end end end end # :startdoc: end blockenspiel-0.5.0/lib/blockenspiel/version.rb0000644000004100000410000000400412652620702021450 0ustar www-datawww-data# ----------------------------------------------------------------------------- # # Blockenspiel version # # ----------------------------------------------------------------------------- # Copyright 2008 Daniel Azuma # # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # * Neither the name of the copyright holder, nor the names of any other # contributors to this software, may be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # ----------------------------------------------------------------------------- ; module Blockenspiel # Current gem version, as a frozen string. VERSION_STRING = ::File.read(::File.dirname(__FILE__)+'/../../Version').strip.freeze autoload(:VERSION, ::File.dirname(__FILE__)+'/versionomy.rb') end blockenspiel-0.5.0/lib/blockenspiel/unmixer_unimplemented.rb0000644000004100000410000000424412652620702024406 0ustar www-datawww-data# ----------------------------------------------------------------------------- # # Blockenspiel unmixer module when unmixer is not implemented # # ----------------------------------------------------------------------------- # Copyright 2010 Daniel Azuma # # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # * Neither the name of the copyright holder, nor the names of any other # contributors to this software, may be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # ----------------------------------------------------------------------------- ; module Blockenspiel # :stopdoc: module Unmixer UNIMPLEMENTED = true # Unmixer stub. # This throws an exception indicating the unmixer is not implemented. def self.unmix(obj_, mod_) # :nodoc: raise "Blockenspiel's mixin behavior is not implemented on this ruby platform." end end # :startdoc: end blockenspiel-0.5.0/lib/blockenspiel/dsl_setup.rb0000644000004100000410000003072312652620702021774 0ustar www-datawww-data# ----------------------------------------------------------------------------- # # Blockenspiel DSL definition # # ----------------------------------------------------------------------------- # Copyright 2008 Daniel Azuma # # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # * Neither the name of the copyright holder, nor the names of any other # contributors to this software, may be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # ----------------------------------------------------------------------------- ; require 'thread' module Blockenspiel # === DSL setup methods # # These class methods are available after you have included the # Blockenspiel::DSL module. # # By default, a class that has DSL capability will automatically make # all public methods available to parameterless blocks, except for the # +initialize+ method, any methods whose names begin with an underscore, # and any methods whose names end with an equals sign. # # If you want to change this behavior, use the directives defined here to # control exactly which methods are available to parameterless blocks. module DSLSetupMethods # :stopdoc: # Called when DSLSetupMethods extends a class. # This sets up the current class, and adds a hook that causes # any subclass of the current class also to be set up. def self.extended(klass_) unless klass_.instance_variable_defined?(:@_blockenspiel_module) _setup_class(klass_) def klass_.inherited(subklass_) ::Blockenspiel::DSLSetupMethods._setup_class(subklass_) super end class << klass_ unless private_method_defined?(:_blockenspiel_default_include) alias_method :_blockenspiel_default_include, :include alias_method :include, :_blockenspiel_custom_include end end end end # :startdoc: # Set up a class. # Creates a DSL module for this class, optionally delegating to the superclass's module. # Also initializes the class's methods hash and active flag. def self._setup_class(klass_) # :nodoc: superclass_ = klass_.superclass superclass_ = nil unless superclass_.respond_to?(:_get_blockenspiel_module) mod_ = ::Module.new if superclass_ mod_.module_eval do include superclass_._get_blockenspiel_module end end klass_.instance_variable_set(:@_blockenspiel_superclass, superclass_) klass_.instance_variable_set(:@_blockenspiel_module, mod_) klass_.instance_variable_set(:@_blockenspiel_methods, {}) klass_.instance_variable_set(:@_blockenspiel_active, nil) end # Automatically make the given method a DSL method according to the current setting. def _blockenspiel_auto_dsl_method(symbol_) # :nodoc: if @_blockenspiel_active dsl_method(symbol_) elsif @_blockenspiel_active.nil? if symbol_ != :initialize && symbol_.to_s !~ /^_/ && symbol_.to_s !~ /=$/ dsl_method(symbol_) end end end # Hook called when a method is added. # This calls _blockenspiel_auto_dsl_method to auto-handle the method, # possibly making it a DSL method according to the current setting. def method_added(symbol_) # :nodoc: _blockenspiel_auto_dsl_method(symbol_) super end # Custom include method. Calls the main include implementation, but also # goes through the public methods of the included module and calls # _blockenspiel_auto_dsl_method on each to make them DSL methods # (possibly) according to the current setting. def _blockenspiel_custom_include(*modules_) # :nodoc: _blockenspiel_default_include(*modules_) modules_.reverse_each do |mod_| mod_.public_instance_methods.each do |method_| _blockenspiel_auto_dsl_method(method_) end end end # Get this class's corresponding DSL module def _get_blockenspiel_module # :nodoc: @_blockenspiel_module end # Get information on the given DSL method name. # Possible values are the name of the delegate method, false for method disabled, # or nil for method never defined. def _get_blockenspiel_delegate(name_) # :nodoc: delegate_ = @_blockenspiel_methods[name_] if delegate_.nil? && @_blockenspiel_superclass @_blockenspiel_superclass._get_blockenspiel_delegate(name_) else delegate_ end end # Make a particular method available to parameterless DSL blocks. # # To explicitly make a method available to parameterless blocks: # dsl_method :my_method # # To explicitly exclude a method from parameterless blocks: # dsl_method :my_method, false # # To explicitly make a method available to parameterless blocks, but # point it to a method of a different name on the target class: # dsl_method :my_method, :target_class_method def dsl_method(name_, delegate_=nil) name_ = name_.to_sym if delegate_ delegate_ = delegate_.to_sym elsif delegate_.nil? delegate_ = name_ end @_blockenspiel_methods[name_] = delegate_ unless @_blockenspiel_module.public_method_defined?(name_) @_blockenspiel_module.module_eval("def #{name_}(*params_, &block_); val_ = ::Blockenspiel._target_dispatch(self, :#{name_}, params_, block_); ::Blockenspiel::NO_VALUE.equal?(val_) ? super(*params_, &block_) : val_; end\n") end end # Control the behavior of methods with respect to parameterless blocks, # or make a list of methods available to parameterless blocks in bulk. # # To enable automatic exporting of methods to parameterless blocks. # After executing this command, all public methods defined in the class # will be available on parameterless blocks, until # dsl_methods false is called: # dsl_methods true # # To disable automatic exporting of methods to parameterless blocks. # After executing this command, methods defined in this class will be # excluded from parameterless blocks, until dsl_methods true # is called: # dsl_methods false # # To make a list of methods available to parameterless blocks in bulk: # dsl_methods :my_method1, :my_method2, ... # # You can also point dsl methods to a method of a different name on the # target class, by using a hash syntax, as follows: # dsl_methods :my_method1 => :target_class_method1, # :my_method2 => :target_class_method2 # # You can mix non-renamed and renamed method declarations as long as # the renamed (hash) methods are at the end. e.g.: # dsl_methods :my_method1, :my_method2 => :target_class_method2 def dsl_methods(*names_) if names_.size == 0 || names_ == [true] @_blockenspiel_active = true elsif names_ == [false] @_blockenspiel_active = false else if names_.last.kind_of?(::Hash) names_.pop.each do |name_, delegate_| dsl_method(name_, delegate_) end end names_.each do |name_| dsl_method(name_, name_) end end end # A DSL-friendly attr_accessor. # # This creates the usual "name" and "name=" methods in the current # class that can be used in the usual way. However, its implementation # of the "name" method (the getter) also takes an optional parameter # that causes it to behave as a setter. This is done because the usual # setter syntax cannot be used in a parameterless block, since it is # syntactically indistinguishable from a local variable assignment. # The "name" method is exposed as a dsl_method. # # For example: # # dsl_attr_accessor :foo # # enables the following: # # my_block do |param| # param.foo = 1 # Usual setter syntax works # param.foo 2 # Alternate setter syntax also works # puts param.foo # Usual getter syntax still works # end # # my_block do # # foo = 1 # Usual setter syntax does NOT work since it # # looks like a local variable assignment # foo 2 # Alternate setter syntax does work # puts foo # Usual getter syntax still works # end def dsl_attr_accessor(*names_) names_.each do |name_| unless name_.kind_of?(::String) || name_.kind_of?(::Symbol) raise ::TypeError, "#{name_.inspect} is not a symbol" end unless name_.to_s =~ /^[_a-zA-Z]\w+$/ raise ::NameError, "invalid attribute name #{name_.inspect}" end module_eval("def #{name_}(value_=::Blockenspiel::NO_VALUE); ::Blockenspiel::NO_VALUE.equal?(value_) ? @#{name_} : @#{name_} = value_; end\n") alias_method("#{name_}=", name_) dsl_method(name_) end end # A DSL-friendly attr_writer. # # This creates the usual "name=" method in the current class that can # be used in the usual way. However, it also creates the method "name", # which also functions as a setter (but not a getter). This is done # because the usual setter syntax cannot be used in a parameterless # block, since it is syntactically indistinguishable from a local # variable assignment. The "name" method is exposed as a dsl_method. # # For example: # # dsl_attr_writer :foo # # is functionally equivalent to: # # attr_writer :foo # alias_method :foo, :foo= # dsl_method :foo # # which enables the following: # # my_block do |param| # param.foo = 1 # Usual setter syntax works # param.foo 2 # Alternate setter syntax also works # end # my_block do # # foo = 1 # Usual setter syntax does NOT work since it # # looks like a local variable assignment # foo(2) # Alternate setter syntax does work # end def dsl_attr_writer(*names_) names_.each do |name_| attr_writer(name_) alias_method(name_, "#{name_}=") dsl_method(name_) end end end # === DSL activation module # # Include this module in a class to mark this class as a DSL class and # make it possible for its methods to be called from a block that does not # take a parameter. # # After you include this module, you can use the directives defined in # DSLSetupMethods to control what methods are available to DSL blocks # that do not take parameters. module DSL def self.included(klass_) # :nodoc: unless klass_.kind_of?(::Class) raise ::Blockenspiel::BlockenspielError, "You cannot include Blockenspiel::DSL in a module (yet)" end klass_.extend(::Blockenspiel::DSLSetupMethods) end end # === DSL activation base class # # Subclasses of this base class are considered DSL classes. # Methods of the class can be made available to be called from a block that # doesn't take an explicit block parameter. # You may use the directives defined in DSLSetupMethods to control how # methods of the class are handled in such blocks. # # Subclassing this base class is functionally equivalent to simply # including Blockenspiel::DSL in the class. class Base include ::Blockenspiel::DSL end end blockenspiel-0.5.0/lib/blockenspiel/impl.rb0000644000004100000410000005124512652620702020735 0ustar www-datawww-data# ----------------------------------------------------------------------------- # # Blockenspiel implementation # # ----------------------------------------------------------------------------- # Copyright 2008 Daniel Azuma # # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # * Neither the name of the copyright holder, nor the names of any other # contributors to this software, may be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # ----------------------------------------------------------------------------- ; require 'thread' module Blockenspiel # === 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. def self.mixin_available? !::Blockenspiel::Unmixer.const_defined?(:UNIMPLEMENTED) end # === 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.) 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 # 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. 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 # 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. 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 # Class for proxy delegators. # The proxy behavior creates one of these delegators, mixes in the dsl # methods, and uses instance_eval to invoke the block. This class delegates # non-handled methods to the context object. class ProxyDelegator # :nodoc: def initialize(delegate_) @_blockenspiel_delegate = delegate_ end def method_missing(symbol_, *params_, &block_) ::Blockenspiel._proxy_dispatch(self, symbol_, params_, block_) end end # :stopdoc: NO_VALUE = ::Object.new # :startdoc: @_target_stacks = {} @_mixin_counts = {} @_proxy_delegators = {} @_mutex = ::Mutex.new # This is the "meat" of Blockenspiel, implementing both the proxy and # mixin methods. 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 # 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. 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 # This implements the proxy fall-back behavior. # We look up the context object, and call the given method on that object. 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 # This returns a current context ID, which includes both the curren thread # object_id and the current fiber object_id (if available). begin require 'fiber' raise ::LoadError unless defined?(::Fiber) def self._current_context_id(object_) # :nodoc: thid_ = ::Thread.current.object_id begin [thid_, ::Fiber.current.object_id, object_.object_id] rescue ::Exception # JRuby hack (see JRUBY-5842) [thid_, 0, object_.object_id] end end rescue ::LoadError def self._current_context_id(object_) # :nodoc: [::Thread.current.object_id, object_.object_id] end end end blockenspiel-0.5.0/lib/blockenspiel/builder.rb0000644000004100000410000001715212652620702021421 0ustar www-datawww-data# ----------------------------------------------------------------------------- # # Blockenspiel dynamic target construction # # ----------------------------------------------------------------------------- # Copyright 2008 Daniel Azuma # # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # * Neither the name of the copyright holder, nor the names of any other # contributors to this software, may be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # ----------------------------------------------------------------------------- ; module Blockenspiel # === Dynamically construct a target # # These methods are available in a block passed to Blockenspiel#invoke and # can be used to dynamically define what methods are available from a block. # See Blockenspiel#invoke for more information. class Builder include ::Blockenspiel::DSL # This is a base class for dynamically constructed targets. # The actual target class is an anonymous subclass of this base class. class Target # :nodoc: include ::Blockenspiel::DSL # Add a method specification to the subclass. def self._add_methodinfo(name_, block_, yields_) (@_blockenspiel_methodinfo ||= {})[name_] = [block_, yields_] module_eval("def #{name_}(*params_, &block_); self.class._invoke_methodinfo(:#{name_}, params_, block_); end\n") end # Attempt to invoke the given method on the subclass. def self._invoke_methodinfo(name_, params_, block_) info_ = @_blockenspiel_methodinfo[name_] case info_[1] when :first params_.unshift(block_) when :last params_.push(block_) end info_[0].call(*params_, &block_) end end # Sets up the dynamic target class. def initialize # :nodoc: @target_class = ::Class.new(::Blockenspiel::Builder::Target) @target_class.dsl_methods(false) end # Creates a new instance of the dynamic target class def _create_target # :nodoc: @target_class.new end # === Declare a DSL method. # # This call creates a method that can be called from the DSL block. # Provide a name for the method, a block defining the method's # implementation, and an optional hash of options. # # By default, a method of the same name is also made available to # parameterless blocks. To change the name of the parameterless method, # provide its name as the value of the :dsl_method option. # To disable this method for parameterless blocks, set the # :dsl_method option to +false+. # # The :mixin option is a deprecated alias for # :dsl_method. # # === Warning about the +return+ keyword # # Because you are implementing your method using a block, remember the # distinction between Proc.new and +lambda+. Invoking +return+ # from the former does not return from the block, but returns from the # surrounding method scope. Since normal blocks passed to methods are # of the former type, be very careful about using the +return+ keyword: # # add_method(:foo) do |param| # puts "foo called with parameter "+param.inspect # return "a return value" # DOESN'T WORK LIKE YOU EXPECT! # end # # To return a value from the method you are creating, set the evaluation # value at the end of the block: # # add_method(:foo) do |param| # puts "foo called with parameter "+param.inspect # "a return value" # Returns from method foo # end # # If you must use the +return+ keyword, create your block as a lambda # as in this example: # # code = lambda do |param| # puts "foo called with parameter "+param.inspect # return "a return value" # Returns from method foo # end # add_method(:foo, &code) # # === Accepting a block argument # # If you want your method to take a block, you have several options # depending on your Ruby version. If you are running the standard Matz # Ruby interpreter (MRI) version 1.8.7 or later (including 1.9.x), or a # compatible interpreter such as JRuby 1.5 or later, you can use the # standard "&" block argument notation to receive the block. # Note that you must call the passed block using the +call+ method since # Ruby doesn't support invoking such a block with +yield+. # For example, to create a method named "foo" that takes one parameter # and a block, do this: # # add_method(:foo) do |param, &block| # puts "foo called with parameter "+param.inspect # puts "the block returned "+block.call.inspect # end # # In your DSL, you can then call: # # foo("hello"){ "a value" } # # If you are using MRI 1.8.6, or another Ruby interpreter that doesn't # fully support this syntax (such as JRuby versions older than 1.5), # Blockenspiel provides an alternative in the form of the :block # option. This option causes blocks provided by the caller to be included # in the normal parameter list to your method, instead of as a block # parameter. It can be set to :first or :last to # prepend or append, respectively, the block (as a +Proc+ object) to # the parameter list. If the caller does not include a block when # calling your DSL method, nil is prepended/appended. For example: # # add_method(:foo, :block => :last) do |param, block| # puts "foo called with parameter "+param.inspect # if block # puts "the block returned "+block.call.inspect # else # puts "no block passed" # end # end # # The :receive_block option is a deprecated alternative. # Setting :receive_block => true is currently equivalent to # setting :block => :last. def add_method(name_, opts_={}, &block_) receive_block_ = opts_[:receive_block] ? :last : opts_[:block] receive_block_ = :first if receive_block_ && receive_block_ != :last @target_class._add_methodinfo(name_, block_, receive_block_) dsl_method_name_ = opts_[:dsl_method] || opts_[:mixin] if dsl_method_name_ != false dsl_method_name_ = name_ if dsl_method_name_.nil? || dsl_method_name_ == true @target_class.dsl_method(dsl_method_name_, name_) end end end end blockenspiel-0.5.0/lib/blockenspiel_unmixer_jruby.jar0000644000004100000410000000337412652620702023127 0ustar www-datawww-dataPKг&H META-INF/þÊPKPKг&HMETA-INF/MANIFEST.MFóMÌËLK-.Ñ K-*ÎÌϳR0Ô3àår.JM,IMÑuª ˜ëÄ›š*hø%&ç¤*8çä%–•kòrñrPKWÔ+CDPKг&H%BlockenspielUnmixerJrubyService.classµU[sÛDþ6¾Èq•¤“4j)¥Ð‚o­¡7hÒKn½8u§ I€"K[G‰"¥’œ)Ãpy¢ðø”f /na¦ 3}à'q?+)±ã$C_͜ݳûíw.{öè·¿üÀØI(J`8‰‹¸”Àe1^I¢#F“À˜„ñ}˜ÀU¡\âº7溄b&…¸)D)‰ÜH଄) å$ú1ÄÛ¸%aFŒ•ÎK˜eˆ_4,ûÌIgæ¢ã¶ÎzJ†ÅËõµ*wfÕªI+UÕ5´’­ê ©tÉvj…§^ý°0Cb8³È¼z_ãëža[®„Û ±ºµfÜgxÜŠvê–g¬ñBµn˜žaŠâøtu…kÞðÿ3mmu8óœ ]OÕV§Ôu?”ʤ„9 óÞaPf‚Ós†kÐþ¨eÙžêÇÏðB‹•6 “‚yŠ{˶NÌQK]£¤&~¯n8\Pt0ô:|ÍÞàeîz\Ÿ²õºÈü©öl›ªë·-èaq‡].÷*õuîø@†B;AˆÝ›áam¨¦¡«Wµe>Á][ºjyD§ìA'&+vÝÑø5C8~ÜÏ7·Üuƒ›·E1pgRœªpgÃÐø©uC•q/2È­X 2±Ä …Çd¼‹÷d¼; GÿƒVÆPúvsRBU††;2t!¸wQ#Ò&ºhif]ßÌÿ¼£®¯ N㌌³8'ã<Þ¤"œ/˜ªU+„iNírK2–aHX‘± “aóPPd2Ö`1dwV¤I/­0æ¿9£ê¨ÎftdÇ'1ìBqzëÅQMê²]©kËA­µ ^j"ЦÉkª9ªiÜu[ ¹&ÄáwMr²±ak~UϪN{-èîí3 ýigÜáT7›Õ›M—š¤Ï1¬Ú¶ØR@D ó»Ôxý¶¥s‡ad‚= pOZò«BçLîÙVx[ÓíðàY1 N„Ï™êÀO$ékõ$@‹š?ñ\í„Jœ¼ .5t@¨3\5Cµ«¶ýѦH/ÛÖf1†«ƒ;¼Þä×LÕÙty÷Ü-í ¢´óÚ·:Õà^{AŸ JÈðÑô¢ÈGœš‡½Jú…VëaS]Ú¹”Ù¹„cô S@m”úbœFê$’v„FFc,ûìM¨°C0Fò^F‡½‚¢4Îõv4™ÊõFó ÄÊ'ˆ§ 5ˆ~»EÐëC¯“Ýôç,’õI¼B+QÚ?Ž NàU¢´¿½D£žŠÎ¡hî$†bJ,˜5°ïk•XòP\‰?Ãy%Þ@—}ˆ¼m ûúýñô|…¤W¢O°_‰ ”ü”Øô*ñ¡Ø7ÿ|—û^ü|ÿNC&9CVÐYÊÌù7Oë ¸JݱDÍqw°•d•> ¯Ñ‰~DÿB·„´„ÌŸ¸D²ûw"¡%( ,rÈã$i) ô+,¹îëE¼A[ÔöÂt~Fc¥,9ê;?!fOÑÓ@*G³‡8)Úy†ÁÜfœÝ*4Š5+æA°¶â@Œä ]Ÿ…s¸‡¸¸‰ºC`$Ùæt^tãз[„÷—I¡³·/úú") T88•§dÒ’²ÉUÊù,]ÿ¡ùfå†?¢ø?Æ >!>Eš‚m7F]?46J«"G„±Ã2FfJ9a&—%UÙn ¨¬ÏÑ…4ÿ‚nîË6r†·ü<\øPK—,Ǭ PKг&H META-INF/þÊPKг&HWÔ+CD=META-INF/MANIFEST.MFPKг&H—,Ǭ %ÂBlockenspielUnmixerJrubyService.classPKÐblockenspiel-0.5.0/lib/blockenspiel.rb0000644000004100000410000000455012652620702017771 0ustar www-datawww-data# ----------------------------------------------------------------------------- # # Blockenspiel entry point # # ----------------------------------------------------------------------------- # Copyright 2008 Daniel Azuma # # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # * Neither the name of the copyright holder, nor the names of any other # contributors to this software, may be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # ----------------------------------------------------------------------------- ; # == Blockenspiel # # The Blockenspiel module provides a namespace for Blockenspiel, as well as # the main entry point method "invoke". module Blockenspiel end case ::RUBY_DESCRIPTION when /^ruby\s/ require 'blockenspiel/unmixer_unimplemented' when /^jruby\s/ require 'blockenspiel_unmixer_jruby' when /^rubinius\s/ require 'blockenspiel/unmixer_rubinius' else require 'blockenspiel/unmixer_unimplemented' end require 'blockenspiel/errors' require 'blockenspiel/dsl_setup' require 'blockenspiel/builder' require 'blockenspiel/impl' require 'blockenspiel/version' blockenspiel-0.5.0/metadata.yml0000644000004100000410000000411612652620702016525 0ustar www-datawww-data--- !ruby/object:Gem::Specification name: blockenspiel version: !ruby/object:Gem::Version version: 0.5.0 platform: ruby authors: - Daniel Azuma autorequire: bindir: bin cert_chain: [] date: 2016-01-07 00:00:00.000000000 Z dependencies: [] description: Blockenspiel is a helper library designed to make it easy to implement DSL blocks. It is designed to be comprehensive and robust, supporting most common usage patterns, and working correctly in the presence of nested blocks and multithreading. email: dazuma@gmail.com executables: [] extensions: [] extra_rdoc_files: - Blockenspiel.rdoc - History.rdoc - ImplementingDSLblocks.rdoc - README.rdoc files: - Blockenspiel.rdoc - History.rdoc - ImplementingDSLblocks.rdoc - README.rdoc - Version - lib/blockenspiel.rb - lib/blockenspiel/builder.rb - lib/blockenspiel/dsl_setup.rb - lib/blockenspiel/errors.rb - lib/blockenspiel/impl.rb - lib/blockenspiel/unmixer_rubinius.rb - lib/blockenspiel/unmixer_unimplemented.rb - lib/blockenspiel/version.rb - lib/blockenspiel/versionomy.rb - lib/blockenspiel_unmixer_jruby.jar - test/files/file1.rb - test/tc_basic.rb - test/tc_behaviors.rb - test/tc_dsl_attrs.rb - test/tc_dsl_methods.rb - test/tc_dynamic.rb - test/tc_embedded_block.rb - test/tc_mixins.rb - test/tc_modules.rb - test/tc_version.rb homepage: http://dazuma.github.com/blockenspiel licenses: - BSD-3-Clause metadata: {} post_install_message: rdoc_options: [] require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: 1.9.3 required_rubygems_version: !ruby/object:Gem::Requirement requirements: - - ">" - !ruby/object:Gem::Version version: 1.3.1 requirements: [] rubyforge_project: virtuoso rubygems_version: 2.5.1 signing_key: specification_version: 4 summary: Blockenspiel is a helper library designed to make it easy to implement DSL blocks. test_files: - test/tc_basic.rb - test/tc_behaviors.rb - test/tc_dsl_attrs.rb - test/tc_dsl_methods.rb - test/tc_dynamic.rb - test/tc_embedded_block.rb - test/tc_mixins.rb - test/tc_modules.rb - test/tc_version.rb blockenspiel-0.5.0/test/0000755000004100000410000000000012652620702015177 5ustar www-datawww-datablockenspiel-0.5.0/test/tc_dsl_attrs.rb0000644000004100000410000001063212652620702020213 0ustar www-datawww-data# ----------------------------------------------------------------------------- # # Blockenspiel dsl attribute tests # # This file contains tests for the dsl attribute directives. # # ----------------------------------------------------------------------------- # Copyright 2008-2011 Daniel Azuma # # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # * Neither the name of the copyright holder, nor the names of any other # contributors to this software, may be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # ----------------------------------------------------------------------------- ; require 'minitest/autorun' require 'blockenspiel' module Blockenspiel module Tests # :nodoc: class TestDSLAttrs < ::Minitest::Test # :nodoc: class WriterTarget < ::Blockenspiel::Base dsl_attr_writer(:attr1, :attr2) end class AccessorTarget < ::Blockenspiel::Base dsl_attr_accessor(:attr1, :attr2) end # Test dsl attr writer in a parametered block # # * Asserts that the standard setter syntax works # * Asserts that the alternate setter syntax works def test_writer_parametered block_ = ::Proc.new do |param_| param_.attr1 = 1 assert_equal(2, param_.attr2(2)) end target_ = WriterTarget.new ::Blockenspiel.invoke(block_, target_) assert_equal(1, target_.instance_variable_get(:@attr1)) assert_equal(2, target_.instance_variable_get(:@attr2)) end # Test dsl attr writer in a parameterless block # # * Asserts that the alternate setter syntax works def test_writer_parameterless block_ = ::Proc.new do assert_equal(2, attr2(2)) end target_ = WriterTarget.new ::Blockenspiel.invoke(block_, target_) assert_equal(false, target_.instance_variable_defined?(:@attr1)) assert_equal(2, target_.instance_variable_get(:@attr2)) end # Test dsl attr accessor in a parametered block # # * Asserts that the standard setter syntax works # * Asserts that the alternate setter syntax works # * Asserts that the getter syntax works def _test_accessor_parametered block_ = ::Proc.new do |param_| param_.attr1 = 1 assert_equal(2, param_.attr2(2)) assert_equal(2, param_.attr2) end target_ = AccessorTarget.new ::Blockenspiel.invoke(block_, target_) assert_equal(1, target_.instance_variable_get(:@attr1)) assert_equal(2, target_.instance_variable_get(:@attr2)) end # Test dsl attr accessor in a parameterless block # # * Asserts that the alternate setter syntax works # * Asserts that the getter syntax works def test_accessor_parameterless block_ = ::Proc.new do assert_equal(2, attr2(2)) assert_equal(2, attr2) end target_ = AccessorTarget.new ::Blockenspiel.invoke(block_, target_) assert_equal(false, target_.instance_variable_defined?(:@attr1)) assert_equal(2, target_.instance_variable_get(:@attr2)) end end end end blockenspiel-0.5.0/test/tc_dsl_methods.rb0000644000004100000410000002120712652620702020521 0ustar www-datawww-data# ----------------------------------------------------------------------------- # # Blockenspiel dsl method tests # # This file contains tests for the dsl method directives. # # ----------------------------------------------------------------------------- # Copyright 2008-2011 Daniel Azuma # # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # * Neither the name of the copyright holder, nor the names of any other # contributors to this software, may be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # ----------------------------------------------------------------------------- ; require 'minitest/autorun' require 'blockenspiel' module Blockenspiel module Tests # :nodoc: class TestDSLMethods < ::Minitest::Test # :nodoc: class Target1 < ::Blockenspiel::Base def initialize(hash_) @hash = hash_ end def set_value1(key_, value_) @hash["#{key_}1"] = value_ end def set_value2(key_) @hash["#{key_}2"] = yield end def _set_value3(key_, value_) @hash["#{key_}3"] = value_ end end class Target2 < ::Blockenspiel::Base def initialize(hash_) @hash = hash_ end dsl_methods false def set_value1(key_, value_) @hash["#{key_}1"] = value_ end dsl_methods true def set_value2(key_) @hash["#{key_}2"] = yield end def _set_value3(key_, value_) @hash["#{key_}3"] = value_ end end class Target3 < ::Blockenspiel::Base def initialize(hash_) @hash = hash_ end dsl_methods false def set_value1(key_, value_) @hash["#{key_}1"] = value_ end dsl_method :set_value1 def set_value2(key_) @hash["#{key_}2"] = yield end dsl_method :renamed_set_value2, :set_value2 dsl_method :another_set_value2, :set_value2 end class Target4 < ::Blockenspiel::Base def initialize(hash_) @hash = hash_ end def set_value1(key_, value_) @hash["#{key_}1"] = value_ end dsl_method :set_value1, false def set_value2(key_) @hash["#{key_}2"] = yield end dsl_method :set_value2, false dsl_method :renamed_set_value2, :set_value2 end class Target5a < ::Blockenspiel::Base def initialize(hash_) @hash = hash_ end def set_value1(key_, value_) @hash["#{key_}1"] = value_ end def set_value2(key_) @hash["#{key_}2"] = yield end def set_value3(key_, value_) @hash["#{key_}3"] = value_ end def set_value4(key_, value_) @hash["#{key_}4"] = value_ end dsl_method :set_value4, false dsl_method :renamed_set_value4, :set_value4 end class Target5b < Target5a def set_value1(key_, value_) @hash["#{key_}1sub"] = value_ end dsl_method :set_value3, false def set_value5(key_, value_) @hash["#{key_}5"] = value_ end end class Target6 < ::Blockenspiel::Base def initialize(hash_) @hash = hash_ end dsl_methods false def set_value1(key_, value_) @hash["#{key_}1"] = value_ end def set_value2(key_) @hash["#{key_}2"] = yield end dsl_methods :set_value1, :renamed_set_value2 => :set_value2 end # Test default dsl method setting. # # * Asserts the right dsl methods are added for the default setting. def test_default_setting hash_ = ::Hash.new block_ = ::Proc.new do set_value1('a', 1) set_value2('b'){ 2 } assert_raises(::NoMethodError){ _set_value3('c', 3) } end ::Blockenspiel.invoke(block_, Target1.new(hash_)) assert_equal(1, hash_['a1']) assert_equal(2, hash_['b2']) assert_nil(hash_['c3']) end # Test dsl_methods true and false switching. # # * Asserts that dsl_methods false turns off automatic method creation. # * Asserts that dsl_methods true turns on automatic method creation. # * Asserts that underscore methods are added in dsl_methods true mode. def test_onoff_switching hash_ = ::Hash.new block_ = ::Proc.new do assert_raises(::NoMethodError){ _set_value1('a', 1) } set_value2('b'){ 2 } _set_value3('c', 3) end ::Blockenspiel.invoke(block_, Target2.new(hash_)) assert_nil(hash_['a1']) assert_equal(2, hash_['b2']) assert_equal(3, hash_['c3']) end # Test dsl_methods explicit adding. # # * Asserts that adding an explicit dsl method works. # * Asserts that adding an explicit dsl method with a different name works. def test_explicit_add hash_ = ::Hash.new block_ = ::Proc.new do set_value1('a', 1) assert_raises(::NoMethodError){ set_value2('b'){ 2 } } renamed_set_value2('c'){ 3 } another_set_value2('d'){ 4 } end ::Blockenspiel.invoke(block_, Target3.new(hash_)) assert_equal(1, hash_['a1']) assert_nil(hash_['b2']) assert_equal(3, hash_['c2']) assert_equal(4, hash_['d2']) end # Test dsl_methods explicit removing. # # * Asserts that removing a dsl method works. # * Asserts that re-adding a removed method with a different name works. def test_explicit_removing hash_ = ::Hash.new block_ = ::Proc.new do assert_raises(::NoMethodError){ set_value1('a', 1) } assert_raises(::NoMethodError){ set_value2('b'){ 2 } } renamed_set_value2('c'){ 3 } end ::Blockenspiel.invoke(block_, Target4.new(hash_)) assert_nil(hash_['a1']) assert_nil(hash_['b2']) assert_equal(3, hash_['c2']) end # Test dsl method setting with subclasses # # * Asserts that modules are properly inherited. # * Asserts that method overriding is done correctly. def test_subclassing hash_ = ::Hash.new block_ = ::Proc.new do set_value1('a', 1) set_value2('b'){ 2 } assert_raises(::NoMethodError){ set_value3('c', 3) } assert_raises(::NoMethodError){ set_value4('d', 4) } renamed_set_value4('e', 5) set_value5('f', 6) end ::Blockenspiel.invoke(block_, Target5b.new(hash_)) assert_equal(1, hash_['a1sub']) assert_equal(2, hash_['b2']) assert_nil(hash_['c3']) assert_nil(hash_['d4']) assert_equal(5, hash_['e4']) assert_equal(6, hash_['f5']) end # Test dsl_methods with multiple parameters. # # * Asserts that hash syntax for dsl_methods works. # * Asserts that combined array and hash parameters works. def test_multiple_dsl_methods hash_ = ::Hash.new block_ = ::Proc.new do set_value1('a', 1) renamed_set_value2('b'){ 2 } assert_raises(::NoMethodError){ set_value2('c', 3) } end ::Blockenspiel.invoke(block_, Target6.new(hash_)) assert_equal(1, hash_['a1']) assert_equal(2, hash_['b2']) end end end end blockenspiel-0.5.0/test/tc_behaviors.rb0000644000004100000410000001537112652620702020203 0ustar www-datawww-data# ----------------------------------------------------------------------------- # # Blockenspiel behavior tests # # This file contains tests for behavior settings. # # ----------------------------------------------------------------------------- # Copyright 2008-2011 Daniel Azuma # # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # * Neither the name of the copyright holder, nor the names of any other # contributors to this software, may be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # ----------------------------------------------------------------------------- ; require 'minitest/autorun' require 'blockenspiel' module Blockenspiel module Tests # :nodoc: class TestBehaviors < ::Minitest::Test # :nodoc: class Target1 < ::Blockenspiel::Base dsl_methods false def initialize(hash_) @hash = hash_ end def set_value1(key_, value_) @hash["#{key_}1"] = value_ end dsl_method :set_value1 def set_value2(key_) @hash["#{key_}2"] = yield end dsl_method :set_value2 def set_value3(key_, value_) @hash["#{key_}3"] = value_ end dsl_method :set_value3_dslversion, :set_value3 end def helper_method true end # Test instance_eval behavior. # # * Asserts that self points at the target. # * Asserts that the target methods are available. # * Asserts that the target methods are not renamed by dsl_method directives. # * Asserts that the caller's instance variables are not available. # * Asserts that the caller's helper methods are not available. def test_instance_eval_behavior hash_ = ::Hash.new context_self_ = self @my_instance_variable_test = :hello block_ = ::Proc.new do set_value1('a', 1) set_value2('b'){ 2 } set_value3('c', 3) context_self_.assert_raises(::NoMethodError){ set_value3_dslversion('d', 4) } context_self_.assert_raises(::NoMethodError){ helper_method() } context_self_.assert(!instance_variable_defined?(:@my_instance_variable_test)) context_self_.assert_instance_of(::Blockenspiel::Tests::TestBehaviors::Target1, self) end ::Blockenspiel.invoke(block_, Target1.new(hash_), :parameterless => :instance) assert_equal(1, hash_['a1']) assert_equal(2, hash_['b2']) assert_equal(3, hash_['c3']) end # Test proxy behavior. # # * Asserts that self doesn't point at the Target nor the original context. # * Asserts that the target methods are available in their dsl renamed forms. # * Asserts that the caller's instance variables are not available. # * Asserts that the caller's helper methods *are* available. def test_proxy_behavior hash_ = ::Hash.new context_self_ = self @my_instance_variable_test = :hello block_ = ::Proc.new do set_value1('a', 1) set_value2('b'){ 2 } set_value3_dslversion('c', 3) context_self_.assert_raises(::NoMethodError){ set_value3('d', 4) } context_self_.assert(helper_method()) context_self_.assert(!instance_variable_defined?(:@my_instance_variable_test)) context_self_.assert(!self.kind_of?(::Blockenspiel::Tests::TestBehaviors::Target1)) context_self_.refute_equal(context_self_, self) end ::Blockenspiel.invoke(block_, Target1.new(hash_), :parameterless => :proxy) assert_equal(1, hash_['a1']) assert_equal(2, hash_['b2']) assert_equal(3, hash_['c3']) end # Test parameterless blocks disabled # # * Asserts that an error is raised if sending a no-parameter block in this case. # * Asserts that sending a one-parameter block still works. def test_disable_parameterless hash_ = ::Hash.new block1_ = ::Proc.new do || set_value1('a', 1) end block2_ = ::Proc.new do |target_| target_.set_value1('b', 2) end block3_ = ::Proc.new do set_value1('c', 3) end assert_raises(::Blockenspiel::BlockParameterError) do ::Blockenspiel.invoke(block1_, Target1.new(hash_), :parameterless => false) end ::Blockenspiel.invoke(block2_, Target1.new(hash_), :parameterless => false) assert_raises(::Blockenspiel::BlockParameterError) do ::Blockenspiel.invoke(block3_, Target1.new(hash_), :parameterless => false) end assert_equal(2, hash_['b1']) end # Test parametered blocks disabled # # * Asserts that an error is raised if sending a one-parameter block in this case. # * Asserts that sending a no-parameter block still works. def test_disable_parametered hash_ = ::Hash.new block1_ = ::Proc.new do || set_value1('a', 1) end block2_ = ::Proc.new do |target_| target_.set_value1('b', 2) end block3_ = ::Proc.new do set_value1('c', 3) end ::Blockenspiel.invoke(block1_, Target1.new(hash_), :parameter => false) assert_raises(::Blockenspiel::BlockParameterError) do ::Blockenspiel.invoke(block2_, Target1.new(hash_), :parameter => false) end ::Blockenspiel.invoke(block3_, Target1.new(hash_), :parameter => false) assert_equal(1, hash_['a1']) assert_equal(3, hash_['c1']) end end end end blockenspiel-0.5.0/test/tc_modules.rb0000644000004100000410000001173512652620702017671 0ustar www-datawww-data# ----------------------------------------------------------------------------- # # Blockenspiel module tests # # This file contains tests for DSL module inclusion. # # ----------------------------------------------------------------------------- # Copyright 2008-2011 Daniel Azuma # # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # * Neither the name of the copyright holder, nor the names of any other # contributors to this software, may be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # ----------------------------------------------------------------------------- ; require 'minitest/autorun' require 'blockenspiel' module Blockenspiel module Tests # :nodoc: class TestModules < ::Minitest::Test # :nodoc: class Target1 include ::Blockenspiel::DSL def initialize @hash = ::Hash.new end def set_value(key_, value_) @hash[key_] = value_ end def _helper_method(key_, value_) @hash[key_] = value_ end def get_value(key_) @hash[key_] end dsl_method :get_value, false def get_value2(key_) @hash[key_] end dsl_method :get_value2, false end module Module2 def set_value(key_, value_) @hash[key_] = value_ end def _get_value(key_) @hash[key_] end end class Target2a include ::Blockenspiel::DSL def initialize @hash = ::Hash.new end include Module2 end class Target2b include ::Blockenspiel::DSL def initialize @hash = ::Hash.new end end class Target2c < Target2b include Module2 end # Helper method def get_value(key_) return :helper end # Test simple usage. # # * Asserts that methods are mixed in to self. # * Asserts that methods are removed from self afterward. # * Asserts that the specified target object still receives the messages. def test_simple_target block_ = ::Proc.new do set_value(:a, 1) end target_ = Target1.new ::Blockenspiel.invoke(block_, target_) assert(!self.respond_to?(:set_value)) assert_equal(1, target_.get_value(:a)) end # Test omissions. # # * Asserts that underscore methods are not mixed in. # * Asserts that methods that are turned off after the fact cannot be called. def test_omissions block_ = ::Proc.new do set_value(:a, 1) assert(!self.respond_to?(:_helper_method)) assert_equal(:helper, get_value(:a)) assert_raises(::NoMethodError) do get_value2(:a) end end target_ = Target1.new ::Blockenspiel.invoke(block_, target_) end # Test module inclusion. # # * Asserts that methods from an included module are handled. def test_simple_module_inclusion block_ = ::Proc.new do set_value(:a, 1) assert(!self.respond_to?(:_get_value)) end target_ = Target2a.new ::Blockenspiel.invoke(block_, target_) assert_equal(1, target_._get_value(:a)) end # Test module inclusion from a subclass # # * Asserts that a module can be included from a DSL subclass. def test_simple_module_inclusion_from_subclass block_ = ::Proc.new do set_value(:a, 1) assert(!self.respond_to?(:_get_value)) end target_ = Target2c.new ::Blockenspiel.invoke(block_, target_) assert_equal(1, target_._get_value(:a)) end end end end blockenspiel-0.5.0/test/files/0000755000004100000410000000000012652620702016301 5ustar www-datawww-datablockenspiel-0.5.0/test/files/file1.rb0000644000004100000410000000005512652620702017626 0ustar www-datawww-dataset_value(:a, 1) set_value_by_block(:b){ 2 } blockenspiel-0.5.0/test/tc_mixins.rb0000644000004100000410000003727412652620702017536 0ustar www-datawww-data# ----------------------------------------------------------------------------- # # Blockenspiel mixin tests # # This file contains tests for various mixin cases, # including nested blocks and multithreading. # # ----------------------------------------------------------------------------- # Copyright 2008-2011 Daniel Azuma # # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # * Neither the name of the copyright holder, nor the names of any other # contributors to this software, may be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # ----------------------------------------------------------------------------- ; require 'minitest/autorun' require 'blockenspiel' module Blockenspiel module Tests # :nodoc: class TestMixins < ::Minitest::Test # :nodoc: class Target1 < ::Blockenspiel::Base def initialize(hash_) @hash = hash_ end def set_value(key_, value_) @hash["#{key_}1"] = value_ end def set_value2(key_) @hash["#{key_}1"] = yield end end class Target2 < ::Blockenspiel::Base dsl_methods false def initialize(hash_=nil) @hash = hash_ || ::Hash.new end def set_value(key_, value_) @hash["#{key_}2"] = value_ end dsl_method :set_value def set_value2(key_) @hash["#{key_}2"] = yield end dsl_method :set_value2_inmixin, :set_value2 end class Target3 < ::Blockenspiel::Base def initialize(hash_) @hash = hash_ end def set_value(key_, value_) @hash[key_] = value_ end def _helper_method(key_) @hash[key_] end def get_value(key_) @hash[key_] end dsl_method :get_value, false def get_value2(key_) @hash[key_] end dsl_method :get_value2, false end def get_value(key_) :helper end # Basic test of mixin mechanism. # # * Asserts that the mixin methods are added and removed for a single mixin. # * Asserts that the methods properly delegate to the target object. # * Asserts that self doesn't change, and instance variables are preserved. def test_basic_mixin skip unless ::Blockenspiel.mixin_available? hash_ = ::Hash.new saved_object_id_ = self.object_id @my_instance_variable_test = :hello assert(!self.respond_to?(:set_value)) assert(!self.respond_to?(:set_value2)) block_ = ::Proc.new do set_value('a', 1) set_value2('b'){ 2 } assert_equal(:hello, @my_instance_variable_test) assert_equal(saved_object_id_, self.object_id) end ::Blockenspiel.invoke(block_, Target1.new(hash_), :parameterless => :mixin) assert(!self.respond_to?(:set_value)) assert(!self.respond_to?(:set_value2)) assert_equal(1, hash_['a1']) assert_equal(2, hash_['b1']) end # Test renaming of mixin methods. # # * Asserts that correctly renamed mixin methods are added and removed. # * Asserts that the methods properly delegate to the target object. def test_mixin_with_renaming skip unless ::Blockenspiel.mixin_available? hash_ = ::Hash.new assert(!self.respond_to?(:set_value)) assert(!self.respond_to?(:set_value2)) assert(!self.respond_to?(:set_value2_inmixin)) block_ = ::Proc.new do set_value('a', 1) set_value2_inmixin('b'){ 2 } assert(!self.respond_to?(:set_value2)) end ::Blockenspiel.invoke(block_, Target2.new(hash_), :parameterless => :mixin) assert(!self.respond_to?(:set_value)) assert(!self.respond_to?(:set_value2)) assert(!self.respond_to?(:set_value2_inmixin)) assert_equal(1, hash_['a2']) assert_equal(2, hash_['b2']) end # Test of two different nested mixins. # # * Asserts that the right methods are added and removed at the right time. # * Asserts that the methods delegate to the right target object, even when # multiple mixins add the same method name def test_nested_different skip unless ::Blockenspiel.mixin_available? hash_ = ::Hash.new assert(!self.respond_to?(:set_value)) assert(!self.respond_to?(:set_value2)) assert(!self.respond_to?(:set_value2_inmixin)) ::Blockenspiel.invoke(::Proc.new do set_value('a', 1) set_value2('b'){ 2 } assert(!self.respond_to?(:set_value2_inmixin)) ::Blockenspiel.invoke(::Proc.new do set_value('c', 1) set_value2_inmixin('d'){ 2 } end, Target2.new(hash_), :parameterless => :mixin) assert(!self.respond_to?(:set_value2_inmixin)) set_value('e', 1) set_value2('f'){ 2 } end, Target1.new(hash_), :parameterless => :mixin) assert(!self.respond_to?(:set_value)) assert(!self.respond_to?(:set_value2)) assert(!self.respond_to?(:set_value2_inmixin)) assert_equal(1, hash_['a1']) assert_equal(2, hash_['b1']) assert_equal(1, hash_['c2']) assert_equal(2, hash_['d2']) assert_equal(1, hash_['e1']) assert_equal(2, hash_['f1']) end # Test of the same mixin nested in itself. # # * Asserts that the methods are added and removed at the right time. def test_nested_same skip unless ::Blockenspiel.mixin_available? hash_ = ::Hash.new assert(!self.respond_to?(:set_value)) assert(!self.respond_to?(:set_value2)) assert(!self.respond_to?(:set_value2_inmixin)) ::Blockenspiel.invoke(::Proc.new do set_value('a', 1) set_value2_inmixin('b'){ 2 } ::Blockenspiel.invoke(::Proc.new do set_value('c', 1) set_value2_inmixin('d'){ 2 } assert(!self.respond_to?(:set_value2)) end, Target2.new(hash_), :parameterless => :mixin) set_value('e', 1) set_value2_inmixin('f'){ 2 } end, Target2.new(hash_), :parameterless => :mixin) assert(!self.respond_to?(:set_value)) assert(!self.respond_to?(:set_value2)) assert(!self.respond_to?(:set_value2_inmixin)) assert_equal(1, hash_['a2']) assert_equal(2, hash_['b2']) assert_equal(1, hash_['c2']) assert_equal(2, hash_['d2']) assert_equal(1, hash_['e2']) assert_equal(2, hash_['f2']) end # Test of two threads mixing the same mixin into the same object # # * Asserts that the mixin is removed only after the second thread is done. def test_threads_same_mixin skip unless ::Blockenspiel.mixin_available? hash_ = ::Hash.new block1_ = ::Proc.new do set_value('a', 1) sleep(0.1) set_value2('b'){ 2 } end block2_ = ::Proc.new do set_value('c', 3) sleep(0.2) set_value2('d'){ 4 } end target_ = Target1.new(hash_) thread1_ = ::Thread.new do ::Blockenspiel.invoke(block1_, target_, :parameterless => :mixin) end thread2_ = ::Thread.new do ::Blockenspiel.invoke(block2_, target_, :parameterless => :mixin) end thread1_.join thread2_.join assert_equal(1, hash_['a1']) assert_equal(2, hash_['b1']) assert_equal(3, hash_['c1']) assert_equal(4, hash_['d1']) end def test_two_threads_different_mixin skip unless ::Blockenspiel.mixin_available? hash_ = {} target1_ = Target1.new(hash_) target2_ = Target2.new(hash_) assert(!self.respond_to?(:set_value)) assert(!self.respond_to?(:set_value2)) assert(!self.respond_to?(:set_value2_inmixin)) t1_ = ::Thread.new do ::Blockenspiel.invoke(::Proc.new do sleep(0.1) set_value('a', 1) sleep(0.1) set_value('e', 5) end, target1_, :parameterless => :mixin) end t2_ = ::Thread.new do ::Blockenspiel.invoke(::Proc.new do sleep(0.1) set_value('A', 11) sleep(0.1) set_value('E', 15) end, target2_, :parameterless => :mixin) end t1_.join t2_.join assert(!self.respond_to?(:set_value)) assert(!self.respond_to?(:set_value2)) assert(!self.respond_to?(:set_value2_inmixin)) assert_equal(1, hash_['a1']) assert_equal(5, hash_['e1']) assert_equal(11, hash_['A2']) assert_equal(15, hash_['E2']) end # A full thread test with the same set of nested mixins done into the same # object twice in two different threads. # # * Asserts that the right methods are added and removed at the right time. # * Asserts that the methods delegate to the right target object, even when # multiple mixins add the same method name, multiple times from different # threads. def test_nested_two_threads skip unless ::Blockenspiel.mixin_available? hash_ = {} target1_ = Target1.new(hash_) target2_ = Target2.new(hash_) assert(!self.respond_to?(:set_value)) assert(!self.respond_to?(:set_value2)) assert(!self.respond_to?(:set_value2_inmixin)) t1_ = ::Thread.new do ::Blockenspiel.invoke(::Proc.new do sleep(0.1) set_value('a', 1) set_value2('b'){ 2 } ::Blockenspiel.invoke(::Proc.new do sleep(0.1) set_value('c', 3) set_value2_inmixin('d'){ 4 } end, target2_, :parameterless => :mixin) sleep(0.1) set_value('e', 5) set_value2('f'){ 6 } end, target1_, :parameterless => :mixin) end t2_ = ::Thread.new do ::Blockenspiel.invoke(::Proc.new do sleep(0.1) set_value('A', 11) set_value2_inmixin('B'){ 12 } ::Blockenspiel.invoke(::Proc.new do sleep(0.1) set_value('C', 13) set_value2('D'){ 14 } end, target1_, :parameterless => :mixin) sleep(0.1) set_value('E', 15) set_value2_inmixin('F'){ 16 } end, target2_, :parameterless => :mixin) end t1_.join t2_.join assert(!self.respond_to?(:set_value)) assert(!self.respond_to?(:set_value2)) assert(!self.respond_to?(:set_value2_inmixin)) assert_equal(1, hash_['a1']) assert_equal(2, hash_['b1']) assert_equal(3, hash_['c2']) assert_equal(4, hash_['d2']) assert_equal(5, hash_['e1']) assert_equal(6, hash_['f1']) assert_equal(11, hash_['A2']) assert_equal(12, hash_['B2']) assert_equal(13, hash_['C1']) assert_equal(14, hash_['D1']) assert_equal(15, hash_['E2']) assert_equal(16, hash_['F2']) end # A full fiber test with the same set of nested mixins done into the same # object twice in two different fibers. # # * Asserts that the right methods are added and removed at the right time. # * Asserts that the methods delegate to the right target object, even when # multiple mixins add the same method name, multiple times from different # fibers. if defined?(::Fiber) def test_nested_two_fibers skip unless ::Blockenspiel.mixin_available? hash_ = {} target1_ = Target1.new(hash_) target2_ = Target2.new(hash_) assert(!self.respond_to?(:set_value)) assert(!self.respond_to?(:set_value2)) assert(!self.respond_to?(:set_value2_inmixin)) f1_ = ::Fiber.new do ::Blockenspiel.invoke(::Proc.new do ::Fiber.yield set_value('a', 1) set_value2('b'){ 2 } ::Blockenspiel.invoke(::Proc.new do ::Fiber.yield set_value('c', 3) set_value2_inmixin('d'){ 4 } end, target2_, :parameterless => :mixin) ::Fiber.yield set_value('e', 5) set_value2('f'){ 6 } end, target1_, :parameterless => :mixin) end f2_ = ::Fiber.new do ::Blockenspiel.invoke(::Proc.new do ::Fiber.yield set_value('A', 11) set_value2_inmixin('B'){ 12 } ::Blockenspiel.invoke(::Proc.new do ::Fiber.yield set_value('C', 13) set_value2('D'){ 14 } end, target1_, :parameterless => :mixin) ::Fiber.yield set_value('E', 15) set_value2_inmixin('F'){ 16 } end, target2_, :parameterless => :mixin) end f1_.resume f2_.resume f1_.resume f2_.resume f1_.resume f2_.resume f1_.resume f2_.resume assert(!self.respond_to?(:set_value)) assert(!self.respond_to?(:set_value2)) assert(!self.respond_to?(:set_value2_inmixin)) assert_equal(1, hash_['a1']) assert_equal(2, hash_['b1']) assert_equal(3, hash_['c2']) assert_equal(4, hash_['d2']) assert_equal(5, hash_['e1']) assert_equal(6, hash_['f1']) assert_equal(11, hash_['A2']) assert_equal(12, hash_['B2']) assert_equal(13, hash_['C1']) assert_equal(14, hash_['D1']) assert_equal(15, hash_['E2']) assert_equal(16, hash_['F2']) end end # Test mixin omissions. # # * Asserts that underscore methods are not mixed in. # * Asserts that methods that are turned off after the fact cannot be called. def test_omissions skip unless ::Blockenspiel.mixin_available? hash_ = ::Hash.new block_ = ::Proc.new do set_value(:a, 1) assert(!self.respond_to?(:_helper_method)) assert_equal(:helper, get_value(:a)) assert_raises(::NoMethodError) do get_value2(:a) end end target_ = Target3.new(hash_) ::Blockenspiel.invoke(block_, target_, :parameterless => :mixin) assert(!self.respond_to?(:set_value)) assert_equal(1, target_.get_value(:a)) end end end end blockenspiel-0.5.0/test/tc_embedded_block.rb0000644000004100000410000000536512652620702021126 0ustar www-datawww-data# ----------------------------------------------------------------------------- # # Blockenspiel behavior tests # # This file contains tests for behavior settings. # # ----------------------------------------------------------------------------- # Copyright 2008-2011 Daniel Azuma # # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # * Neither the name of the copyright holder, nor the names of any other # contributors to this software, may be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # ----------------------------------------------------------------------------- ; require 'minitest/autorun' require 'blockenspiel' module Blockenspiel module Tests # :nodoc: class TestEmbeddedBlock < ::Minitest::Test # :nodoc: class Target1 < ::Blockenspiel::Base def initialize(value_) @value = value_ @block = nil end def set_block(&block_) @block = block_ end def value @value end dsl_methods false def call_block @block.call end end BLOCK = ::Proc.new do set_block do self.value end end # Test an embedded block with a proxy. def test_proxy_embedded_block if false # TEMP target_ = Target1.new(23) ::Blockenspiel.invoke(BLOCK, target_, :parameterless => :proxy) assert_equal(23, target_.call_block) end end end end end blockenspiel-0.5.0/test/tc_version.rb0000644000004100000410000000421012652620702017674 0ustar www-datawww-data# ----------------------------------------------------------------------------- # # Blockenspiel version tests # # This file contains tests for the version. # # ----------------------------------------------------------------------------- # Copyright 2008-2011 Daniel Azuma # # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # * Neither the name of the copyright holder, nor the names of any other # contributors to this software, may be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # ----------------------------------------------------------------------------- ; require 'minitest/autorun' require 'blockenspiel' module Blockenspiel module Tests # :nodoc: class TestVersion < ::Minitest::Test # :nodoc: # Test that the version autoload works. def test_version refute_nil(::Blockenspiel::VERSION) end end end end blockenspiel-0.5.0/test/tc_dynamic.rb0000644000004100000410000001517612652620702017650 0ustar www-datawww-data# ----------------------------------------------------------------------------- # # Blockenspiel dynamic tests # # This file contains tests for dynamic DSL generation. # # ----------------------------------------------------------------------------- # Copyright 2008-2011 Daniel Azuma # # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # * Neither the name of the copyright holder, nor the names of any other # contributors to this software, may be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # ----------------------------------------------------------------------------- ; require 'minitest/autorun' require 'blockenspiel' module Blockenspiel module Tests # :nodoc: class TestDynamic < ::Minitest::Test # :nodoc: # Test the simple case. # # * Asserts that the simplest case works. def test_simple block_ = ::Proc.new do set_value(:a, 1) end hash_ = ::Hash.new ::Blockenspiel.invoke(block_) do add_method(:set_value) do |key_, value_| hash_[key_] = value_ end end assert_equal(1, hash_[:a]) end # Test renaming. # # * Asserts that the method appears renamed in a parameterless block. # * Asserts that the method appears in its original name in a parametered block. def test_renaming hash_ = ::Hash.new dsl_definition_ = ::Proc.new do add_method(:set_value, :dsl_method => :renamed_set_value) do |key_, value_| hash_[key_] = value_ end end block1_ = ::Proc.new do renamed_set_value(:a, 1) assert_raises(::NoMethodError){ set_value(:b, 2) } end ::Blockenspiel.invoke(block1_, &dsl_definition_) block2_ = ::Proc.new do |dsl_| dsl_.set_value(:c, 3) assert_raises(::NoMethodError){ renamed_set_value(:d, 4) } end ::Blockenspiel.invoke(block2_, &dsl_definition_) assert_equal(1, hash_[:a]) assert_nil(hash_[:b]) assert_equal(3, hash_[:c]) assert_nil(hash_[:d]) end # Test calls with blocks passed the usual way. # Note: this will fail in MRI < 1.8.7 and JRuby < 1.5. # # * Asserts that a block passed the usual way works # * Asserts that we can detect when a block has not been passed def test_blocks_normal hash_ = ::Hash.new block_ = ::Proc.new do set_value1(:a){ 1 } set_value2(:b){ 2 } set_value2(:c) end ::Blockenspiel.invoke(block_) do add_method(:set_value1) do |key_, &bl_| hash_[key_] = bl_.call end add_method(:set_value2) do |key_, &bl_| hash_[key_] = bl_ ? true : false end end assert_equal(1, hash_[:a]) assert_equal(true, hash_[:b]) assert_equal(false, hash_[:c]) end # Test calls with blocks passed as non-block parameters. # # * Asserts that a block passed "first" works. # * Asserts that a block passed "last" works. # * Asserts that a block passed "true" works. def test_blocks_first_and_last hash_ = ::Hash.new block_ = ::Proc.new do set_value1(:a){ 1 } set_value2(:b){ 2 } set_value2(:c){ 3 } end ::Blockenspiel.invoke(block_) do add_method(:set_value1, :block => :first) do |bl_, key_| hash_[key_] = bl_.call end add_method(:set_value2, :block => :last) do |key_, bl_| hash_[key_] = bl_.call end add_method(:set_value3, :block => true) do |bl_, key_| hash_[key_] = bl_.call end end assert_equal(1, hash_[:a]) assert_equal(2, hash_[:b]) assert_equal(3, hash_[:c]) end # Test calls with blocks not passed. # # * Asserts that if a block isn't given, it is set to nil. def test_blocks_nil hash_ = ::Hash.new block_ = ::Proc.new do set_value1(:a) set_value2(:b) end ::Blockenspiel.invoke(block_) do add_method(:set_value1, :block => :first) do |bl_, key_| assert_nil(bl_) end add_method(:set_value2, :block => :last) do |key_, bl_| assert_nil(bl_) end end assert_nil(hash_[:a]) assert_nil(hash_[:b]) end # Test calls with blocks (legacy api) # # * Asserts that a block with receive_block works. def test_blocks_legacy hash_ = ::Hash.new block_ = ::Proc.new do set_value(:a){ 1 } end ::Blockenspiel.invoke(block_) do add_method(:set_value, :receive_block => true) do |key_, bl_| hash_[key_] = bl_.call end end assert_equal(1, hash_[:a]) end # Test passing options in. # # * Asserts that the "parameter" option is recognized def test_options_recognized block_ = ::Proc.new do set_value(:a, 1) end hash_ = ::Hash.new assert_raises(::Blockenspiel::BlockParameterError) do ::Blockenspiel.invoke(block_, :parameterless => false) do add_method(:set_value) do |key_, value_| hash_[key_] = value_ end end end end end end end blockenspiel-0.5.0/test/tc_basic.rb0000644000004100000410000001245112652620702017276 0ustar www-datawww-data# ----------------------------------------------------------------------------- # # Blockenspiel basic tests # # This file contains tests for the simple use cases. # # ----------------------------------------------------------------------------- # Copyright 2008 Daniel Azuma # # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # * Neither the name of the copyright holder, nor the names of any other # contributors to this software, may be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # ----------------------------------------------------------------------------- ; require 'minitest/autorun' require 'blockenspiel' module Blockenspiel module Tests # :nodoc: class TestBasic < ::Minitest::Test # :nodoc: class SimpleTarget < ::Blockenspiel::Base def initialize @hash = ::Hash.new end def set_value(key_, value_) @hash[key_] = value_ end def set_value_by_block(key_) @hash[key_] = yield end def get_value(key_) @hash[key_] end dsl_method :get_value, false end # Test basic usage with a parameter object. # # * Asserts that methods are not mixed in to self. # * Asserts that the specified target object does in fact receive the block messages. def test_basic_param block_ = ::Proc.new do |t_| t_.set_value(:a, 1) t_.set_value_by_block(:b){ 2 } assert(!self.respond_to?(:set_value)) assert(!self.respond_to?(:set_value_by_block)) end target_ = SimpleTarget.new ::Blockenspiel.invoke(block_, target_) assert_equal(1, target_.get_value(:a)) assert_equal(2, target_.get_value(:b)) end # Test basic usage with a delegator. # # * Asserts that the specified target object receives the messages. # * Asserts that methods from the surrounding context are also available. # * Asserts that methods are not present in self afterward. def test_basic_parameterless block_ = ::Proc.new do set_value(:a, 1) set_value_by_block(:b){ 2 } assert(true) end target_ = SimpleTarget.new ::Blockenspiel.invoke(block_, target_) assert(!self.respond_to?(:set_value)) assert(!self.respond_to?(:set_value_by_block)) assert_equal(1, target_.get_value(:a)) assert_equal(2, target_.get_value(:b)) end # Test basic usage with a builder. # # * Asserts that the receivers are called. # * Asserts that receivers with blocks are handled properly. def test_basic_builder block_ = ::Proc.new do set_value(:a, 1) set_value_by_block(:b){ 2 } end hash_ = ::Hash.new ::Blockenspiel.invoke(block_) do add_method(:set_value) do |key_, value_| hash_[key_] = value_ end add_method(:set_value_by_block, :block => true) do |bl_, key_| hash_[key_] = bl_.call end end assert_equal(1, hash_[:a]) assert_equal(2, hash_[:b]) end # Test basic usage with a string. # # * Asserts that the specified target object receives the messages. def test_basic_string string_ = <<-STR set_value(:a, 1) set_value_by_block(:b){ 2 } STR target_ = SimpleTarget.new ::Blockenspiel.invoke(string_, target_) assert(!self.respond_to?(:set_value)) assert_equal(1, target_.get_value(:a)) assert_equal(2, target_.get_value(:b)) end # Test basic usage with a file. # # * Asserts that the specified target object receives the messages. def test_basic_file target_ = SimpleTarget.new ::Blockenspiel.invoke(target_, :file => "#{File.dirname(__FILE__)}/files/file1.rb") assert(!self.respond_to?(:set_value)) assert_equal(1, target_.get_value(:a)) assert_equal(2, target_.get_value(:b)) end end end end blockenspiel-0.5.0/ImplementingDSLblocks.rdoc0000644000004100000410000017232512652620702021274 0ustar www-datawww-data== Implementing DSL Blocks by Daniel Azuma A DSL block is a construct commonly used in Ruby APIs, in which a DSL (domain-specific language) is made available inside a block passed to an API call. In this paper I present an overview of different implementation strategies for this important pattern. I will first describe the features of DSL blocks, utilizing illustrations from several well-known Ruby libraries. I will then survey and critique five implementation strategies that have been put forth. Finally, I will present a new library, {Blockenspiel}[http://virtuoso.rubyforge.org/blockenspiel], designed to be a comprehensive implementation of DSL blocks. Originally written on 29 October 2008. Minor modifications on 28 October 2009 to deal with Why's disappearance. === An illustrative overview of DSL blocks If you've done much Ruby programming, chances are you've run into mini-DSLs (domain-specific languages) that live inside blocks. Perhaps you've encountered them in Ruby standard library calls, such as File#open, a call that lets you interact with a stream while performing automatic setup and cleanup for you: File.open("myfile.txt") do |io| io.each_line do |line| puts line unless line =~ /^\s*#/ end end Perhaps you've used the XML {builder}[http://builder.rubyforge.org/] library, which uses nested blocks to match the structure of the XML being generated: builder = Builder::XmlMarkup.new builder.page do builder.element1('hello') builder.element2('world') builder.collection do builder.interior do builder.element3('foo') end end end The {Markaby}[http://github.com/markaby/markaby] library also uses nested blocks to generate html, but is able to do so more succinctly without requiring you to explicitly reference a builder object: Markaby::Builder.new.html do head { title "Boats.com" } body do h1 "Boats.com has great deals" ul do li "$49 for a canoe" li "$39 for a raft" li "$29 for a huge boot that floats and can fit 5 people" end end end Perhaps you've described testing scenarios using {RSpec}[http://rspec.info/], building and documenting test cases using English-sounding commands such as "describe" and "it_should_behave_like": describe Stack do before(:each) do @stack = Stack.new end describe "(empty)" do it { @stack.should be_empty } it_should_behave_like "non-full Stack" it "should complain when sent #peek" do lambda { @stack.peek }.should raise_error(StackUnderflowError) end it "should complain when sent #pop" do lambda { @stack.pop }.should raise_error(StackUnderflowError) end end # etc... Perhaps you were introduced to Ruby via the {Rails}[http://www.rubyonrails.org/] framework, which sets up configuration via blocks: ActionController::Routing::Routes.draw do |map| map.connect ':controller/:action/:id' map.connect ':controller/:action/:page/:format' # etc... end Rails::Initializer.run do |config| config.time_zone = 'UTC' config.log_level = :debug # etc... end Blocks are central to Ruby as a language, and it feels natural to Ruby programmers to use them to delimit specialized code. When designing an API for a Ruby library, blocks like these are, in many cases, a natural and effective pattern. === Defining DSL blocks Blocks in Ruby are used for a variety of purposes. In many cases, they are used to provide _callbacks_, specifying functionality to inject into an operation. If you come from a functional programming background, you might see them as lambda expressions; in object-oriented-speak, they implement the Visitor pattern. A simple example is the +each+ method, which iterates over a collection, using the given block as a callback that allows the caller to specify processing to perform on each element. When we speak of DSL blocks, we are describing something conceptually and semanticaly different. Rather than looking for a specification of _functionality_, the method wants to provide the caller with a _language_ to _describe_ something. The block merely serves as a space in which to use that language. Consider the Rails Routing example above. The Rails application needs to specify how URLs should be interpreted as commands sent to controllers, and, conversely, how command descriptions should be expressed as URLs. Rails thus defines a language that can be used to describe these mappings. The language uses the "connect" verb, which interprets a string with embedded codes describing the URL's various parts, and optional parameters that specify further details about the mapping. The Rails Initializer illustrates another common pattern: that of using a DSL block to perform extended configuration of the method call. Again, a language is being defined here: certain property names such as "time_zone" have meanings understood by the Rails framework. Note that in both this case and the Routing case, the information contained in the block is descriptive. It is possible to imagine a syntax in which all the necessary information is passed into the method (Routes#draw or Initializer#run) as parameters, perhaps as a large hash or other complex data structure. However, in many cases, providing this information via a block-based language makes the code much more readable. The RSpec example illustrates a more sophisticated case with many keywords and multiple levels of blocks, but it shares common features with the Rails examples. Again, a language is being defined to describe things that could conceivably have been passed in as parameters, but are being specified in a block for clarity and readability. Based on this discussion, we can see that DSL blocks have the following properties: * An API requires a caller to communicate complex descriptive information. * The API defines a domain-specific language designed to express this information. * A method accepts a block from the caller, and executes the block exactly once. * The domain-specific language is available to the caller lexically within the block. As far as I have been able to determine, the term "DSL block" originated in 2007 with a {blog post}[http://blog.8thlight.com/articles/2007/05/20/] by Micah Martin. In it, he describes a way to implement certain types of DSL blocks using instance_eval, calling the technique the "DSL Block Pattern". We will discuss the nuances of the instance_eval implementation in greater detail below. But first, let us ease into the implementation discussion by describing a simple strategy that has worked very well for many libraries, including Rails. === Implementation strategy 1: block parameters In 2006, Jamis Buck, one of the Rails core developers, posted a set of articles describing the Rails routing implementation. Tucked away at the top the {first article}[http://weblog.jamisbuck.org/2006/10/2/under-the-hood-rails-routing-dsl] is a code snippet showing the DSL block implementation for Rails routing. This code, along with some of its context in the file action_controller/routing/route_set.rb (from Rails version 2.1.1), is listed below. class RouteSet class Mapper def initialize(set) @set = set end def connect(path, options = {}) @set.add_route(path, options) end # ... end # ... def draw clear! yield Mapper.new(self) named_routes.install end # ... def add_route(path, options = {}) # ... Recall how we specify routes in Rails: we call the +draw+ method, and pass it a block. The block receives a parameter that we call "+map+". We can then create routes by calling the +connect+ method on the parameter, as follows: ActionController::Routing::Routes.draw do |map| map.connect ':controller/:action/:id' map.connect ':controller/:action/:page/:format' # etc. end It should be fairly easy to see how the code above accomplishes this. The +draw+ method creates an object of class +Mapper+. The +Mapper+ class defines the domain-specific language, in particular the +connect+ method that we are familiar with. Note how its implementation is simply to proxy calls into the routing system: it keeps an instance variable called "@set" that points back at the +RouteSet+ we are modifying. Then, +draw+ yields the mapper instance back to the block, where we receive it as our +map+ variable. A large number of DSL block implementations are variations on this theme. We define a proxy class (+Mapper+ in this case) that exposes the domain-specific language we want and communicates back to the system we are describing. We then yield an instance of that proxy back to the block, which receives it as a parameter. The block then manipulates the DSL using its parameter. This pattern is extremely powerful and pervasive. It is simple and clean to implement, and straightforward to use by the caller. The caller knows exactly when it is interacting with the DSL: when it calls methods on the block parameter. However, some have argued that it is too verbose. Why, in a DSL, is it necessary to litter the entire block with references to the block variable? If we know that the caller is supposed to be interacting with the DSL in the block, is it really necessary to have the explicit parameter? Perhaps Rails routing, for example, could be specified more succinctly like the following, in which the +map+ variable is implied. ActionController::Routing::Routes.draw do connect ':controller/:action/:id' connect ':controller/:action/:page/:format' # etc. end In the next section we will look more closely at the pros and cons of this alternate syntax. But first, let us summarize our discussion of the "block parameter" implementation. *Implementation*: * Create a proxy class defining the DSL. * Yield the proxy object to the block as a parameter. *Pros*: * Easy to implement. * Clear syntax for the caller. * Clear separation between the DSL and surrounding code. *Cons*: * Requires a block parameter, sometimes resulting in verbose or clumsy syntax. Use it when: you want a simple, effective DSL block and don't mind requiring a parameter. === The parameterless block syntax Much of the recent discussion surrounding DSL blocks originates from a desire to eliminate the block parameter. A domain-specific _language_, it is reasoned, should be as natural and concise as possible, and should not be tied down to the syntax of method invocation. In many cases, eliminating the block parameter would have an enormous impact on the readability of a DSL block. One common example is the case of nested blocks, which, because of Ruby 1.8's scoping semantics, require different variable and parameter names. Consider an imaginary DSL block that looks like this: create_container do |container| container.create_subcontainer do |subcontainer1| subcontainer1.create_subcontainer do |subcontainer2| subcontainer2.create_object do |objconfig| objconfig.set_value(3) end end subcontainer1.create_subcontainer do |subcontainer3| subcontainer3.create_object do |objconfig2| objconfig2.set_value(1) end end end end That was clunky. Wouldn't it be nice to instead see this?... create_container do create_subcontainer do create_subcontainer do create_object do set_value(3) end end create_subcontainer do create_object do set_value(1) end end end end While this appears to be an improvement, it does come at a cost. First, certain method names become syntactically unavailable when you eliminate the method call syntax. Consider, for example, this simple DSL proxy object that uses attr_writer... class ConfigMethods attr_writer :author attr_writer :title end You might interact with it in a DSL block that uses parameters, like so: create_paper do |config| config.author = "Daniel Azuma" config.title = "Implementing DSL Blocks" end However, if you try to eliminate the block parameter, you run into this dilemma: create_paper do author = "Daniel Azuma" # Whoops! These no longer work because they title = "Implementing DSL Blocks" # look like local variable assignments! end If you want to retain the attr_writer syntax, you must make it clear to the Ruby parser that you are invoking a method call. For example: create_paper do self.author = "Daniel Azuma" # These are now clearly method calls self.title = "Implementing DSL Blocks" end Unfortunately, this negates some of the benefit of removing the block parameter in the first place. A similar syntactic issue occurs with many operators, notably []=. Second, and more importantly, by eliminating the block parameter, we eliminate the primary means of distinguishing which methods belong to the DSL, and which methods do not. For example, in our routing example, if we eliminate the parameter, like so: ActionController::Routing::Routes.draw do connect ':controller/:action/:id' connect ':controller/:action/:page/:format' # etc. end ...we now _assume_ that the +connect+ method is part of the DSL, but that is no longer explicit in the syntax. If, +connect+ also happens to be a method of whatever object was +self+ in the context of the block, which method should be called? There is a method lookup ambiguity inherent to the syntax itself, and, as we shall see, different implementations of parameterless blocks will resolve this ambiguity in different, and sometimes confusing, ways. Despite the above caveats inherent to the syntax, the desire to eliminate the block parameter is quite strong. Let's consider how it can be done. === Implementation strategy 2: instance_eval Micah Martin's {blog post}[http://blog.8thlight.com/articles/2007/05/20/] describes an implementation strategy that does not require the block to take a parameter. He suggests using a powerful, if sometimes confusing, Ruby metaprogramming tool called instance_eval. This method, defined on the +Object+ class so it is available to every object, has a simple function: it executes a block given it, but does so with the +self+ reference redirected to the receiver. Hence, within the block, calling a method, or accessing an instance variable or class variable, (or, in Ruby 1.9, accessing a constant), will begin the lookup process at a different place. It is perhaps instructive to see an example. Let's create a simple class Class MyClass def initialize @instvar = 1 end def foo puts "in foo: var=#{@instvar}" end end Things to note here is that the method +foo+ and the instance variable @instvar are defined on instances of +MyClass+. Now let's instance_eval an instance of +MyClass+ from another class. class Tester def test puts @instvar.inspect # prints "nil" since the Tester object has no @instvar x = MyClass.new # create a new instance of MyClass x.instance_eval do # change self to point to x during the block puts @instvar.inspect # prints "1" since self now points at x @instvar = 2 # changes x's @instvar to 2 foo # calls x's foo and prints "in foo: var=2" puts x == self # prints "true". The local variable x is still accessible end # end of the block. self is now back to the Tester instance puts x == self # prints "false" puts @instvar.inspect # prints "nil" since Tester still has no @instvar foo # NameError since Tester has no foo method. end end Tester.new.test # Runs the above test How does this help us? Notice that within the instance_eval block, the methods of +x+ can be called without explicitly naming +x+ because the +self+ reference points to +x+. So in the Rails Routing example, if we used instance_eval to get +self+ to point to the +Mapper+ instance in the block, then we wouldn't need to pass it explicitly as a parameter, and the block could call methods on it without explicitly naming it. Here is a revised version of the Rails routing code: class RouteSet class Mapper def initialize(set) @set = set end def connect(path, options = {}) @set.add_route(path, options) end # ... end # ... # We need to pass the block itself to instance_eval, so get it # as a parameter to the draw method. def draw(&block) clear! map = Mapper.new(self) # Create the proxy object as before map.instance_eval(&block) # Call the block, setting self to point to map. named_routes.install end # ... def add_route(path, options = {}) # ... This modified version of the routing API now no longer requires a block parameter, and the DSL is correspondingly more succinct. Sounds like a win all around, right? Well, not so fast. Our implementation here has a number of subtle and surprising side effects. Suppose, for instance, we were to write a little helper method to help us generate URLs: def makeurl(*params) 'mywebsite/:controller/:action/' + params.map{ |e| e.inspect }.join('/') end Using the above method, it becomes easy to generate URL strings: makeurl(:id, :style) # --> "mywebsite/:controller/:action/:id/:style" Our routes.rb file, utilizing our "improvement" to the routing DSL, might now like this: def makeurl(*params) 'mywebsite/:controller/:action/' + params.map{ |e| e.inspect }.join('/') end ActionController::Routing::Routes.draw do connect makeurl :id connect makeurl :page, :format # etc. end Looks nice, right? Except that when we try to run it, we get: NoMethodError: undefined method `[]' for :id:Symbol from /usr/local/lib/ruby/gems/1.8/gems/actionpack-2.1.1/lib/action_controller/routing/builder.rb:168:in `build' from /usr/local/lib/ruby/gems/1.8/gems/actionpack-2.1.1/lib/action_controller/routing/route_set.rb:261:in `add_route' ... What's up with that cryptic error? After some furious digging into the guts of Rails, we discover to our surprise Ruby is trying to call +makeurl+ on the +Mapper+ object, rather than calling our +makeurl+ helper method. And then it dawns on us. We used instance_eval to change +self+ to point to the +Mapper+ proxy inside the block, and it did exactly what we asked. It let us call the +connect+ method on the +Mapper+ without having to pass it in as a block parameter. But it similarly also tried to call +makeurl+ on the +Mapper+. The helper method we so cleverly wrote is being bypassed. The problem gets worse. Changing +self+ affects not only how methods are looked up, but also how instance variables are looked up. For example, we are now able to do this: ActionController::Routing::Routes.draw do @set = nil connect ':controller/:action/:id' # Exception raised here! connect ':controller/:action/:page/:format' # etc. end What happened? If we recall, @set is used by the +Mapper+ object to point back to the routing +RouteSet+. It is how the proxy knows what it is proxying for. But since we've used instance_eval, we now have free access to the +Mapper+ object's internal instance variables, including the ability to clobber them. And that's precisely what we did here. Furthermore, maybe we were actually expecting to access our own @set variable, and we haven't done that. Any instance variables from the caller's closure are in fact no longer accessible inside the block. Similarly, if you are using Ruby 1.9, constants are also looked up using +self+ as the starting point. So by changing +self+, instance_eval affects the availability of constants in surprising ways. The problem gets even worse. If we think about the cryptic error message we got when we tried to use our +makeurl+ helper method, we begin to realize that we've run into the method lookup ambiguity discussed in the previous section. If +self+ has changed inside the block, and we tried to call +makeurl+, we might expect a +NoMethodError+ to be raised for +makeurl+ on the +Mapper+ class, rather than for "[]" on the +Symbol+ class. However, things change when we recall that Rails's routing DSL supports named routes. You do not have to call the specific +connect+ method to create a route. In fact, you can call _any_ method name. Any name is a valid DSL method name. It is thus ambiguous, when we invoke +makeurl+, whether we mean our helper method or a named route called "makeurl". Rails assumed we meant the named route, but in fact that isn't what we had intended. This all sounds pretty bad. Do we give up on instance_eval? Some members of the Ruby community have, and indeed the technique has generally fallen out of favor in many major libraries. Jim Weirich, for instance, {originally}[http://onestepback.org/index.cgi/Tech/Ruby/BuilderObjects.rdoc] utilized instance_eval in the XML Builder library illustrated earlier, but later deprecated and removed it because of its surprising behavior. Why's {Markaby}[http://github.com/markaby/markaby] still uses instance_eval but includes a caveat in the {documentation}[http://markaby.rubyforge.org/] explaining the issues and recommending caution. There are, however, a few specific cases when instance_eval may be uniquely appropriate. RSpec's DSL is intended as a class-constructive language: it constructs ruby classes behind the scenes. In the RSpec example at the beginning of this paper, you may notice the use of the @stack instance variable. In fact, this is intended as an instance variable of the RSpec test story being written, and as such, instance_eval is required because of the kind of language that RSpec wants to use. But in more common cases, such as specifying configuration, instance_eval does not give us the most desirable behavior. The general consensus now, expressed for example in recent articles from Why (no longer available) and {Ola Bini}[http://olabini.com/blog/2008/09/dont-overuse-instance_eval-and-instance_exec/], is that it should be avoided. So does this mean we're stuck with block parameters for better or worse? Not quite. Several alternatives have been proposed recently, and we'll take a look at them in the next few sections. But first, let's summarize the discussion of instance_eval. *Implementation*: * Create a proxy class defining the DSL. * Use instance_eval to change +self+ to the proxy in the block. *Pros*: * Easy to implement. * Concise: does not require a block parameter. * Useful for class-constructive DSLs. *Cons*: * Surprising lookup behavior for helper methods. * Surprising lookup behavior for instance variables. * Breaks encapuslation of the proxy class. * Encounters the helper method vs DSL method ambiguity. Use it when: you are writing a DSL that constructs classes or modifies class internals. === Implementation strategy 3: delegation In our discussion of instance_eval, a major problem we identified is that helper methods, and indeed all other methods from the calling context, are not available within the block. One way to improve the situation, perhaps, is by redirecting any methods not defined in the DSL (that is, not defined on the proxy object) back to the original context. That way, we still have access to our helper methods--they'll appear to be part of the DSL. This "delegation" approach was proposed by Dan Manges in his {blog}[http://www.dcmanges.com/blog/ruby-dsls-instance-eval-with-delegation]. The basic implementation here is not difficult, if we pull out another tool from Ruby's metaprogramming toolbox, method_missing. This method is called whenever you call a method that is not explicitly defined on an object's class. It provides a "last ditch" opportunity to handle the method before Ruby bails with a dreaded +NoMethodError+. Again, an example is probably useful here. class MyClass def foo puts "in foo" end def method_missing(name, *params) puts "last ditch method #{name.inspect} called with params: #{params.inspect}" end end x = MyClass.new x.foo # prints "in foo" x.bar # prints "last ditch method :bar called with params: []" x.baz(1,2) # prints "last ditch method :baz called with params: [1,2]" How does this help us? Well, our goal is to redirect any calls that aren't available in the DSL, back to the block's original context. To do that, we simply define method_missing on our proxy class. In that method, we delegate the call, using +send+, back to the original +self+ from the block's context. The remaining trick is how to get the block's original +self+. This can be done with a little bit of hackery if we realize that any +Proc+ object lets you access the binding of the context where it came from. We can get the original +self+ reference by eval-ing "self" in that binding. Going back to our modification of the Rails routing code, let's see what this looks like. class RouteSet class Mapper # We save the block's original "self" reference also, so that we # can redirect unhandled methods back to the original context. def initialize(set, original_self) @set = set @original_self = original_self end def connect(path, options = {}) @set.add_route(path, options) end # ... # Redirect all other methods def method_missing(name, *params, &blk) @original_self.send(name, *params, &blk) end end # ... def draw(&block) clear! original_self = Kernel.eval('self', block.binding) # Get block's context self map = Mapper.new(self, original_self) # Give it to the proxy map.instance_eval(&block) named_routes.install end # ... def add_route(path, options = {}) # ... Now people familiar with how Rails is implemented will probably object that +Mapper+ already _has_ a method_missing defined. It's used to implement the named routes that caused the ambiguity we described earlier. We have not solved that ambiguity: by replacing Rails's method_missing with my own method_missing, I effectively disable named routes. Granted, I'm ignoring that issue right now, and just trying to illustrate how method delegation works. As long as we don't use named routes, our +makeurl+ example will now work as we expect: def makeurl(*params) 'mywebsite/:controller/:action/' + params.map{ |e| e.inspect }.join('/') end ActionController::Routing::Routes.draw do connect makeurl :id connect makeurl :page, :format # etc. end While this would appear to have solved the helper method issue, so far it does nothing to address the other issues we encountered. For example, invoking instance variables inside the block will still reference the instance variables of the +Mapper+ proxy object. By using instance_eval, we still break encapsulation of the proxy class, and lose access to any instance variables from the block's context. Addressing the instance variable issue is not as straightforward as delegating method calls. There is, as far as I know, no direct way to delegate instance variable lookup, and Manges's blog posting does not attempt to provide a solution either. However, we can imagine a few techniques to mitigate the problem. First, we could eliminate the proxy object's dependence on instance variables altogether, by replacing them with a global hash. In our example, instead of keeping a reference to the +RouteSet+ as an instance variable of +Mapper+, we can maintain a global hash that looks up the +RouteSet+ using the +Mapper+ instance as the key. In this way, we eliminate the risk of the block clobbering the proxy's state, and minimize the problem of breaking encapsulation of the proxy object. Second, we could make instance variables from the block's context partially available through a "pull-push" technique using instance_variable_set and instance_variable_get calls. Before calling the block, we "pull" in the block context object's instance variables, by iterating over them and setting the same instance variables on the proxy object. Then those instance variables will appear to be still available during the block. On completing the block, we then "push" any changes back to the block context object, by iterating over the proxy's instance variables and setting them on the block context object. Here is a sample implementation of these two techniques for handling instance variables: class RouteSet class Mapper @@routeset_map = Hash.new # Global hashes to replace @@original_self_map = Hash.new # Mapper's instance variables def initialize(set, original_self) @@routeset_map[self] = set # Add me to global hashes @@original_self_map[self] = original_self original_self.instance_variables.each do |name| # "pull" instance variables instance_variable_set(name, original_self.instance_variable_get(name)) end end def cleanup @@routeset_map.delete(self) # Remove from global hashes original_self = @@original_self_map.delete(self) instance_variables.each do |name| # "push" instance variables original_self.instance_variable_set(name, instance_variable_get(name)) end end def connect(path, options = {}) @@routeset_map[self].add_route(path, options) # Lookup set from global hash end # ... def method_missing(name, *params, &blk) # Lookup original self @@original_self_map[self].send(name, *params, &blk) # from global hash end end # ... def draw(&block) clear! original_self = Kernel.eval('self', block.binding) map = Mapper.new(self, original_self) begin map.instance_eval(&block) ensure # Ensure the hashes are cleaned up and instance map.cleanup # variables are pushed back to original_self, end # even if the block threw an exception named_routes.install end # ... def add_route(path, options = {}) # ... While these measures seem to handle most of the cases, the implementation is getting more complex, and includes the additional overhead of hash lookups and copying of instance variables. More significantly, the "pull-push" technique does not quite preserve the expected semantics of instance variables. For instance, if you change an instance variable's value inside the block, it will get "pushed" back to the context object after the block is completed, but until then, the context object will not know about the change. So if, in the meantime, you called a helper method that relies on that instance variable, you will get the old value, and this can result in confusion. Using global hashes might be an effective means of protecting the proxy object's internals from the block. However, I find the "pull-push" technique to delegate instance variables to be of questionable value. Several variations on the delegation theme have been proposed. One such variation uses a technique proposed by Jim Weirich called {MethodDirector}[http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-core/19153]. In this variation, we create a small object whose sole purpose is to receive methods and delegate them to whatever object it thinks should handle them. Utilizing Jim's +MethodDirector+ implementation rather than adding a method_missing to our +Mapper+ proxy, we could rewrite the +draw+ method as follows: def draw(&block) clear! original_self = Kernel.eval('self', block.binding) # Get the block's context self map = Mapper.new(self) # Get the proxy director = MethodDirector.new([map, original_self]) # Create a director director.instance_eval(&block) # Use the director as self named_routes.install end The upshot is not much different from Manges's delegation technique. Method calls get delegated in approximately the same way (though Weirich speculates that +MethodDirector+'s dispatch process may be slow). Within the block, +self+ now points to the +MethodDirector+ object rather than the +Mapper+ object. This means that we're no longer breaking encapsulation of the mapper proxy (but we are breaking the encapsulation of the +MethodDirector+ itself.) We still cannot access instance variables from the block's context. We no longer clobber +Mapper+'s instance variables, but now we can clobber +MethodDirector+'s. In short, it might be considered a slight improvement, but not much, at a possible performance cost. Let's wrap up our discussion of delegation and then delve into an entirely different approach. *Implementation*: * Create a proxy class defining the DSL. * Use method_missing to delegate unhandled methods back to the block's context. * Use instance_eval to change +self+ to the proxy in the block. *Pros*: * Concise: does not require a block parameter. * Better than a straight instance_eval in that it handles helper methods. *Cons*: * No complete way to eliminate the surprising lookup behavior for instance variables. * Does not solve the helper method vs DSL method ambiguity. * Harder to implement than a simple instance_eval. Use it when: you have a case where instance_eval is appropriate (i.e. if you are writing a DSL that constructs classes or modifies class internals) but you want to retain helper methods. === Implementation strategy 4: arity detection Intrigued by the discussion surrounding instance_eval and DSL blocks, James Edward Gray II (of {RubyQuiz}[http://rubyquiz.com/] fame) chimed in with a compromise. In his {blog}[http://blog.grayproductions.net/articles/dsl_block_styles], he argues that the the issue boils down to two basic strategies: block parameters and instance_eval, both of which have their own strengths and weaknesses. On one hand, block parameters avoid surprising behavior and ambiguity in exchange for somewhat more verbose syntax. On the other hand, instance_eval offers a more concise and perhaps more pleasing syntax in exchange for some ambiguity and surprising side effects. Neither solution is clearly better than the other, and either might be more appropriate in different circumstances. Thus, why not let the _caller_ decide which one to use? This is in fact easier to do than we might think. When you call a method using a DSL block, you've already make the choice to have your block take a parameter or not. The caller does one of the following: ActionController::Routing::Routes.draw do |map| map.connect ':controller/:action/:id' map.connect ':controller/:action/:page/:format' # etc. end or ActionController::Routing::Routes.draw do connect ':controller/:action/:id' connect ':controller/:action/:page/:format' # etc. end It is possible for the method itself to detect which case it is, just by examining the block. Every +Proc+ object provides a method called +arity+, which returns a notion of how many parameters the block expects. If you receive a block that expects a parameter, use the block parameter strategy; if you receive a block that doesn't expect a parmaeter, use instance_eval or one of its modifications. Under this technique, our Routing +draw+ method might look like this: def draw(&block) clear! map = Mapper.new(self) # Create the proxy object as before if block.arity == 1 block.call(map) # Block takes one parameter: use block parameter technique else map.instance_eval(&block) # otherwise, use instance_eval technique. end named_routes.install end Gray's proposal has a compelling advantage. The basis for the entire discussion is the suggestion that eliminating block parameters is desirable for the caller, and the objections raised are also, almost without exception, based on the experience of the caller. The basic question is thus whether the _caller_ ought to consider the benefits of eliminating block parameters to outweigh the costs. Therefore, it makes sense to put that choice in the hands of the caller rather than letting the library API designer dictate one choice or the other. For example, one apparently inherent issue with a DSL block style that eliminates block parameters is the ambiguity between DSL methods and helper methods. By giving the caller the choice, we at once solve the ambiguity by providing a language for it. If the caller does not need to distinguish between the two, because she is not using helper methods or named routes, then she can choose to omit the block parameter and use instance_eval without harm. If, on the other hand, she _does_ need to distinguish between the two, as in the case of Rails routing where any method name could be a DSL method because of the named routes feature, then she can choose to make the block parameter explicit. There is, however, a subtle disadvantage to providing the choice. By effectively allowing two DSL styles, a library that offers Gray's choice dilutes the identity and "branding" of its DSL. If there are two "dialects" of the DSL, one that uses a block parameter and one that does not, it becomes harder for programmers to recognize the language. The two dialects might develop separate followings and distinct "best-practices" on account of their syntactic differences, and the schism would diminish the overall power of the DSL. While the actual cost of this diluting effect can be difficult to measure, it cannot be ignored, because the whole point of defining a DSL is to make code more understandable and recognizable. Finally, there are some cases when one choice is specifically called for by the nature of the DSL being implemented. RSpec is a good example: it requires instance_eval in order to support access to the test story's instance variables. Allowing the caller to choose would not make sense in this case. Let us summarize Gray's arity detection technique, and then proceed to an interesting new idea recently proposed by Why The Lucky Stiff. *Implementation*: * Create a proxy class defining the DSL. * Detect the choice of the caller based on block arity. * Use either a block parameter or instance_eval to invoke the block. *Pros*: * Gives the caller the ability to choose which syntax works best. * Solves method lookup ambiguity. * Implementation cost is not significant. *Cons*: * Not an all-encompassing solution-- either choice still has its own pros and cons. * Possibility of dilution of DSL branding. Use it when: it is not clear whether block parameters or instance_eval is better, or if you need a way to mitigate the method lookup ambiguity. === Implementation strategy 5: mixins One of the most interesting entries into the DSL blocks discussion was proposed by Why The Lucky Stiff in his blog. Unfortunately, with Why's disappearance, the original article is no longer available, but we can summarize its contents here. Why observes that the problem with instance_eval is that it does too much. Most DSL blocks merely want to be able to intercept and respond to certain method calls, whereas instance_eval actually changes +self+, which has the additional side effects of blocking access to other methods and instance variables, and breaking encapsulation. A better solution, he maintains, is not to change +self+, but instead temporarily to add the DSL's methods to the block's context for the duration of the block. That is, instead of having the DSL proxy object delegate back to the block's context object, do the opposite: cause the block's context object to delegate to the DSL proxy object. Implementing this is actually harder than it sounds. We need to take the block context object, dynamically add methods to it before calling the block, and then dynamically remove them afterward. We already know how to get the block context object, but adding and removing methods requires some more Ruby metaprogramming wizardry. And now we're stretching our toolbox to the breaking point. Ruby provides tools for dynamically defining methods on and removing methods from an existing module. We might be tempted to try something like this: def draw(&block) clear! save_self = self original_self = Kernel.eval('self', block.binding) original_self.class.module_eval do define_method(:connect) do |path,options| save_self.add_route(path,options) end end yield original_self.class.module_eval do remove_method(:connect) end named_routes.install end This implementation, however, is fraught with problems. Notably, we are modifying the entire class of objects, including instances other than original_self, which is probably not what we intended. In addition, we could be unknowingly clobbering another +connect+ method defined on original_self's class. (There are, of course, many other problems that I'm just ignoring for the sake of clarity, such as exception safety, and the fact that the +options+ parameter cannot take a default value when using define_method. Suffice to say that the above implementation is quite broken.) What we would really like is a way to add methods to just one object temporarily, and then remove them, restoring the original state (including any methods we may have overridden when we added ours.) Ruby _almost_ provides a reasonable way to do this, using the +extend+ method. This method lets you add a module's methods to a single specific object, like this: module MyExtension def foo puts "foo called" end end s1 = 'hello' s2 = 'world' s1.extend(MyExtension) # adds the "foo" method only to object s1, # not to the entire string class. s1.foo # prints "foo called" s2.foo # NameError: s2 is unchanged Unfortunately, there is no way to remove the module from the object. Ruby has no "unextend" capability. This omission led Why to implement it himself as a Ruby language extension called {Mixico}[http://github.com/rkh/mixico]. The name comes from the library's ability to add and remove "mixins" at will. A similar library exists as a gem called {Mixology}[http://www.somethingnimble.com/bliki/mixology]. The two libraries use different APIs but perform the same basic function. For the discussion below, I will assume Mixico is installed. However, the library I describe in the next section uses a custom implementation that is compatible with MRI 1.9 and JRuby. Using Mixico, we can now write the +draw+ method like this: def draw(&block) clear! Module.mix_eval(MapperModule, &block) named_routes.install end Wow! That was simple. Mixico even handles all the eval-block-binding hackery for us. But the simplicity is a little deceptive: when we want to do a robust implementation, we run into two issues. First, we run into a challenge if we want to support multiple DSL blocks being invoked at once: for example in the case of nested blocks or multithreading. It is possible in such cases that a MapperModule is already mixed into the block's context. The mix_eval method by itself, as of this writing, doesn't handle this case well: the inner invocation will remove the module prematurely. Additional logic is necessary to track how many nested invocations (or invocations from other threads) want to mix-in each particular module into each object. The other challenge is that of creating the +MapperModule+ module, implementing the +connect+ method and any others we want to mix-in. Because we're adding methods to someone else's object, we need to be as unobtrusive as possible, yet we need to provide the necessary functionality, including invoking the add_route method back on the +RouteSet+. This is unfortunately not trivial. In particular, we need to give +MapperModule+ a way to reference the +RouteSet+. I'll describe a full implementation of this in the next section, but for now let's explore some possible approaches. Rails's original +Mapper+ proxy class, we recall from our earlier discussion, used an instance variable, @set, which pointed back to the +RouteSet+ instance and thus provided a way to invoke add_route. One approach could be to add such an instance variable to the block's context object, so it's available in methods of +MapperModule+. This seems to be the easiest approach, but it is also dangerous because it intrudes on the context object, adding an instance variable and potentially clobbering one used by the caller. Furthermore, in the case of nested blocks that try to add methods to the same object, the two blocks may clobber each other's instance variables. Instead of adding information to the block's context object, we could stash the information away in a global location, such as a class variable, that can be accessed by the +MapperModule+ from within the block. This is of course the same strategy we used to eliminate instance variables in the section on delegation. Again, this seems to work, until you have nested or multithreaded usage. It then becomes neccessary to keep a stack of references to handle nesting, and thread-local variables to handle multithreading-- all feasible to do, but a lot of work. A third approach involves dynamically generating a singleton module, "hard coding" a reference to the +RouteSet+ in the module. For example: def draw(&block) clear! save_self = self mapper_module = Module.new mapper_module.module_eval do define_method(:connect) do |path,options| save_self.add_route(path,options) end end Module.mix_eval(mapper_module, &block) named_routes.install end This probably can be made to work, and it also has the benefit of solving the nesting and multithreading issue neatly since each mixin is done exactly once. However, it seems to be a fairly heavyweight solution: creating a new module for every DSL block invocation may have performance implications. It is also not clear how to support constructs that are not available to define_method, such as blocks and parameter default values. However, such an approach may still be useful in certain cases when you need to generate a DSL dynamically based on the context. One more issue with the mixin strategy is that, like all implementations that drop the block parameter, there remains an ambiguity regarding whether methods should be directed to the DSL or to the surrounding context. In the implementations we've discussed previously, based on instance_eval, the actual behavior is fairly straightforward to reason about. A simple instance_eval disables method calls to the block's context altogether: you can call _only_ the DSL methods. An instance_eval with delegation re-enables method calls to the block's context but gives the DSL priority. If both the DSL and the surrounding block define the same method name, the DSL's method will be take precedence. Mixin's behavior is less straightforward, because of a subtlety in Ruby's method lookup behavior. Under most cases, it behaves similarly to an instance_eval with delegation: the DSL's methods take priority. However, if methods have been added directly to the object, they will take precedence over the DSL's methods. Following is an example of this case: # Suppose we have a DSL block available, via "call_my_dsl", # that implements the methods "foo" and "bar"... # First, let's implement a simple class class MyClass # A test method def foo puts "in foo" end end # Create an instance of MyClass obj = MyClass.new # Now, add a new method "bar" to the object. def obj.bar puts "in bar" end # Finally, add a method "run" that runs a DSL block def obj.run call_my_dsl do foo # DSL "foo" method takes precedence over MyClass#foo bar # The object's "bar" method takes precedence over DSL "bar" end end # At this point, obj has methods "foo", "bar", and "run" # Run the DSL block to test the behavior obj.run In the above example, suppose both +foo+ and +bar+ are methods of the DSL. They are also both defined as methods of +obj+. (+foo+ is available because it is a method of +MyClass+, while +bar+ is available because it is explicitly added to +obj+.) However, if you run the code, it calls the DSL's +foo+ but +obj+'s +bar+. Why? The reason points to a subtlety in how Ruby does method lookup. When you define a method in the way +foo+ is defined, it is just added to the class. However, when you define a method in the way +bar+ is defined, it is defined as a "singleton method", and added to the "singleton class", which is an anonymous class that holds methods defined directly on a particular object. It turns out that the singleton class is always given the highest priority in method lookup. So, for example, the lookup order for methods of +obj+ within the block would look like this: singleton methods of obj -> mixin module from the DSL -> methods of MyClass (e.g. bar, run) (e.g. foo, bar) (e.g. foo) So when the +foo+ method is called, it is not found in the singleton class, but it is found in the mixin, so the mixin's version is invoked. However, when +bar+ is called, it is found in the singleton class, so that version is invoked in favor of the mixin's version. Does this esoteric-sounding case actually happen in practice? In fact it does, quite frequently: class methods are singleton methods of the class object, so you should beware of this issue when designing a DSL block that will be called from a class method. Well, that was confusing. It is on account of such behavior that we need to take the method lookup ambiguity seriously when dealing with mixins. In fact, I would go so far as to suggest that the mixin implementation should always go hand-in-hand with a way to mitigate that ambiguity, such as Gray's arity check. As we have seen, the mixin idea seems like it may be a compelling solution, particularly in conjunction with Gray's arity check, but the implementation details present some challenges. It may be viable if a library can be written to hide the implementation complexity. Let's summarize this approach, and then proceed to examine such a library, one that uses some of the best of what we've discussed to make implementing DSL blocks simple. *Implementation*: * Install a mixin library such as mixico or mixology (or roll your own if necessary). * Define the DSL methods in a module. * Mix the module into the block's context before invoking the block, and remove it afterwards. * Carefully handle any issues involving nested blocks and multithreading while remaining unobtrusive. *Pros*: * Allows the concise syntax without a block parameter. * Doesn't change +self+, thus preserving the right behavior regarding helper methods and instance variables. *Cons*: * Requires an extension to Ruby to implement mixin removal. * Implementation is complicated and error-prone. * The helper method vs DSL method ambiguity remains, exhibiting surprising behavior in the presence of singleton methods. Use it when: parameterless blocks are desired and the method lookup ambiguity can be mitigated, as long as a library is available to handle the details of the implementation. === Blockenspiel: a comprehensive implementation Some of the implementations we have covered, especially the mixin implementation, have some compelling qualities, but are hampered by the difficulty of implementing them in a robust way. They could be viable if a library were present to handle the details. {Blockenspiel}[http://virtuoso.rubyforge.org/blockenspiel] was written to be that library. It first provides a comprehensive and robust implementation of the mixin strategy, correctly handling nesting and multithreading. It offers the option to perform an arity check, giving the caller the choice of whether or not to use a block parameter. You can even tell blockenspiel to use an alternate implementation, such as instance_eval, instead of a mixin, in those cases when it is appropriate. Finally, blockenspiel also provides an API for dynamic construction of DSLs. But most importantly, it is easy to use. To write a basic DSL, just follow the first and easiest implementation strategy, creating a proxy class that can be passed into the block as a parameter. Then instead of yielding the proxy object, pass it to blockenspiel, and it will do the rest. Our Rails routing example implemented using blockenspiel might look like this: class RouteSet class Mapper include Blockenspiel::DSL # tell blockenspiel this is a DSL proxy def initialize(set) @set = set end def connect(path, options = {}) @set.add_route(path, options) end # ... end # ... def draw(&block) clear! Blockenspiel.invoke(block, Mapper.new(self)) # blockenspiel does the rest named_routes.install end # ... def add_route(path, options = {}) # ... The code above is as simple as a block parameter or instance_eval implementation. However, it performs a full-fledged mixin implementation, and even throws in the arity check. We recall from the previous section that one of the chief challenges is to mediate communication between the mixin and proxy in a re-entrant and thread-safe way. The blockenspiel library implements this mediation using a global hash, avoiding the compatibility risk of adding instance variables to the block's context object, and avoiding the performance hit of dynamically generating proxies. All the implementation details are carefully handled behind the scenes. Atop this basic usage, blockenspiel provides two types of customization. First, you can customize the DSL, using a few simple directives to specify which methods on your proxy should be available in the mixin implementation. You can also cause methods to be available in the mixin under different names, thus sidestepping the attr_writer issue we discussed earlier. If you want methods of the form "attribute=" on your proxy object, blockenspiel provides a simple syntax for renaming them: class ConfigMethods include Blockenspiel::DSL attr_writer :author attr_writer :title dsl_method :set_author, :author= # Make the methods available in parameterless dsl_method :set_title, :title= # blocks under these alternate names. end Now, when we use block parameters, we use the methods of the original +ConfigMethods+ class: create_paper do |config| config.author = "Daniel Azuma" config.title = "Implementing DSL Blocks" end And, when we omit the parameter, the alternate method names are mixed in: create_paper do set_author "Daniel Azuma" set_title "Implementing DSL Blocks" end Second, you can customize the invocation-- for example specifying whether to perform an arity check, whether to use instance_eval instead of mixins, and various other minor behavioral adjustments-- simply by providing parameters to the Blockenspiel#invoke method. All the implementation details are handled by the blockenspiel library, leaving you free to focus on your API. Third, blockenspiel provides an API, itself a DSL block, letting you dynamically construct DSLs. Suppose, for the sake of argument, we wanted to let the caller optionally rename the +connect+ method. (Maybe we want to make the name "connect" available for named routes.) That is, suppose we wanted to provide this behavior: ActionController::Routing::Routes.draw(:method => :myconnect) do |map| map.myconnect ':controller/:action/:id' map.myconnect ':controller/:action/:page/:format' # etc. end This requires dynamic generation of the proxy class. We could implement it using blockenspiel as follows: class RouteSet # We don't define a static Mapper class anymore. Now it's dynamically generated. def draw(options={}, &block) clear! method_name = options[:method] || :connect # The method name for the DSL to use save_self = self # Save a reference to the RouteSet Blockenspiel.invoke(block) do # Dynamically create a "mapper" object add_method(method_name) do |path, *args| # Dynamically add the method save_self.add_route(path, *args) # Call back to the RouteSet end end named_routes.install end # ... def add_route(path, options = {}) # ... You can install blockenspiel as a gem. It is compatible with MRI 1.8.7 or later, MRI 1.9.1 or later, and JRuby 1.5 or later. gem install blockenspiel More information is available on blockenspiel's Rubyforge page at http://virtuoso.rubyforge.org/blockenspiel Source code is available on Github at http://github.com/dazuma/blockenspiel === Summary DSL blocks are a valuable and ubiquitous pattern for designing Ruby APIs. A flurry of discussion has recently surrounded the implementation of DSL blocks, particularly addressing the desire to eliminate block parameters. We have discussed several different strategies for DSL block implementation, each with its own advantages and disadvantages. The simplest strategy, creating a proxy object and passing a reference to the block as a parameter, is straightforward, safe, and widely used. However, sometimes we might want to provide a cleaner API by eliminating the block parameter. Parameterless blocks inherently pose some syntactic issues. First, it may be ambiguous whether a method is meant to be directed to the DSL or to the block's surrounding context. Second, certain constructions, such as those created by attr_writer, are syntactically not allowed and must be renamed. The simplest way to eliminate the block parameter is to change +self+ inside the block using instance_eval. This has the side effects of opening the implementation of the proxy object, and cutting off access to the context's helper methods and instance variables. It is possible to mitigate these side effects by delegating methods, and partially delegating instance variables, back to the context object. These are not foolproof mechanisms and are subject to a few cases of surprising behavior. The mixin strategy takes a different approach to parameterless blocks by temporarily "mixing" the DSL methods into the context object itself. This eliminates the side effects of changing the +self+ reference, but requires a more complex implementation, and somewhat exacerbates the method lookup ambiguity. Since the question of whether or not to take a block parameter may be best answered by the caller, it is often useful for an implementation to check the block's arity to determine whether to use a block parameter or a parameterless implementation. However, it is possible for this step to lead to dilution of the DSL's branding. The Blockenspiel library provides a concrete and robust implementation of DSL blocks, based on the best of these ideas. It hides the implementation complexity while providing a number of features useful for writing DSL blocks. === References {Daniel Azuma}[http://www.daniel-azuma.com/], {Blockenspiel}[http://virtuoso.rubyforge.org/blockenspiel] (Ruby library), 2008. {Ola Bini}[http://olabini.com/], {Don't overuse instance_eval and instance_exec}[http://olabini.com/blog/2008/09/dont-overuse-instance_eval-and-instance_exec], 2008.09.18 {Jamis Buck}[http://jamisbuck.org], {Under the hood: Rails' routing DSL}[http://weblog.jamisbuck.org/2006/10/2/under-the-hood-rails-routing-dsl], 2006.10.02. {James Edward Gray II}[http://blog.grayproductions.net/], {DSL Block Styles}[http://blog.grayproductions.net/articles/dsl_block_styles], 2008.10.07 {Dan Manges}[http://www.dcmanges.com], {Ruby DSLs: instance_eval with delegation}[http://www.dcmanges.com/blog/ruby-dsls-instance-eval-with-delegation], 2008.10.07 {Micah Martin}[http://www.8thlight.com/main/bios/micah], {Ruby DSL Blocks}[http://blog.8thlight.com/articles/2007/05/20/], 2007.05.20. {Mixology}[http://www.somethingnimble.com/bliki/mixology] (Ruby library), 2007. {RSpec}[http://rspec.info/] (Ruby library), 2005-2008. {Jim Weirich}[http://onestepback.org/], {Builder}[http://builder.rubyforge.org] (Ruby library), 2004-2008. {Jim Weirich}[http://onestepback.org/], {Builder Objects}[http://onestepback.org/index.cgi/Tech/Ruby/BuilderObjects.rdoc] 2004.08.24. {Jim Weirich}[http://onestepback.org/], {ruby-core:19153}[http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-core/19153], 2008.10.07 {Why The Lucky Stiff}[http://en.wikipedia.org/wiki/Why_the_lucky_stiff], {Markaby}[http://github.com/markaby/markaby] (Ruby library), 2006. {Why The Lucky Stiff}[http://en.wikipedia.org/wiki/Why_the_lucky_stiff], {Mixico}[http://github.com/rkh/mixico] (Ruby library), 2008. {Why The Lucky Stiff}[http://en.wikipedia.org/wiki/Why_the_lucky_stiff], Mixing Our Way Out Of Instance Eval? (no longer online), 2008.10.06. === About the author Daniel Azuma is Chief Software Architect at GeoPage. He has been working with Ruby since 2005, and finds the language generally pleasant to work with, though he thinks the scoping rules could use some improvement. His home page is at http://www.daniel-azuma.com/ blockenspiel-0.5.0/Blockenspiel.rdoc0000644000004100000410000003426612652620702017516 0ustar www-datawww-data== Blockenspiel Blockenspiel is a helper library designed to make it easy to implement DSL blocks. It is designed to be comprehensive and robust, supporting most common usage patterns, and working correctly in the presence of nested blocks and multithreading. This is an introduction to DSL blocks and the features of Blockenspiel. === What's a DSL block? A DSL block is an API pattern in which a method call takes a block that can provide further configuration for the call. A classic example is the {Rails}[http://www.rubyonrails.org/] route definition: ActionController::Routing::Routes.draw do |map| map.connect ':controller/:action/:id' map.connect ':controller/:action/:id.:format' end Some libraries go one step further and eliminate the need for a block parameter. {RSpec}[http://rspec.info/] is a well-known example: describe Stack do before(:each) do @stack = Stack.new end describe "(empty)" do it { @stack.should be_empty } it "should complain when sent #peek" do lambda { @stack.peek }.should raise_error(StackUnderflowError) end end end In both cases, the caller provides descriptive information in the block, using a domain-specific language. The second form, which eliminates the block parameter, often appears cleaner; however it is also sometimes less clear what is actually going on. === How does one implement such a beast? Implementing the first form is fairly straightforward. You would create a class defining the methods (such as +connect+ in our Rails routing example above) that should be available within the block. When, for example, the draw method is called with a block, you instantiate the class and yield it to the block. The second form is perhaps more mystifying. Somehow you would need to make the DSL methods available on the "self" object inside the block. There are several plausible ways to do this, such as using instance_eval. However, there are many subtle pitfalls in such techniques, and quite a bit of discussion has taken place in the Ruby community regarding how--or whether--to safely implement such a syntax. I have included a critical survey of the discussion in the document {ImplementingDSLblocks.rdoc}[link:ImplementingDSLblocks\_rdoc.html] for the curious. Blockenspiel takes what I consider the best of the solutions and implements them in a comprehensive way, shielding you from the complexity of the Ruby metaprogramming while offering a simple way to implement both forms of DSL blocks. === So what _is_ Blockenspiel? Blockenspiel operates on the following observations: * Implementing a DSL block that takes a parameter is straightforward. * Safely implementing a DSL block that doesn't take a parameter is tricky. With that in mind, Blockenspiel provides a set of tools that allow you to take an implementation of the first form of a DSL block, one that takes a parameter, and turn it into an implementation of the second form, one that doesn't take a parameter. Suppose you wanted to write a simple DSL block that takes a parameter: configure_me do |config| config.add_foo(1) config.add_bar(2) end You could write this as follows: class ConfigMethods def add_foo(value) # do something end def add_bar(value) # do something end end def configure_me yield ConfigMethods.new end That was easy. However, now suppose you wanted to support usage _without_ the "config" parameter. e.g. configure_me do add_foo(1) add_bar(2) end With Blockenspiel, you can do this in two quick steps. First, tell Blockenspiel that your +ConfigMethods+ class is a DSL. class ConfigMethods include Blockenspiel::DSL # <--- Add this line def add_foo(value) # do something end def add_bar(value) # do something end end Next, write your configure_me method using Blockenspiel: def configure_me(&block) Blockenspiel.invoke(block, ConfigMethods.new) end Now, your configure_me method supports _both_ DSL block forms. A caller can opt to use the first form, with a parameter, simply by providing a block that takes a parameter. Or, if the caller provides a block that doesn't take a parameter, the second form without a parameter is used. === How does that help me? (Or, why not just use instance_eval?) As noted earlier, some libraries that provide parameter-less DSL blocks use a simple instance_eval, and they could even support both the parameter and parameter-less mechanisms by checking the block arity: def configure_me(&block) if block.arity == 1 yield ConfigMethods.new else ConfigMethods.new.instance_eval(&block) end end That seems like a simple and effective technique that doesn't require a separate library, so why use Blockenspiel? Because instance_eval introduces a number of surprising problems. I discuss these issues in detail in {ImplementingDSLblocks.rdoc}[link:ImplementingDSLblocks\_rdoc.html], but just to get your feet wet, suppose the caller wanted to call its own methods inside the block: def callers_helper_method # ... end configure_me do add_foo(1) callers_helper_method # Error! self is now an instance of ConfigMethods # so this will fail with a NameError add_bar(2) end Blockenspiel employs a number of techniques to mitigate the ill effects of instance_eval. It delegates methods that are not part of the DSL, back to the enclosing context object, so that the caller retains access to helper methods. It also includes an optional experimental technique (not available on all ruby platforms) that temporarily mixes the DSL methods directly into the caller's +self+ object, so that instance variable access is retained. === Is that it? Although the basic usage is very simple, Blockenspiel is designed to be _comprehensive_. It supports all the use cases that I've run into during my own implementation of DSL blocks. Notably: By default, Blockenspiel lets the caller choose to use a parametered block or a parameterless block, based on whether or not the block actually takes a parameter. You can also disable one or the other, to force the use of either a parametered or parameterless block. You can also let the caller use your DSL by passing you a string or a file rather than a block. That is, you can create file-based DSLs such as the Rails routes file. You can control wich methods of the class are available from parameterless blocks, and/or make some methods available under different names. Here are a few examples: class ConfigMethods include Blockenspiel::DSL def add_foo # automatically added to the dsl # do stuff... end def my_private_method # do stuff... end dsl_method :my_private_method, false # remove from the dsl dsl_methods false # stop automatically adding methods to the dsl def another_private_method # not added # do stuff... end dsl_methods true # resume automatically adding methods to the dsl def add_bar # this method is automatically added # do stuff... end def add_baz # do stuff end dsl_method :add_baz_in_dsl, :add_baz # Method named differently # in a parameterless block end This is also useful, for example, when you use attr_writer. Parameterless blocks do not support attr_writer (or, by corollary, attr_accessor) well because methods with names of the form "attribute=" are syntactically indistinguishable from variable assignments: configure_me do |config| config.foo = 1 # works fine when the block has a parameter end configure_me do # foo = 1 # <--- Doesn't work: looks like a variable assignment set_foo(1) # <--- Fix it by renaming to this instead end # This is implemented like this:: class ConfigMethods include Blockenspiel::DSL attr_writer :foo dsl_method :set_foo, :foo= # Make "foo=" available as "set_foo" end This is in fact a common enough case that Blockenspiel includes conveninence tools for a DSL-friendly attr_writer and attr_accessor, providing an alternate syntax for setting attributes within a parameterless block: configure_me do # foo = 1 # This syntax wouldn't work, but foo 1 # this syntax is now supported. puts "foo is #{foo}" # The getter still works. end # This is implemented like this:: class ConfigMethods include Blockenspiel::DSL dsl_attr_accessor :foo # DSL-friendly attr_accessor end In some cases, you might want to dynamically generate a DSL object rather than defining a static class. Blockenspiel provides a tool to do just that. Here's an example: Blockenspiel.invoke(block) do add_method(:set_foo) do |value| my_foo = value end add_method(:set_things_using_block) do |value, &blk| my_foo = value my_bar = blk.call end end That API is itself a DSL block, and yes, Blockenspiel uses itself to implement this feature. By default Blockenspiel uses mixins, which usually exhibit fairly safe and non-surprising behavior. However, there are a few cases when you might want the instance_eval behavior anyway. RSpec is a good example of such a case, since the DSL is being used to construct objects, so it makes sense for instance variables inside the block to belong to the object being constructed. Blockenspiel gives you the option of choosing instance_eval in case you need it. Blockenspiel also provides a compromise behavior that uses a proxy to dispatch methods to the DSL object or the block's context. Blockenspiel also correctly handles nested blocks. e.g. configure_me do set_foo(1) configure_another do # A block within another block set_bar(2) configure_another do # A block within itself set_bar(3) end end end Blockenspiel provides three strategies for doing parameterless DSL blocks. The default strategy uses a proxy object that delegates unrecognized methods out to the calling context. It should work well for most cases. Second, some applications might want to use the simple instance_eval behavior. RSpec is a good example of such a case, since the DSL is being used to construct objects, so it makes sense for instance variables inside the block to belong to the object being constructed. Third, an experimental mixin strategy is provided, which adds the DSL methods directly to the context's self object, and removes them afterward. This is available on Rubinius and JRuby but not on MRI. Finally, Blockenspiel is thread safe, correctly handling, for example, the case of multiple threads trying to mix methods into the same object concurrently. === Requirements * Ruby 1.9.3 or later, JRuby 1.5 or later, or Rubinius 1.0 or later. === Installation gem install blockenspiel === Known issues and to-do items * Implementing wildcard DSL methods using method_missing doesn't work. I haven't yet decided on the right semantics for this case, or whether it is even a reasonable feature at all. * Including Blockenspiel::DSL in a module (rather than a class) is not yet supported, but this is planned for a future release. * Find a way to implement mixin behavior reliably on MRI. === Development and support Documentation is available at http://dazuma.github.com/blockenspiel/rdoc Source code is hosted on Github at http://github.com/dazuma/blockenspiel Contributions are welcome. Fork the project on Github. Build status: {}[http://travis-ci.org/dazuma/blockenspiel] Report bugs on Github issues at http://github.org/dazuma/blockenspiel/issues Contact the author at dazuma at gmail dot com. === Author / Credits Blockenspiel is written by Daniel Azuma (http://www.daniel-azuma.com/). The mixin implementation is based on a concept by the late Why The Lucky Stiff, documented in his 6 October 2008 blog posting entitled "Mixing Our Way Out Of Instance Eval?". The original link is gone, but you may find copies or mirrors out there. The unmixer code is based on {Mixology}[http://rubyforge.org/projects/mixology], version by Patrick Farley, anonymous z, Dan Manges, and Clint Bishop. The JRuby code is adapted from Mixology 0.1, and has been stripped down and modified to support JRuby >= 1.2. The Rubinius code was adapted from unreleased code in the Mixology source tree and modified to support Rubinius 1.0. I know Mixology 0.2 is now available, but its Rubinius support is not active, and I'd rather keep the unmixer bundled with Blockenspiel for now to reduce dependencies. Earlier versions of Blockenspiel also included a C extension, adapted from Mixology, to support mixins for MRI, but this code has been disabled due to issues with newer versions of Ruby. The dsl_attr_writer and dsl_attr_accessor feature came from a suggestion by Luis Lavena. === License Copyright 2008 Daniel Azuma. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder, nor the names of any other contributors to this software, may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.