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/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
|