blockenspiel 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/Manifest.txt ADDED
@@ -0,0 +1,9 @@
1
+ History.txt
2
+ ImplementingDSLblocks.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ lib/blockenspiel.rb
7
+ tests/tc_basic.rb
8
+ tests/tc_mixins.rb
9
+ tests/tc_dsl_methods.rb
data/README.txt ADDED
@@ -0,0 +1,346 @@
1
+ == Blockenspiel
2
+
3
+ Blockenspiel is a helper library designed to make it easy to implement DSL
4
+ blocks. It is designed to be comprehensive and robust, supporting most common
5
+ usage patterns, and working correctly in the presence of nested blocks and
6
+ multithreading.
7
+
8
+ === What's a DSL block?
9
+
10
+ A DSL block is an API pattern in which a method call takes a block that can
11
+ provide further configuration for the call. A classic example is the
12
+ {Rails}[http://www.rubyonrails.org/] route definition:
13
+
14
+ ActionController::Routing::Routes.draw do |map|
15
+ map.connect ':controller/:action/:id'
16
+ map.connect ':controller/:action/:id.:format'
17
+ end
18
+
19
+ Some libraries go one step further and eliminate the need for a block
20
+ parameter. {RSpec}[http://rspec.info/] is a well-known example:
21
+
22
+ describe Stack do
23
+ before(:each) do
24
+ @stack = Stack.new
25
+ end
26
+ describe "(empty)" do
27
+ it { @stack.should be_empty }
28
+ it "should complain when sent #peek" do
29
+ lambda { @stack.peek }.should raise_error(StackUnderflowError)
30
+ end
31
+ end
32
+ end
33
+
34
+ In both cases, the caller provides descriptive information in the block,
35
+ using a domain-specific language. The second form, which eliminates the block
36
+ parameter, often appears cleaner; however it is also sometimes less clear
37
+ what is actually going on.
38
+
39
+ === How does one implement such a beast?
40
+
41
+ Implementing the first form is fairly straightforward. You would create a
42
+ class defining the methods (such as +connect+ in our Rails routing example
43
+ above) that should be available within the block. When, for example, the
44
+ <tt>draw</tt> method is called with a block, you instantiate the class and
45
+ yield it to the block.
46
+
47
+ The second form is perhaps more mystifying. Somehow you would need to make
48
+ the DSL methods available on the "self" object inside the block. There are
49
+ several plausible ways to do this, such as using <tt>instance_eval</tt>.
50
+ However, there are many subtle pitfalls in such techniques, and quite a bit
51
+ of discussion has taken place in the Ruby community regarding how--or
52
+ whether--to safely implement such a syntax.
53
+
54
+ I have included a critical survey of the debate in the document
55
+ {ImplementingDSLblocks.txt}[link:files/ImplementingDSLblocks\_txt.html] for
56
+ the curious. Blockenspiel takes what I consider the best of the solutions and
57
+ implements them in a comprehensive way, shielding you from the complexity of
58
+ the Ruby metaprogramming while offering a simple way to implement both forms
59
+ of DSL blocks.
60
+
61
+ === So what _is_ Blockenspiel?
62
+
63
+ Blockenspiel operates on the following observations:
64
+
65
+ * Implementing a DSL block that takes a parameter is straightforward.
66
+ * Safely implementing a DSL block that <em>doesn't</em> take a parameter is tricky.
67
+
68
+ With that in mind, Blockenspiel provides a set of tools that allow you to
69
+ take an implementation of the first form of a DSL block, one that takes a
70
+ parameter, and turn it into an implementation of the second form, one that
71
+ doesn't take a parameter.
72
+
73
+ Suppose you wanted to write a simple DSL block that takes a parameter:
74
+
75
+ configure_me do |config|
76
+ config.add_foo(1)
77
+ config.add_bar(2)
78
+ end
79
+
80
+ You could write this as follows:
81
+
82
+ class ConfigMethods
83
+ def add_foo(value)
84
+ # do something
85
+ end
86
+ def add_bar(value)
87
+ # do something
88
+ end
89
+ end
90
+
91
+ def configure_me
92
+ yield ConfigMethods.new
93
+ end
94
+
95
+ That was easy. However, now suppose you wanted to support usage _without_
96
+ the "config" parameter. e.g.
97
+
98
+ configure_me do
99
+ add_foo(1)
100
+ add_bar(2)
101
+ end
102
+
103
+ With Blockenspiel, you can do this in two quick steps.
104
+ First, tell Blockenspiel that your +ConfigMethods+ class is a DSL.
105
+
106
+ class ConfigMethods
107
+ include Blockenspiel::DSL # <--- Add this line
108
+ def add_foo(value)
109
+ # do something
110
+ end
111
+ def add_bar(value)
112
+ # do something
113
+ end
114
+ end
115
+
116
+ Next, write your <tt>configure_me</tt> method using Blockenspiel:
117
+
118
+ def configure_me(&block)
119
+ Blockenspiel.invoke(block, ConfigMethods.new)
120
+ end
121
+
122
+ Now, your <tt>configure_me</tt> method supports _both_ DSL block forms. A
123
+ caller can opt to use the first form, with a parameter, simply by providing
124
+ a block that takes a parameter. Or, if the caller provides a block that
125
+ doesn't take a parameter, the second form without a parameter is used.
126
+
127
+ === How does that help me? (Or, why not just use instance_eval?)
128
+
129
+ As noted earlier, some libraries that provide parameter-less DSL blocks use
130
+ <tt>instance_eval</tt>, and they could even support both the parameter and
131
+ parameter-less mechanisms by checking the block arity:
132
+
133
+ def configure_me(&block)
134
+ if block.arity == 1
135
+ yield ConfigMethods.new
136
+ else
137
+ ConfigMethods.new.instance_eval(&block)
138
+ end
139
+ end
140
+
141
+ That seems like a simple and effective technique that doesn't require a
142
+ separate library, so why use Blockenspiel? Because <tt>instance_eval</tt>
143
+ introduces a number of surprising problems. I discuss these issues in detail
144
+ in {ImplementingDSLblocks.txt}[link:files/ImplementingDSLblocks\_txt.html],
145
+ but just to get your feet wet, suppose the caller wanted to call its own
146
+ methods inside the block:
147
+
148
+ def callers_helper_method
149
+ # ...
150
+ end
151
+
152
+ configure_me do
153
+ add_foo(1)
154
+ callers_helper_method # Error! self is now an instance of ConfigMethods
155
+ # so this will fail with a NameError
156
+ add_bar(2)
157
+ end
158
+
159
+ Blockenspiel by default does _not_ use the <tt>instance_eval</tt> technique.
160
+ Instead, it implements a mechanism using mixin modules, a technique first
161
+ {proposed}[http://hackety.org/2008/10/06/mixingOurWayOutOfInstanceEval.html]
162
+ by Why. In this technique, the <tt>add_foo</tt> and <tt>add_bar</tt> methods
163
+ are temporarily mixed into the caller's +self+ object. That is, +self+ does
164
+ not change, as it would if we used <tt>instance_eval</tt>, so helper methods
165
+ like <tt>callers_helper_method</tt> still remain available as expected. But,
166
+ the <tt>add_foo</tt> and <tt>add_bar</tt> methods are also made available
167
+ temporarily for the duration of the block. When called, they are intercepted
168
+ and redirected to your +ConfigMethods+ instance just as if you had called
169
+ them directly via a block parameter. Blockenspiel handles the object
170
+ redirection behind the scenes so you do not have to think about it. With
171
+ Blockenspiel, the caller retains access to its helper methods, and even its
172
+ own instance variables, within the block, because +self+ has not been
173
+ modified.
174
+
175
+ === Is that it?
176
+
177
+ Although the basic usage is very simple, Blockenspiel is designed to be
178
+ _comprehensive_. It supports all the use cases that I've run into during my
179
+ own implementation of DSL blocks. Notably:
180
+
181
+ By default, Blockenspiel lets the caller choose to use a parametered block
182
+ or a parameterless block, based on whether or not the block actually takes a
183
+ parameter. You can also disable one or the other, to force the use of either
184
+ a parametered or parameterless block.
185
+
186
+ You can control wich methods of the class are available from parameterless
187
+ blocks, and/or make some methods available under different names. Here are
188
+ a few examples:
189
+
190
+ class ConfigMethods
191
+ include Blockenspiel::DSL
192
+
193
+ def add_foo # automatically added to the dsl
194
+ # do stuff...
195
+ end
196
+
197
+ def my_private_method
198
+ # do stuff...
199
+ end
200
+ dsl_method :my_private_method, false # remove from the dsl
201
+
202
+ dsl_methods false # stop automatically adding methods to the dsl
203
+
204
+ def another_private_method # not added
205
+ # do stuff...
206
+ end
207
+
208
+ dsl_methods true # resume automatically adding methods to the dsl
209
+
210
+ def add_bar # this method is automatically added
211
+ # do stuff...
212
+ end
213
+
214
+ def add_baz
215
+ # do stuff
216
+ end
217
+ dsl_method :add_baz_in_dsl, :add_baz # Method named differently
218
+ # in a parameterless block
219
+ end
220
+
221
+ This is also useful, for example, when you use <tt>attr_writer</tt>.
222
+ Parameterless blocks do not support <tt>attr_writer</tt> (or, by corollary,
223
+ <tt>attr_accessor</tt>) well because methods with names of the form
224
+ "attribute=" are syntactically indistinguishable from variable assignments:
225
+
226
+ configure_me do |config|
227
+ config.foo = 1 # works fine when the block has a parameter
228
+ end
229
+
230
+ configure_me do
231
+ # foo = 1 # <--- Doesn't work: looks like a variable assignment
232
+ set_foo(1) # <--- Renamed to this instead
233
+ end
234
+
235
+ # This is implemented like this::
236
+ class ConfigMethods
237
+ include Blockenspiel::DSL
238
+ attr_writer :foo
239
+ dsl_method :set_foo, :foo= # Make "foo=" available as "set_foo"
240
+ end
241
+
242
+ In some cases, you might want to dynamically generate a DSL object rather
243
+ than defining a static class. Blockenspiel provides a tool to do just that.
244
+ Here's an example:
245
+
246
+ Blockenspiel.invoke(block) do
247
+ add_method(:set_foo) do |value|
248
+ my_foo = value
249
+ end
250
+ add_method(:set_things_using_block, :receive_block => true) do |value, blk|
251
+ my_foo = value
252
+ my_bar = blk.call
253
+ end
254
+ end
255
+
256
+ That API is in itself a DSL block, and yes, Blockenspiel uses itself to
257
+ implement this feature.
258
+
259
+ By default Blockenspiel uses mixins, which usually exhibit safe and
260
+ non-surprising behavior. However, there are a few cases when you might
261
+ want the <tt>instance_eval</tt> behavior anyway. RSpec is a good example of
262
+ such a case, since the DSL is being used to construct objects, so it makes
263
+ sense for instance variables inside the block to belong to the object
264
+ being constructed. Blockenspiel gives you the option of choosing
265
+ <tt>instance_eval</tt> in case you need it.
266
+
267
+ Blockenspiel also correctly handles nested blocks. e.g.
268
+
269
+ configure_me do
270
+ set_foo(1)
271
+ configure_another do # A block within another block
272
+ set_bar(2)
273
+ configure_another do # A block within itself
274
+ set_bar(3)
275
+ end
276
+ end
277
+ end
278
+
279
+ Finally, it is completely thread safe, correctly handling, for example, the
280
+ case of multiple threads trying to mix methods into the same object
281
+ concurrently.
282
+
283
+ === Requirements
284
+
285
+ * Ruby 1.8.6 or later.
286
+ * Rubygems
287
+ * mixology gem.
288
+
289
+ === Installation
290
+
291
+ sudo gem install blockenspiel
292
+
293
+ === Known issues and limitations
294
+
295
+ * Implementing wildcard DSL methods using <tt>method_missing</tt> doesn't
296
+ work. I haven't yet figured out the right semantics for this case.
297
+ * Ruby 1.9 and JRuby status not yet known.
298
+
299
+ === Development and support
300
+
301
+ Documentation is available at http://virtuoso.rubyforge.org/blockenspiel.
302
+
303
+ Source code is hosted by Github at http://github.com/dazuma/blockenspiel/tree.
304
+
305
+ Report bugs on RubyForge at http://rubyforge.org/projects/virtuoso.
306
+
307
+ Contact the author at dazuma at gmail dot com.
308
+
309
+ === Author / Credits
310
+
311
+ Blockenspiel is written by Daniel Azuma (http://www.daniel-azuma.com/).
312
+
313
+ The mixin implementation is based on a concept by Why The Lucky Stiff.
314
+ See his 6 October 2008 blog posting,
315
+ <em>{Mixing Our Way Out Of Instance Eval?}[http://hackety.org/2008/10/06/mixingOurWayOutOfInstanceEval.html]</em>
316
+ for further discussion.
317
+
318
+ === License
319
+
320
+ Copyright 2008 Daniel Azuma.
321
+
322
+ All rights reserved.
323
+
324
+ Redistribution and use in source and binary forms, with or without
325
+ modification, are permitted provided that the following conditions are met:
326
+
327
+ * Redistributions of source code must retain the above copyright notice,
328
+ this list of conditions and the following disclaimer.
329
+ * Redistributions in binary form must reproduce the above copyright notice,
330
+ this list of conditions and the following disclaimer in the documentation
331
+ and/or other materials provided with the distribution.
332
+ * Neither the name of the copyright holder, nor the names of any other
333
+ contributors to this software, may be used to endorse or promote products
334
+ derived from this software without specific prior written permission.
335
+
336
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
337
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
338
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
339
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
340
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
341
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
342
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
343
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
344
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
345
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
346
+ POSSIBILITY OF SUCH DAMAGE.
data/Rakefile ADDED
@@ -0,0 +1,49 @@
1
+ # -----------------------------------------------------------------------------
2
+ #
3
+ # Blockenspiel Rakefile
4
+ #
5
+ # -----------------------------------------------------------------------------
6
+ # Copyright 2008 Daniel Azuma
7
+ #
8
+ # All rights reserved.
9
+ #
10
+ # Redistribution and use in source and binary forms, with or without
11
+ # modification, are permitted provided that the following conditions are met:
12
+ #
13
+ # * Redistributions of source code must retain the above copyright notice,
14
+ # this list of conditions and the following disclaimer.
15
+ # * Redistributions in binary form must reproduce the above copyright notice,
16
+ # this list of conditions and the following disclaimer in the documentation
17
+ # and/or other materials provided with the distribution.
18
+ # * Neither the name of the copyright holder, nor the names of any other
19
+ # contributors to this software, may be used to endorse or promote products
20
+ # derived from this software without specific prior written permission.
21
+ #
22
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
26
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
27
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
28
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
29
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
30
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32
+ # POSSIBILITY OF SUCH DAMAGE.
33
+ # -----------------------------------------------------------------------------
34
+
35
+
36
+ require 'rubygems'
37
+ require 'hoe'
38
+ require File.expand_path("#{File.dirname(__FILE__)}/lib/blockenspiel.rb")
39
+
40
+ Hoe.new('blockenspiel', Blockenspiel::VERSION_STRING) do |p_|
41
+ p_.rubyforge_name = 'virtuoso'
42
+ p_.developer('Daniel Azuma', 'dazuma@gmail.com')
43
+ p_.author = ['Daniel Azuma']
44
+ p_.email = ['dazuma@gmail.com']
45
+ p_.test_globs = ['tests/tc_*.rb']
46
+ p_.extra_deps = [['mixology', '>= 0.1.0']]
47
+ p_.description_sections = ['blockenspiel']
48
+ p_.url = 'http://virtuoso.rubyforge.org/blockenspiel'
49
+ end
@@ -0,0 +1,544 @@
1
+ # -----------------------------------------------------------------------------
2
+ #
3
+ # Blockenspiel implementation
4
+ #
5
+ # -----------------------------------------------------------------------------
6
+ # Copyright 2008 Daniel Azuma
7
+ #
8
+ # All rights reserved.
9
+ #
10
+ # Redistribution and use in source and binary forms, with or without
11
+ # modification, are permitted provided that the following conditions are met:
12
+ #
13
+ # * Redistributions of source code must retain the above copyright notice,
14
+ # this list of conditions and the following disclaimer.
15
+ # * Redistributions in binary form must reproduce the above copyright notice,
16
+ # this list of conditions and the following disclaimer in the documentation
17
+ # and/or other materials provided with the distribution.
18
+ # * Neither the name of the copyright holder, nor the names of any other
19
+ # contributors to this software, may be used to endorse or promote products
20
+ # derived from this software without specific prior written permission.
21
+ #
22
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
26
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
27
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
28
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
29
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
30
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32
+ # POSSIBILITY OF SUCH DAMAGE.
33
+ # -----------------------------------------------------------------------------
34
+
35
+
36
+ require 'rubygems'
37
+ require 'mixology'
38
+
39
+
40
+ # == Blockenspiel
41
+ #
42
+ # The Blockenspiel module provides a namespace for Blockenspiel, as well as
43
+ # the main entry point method "invoke".
44
+
45
+ module Blockenspiel
46
+
47
+ # Current gem version
48
+ VERSION_STRING = '0.0.1'
49
+
50
+
51
+ # === DSL setup methods
52
+ #
53
+ # These class methods are available after you have included the
54
+ # Blockenspiel::DSL module.
55
+ #
56
+ # By default, a class that has DSL capability will automatically make
57
+ # all public methods available to parameterless blocks, except for the
58
+ # +initialize+ method, any methods whose names begin with an underscore,
59
+ # and any methods whose names end with an equals sign.
60
+ #
61
+ # If you want to change this behavior, use the directives defined here to
62
+ # control exactly which methods are available to parameterless blocks.
63
+
64
+ module DSLSetupMethods
65
+
66
+ # Called when DSLSetupMethods extends a class.
67
+ # This sets up the current class, and adds a hook that causes
68
+ # any subclass of the current class to also be set up.
69
+
70
+ def self.extended(klass_) # :nodoc:
71
+ unless klass_.instance_variable_defined?(:@_blockenspiel_module)
72
+ _setup_class(klass_)
73
+ def klass_.inherited(subklass_)
74
+ Blockenspiel::DSLSetupMethods._setup_class(subklass_)
75
+ super
76
+ end
77
+ end
78
+ end
79
+
80
+
81
+ # Set up a class.
82
+ # Creates a DSL module for this class, optionally delegating to the superclass's module.
83
+ # Also initializes the class's methods hash and active flag.
84
+
85
+ def self._setup_class(klass_) # :nodoc:
86
+ superclass_ = klass_.superclass
87
+ superclass_ = nil unless superclass_.respond_to?(:_get_blockenspiel_module)
88
+ mod_ = Module.new
89
+ if superclass_
90
+ mod_.module_eval do
91
+ include superclass_._get_blockenspiel_module
92
+ end
93
+ end
94
+ klass_.instance_variable_set(:@_blockenspiel_superclass, superclass_)
95
+ klass_.instance_variable_set(:@_blockenspiel_module, mod_)
96
+ klass_.instance_variable_set(:@_blockenspiel_methods, Hash.new)
97
+ klass_.instance_variable_set(:@_blockenspiel_active, nil)
98
+ end
99
+
100
+
101
+ # Hook called when a method is added.
102
+ # This automatically makes the method a DSL method according to the current setting.
103
+
104
+ def method_added(symbol_) # :nodoc:
105
+ if @_blockenspiel_active
106
+ dsl_method(symbol_)
107
+ elsif @_blockenspiel_active.nil?
108
+ if symbol_ != :initialize && symbol_.to_s !~ /^_/ && symbol_.to_s !~ /=$/
109
+ dsl_method(symbol_)
110
+ end
111
+ end
112
+ super
113
+ end
114
+
115
+
116
+ # Get this class's corresponding DSL module
117
+
118
+ def _get_blockenspiel_module # :nodoc:
119
+ @_blockenspiel_module
120
+ end
121
+
122
+
123
+ # Get information on the given DSL method name.
124
+ # Possible values are the name of the delegate method, false for method disabled,
125
+ # or nil for method never defined.
126
+
127
+ def _get_blockenspiel_delegate(name_) # :nodoc:
128
+ delegate_ = @_blockenspiel_methods[name_]
129
+ if delegate_.nil? && @_blockenspiel_superclass
130
+ @_blockenspiel_superclass._get_blockenspiel_delegate(name_)
131
+ else
132
+ delegate_
133
+ end
134
+ end
135
+
136
+
137
+ # Make a particular method available to parameterless DSL blocks.
138
+ #
139
+ # To explicitly make a method available to parameterless blocks:
140
+ # dsl_method :my_method
141
+ #
142
+ # To explicitly exclude a method from parameterless blocks:
143
+ # dsl_method :my_method, false
144
+ #
145
+ # To explicitly make a method available to parameterless blocks, but
146
+ # point it to a method of a different name on the target class:
147
+ # dsl_method :my_method, :target_class_method
148
+
149
+ def dsl_method(name_, delegate_=nil)
150
+ name_ = name_.to_sym
151
+ if delegate_
152
+ delegate_ = delegate_.to_sym
153
+ elsif delegate_.nil?
154
+ delegate_ = name_
155
+ end
156
+ @_blockenspiel_methods[name_] = delegate_
157
+ unless @_blockenspiel_module.public_method_defined?(name_)
158
+ @_blockenspiel_module.module_eval("
159
+ def #{name_}(*params_, &block_)
160
+ val_ = Blockenspiel._delegate(:#{name_}, params_, block_)
161
+ val_ == Blockenspiel::TARGET_MISMATCH ? super(*params_, &block_) : val_
162
+ end
163
+ ")
164
+ end
165
+ end
166
+
167
+
168
+ # Control the behavior of methods with respect to parameterless blocks,
169
+ # or make a list of methods available to parameterless blocks in bulk.
170
+ #
171
+ # To enable automatic exporting of methods to parameterless blocks.
172
+ # After executing this command, all public methods defined in the class
173
+ # will be available on parameterless blocks, until
174
+ # <tt>dsl_methods false</tt> is called.
175
+ # dsl_methods true
176
+ #
177
+ # To disable automatic exporting of methods to parameterless blocks.
178
+ # After executing this command, methods defined in this class will be
179
+ # excluded from parameterless blocks, until <tt>dsl_methods true</tt>
180
+ # is called.
181
+ # dsl_methods false
182
+ #
183
+ # To make a list of methods available to parameterless blocks in bulk:
184
+ # dsl_methods :my_method1, :my_method2, ...
185
+
186
+ def dsl_methods(*names_)
187
+ if names_.size == 0 || names_ == [true]
188
+ @_blockenspiel_active = true
189
+ elsif names_ == [false]
190
+ @_blockenspiel_active = false
191
+ else
192
+ if names_.last.kind_of?(Hash)
193
+ names_.pop.each do |name_, delegate_|
194
+ dsl_method(name_, delegate_)
195
+ end
196
+ end
197
+ names_.each do |name_|
198
+ dsl_method(name_, name_)
199
+ end
200
+ end
201
+ end
202
+
203
+ end
204
+
205
+
206
+ # === DSL activation module
207
+ #
208
+ # Include this module in a class to mark this class as a DSL class and
209
+ # make it possible for its methods to be called from a block that does not
210
+ # take a parameter.
211
+ #
212
+ # After you include this module, you can use the directives defined in
213
+ # DSLSetupMethods to control what methods are available to DSL blocks
214
+ # that do not take parameters.
215
+
216
+ module DSL
217
+
218
+ def self.included(klass_) # :nodoc:
219
+ klass_.extend(Blockenspiel::DSLSetupMethods)
220
+ end
221
+
222
+ end
223
+
224
+
225
+ # === DSL activation base class
226
+ #
227
+ # Subclasses of this base class are considered DSL classes.
228
+ # Methods of the class can be made available to be called from a block that
229
+ # doesn't take an explicit block parameter.
230
+ # You may use the directives defined in DSLSetupMethods to control how
231
+ # methods of the class are handled in such blocks.
232
+ #
233
+ # Subclassing this base class is functionally equivalent to simply
234
+ # including Blockenspiel::DSL in the class.
235
+
236
+ class Base
237
+
238
+ include Blockenspiel::DSL
239
+
240
+ end
241
+
242
+
243
+ # === Dynamically construct a target
244
+ #
245
+ # These methods are available in a block passed to Blockenspiel#invoke and
246
+ # can be used to dynamically define what methods are available from a block.
247
+ # See Blockenspiel#invoke for more information.
248
+
249
+ class Builder
250
+
251
+ include Blockenspiel::DSL
252
+
253
+
254
+ # This is a base class for dynamically constructed targets.
255
+ # The actual target class is an anonymous subclass of this base class.
256
+
257
+ class Target # :nodoc:
258
+
259
+ include Blockenspiel::DSL
260
+
261
+
262
+ # Add a method specification to the subclass.
263
+
264
+ def self._add_methodinfo(name_, block_, yields_)
265
+ (@_blockenspiel_methodinfo ||= Hash.new)[name_] = [block_, yields_]
266
+ module_eval("
267
+ def #{name_}(*params_, &block_)
268
+ self.class._invoke_methodinfo(:#{name_}, params_, block_)
269
+ end
270
+ ")
271
+ end
272
+
273
+
274
+ # Attempt to invoke the given method on the subclass.
275
+
276
+ def self._invoke_methodinfo(name_, params_, block_)
277
+ info_ = @_blockenspiel_methodinfo[name_]
278
+ if info_[1]
279
+ realparams_ = params_ + [block_]
280
+ info_[0].call(*realparams_)
281
+ else
282
+ info_[0].call(*params_)
283
+ end
284
+ end
285
+
286
+ end
287
+
288
+
289
+ # Sets up the dynamic target class.
290
+
291
+ def initialize # :nodoc:
292
+ @target_class = Class.new(Blockenspiel::Builder::Target)
293
+ @target_class.dsl_methods(false)
294
+ end
295
+
296
+
297
+ # Creates a new instance of the dynamic target class
298
+
299
+ def _create_target # :nodoc:
300
+ @target_class.new
301
+ end
302
+
303
+
304
+ # Make a method available within the block.
305
+ #
306
+ # Provide a name for the method, and a block defining the method's
307
+ # implementation.
308
+ #
309
+ # By default, a method of the same name is also made available in
310
+ # mixin mode. To change the name of the mixin method, set its name
311
+ # as the value of the <tt>:mixin</tt> parameter. To disable the
312
+ # mixin method, set the <tt>:mixin</tt> parameter to +false+.
313
+
314
+ def add_method(name_, opts_={}, &block_)
315
+ @target_class._add_methodinfo(name_, block_, opts_[:receive_block])
316
+ mixin_name_ = opts_[:mixin]
317
+ if mixin_name_ != false
318
+ mixin_name_ = name_ if mixin_name_.nil? || mixin_name_ == true
319
+ @target_class.dsl_method(mixin_name_, name_)
320
+ end
321
+ end
322
+
323
+ end
324
+
325
+
326
+ # :stopdoc:
327
+ TARGET_MISMATCH = Object.new
328
+ # :startdoc:
329
+
330
+ @_target_stacks = Hash.new
331
+ @_mixin_counts = Hash.new
332
+ @_mutex = Mutex.new
333
+
334
+
335
+ # === Invoke a given block.
336
+ #
337
+ # This is the meat of Blockenspiel. Call this function to invoke a block
338
+ # provided by the user of your API.
339
+ #
340
+ # Normally, this method will check the block's arity to see whether it
341
+ # takes a parameter. If so, it will pass the given target to the block.
342
+ # If the block takes no parameter, and the given target is an instance of
343
+ # a class with DSL capability, the DSL methods are made available on the
344
+ # caller's self object so they may be called without a block parameter.
345
+ #
346
+ # Recognized options include:
347
+ #
348
+ # <tt>:parameterless</tt>::
349
+ # If set to false, disables parameterless blocks and always attempts to
350
+ # pass a parameter to the block. Otherwise, you may set it to one of
351
+ # three behaviors for parameterless blocks: <tt>:mixin</tt> (the
352
+ # default), <tt>:mixin_inheriting</tt>, and <tt>:instance</tt>. See
353
+ # below for a description of these behaviors.
354
+ # <tt>:parameter</tt>::
355
+ # If set to false, disables blocks with parameters, and always attempts
356
+ # to use parameterless blocks. Default is true, enabling parameter mode.
357
+ #
358
+ # The following values control the precise behavior of parameterless
359
+ # blocks. These are values for the <tt>:parameterless</tt> option.
360
+ #
361
+ # <tt>:mixin</tt>::
362
+ # This is the default behavior. DSL methods from the target are
363
+ # temporarily overlayed on the caller's self object, but self is itself
364
+ # not modified, so the helper methods and instance variables from the
365
+ # caller's closure remain available. The DSL methods are removed when
366
+ # the block completes.
367
+ # <tt>:mixin_inheriting</tt>::
368
+ # This behavior is the same as mixin, with an additional feature when
369
+ # DSL blocks are nested. Under normal mixin, only the current block's
370
+ # DSL methods are available; any outer blocks have their methods
371
+ # disabled. If you use mixin_inheriting, and a method is not implemented
372
+ # in the current block, then the next outer block is given a chance to
373
+ # handle it-- that is, this block "inherits" methods from any block it
374
+ # is nested within.
375
+ # <tt>:instance</tt>::
376
+ # This behavior actually changes +self+ to the target object using
377
+ # <tt>instance_eval</tt>. Thus, the caller loses access to its own
378
+ # helper methods and instance variables, and instead gains access to the
379
+ # target object's instance variables.
380
+ #
381
+ # === Dynamic target generation
382
+ #
383
+ # It is also possible to dynamically generate a target object by passing
384
+ # a block to this method. This is probably best illustrated by example:
385
+ #
386
+ # Blockenspiel.invoke(block) do
387
+ # add_method(:set_foo) do |value|
388
+ # my_foo = value
389
+ # end
390
+ # add_method(:set_things_from_block, :receive_block => true) do |value,blk|
391
+ # my_foo = value
392
+ # my_bar = blk.call
393
+ # end
394
+ # end
395
+ #
396
+ # The above is roughly equivalent to invoking Blockenspiel with an
397
+ # instance of this target class:
398
+ #
399
+ # class MyFooTarget
400
+ # include Blockenspiel::DSL
401
+ # def set_foo(value)
402
+ # set_my_foo_from(value)
403
+ # end
404
+ # def set_things_from_block(value)
405
+ # set_my_foo_from(value)
406
+ # set_my_bar_from(yield)
407
+ # end
408
+ # end
409
+ #
410
+ # Blockenspiel.invoke(block, MyFooTarget.new)
411
+ #
412
+ # The obvious advantage of using dynamic object generation is that you are
413
+ # creating methods using closures, which provides the opportunity to, for
414
+ # example, modify closure variables such as my_foo. This is more difficult
415
+ # to do when you create a target class since its methods do not have access
416
+ # to outside data. Hence, in the above example, we hand-waved, assuming the
417
+ # existence of some method called "set_my_foo_from".
418
+ #
419
+ # The disadvantage is performance. If you dynamically generate a target
420
+ # object, it involves parsing and creating a new class whenever it is
421
+ # invoked. Thus, it is recommended that you use this technique for calls
422
+ # that are not used repeatedly, such as one-time configuration.
423
+ #
424
+ # See the Blockenspiel::Builder class for more details on add_method.
425
+ #
426
+ # (And yes, you guessed it: this API is a DSL block, and is itself
427
+ # implemented using Blockenspiel.)
428
+
429
+ def self.invoke(block_, target_=nil, opts_={}, &builder_block_)
430
+
431
+ # Handle this case gracefully
432
+ return nil unless block_
433
+
434
+ # Handle dynamic target generation
435
+ if builder_block_
436
+ opts_ = target_ || opts_
437
+ builder_ = Blockenspiel::Builder.new
438
+ invoke(builder_block_, builder_)
439
+ target_ = builder_._create_target
440
+ end
441
+
442
+ # Attempt parameterless block
443
+ parameterless_ = opts_[:parameterless]
444
+ if parameterless_ != false && (block_.arity == 0 || block_.arity == -1)
445
+ if parameterless_ == :instance
446
+
447
+ # Instance-eval behavior.
448
+ # Note: this does not honor DSL method renaming, etc.
449
+ # Not sure how best to handle those cases, since we cannot
450
+ # overlay the module on its own target.
451
+ return target_.instance_eval(&block_)
452
+
453
+ else
454
+
455
+ # Mixin behavior
456
+ mod_ = target_.class._get_blockenspiel_module rescue nil
457
+ if mod_
458
+
459
+ # Get the thread and self context
460
+ thread_id_ = Thread.current.object_id
461
+ object_ = Kernel.eval('self', block_.binding)
462
+ object_id_ = object_.object_id
463
+
464
+ # Store the target for inheriting.
465
+ # We maintain a target call stack per thread.
466
+ target_stack_ = @_target_stacks[thread_id_] ||= Array.new
467
+ target_stack_.push([target_, parameterless_ == :mixin_inheriting])
468
+
469
+ # Mix this module into the object, if required.
470
+ # This ensures that we keep track of the number of requests to
471
+ # mix this module in, from nested blocks and possibly multiple threads.
472
+ @_mutex.synchronize do
473
+ count_ = @_mixin_counts[[object_id_, mod_]]
474
+ if count_
475
+ @_mixin_counts[[object_id_, mod_]] = count_ + 1
476
+ else
477
+ @_mixin_counts[[object_id_, mod_]] = 1
478
+ object_.mixin(mod_)
479
+ end
480
+ end
481
+
482
+ begin
483
+
484
+ # Now call the block
485
+ return block_.call
486
+
487
+ ensure
488
+
489
+ # Clean up the target stack
490
+ target_stack_.pop
491
+ @_target_stacks.delete(thread_id_) if target_stack_.size == 0
492
+
493
+ # Remove the mixin from the object, if required.
494
+ @_mutex.synchronize do
495
+ count_ = @_mixin_counts[[object_id_, mod_]]
496
+ if count_ == 1
497
+ @_mixin_counts.delete([object_id_, mod_])
498
+ object_.unmix(mod_)
499
+ else
500
+ @_mixin_counts[[object_id_, mod_]] = count_ - 1
501
+ end
502
+ end
503
+
504
+ end
505
+
506
+ end
507
+ # End mixin behavior
508
+
509
+ end
510
+ end
511
+
512
+ # Attempt parametered block
513
+ if opts_[:parameter] != false && block_.arity != 0
514
+ return block_.call(target_)
515
+ end
516
+
517
+ # Last resort fall-back
518
+ return block_.call
519
+
520
+ end
521
+
522
+
523
+ # This implements the mapping between DSL module methods and target object methods.
524
+ # We look up the current target object based on the current thread.
525
+ # Then we attempt to call the given method on that object.
526
+ # If we can't find an appropriate method to call, return the special value TARGET_MISMATCH.
527
+
528
+ def self._delegate(name_, params_, block_) # :nodoc:
529
+ target_stack_ = @_target_stacks[Thread.current.object_id]
530
+ return TARGET_MISMATCH unless target_stack_
531
+ target_stack_.reverse_each do |elem_|
532
+ target_ = elem_[0]
533
+ target_class_ = target_.class
534
+ delegate_ = target_class_._get_blockenspiel_delegate(name_)
535
+ if delegate_ && target_class_.public_method_defined?(delegate_)
536
+ return target_.send(delegate_, *params_, &block_)
537
+ end
538
+ return TARGET_MISMATCH unless elem_[1]
539
+ end
540
+ return TARGET_MISMATCH
541
+ end
542
+
543
+
544
+ end