blockenspiel 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/ImplementingDSLblocks.txt +686 -0
- data/Manifest.txt +9 -0
- data/README.txt +346 -0
- data/Rakefile +49 -0
- data/lib/blockenspiel.rb +544 -0
- data/tests/tc_basic.rb +135 -0
- data/tests/tc_dsl_methods.rb +283 -0
- data/tests/tc_mixins.rb +206 -0
- metadata +87 -0
data/Manifest.txt
ADDED
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
|
data/lib/blockenspiel.rb
ADDED
@@ -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
|