blockenspiel 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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