blockenspiel 0.0.4 → 0.1.0
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 +7 -0
- data/ImplementingDSLblocks.txt +84 -61
- data/lib/blockenspiel.rb +6 -3
- data/tests/tc_behaviors.rb +53 -1
- data/tests/tc_mixins.rb +32 -0
- metadata +4 -4
data/History.txt
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
=== 0.1.0 / 2008-10-29
|
2
|
+
|
3
|
+
* Alpha release, opened for public feedback
|
4
|
+
* Tightened constraints on block parameters
|
5
|
+
* Added some test cases for threads and parameter constraints
|
6
|
+
* Revisions to the Implementing DSL Blocks paper
|
7
|
+
|
1
8
|
=== 0.0.4 / 2008-10-24
|
2
9
|
|
3
10
|
* Improvements to the logic for choosing behaviors
|
data/ImplementingDSLblocks.txt
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
== Implementing DSL Blocks
|
2
2
|
|
3
|
-
by Daniel Azuma,
|
3
|
+
by Daniel Azuma, 29 October 2008
|
4
4
|
|
5
|
-
A <em>DSL block</em> is a construct commonly used in Ruby APIs. In this paper I present an overview of
|
5
|
+
A <em>DSL block</em> is a construct commonly used in Ruby APIs, in which a DSL (domain-specific language) is made available inside a block passed to an API call. In this paper I present an overview of different implementation strategies for this important pattern. I will first describe the features of DSL blocks, utilizing illustrations from several well-known Ruby libraries. I will then survey and critique five implementation strategies that have been put forth. Finally, I will present a new library, {Blockenspiel}[http://virtuoso.rubyforge.org/blockenspiel], designed to be a comprehensive implementation of DSL blocks.
|
6
6
|
|
7
7
|
=== An illustrative overview of DSL blocks
|
8
8
|
|
9
|
-
If you've done much Ruby programming, chances are you've run into mini-DSLs (
|
9
|
+
If you've done much Ruby programming, chances are you've run into mini-DSLs (domain-specific languages) that live inside blocks. Perhaps you've encountered them in Ruby standard library calls, such as <tt>File#open</tt>, a call that lets you interact with a stream while performing automatic setup and cleanup for you:
|
10
10
|
|
11
11
|
File.open("myfile.txt") do |io|
|
12
12
|
io.each_line do |line|
|
@@ -14,7 +14,7 @@ If you've done much Ruby programming, chances are you've run into mini-DSLs (Dom
|
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
|
-
|
17
|
+
Perhaps you've used the XML {builder}[http://builder.rubyforge.org/] library, which uses nested blocks to match the structure of the XML being generated:
|
18
18
|
|
19
19
|
builder = Builder::XmlMarkup.new
|
20
20
|
builder.page do
|
@@ -27,7 +27,21 @@ Or perhaps you've used the XML {builder}[http://builder.rubyforge.org/] library,
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
|
-
|
30
|
+
The {Markaby}[http://code.whytheluckystiff.net/markaby/] library also uses nested blocks to generate html, but is able to do so more succinctly without requiring you to explicitly reference a builder object:
|
31
|
+
|
32
|
+
Markaby::Builder.new.html do
|
33
|
+
head { title "Boats.com" }
|
34
|
+
body do
|
35
|
+
h1 "Boats.com has great deals"
|
36
|
+
ul do
|
37
|
+
li "$49 for a canoe"
|
38
|
+
li "$39 for a raft"
|
39
|
+
li "$29 for a huge boot that floats and can fit 5 people"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
Perhaps you've described testing scenarios using {RSpec}[http://rspec.info/], building and documenting test cases using English-sounding commands such as "describe" and "it_should_behave_like":
|
31
45
|
|
32
46
|
describe Stack do
|
33
47
|
|
@@ -53,7 +67,7 @@ Or perhaps you've described testing scenarios using {RSpec}[http://rspec.info/],
|
|
53
67
|
|
54
68
|
# etc...
|
55
69
|
|
56
|
-
|
70
|
+
Perhaps you were introduced to Ruby via the {Rails}[http://www.rubyonrails.org/] framework, which sets up configuration via blocks:
|
57
71
|
|
58
72
|
ActionController::Routing::Routes.draw do |map|
|
59
73
|
map.connect ':controller/:action/:id'
|
@@ -71,15 +85,15 @@ Blocks are central to Ruby as a language, and it feels natural to Ruby programme
|
|
71
85
|
|
72
86
|
=== Let's be more precise about what we mean by "DSL block".
|
73
87
|
|
74
|
-
Blocks in Ruby are used for a variety of purposes. In many cases, they are used to provide _callbacks_, specifying functionality to inject into an operation. If you come from a functional programming background, you might see them as lambda expressions. A simple example is the +each+ method, which iterates over a collection, using the given block as a callback that allows the caller to specify processing to perform on each element.
|
88
|
+
Blocks in Ruby are used for a variety of purposes. In many cases, they are used to provide _callbacks_, specifying functionality to inject into an operation. If you come from a functional programming background, you might see them as lambda expressions; in object-oriented-speak, they implement the Visitor pattern. A simple example is the +each+ method, which iterates over a collection, using the given block as a callback that allows the caller to specify processing to perform on each element.
|
75
89
|
|
76
90
|
When we speak of DSL blocks, we are describing something conceptually and semanticaly different. Rather than looking for a specification of _functionality_, the method wants to provide the caller with a _language_ to _describe_ something. The block merely serves as a space in which to use that language.
|
77
91
|
|
78
|
-
Consider the Rails Routing example above. The Rails application needs to specify how URLs should be interpreted as commands sent to controllers, and, conversely, how command descriptions should be expressed as URLs. Rails thus defines a language that can be used to describe these mappings. The language uses the "connect" verb, a string with embedded codes describing the URL's various parts, and optional parameters that specify further details about the mapping.
|
92
|
+
Consider the Rails Routing example above. The Rails application needs to specify how URLs should be interpreted as commands sent to controllers, and, conversely, how command descriptions should be expressed as URLs. Rails thus defines a language that can be used to describe these mappings. The language uses the "connect" verb, which interprets a string with embedded codes describing the URL's various parts, and optional parameters that specify further details about the mapping.
|
79
93
|
|
80
94
|
The Rails Initializer illustrates another common pattern: that of using a DSL block to perform extended configuration of the method call. Again, a language is being defined here: certain property names such as "time_zone" have meanings understood by the Rails framework.
|
81
95
|
|
82
|
-
Note that in both this case and the Routing case, the information contained in the block is descriptive. It is possible to imagine a syntax in which all the necessary information is passed into the method (<tt>Routes#draw</tt> or <tt>Initializer#run</tt>) as parameters
|
96
|
+
Note that in both this case and the Routing case, the information contained in the block is descriptive. It is possible to imagine a syntax in which all the necessary information is passed into the method (<tt>Routes#draw</tt> or <tt>Initializer#run</tt>) as parameters, perhaps as a large hash or other complex data structure. However, in many cases, providing this information via a block-based language makes the code much more readable.
|
83
97
|
|
84
98
|
The RSpec example illustrates a more sophisticated case with many keywords and multiple levels of blocks, but it shares common features with the Rails examples. Again, a language is being defined to describe things that could conceivably have been passed in as parameters, but are being specified in a block for clarity and readability.
|
85
99
|
|
@@ -90,11 +104,11 @@ So far, we can see that DSL blocks have the following properties:
|
|
90
104
|
* A method accepts a block from the caller, and executes the block exactly once.
|
91
105
|
* The domain-specific language is available to the caller lexically within the block.
|
92
106
|
|
93
|
-
As far as I have been able to determine, the term "DSL block" originated in 2007 with a {blog post}[http://blog.8thlight.com/articles/2007/05/20/] by Micah Martin. In it, he describes a way to implement certain types of DSL blocks using <tt>instance_eval</tt>, calling the technique the "DSL
|
107
|
+
As far as I have been able to determine, the term "DSL block" originated in 2007 with a {blog post}[http://blog.8thlight.com/articles/2007/05/20/] by Micah Martin. In it, he describes a way to implement certain types of DSL blocks using <tt>instance_eval</tt>, calling the technique the "DSL Block Pattern". We will discuss the nuances of the <tt>instance_eval</tt> implementation in greater detail below. But first, let us ease into the implementation discussion by describing a simple strategy that has worked very well for many libraries, including Rails.
|
94
108
|
|
95
109
|
=== Implementation strategy 1: block parameters
|
96
110
|
|
97
|
-
In 2006, Jamis Buck, one of the Rails core developers, posted a set of articles describing the Rails routing implementation. Tucked away at the top the {first article}[http://weblog.jamisbuck.org/2006/10/2/under-the-hood-rails-routing-dsl] is a code snippet showing the DSL block implementation for Rails routing. This code, along with some of its context
|
111
|
+
In 2006, Jamis Buck, one of the Rails core developers, posted a set of articles describing the Rails routing implementation. Tucked away at the top the {first article}[http://weblog.jamisbuck.org/2006/10/2/under-the-hood-rails-routing-dsl] is a code snippet showing the DSL block implementation for Rails routing. This code, along with some of its context in the file <tt>action_controller/routing/route_set.rb</tt> (from Rails version 2.1.1), is listed below.
|
98
112
|
|
99
113
|
class RouteSet
|
100
114
|
|
@@ -122,7 +136,7 @@ In 2006, Jamis Buck, one of the Rails core developers, posted a set of articles
|
|
122
136
|
def add_route(path, options = {})
|
123
137
|
# ...
|
124
138
|
|
125
|
-
Recall how we specify routes in Rails: we call the +draw+ method, and pass it a block. The block receives a parameter that we call "+map+". We can then create routes by calling the +connect+ method on the parameter
|
139
|
+
Recall how we specify routes in Rails: we call the +draw+ method, and pass it a block. The block receives a parameter that we call "+map+". We can then create routes by calling the +connect+ method on the parameter, as follows:
|
126
140
|
|
127
141
|
ActionController::Routing::Routes.draw do |map|
|
128
142
|
map.connect ':controller/:action/:id'
|
@@ -144,7 +158,7 @@ However, some have argued that it is too verbose. Why, in a DSL, is it necessary
|
|
144
158
|
# etc.
|
145
159
|
end
|
146
160
|
|
147
|
-
In the next section we will look at
|
161
|
+
In the next section we will look more closely at the pros and cons of this alternate syntax. But first, let us summarize our discussion of the "block parameter" implementation.
|
148
162
|
|
149
163
|
*Implementation*:
|
150
164
|
|
@@ -163,9 +177,9 @@ In the next section we will look at this possible syntax improvement more closel
|
|
163
177
|
|
164
178
|
<b>Use it when</b>: you want a simple, effective DSL block and don't mind requiring a parameter.
|
165
179
|
|
166
|
-
===
|
180
|
+
=== The parameterless block syntax
|
167
181
|
|
168
|
-
Much of the recent discussion surrounding DSL blocks originates from a desire to eliminate the block parameter. A domain-specific _language_, it is reasoned, should be as natural and concise as possible, and should not be tied down to the syntax of method invocation. In many cases, eliminating
|
182
|
+
Much of the recent discussion surrounding DSL blocks originates from a desire to eliminate the block parameter. A domain-specific _language_, it is reasoned, should be as natural and concise as possible, and should not be tied down to the syntax of method invocation. In many cases, eliminating the block parameter would have an enormous impact on the readability of a DSL block. One common example is the case of nested blocks, which, because of Ruby 1.8's scoping semantics, require different variable and parameter names. Consider an imaginary DSL block that looks like this:
|
169
183
|
|
170
184
|
create_container do |container|
|
171
185
|
container.create_subcontainer do |subcontainer1|
|
@@ -199,7 +213,7 @@ That was clunky. Wouldn't it be nice to instead see this?...
|
|
199
213
|
end
|
200
214
|
end
|
201
215
|
|
202
|
-
While this
|
216
|
+
While this appears to be an improvement, it does come at a cost. First, certain method names become syntactically unavailable when you eliminate the method call syntax. Consider, for example, this simple DSL proxy object that uses <tt>attr_writer</tt>...
|
203
217
|
|
204
218
|
class ConfigMethods
|
205
219
|
attr_writer :author
|
@@ -220,7 +234,14 @@ However, if you try to eliminate the block parameter, you run into this dilemma:
|
|
220
234
|
title = "Implementing DSL Blocks" # look like local variable assignments!
|
221
235
|
end
|
222
236
|
|
223
|
-
|
237
|
+
If you want to retain the <tt>attr_writer</tt> syntax, you must make it clear to the Ruby parser that you are invoking a method call. For example:
|
238
|
+
|
239
|
+
create_paper do
|
240
|
+
self.author = "Daniel Azuma" # These are now clearly method calls
|
241
|
+
self.title = "Implementing DSL Blocks"
|
242
|
+
end
|
243
|
+
|
244
|
+
Unfortunately, this negates some of the benefit of removing the block parameter in the first place. A similar syntactic issue occurs with many operators, notably <tt>[]=</tt>.
|
224
245
|
|
225
246
|
Second, and more importantly, by eliminating the block parameter, we eliminate the primary means of distinguishing which methods belong to the DSL, and which methods do not. For example, in our routing example, if we eliminate the parameter, like so:
|
226
247
|
|
@@ -230,13 +251,13 @@ Second, and more importantly, by eliminating the block parameter, we eliminate t
|
|
230
251
|
# etc.
|
231
252
|
end
|
232
253
|
|
233
|
-
...we now _assume_ that the +connect+ method is part of the DSL, but that is no longer explicit in the syntax. If,
|
254
|
+
...we now _assume_ that the +connect+ method is part of the DSL, but that is no longer explicit in the syntax. If, +connect+ also happens to be a method of whatever object was +self+ in the context of the block, which method should be called? There is a method lookup ambiguity inherent to the syntax itself, and, as we shall see, different implementations of parameterless blocks will resolve this ambiguity in different, and sometimes confusing, ways.
|
234
255
|
|
235
256
|
Despite the above caveats inherent to the syntax, the desire to eliminate the block parameter is quite strong. Let's consider how it can be done.
|
236
257
|
|
237
258
|
=== Implementation strategy 2: instance_eval
|
238
259
|
|
239
|
-
Micah Martin's {blog post}[http://blog.8thlight.com/articles/2007/05/20/] describes an implementation strategy that does not require the block to take a parameter. He suggests using a powerful, if sometimes confusing, Ruby metaprogramming tool called <tt>instance_eval</tt>. This method, defined on the +Object+ class so it is available to every object, has a simple function: it executes a block given it, but does so with the +self+ reference redirected to the receiver. Hence, within the block, calling a method, or accessing an instance variable or class variable, (or, in Ruby 1.9, accessing a constant), will begin at a different place.
|
260
|
+
Micah Martin's {blog post}[http://blog.8thlight.com/articles/2007/05/20/] describes an implementation strategy that does not require the block to take a parameter. He suggests using a powerful, if sometimes confusing, Ruby metaprogramming tool called <tt>instance_eval</tt>. This method, defined on the +Object+ class so it is available to every object, has a simple function: it executes a block given it, but does so with the +self+ reference redirected to the receiver. Hence, within the block, calling a method, or accessing an instance variable or class variable, (or, in Ruby 1.9, accessing a constant), will begin the lookup process at a different place.
|
240
261
|
|
241
262
|
It is perhaps instructive to see an example. Let's create a simple class
|
242
263
|
|
@@ -245,7 +266,7 @@ It is perhaps instructive to see an example. Let's create a simple class
|
|
245
266
|
@instvar = 1
|
246
267
|
end
|
247
268
|
def foo
|
248
|
-
puts "in foo"
|
269
|
+
puts "in foo: var=#{@instvar}"
|
249
270
|
end
|
250
271
|
end
|
251
272
|
|
@@ -258,9 +279,10 @@ Things to note here is that the method +foo+ and the instance variable <tt>@inst
|
|
258
279
|
x.instance_eval do # change self to point to x during the block
|
259
280
|
puts @instvar.inspect # prints "1" since self now points at x
|
260
281
|
@instvar = 2 # changes x's @instvar to 2
|
261
|
-
foo # calls x's foo and prints "in foo"
|
282
|
+
foo # calls x's foo and prints "in foo: var=2"
|
262
283
|
puts x == self # prints "true". The local variable x is still accessible
|
263
284
|
end # end of the block. self is now back to the Tester instance
|
285
|
+
puts x == self # prints "false"
|
264
286
|
puts @instvar.inspect # prints "nil" since Tester still has no @instvar
|
265
287
|
foo # NameError since Tester has no foo method.
|
266
288
|
end
|
@@ -304,9 +326,8 @@ This modified version of the routing API now no longer requires a block paramete
|
|
304
326
|
|
305
327
|
Well, not so fast. Our implementation here has a number of subtle and surprising side effects. Suppose, for instance, we were to write a little helper method to help us generate URLs:
|
306
328
|
|
307
|
-
URL_PREFIX = 'mywebsite/:controller/:action/'
|
308
329
|
def makeurl(*params)
|
309
|
-
|
330
|
+
'mywebsite/:controller/:action/' + params.map{ |e| e.inspect }.join('/')
|
310
331
|
end
|
311
332
|
|
312
333
|
Using the above method, it becomes easy to generate URL strings:
|
@@ -344,13 +365,13 @@ The problem gets worse. Changing +self+ affects not only how methods are looked
|
|
344
365
|
# etc.
|
345
366
|
end
|
346
367
|
|
347
|
-
What happened? If we recall, <tt>@set</tt> is used by the +Mapper+ object to point back to the routing +RouteSet+. It is how the proxy knows what it is proxying for. But since we've used <tt>instance_eval</tt>, we now have free
|
368
|
+
What happened? If we recall, <tt>@set</tt> is used by the +Mapper+ object to point back to the routing +RouteSet+. It is how the proxy knows what it is proxying for. But since we've used <tt>instance_eval</tt>, we now have free access to the +Mapper+ object's internal instance variables, including the ability to clobber them. And that's precisely what we did here. Furthermore, maybe we were actually expecting to access our own <tt>@set</tt> variable, and we haven't done that. Any instance variables from the caller's closure are in fact no longer accessible inside the block.
|
348
369
|
|
349
370
|
The problem gets even worse. If we think about the cryptic error message we got when we tried to use our +makeurl+ helper method, we begin to realize that we've run into the method lookup ambiguity discussed in the previous section. If +self+ has changed inside the block, and we tried to call +makeurl+, we might expect a +NoMethodError+ to be raised for +makeurl+ on the +Mapper+ class, rather than for "<tt>[]</tt>" on the +Symbol+ class. However, things change when we recall that Rails's routing DSL supports named routes. You do not have to call the specific +connect+ method to create a route. In fact, you can call _any_ method name. Any name is a valid DSL method name. It is thus ambiguous, when we invoke +makeurl+, whether we mean our helper method or a named route called "makeurl". Rails assumed we meant the named route, but in fact that isn't what we had intended.
|
350
371
|
|
351
|
-
This all sounds pretty bad. Do we give up on <tt>instance_eval</tt>? Some members of the Ruby community have, and indeed the technique has generally fallen out of favor in major libraries. Jim Weirich, for instance, {originally}[http://onestepback.org/index.cgi/Tech/Ruby/BuilderObjects.rdoc] utilized <tt>instance_eval</tt> in the XML Builder library illustrated earlier, but later deprecated and removed it because of its surprising behavior.
|
372
|
+
This all sounds pretty bad. Do we give up on <tt>instance_eval</tt>? Some members of the Ruby community have, and indeed the technique has generally fallen out of favor in many major libraries. Jim Weirich, for instance, {originally}[http://onestepback.org/index.cgi/Tech/Ruby/BuilderObjects.rdoc] utilized <tt>instance_eval</tt> in the XML Builder library illustrated earlier, but later deprecated and removed it because of its surprising behavior. Why's {Markaby}[http://code.whytheluckystiff.net/markaby/] still uses <tt>instance_eval</tt> but includes a caveat in the {documentation}[http://markaby.rubyforge.org/] explaining the issues and recommending caution.
|
352
373
|
|
353
|
-
There are, however, a few specific
|
374
|
+
There are, however, a few specific cases when <tt>instance_eval</tt> may be uniquely appropriate. RSpec's DSL is intended as a class-constructive language: it constructs ruby classes behind the scenes. In the RSpec example at the beginning of this paper, you may notice the use of the <tt>@stack</tt> instance variable. In fact, this is intended as an instance variable of the RSpec test story being written, and as such, <tt>instance_eval</tt> is required because of the kind of language that RSpec wants to use. But in more common cases, such as specifying configuration, <tt>instance_eval</tt> does not give us the most desirable behavior. The general consensus now, expressed for example in recent articles from {Why}[http://hackety.org/2008/10/06/mixingOurWayOutOfInstanceEval.html] and {Ola Bini}[http://olabini.com/blog/2008/09/dont-overuse-instance_eval-and-instance_exec/], is that it should be avoided.
|
354
375
|
|
355
376
|
So does this mean we're stuck with block parameters for better or worse? Not quite. Several alternatives have been proposed recently, and we'll take a look at them in the next few sections. But first, let's summarize the discussion of <tt>instance_eval</tt>.
|
356
377
|
|
@@ -396,7 +417,7 @@ The basic implementation here is not difficult, if we pull out another tool from
|
|
396
417
|
|
397
418
|
How does this help us? Well, our goal is to redirect any calls that aren't available in the DSL, back to the block's original context. To do that, we simply define <tt>method_missing</tt> on our proxy class. In that method, we delegate the call, using +send+, back to the original +self+ from the block's context.
|
398
419
|
|
399
|
-
The remaining trick is how to get the block's original +self+. This can be done with a little bit of hackery if we realize that any +Proc+ object lets you access the binding of the context where it came from. We can get the original +self+ reference by
|
420
|
+
The remaining trick is how to get the block's original +self+. This can be done with a little bit of hackery if we realize that any +Proc+ object lets you access the binding of the context where it came from. We can get the original +self+ reference by eval-ing "self" in that binding.
|
400
421
|
|
401
422
|
Going back to our modification of the Rails routing code, let's see what this looks like.
|
402
423
|
|
@@ -439,9 +460,8 @@ Going back to our modification of the Rails routing code, let's see what this lo
|
|
439
460
|
|
440
461
|
Now people familiar with how Rails is implemented will probably object that +Mapper+ already _has_ a <tt>method_missing</tt> defined. It's used to implement the named routes that caused the ambiguity we described earlier. We have not solved that ambiguity: by replacing Rails's <tt>method_missing</tt> with my own <tt>method_missing</tt>, I effectively disable named routes. Granted, I'm ignoring that issue right now, and just trying to illustrate how method delegation works. As long as we don't use named routes, our +makeurl+ example will now work as we expect:
|
441
462
|
|
442
|
-
URL_PREFIX = 'mywebsite/:controller/:action/'
|
443
463
|
def makeurl(*params)
|
444
|
-
|
464
|
+
'mywebsite/:controller/:action/' + params.map{ |e| e.inspect }.join('/')
|
445
465
|
end
|
446
466
|
|
447
467
|
ActionController::Routing::Routes.draw do
|
@@ -450,11 +470,11 @@ Now people familiar with how Rails is implemented will probably object that +Map
|
|
450
470
|
# etc.
|
451
471
|
end
|
452
472
|
|
453
|
-
While this would appear to have solved the helper method issue, so far it does nothing to address the other issues we encountered. For example, invoking instance variables inside the block will still reference the instance variables of the +Mapper+ proxy object. By using <tt>instance_eval</tt>, we still break encapsulation of the proxy class.
|
473
|
+
While this would appear to have solved the helper method issue, so far it does nothing to address the other issues we encountered. For example, invoking instance variables inside the block will still reference the instance variables of the +Mapper+ proxy object. By using <tt>instance_eval</tt>, we still break encapsulation of the proxy class, and lose access to any instance variables from the block's context.
|
454
474
|
|
455
475
|
Addressing the instance variable issue is not as straightforward as delegating method calls. There is, as far as I know, no direct way to delegate instance variable lookup, and Manges's blog posting does not attempt to provide a solution either. However, we can imagine a few techniques to mitigate the problem. First, we could eliminate the proxy object's dependence on instance variables altogether, by replacing them with a global hash. In our example, instead of keeping a reference to the +RouteSet+ as an instance variable of +Mapper+, we can maintain a global hash that looks up the +RouteSet+ using the +Mapper+ instance as the key. In this way, we eliminate the risk of the block clobbering the proxy's state, and minimize the problem of breaking encapsulation of the proxy object.
|
456
476
|
|
457
|
-
Second, we could make instance variables from the block's context partially available through a "pull-push" technique using <tt>instance_variable_set</tt> and <tt>instance_variable_get</tt> calls. Before calling the block, we "pull" in the block context object's instance variables,
|
477
|
+
Second, we could make instance variables from the block's context partially available through a "pull-push" technique using <tt>instance_variable_set</tt> and <tt>instance_variable_get</tt> calls. Before calling the block, we "pull" in the block context object's instance variables, by iterating over them and setting the same instance variables on the proxy object. Then those instance variables will appear to be still available during the block. On completing the block, we then "push" any changes back to the block context object, by iterating over the proxy's instance variables and setting them on the block context object.
|
458
478
|
|
459
479
|
Here is a sample implementation of these two techniques for handling instance variables:
|
460
480
|
|
@@ -498,8 +518,11 @@ Here is a sample implementation of these two techniques for handling instance va
|
|
498
518
|
clear!
|
499
519
|
original_self = Kernel.eval('self', block.binding)
|
500
520
|
map = Mapper.new(self, original_self)
|
501
|
-
|
502
|
-
|
521
|
+
begin
|
522
|
+
map.instance_eval(&block)
|
523
|
+
ensure # Ensure the hashes are cleaned up and instance
|
524
|
+
map.cleanup # variables are pushed back to original_self,
|
525
|
+
end # even if the block threw and exception
|
503
526
|
named_routes.install
|
504
527
|
end
|
505
528
|
|
@@ -542,7 +565,7 @@ Let's wrap up our discussion of delegation and then delve into an entirely diffe
|
|
542
565
|
* Does not solve the helper method vs DSL method ambiguity.
|
543
566
|
* Harder to implement than a simple <tt>instance_eval</tt>.
|
544
567
|
|
545
|
-
<b>Use it when</b>: you have a case where <tt>instance_eval</tt> is appropriate (i.e. if you are writing a DSL that constructs classes or modifies class internals) but you
|
568
|
+
<b>Use it when</b>: you have a case where <tt>instance_eval</tt> is appropriate (i.e. if you are writing a DSL that constructs classes or modifies class internals) but you want to retain helper methods.
|
546
569
|
|
547
570
|
=== Implementation strategy 4: arity detection
|
548
571
|
|
@@ -608,7 +631,7 @@ Let us summarize Gray's arity detection technique, and then proceed to an intere
|
|
608
631
|
|
609
632
|
=== Implementation strategy 5: mixins
|
610
633
|
|
611
|
-
One of the most interesting entries into the DSL blocks discussion was proposed by Why The Lucky Stiff in his {blog}[http://hackety.org/2008/10/06/mixingOurWayOutOfInstanceEval.html]. Why observes that the problem with <tt>instance_eval</tt> is that it does too much.
|
634
|
+
One of the most interesting entries into the DSL blocks discussion was proposed by Why The Lucky Stiff in his {blog}[http://hackety.org/2008/10/06/mixingOurWayOutOfInstanceEval.html]. Why observes that the problem with <tt>instance_eval</tt> is that it does too much. Most DSL blocks merely want to be able to intercept and respond to certain method calls, whereas <tt>instance_eval</tt> actually changes +self+, which has the additional side effects of blocking access to other methods and instance variables, and breaking encapsulation. A better solution, he maintains, is not to change +self+, but instead temporarily to add the DSL's methods to the block's context for the duration of the block. That is, instead of having the DSL proxy object delegate back to the block's context object, do the opposite: cause the block's context object to delegate to the DSL proxy object.
|
612
635
|
|
613
636
|
Implementing this is actually harder than it sounds. We need to take the block context object, dynamically add methods to it before calling the block, and then dynamically remove them afterward. We already know how to get the block context object, but adding and removing methods requires some more Ruby metaprogramming wizardry. And now we're stretching our toolbox to the breaking point.
|
614
637
|
|
@@ -630,9 +653,9 @@ Ruby provides tools for dynamically defining methods on and removing methods fro
|
|
630
653
|
named_routes.install
|
631
654
|
end
|
632
655
|
|
633
|
-
This implementation, however, is fraught with problems. Notably, we are modifying the entire class of objects, including instances other than <tt>original_self</tt>, which is probably not what we intended. In addition, we could be unknowingly clobbering another +connect+ method defined on <tt>original_self</tt>'s class. (There are, of course, many other problems that I'm just ignoring for the sake of clarity, such as exception safety, and the fact that the +options+ parameter cannot take a default value when using <tt>define_method</tt>. Suffice to say that the above implementation is
|
656
|
+
This implementation, however, is fraught with problems. Notably, we are modifying the entire class of objects, including instances other than <tt>original_self</tt>, which is probably not what we intended. In addition, we could be unknowingly clobbering another +connect+ method defined on <tt>original_self</tt>'s class. (There are, of course, many other problems that I'm just ignoring for the sake of clarity, such as exception safety, and the fact that the +options+ parameter cannot take a default value when using <tt>define_method</tt>. Suffice to say that the above implementation is quite broken.)
|
634
657
|
|
635
|
-
What we would really like is a way to add methods to just one object temporarily, and then remove them, restoring the original state (including any methods we may have overridden when we added ours.) Ruby _almost_ provides a reasonable way to do this, using the +extend+ method. This method lets you add a module's methods to a single object, like this:
|
658
|
+
What we would really like is a way to add methods to just one object temporarily, and then remove them, restoring the original state (including any methods we may have overridden when we added ours.) Ruby _almost_ provides a reasonable way to do this, using the +extend+ method. This method lets you add a module's methods to a single specific object, like this:
|
636
659
|
|
637
660
|
module MyExtension
|
638
661
|
def foo
|
@@ -647,7 +670,7 @@ What we would really like is a way to add methods to just one object temporarily
|
|
647
670
|
s1.foo # prints "foo called"
|
648
671
|
s2.foo # NameError: s2 is unchanged
|
649
672
|
|
650
|
-
Unfortunately, there is no way to remove the module from the object. Ruby has no "unextend" capability. This omission led Why to implement it himself as a Ruby language extension, lovingly entitled {
|
673
|
+
Unfortunately, there is no way to remove the module from the object. Ruby has no "unextend" capability. This omission led Why to implement it himself as a Ruby language extension, lovingly entitled {mixico}[http://github.com/why/mixico/tree/master]. The name comes from the library's ability to add and remove "mixins" at will. A similar library exists as a gem called {mixology}[http://www.somethingnimble.com/bliki/mixology]. The two libraries use different APIs but perform the same basic function. For the discussion below, I will assume mixico is installed. However, the library I describe in the next section uses mixology because it is available as a gem.
|
651
674
|
|
652
675
|
Using mixico, we can now write the +draw+ method like this:
|
653
676
|
|
@@ -659,11 +682,11 @@ Using mixico, we can now write the +draw+ method like this:
|
|
659
682
|
|
660
683
|
Wow! That was simple. Mixico even handles all the eval-block-binding hackery for us. But the simplicity is a little deceptive: when we want to do a robust implementation, we run into two issues. First, we run into a challenge if we want to support multiple DSL blocks being invoked at once: for example in the case of nested blocks or multithreading. It is possible in such cases that a MapperModule is already mixed into the block's context. The <tt>mix_eval</tt> method by itself, as of this writing, doesn't handle this case well: the inner invocation will remove the module prematurely. Additional logic is necessary to track how many nested invocations (or invocations from other threads) want to mix-in each particular module into each object.
|
661
684
|
|
662
|
-
The other challenge is that of creating the +MapperModule+ module, implementing the +connect+ method and any others we want to mix-in. Because we're adding methods to someone else's object, we need to be as unobtrusive as possible, yet we need to provide the necessary functionality, including invoking the <tt>add_route</tt> method back on the +RouteSet+. This is unfortunately not trivial. I'll describe a full implementation in the next section, but for now let's explore some possible approaches.
|
685
|
+
The other challenge is that of creating the +MapperModule+ module, implementing the +connect+ method and any others we want to mix-in. Because we're adding methods to someone else's object, we need to be as unobtrusive as possible, yet we need to provide the necessary functionality, including invoking the <tt>add_route</tt> method back on the +RouteSet+. This is unfortunately not trivial. In particular, we need to give +MapperModule+ a way to reference the +RouteSet+. I'll describe a full implementation of this in the next section, but for now let's explore some possible approaches.
|
663
686
|
|
664
687
|
Rails's original +Mapper+ proxy class, we recall from our earlier discussion, used an instance variable, <tt>@set</tt>, which pointed back to the +RouteSet+ instance and thus provided a way to invoke <tt>add_route</tt>. One approach could be to add such an instance variable to the block's context object, so it's available in methods of +MapperModule+. This seems to be the easiest approach, but it is also dangerous because it intrudes on the context object, adding an instance variable and potentially clobbering one used by the caller. Furthermore, in the case of nested blocks that try to add methods to the same object, the two blocks may clobber each other's instance variables.
|
665
688
|
|
666
|
-
Instead of adding information to the block's context object,
|
689
|
+
Instead of adding information to the block's context object, we could stash the information away in a global location, such as a class variable, that can be accessed by the +MapperModule+ from within the block. This is of course the same strategy we used to eliminate instance variables in the section on delegation. Again, this seems to work, until you have nested or multithreaded usage. It then becomes neccessary to keep a stack of references to handle nesting, and thread-local variables to handle multithreading-- all feasible to do, but a lot of work.
|
667
690
|
|
668
691
|
A third approach involves dynamically generating a singleton module, "hard coding" a reference to the +RouteSet+ in the module. For example:
|
669
692
|
|
@@ -680,9 +703,9 @@ A third approach involves dynamically generating a singleton module, "hard codin
|
|
680
703
|
named_routes.install
|
681
704
|
end
|
682
705
|
|
683
|
-
This probably can be made to work, and it also has the benefit of solving the nesting and multithreading issue neatly since each mixin is done exactly once. However, it seems to be a fairly heavyweight solution: creating a new module for every DSL block invocation may have performance implications. It is also not clear how to support constructs that are not available to <tt>define_method</tt>, such as blocks and parameter default values. However, such an approach may still be useful in certain cases when you need
|
706
|
+
This probably can be made to work, and it also has the benefit of solving the nesting and multithreading issue neatly since each mixin is done exactly once. However, it seems to be a fairly heavyweight solution: creating a new module for every DSL block invocation may have performance implications. It is also not clear how to support constructs that are not available to <tt>define_method</tt>, such as blocks and parameter default values. However, such an approach may still be useful in certain cases when you need to generate a DSL dynamically based on the context.
|
684
707
|
|
685
|
-
One more issue with the mixin strategy is that, like all implementations that drop the block parameter, there remains an ambiguity regarding whether methods should be directed to the DSL or to the surrounding context. In the implementations we've discussed previously, based on <tt>instance_eval</tt>, the actual behavior is fairly straightforward to reason about. A simple <tt>instance_eval</tt> disables method calls to the block's context altogether: you can call _only_ the DSL methods. An <tt>instance_eval</tt> with delegation re-enables method calls to the block's context but gives the DSL priority. If both the DSL and the surrounding block define the same method name, the DSL's method will be
|
708
|
+
One more issue with the mixin strategy is that, like all implementations that drop the block parameter, there remains an ambiguity regarding whether methods should be directed to the DSL or to the surrounding context. In the implementations we've discussed previously, based on <tt>instance_eval</tt>, the actual behavior is fairly straightforward to reason about. A simple <tt>instance_eval</tt> disables method calls to the block's context altogether: you can call _only_ the DSL methods. An <tt>instance_eval</tt> with delegation re-enables method calls to the block's context but gives the DSL priority. If both the DSL and the surrounding block define the same method name, the DSL's method will be take precedence.
|
686
709
|
|
687
710
|
Mixin's behavior is less straightforward, because of a subtlety in Ruby's method lookup behavior. Under most cases, it behaves similarly to an <tt>instance_eval</tt> with delegation: the DSL's methods take priority. However, if methods have been added directly to the object, they will take precedence over the DSL's methods. Following is an example of this case:
|
688
711
|
|
@@ -721,7 +744,7 @@ Mixin's behavior is less straightforward, because of a subtlety in Ruby's method
|
|
721
744
|
|
722
745
|
In the above example, suppose both +foo+ and +bar+ are methods of the DSL. They are also both defined as methods of +obj+. (+foo+ is available because it is a method of +MyClass+, while +bar+ is available because it is explicitly added to +obj+.) However, if you run the code, it calls the DSL's +foo+ but +obj+'s +bar+. Why?
|
723
746
|
|
724
|
-
The reason
|
747
|
+
The reason points to a subtlety in how Ruby does method lookup. When you define a method in the way +foo+ is defined, it is just added to the class. However, when you define a method in the way +bar+ is defined, it is defined as a "singleton method", and added to the "singleton class", which is an anonymous class that holds methods defined directly on a particular object. It turns out that the singleton class is always given the highest priority in method lookup. So, for example, the lookup order for methods of +obj+ within the block would look like this:
|
725
748
|
|
726
749
|
singleton methods of obj -> mixin module from the DSL -> methods of MyClass
|
727
750
|
(e.g. bar, run) (e.g. foo, bar) (e.g. foo)
|
@@ -732,7 +755,7 @@ Does this esoteric-sounding case actually happen in practice? In fact it does, q
|
|
732
755
|
|
733
756
|
Well, that was confusing. It is on account of such behavior that we need to take the method lookup ambiguity seriously when dealing with mixins. In fact, I would go so far as to suggest that the mixin implementation should always go hand-in-hand with a way to mitigate that ambiguity, such as Gray's arity check.
|
734
757
|
|
735
|
-
As we have seen, the mixin idea seems like it may be a compelling solution, particularly in conjunction with Gray's arity check, but the implementation details present some challenges. It may be
|
758
|
+
As we have seen, the mixin idea seems like it may be a compelling solution, particularly in conjunction with Gray's arity check, but the implementation details present some challenges. It may be viable if a library can be written to hide the implementation complexity. Let's summarize this approach, and then proceed to examine such a library, one that uses some of the best of what we've discussed to make implementing DSL blocks simple.
|
736
759
|
|
737
760
|
*Implementation*:
|
738
761
|
|
@@ -756,18 +779,18 @@ As we have seen, the mixin idea seems like it may be a compelling solution, part
|
|
756
779
|
|
757
780
|
=== Blockenspiel: a comprehensive implementation
|
758
781
|
|
759
|
-
Some of the implementations we have covered, especially the mixin implementation, have some compelling qualities, but are hampered by the difficulty of implementing them in a robust way.
|
782
|
+
Some of the implementations we have covered, especially the mixin implementation, have some compelling qualities, but are hampered by the difficulty of implementing them in a robust way. They could be viable if a library were present to handle the details.
|
760
783
|
|
761
|
-
Blockenspiel was written to be that library. It first provides a comprehensive and robust implementation of the mixin strategy, correctly handling nesting and multithreading. It offers the option to perform an arity check, giving the caller the choice of whether or not to use a block parameter. You can even tell
|
784
|
+
{Blockenspiel}[http://virtuoso.rubyforge.org/blockenspiel] was written to be that library. It first provides a comprehensive and robust implementation of the mixin strategy, correctly handling nesting and multithreading. It offers the option to perform an arity check, giving the caller the choice of whether or not to use a block parameter. You can even tell blockenspiel to use an alternate implementation, such as <tt>instance_eval</tt>, instead of a mixin, in those cases when it is appropriate. Finally, blockenspiel also provides an API for dynamic construction of DSLs.
|
762
785
|
|
763
|
-
But most importantly, it is easy to use. To write a basic DSL, just follow the first and easiest implementation strategy, creating a proxy class that can be passed into the block as a parameter. Then instead of yielding the proxy object, pass it to
|
786
|
+
But most importantly, it is easy to use. To write a basic DSL, just follow the first and easiest implementation strategy, creating a proxy class that can be passed into the block as a parameter. Then instead of yielding the proxy object, pass it to blockenspiel, and it will do the rest.
|
764
787
|
|
765
|
-
Our Rails routing example implemented using
|
788
|
+
Our Rails routing example implemented using blockenspiel might look like this:
|
766
789
|
|
767
790
|
class RouteSet
|
768
791
|
|
769
792
|
class Mapper
|
770
|
-
include Blockenspiel::DSL #
|
793
|
+
include Blockenspiel::DSL # tell blockenspiel this is a DSL proxy
|
771
794
|
|
772
795
|
def initialize(set)
|
773
796
|
@set = set
|
@@ -783,7 +806,7 @@ Our Rails routing example implemented using Blockenspiel might look like this:
|
|
783
806
|
|
784
807
|
def draw(&block)
|
785
808
|
clear!
|
786
|
-
Blockenspiel.invoke(block, Mapper.new(self)) #
|
809
|
+
Blockenspiel.invoke(block, Mapper.new(self)) # blockenspiel does the rest
|
787
810
|
named_routes.install
|
788
811
|
end
|
789
812
|
|
@@ -792,9 +815,9 @@ Our Rails routing example implemented using Blockenspiel might look like this:
|
|
792
815
|
def add_route(path, options = {})
|
793
816
|
# ...
|
794
817
|
|
795
|
-
The code above is as simple as a block parameter or <tt>instance_eval</tt> implementation. However, it performs a full-fledged mixin implementation, and even throws in the arity check. We recall from the previous section that one of the chief challenges is to mediate communication between the mixin and proxy in a re-entrant and thread-safe way.
|
818
|
+
The code above is as simple as a block parameter or <tt>instance_eval</tt> implementation. However, it performs a full-fledged mixin implementation, and even throws in the arity check. We recall from the previous section that one of the chief challenges is to mediate communication between the mixin and proxy in a re-entrant and thread-safe way. The blockenspiel library implements this mediation using a global hash, avoiding the compatibility risk of adding instance variables to the block's context object, and avoiding the performance hit of dynamically generating proxies. All the implementation details are carefully handled behind the scenes.
|
796
819
|
|
797
|
-
Atop this basic usage,
|
820
|
+
Atop this basic usage, blockenspiel provides two types of customization. First, you can customize the DSL, using a few simple directives to specify which methods on your proxy should be available in the mixin implementation. You can also cause methods to be available in the mixin under different names, thus sidestepping the <tt>attr_writer</tt> issue we discussed earlier. If you want methods of the form "attribute=" on your proxy object, blockenspiel provides a simple syntax for renaming them:
|
798
821
|
|
799
822
|
class ConfigMethods
|
800
823
|
include Blockenspiel::DSL
|
@@ -818,9 +841,9 @@ And omitting the parameter, the alternate method names are mixed in:
|
|
818
841
|
set_title "Implementing DSL Blocks"
|
819
842
|
end
|
820
843
|
|
821
|
-
Second, you can customize the invocation-- for example specifying whether to perform an arity check, whether to use <tt>instance_eval</tt> instead of mixins, and various other minor behavioral adjustments-- simply by providing parameters to the <tt>Blockenspiel#invoke</tt> method. All the implementation details are handled by the
|
844
|
+
Second, you can customize the invocation-- for example specifying whether to perform an arity check, whether to use <tt>instance_eval</tt> instead of mixins, and various other minor behavioral adjustments-- simply by providing parameters to the <tt>Blockenspiel#invoke</tt> method. All the implementation details are handled by the blockenspiel library, leaving you free to focus on your API.
|
822
845
|
|
823
|
-
Third,
|
846
|
+
Third, blockenspiel provides an API, itself a DSL block, letting you dynamically construct DSLs. Suppose, for the sake of argument, we wanted to let the caller optionally rename the +connect+ method. (Maybe we want to make the name "connect" available for named routes.) That is, suppose we wanted to provide this behavior:
|
824
847
|
|
825
848
|
ActionController::Routing::Routes.draw(:method => :myconnect) do |map|
|
826
849
|
map.myconnect ':controller/:action/:id'
|
@@ -828,11 +851,11 @@ Third, Blockenspiel provides an API, itself a DSL block, letting you dynamically
|
|
828
851
|
# etc.
|
829
852
|
end
|
830
853
|
|
831
|
-
This
|
854
|
+
This requires dynamic generation of the proxy class. We could implement it using blockenspiel as follows:
|
832
855
|
|
833
856
|
class RouteSet
|
834
857
|
|
835
|
-
# We don't define a Mapper class anymore
|
858
|
+
# We don't define a static Mapper class anymore. Now it's dynamically generated.
|
836
859
|
|
837
860
|
def draw(options={}, &block)
|
838
861
|
clear!
|
@@ -851,19 +874,17 @@ This can be implemented by using Blockenspiel to dynamically generate the proxy
|
|
851
874
|
def add_route(path, options = {})
|
852
875
|
# ...
|
853
876
|
|
854
|
-
|
855
|
-
|
856
|
-
Blockenspiel is available now as a gem for MRI 1.8.x
|
877
|
+
You can install blockenspiel as a gem for MRI 1.8.x
|
857
878
|
|
858
879
|
gem install blockenspiel
|
859
880
|
|
860
|
-
More information is available on
|
881
|
+
More information is available on blockenspiel's Rubyforge page at http://virtuoso.rubyforge.org/blockenspiel
|
861
882
|
|
862
883
|
Source code is available on Github at http://github.com/dazuma/blockenspiel
|
863
884
|
|
864
885
|
=== Summary
|
865
886
|
|
866
|
-
DSL blocks are a valuable and ubiquitous pattern for designing Ruby APIs. A flurry of discussion has recently surrounded the implementation of DSL blocks, particularly addressing the desire to eliminate
|
887
|
+
DSL blocks are a valuable and ubiquitous pattern for designing Ruby APIs. A flurry of discussion has recently surrounded the implementation of DSL blocks, particularly addressing the desire to eliminate block parameters. We have discussed several different strategies for DSL block implementation, each with its own advantages and disadvantages.
|
867
888
|
|
868
889
|
The simplest strategy, creating a proxy object and passing a reference to the block as a parameter, is straightforward, safe, and widely used. However, sometimes we might want to provide a cleaner API by eliminating the block parameter.
|
869
890
|
|
@@ -903,6 +924,8 @@ The Blockenspiel library provides a concrete and robust implementation of DSL bl
|
|
903
924
|
|
904
925
|
{Jim Weirich}[http://onestepback.org/], <em>{ruby-core:19153}[http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-core/19153]</em>, 2008.10.07
|
905
926
|
|
927
|
+
{Why The Lucky Stiff}[http://whytheluckystiff.net/], <em>{Markaby}[http://code.whytheluckystiff.net/markaby/]</em> (Ruby library), 2006.
|
928
|
+
|
906
929
|
{Why The Lucky Stiff}[http://whytheluckystiff.net/], <em>{Mixico}[http://github.com/why/mixico/tree/master]</em> (Ruby library), 2008.
|
907
930
|
|
908
931
|
{Why The Lucky Stiff}[http://whytheluckystiff.net/], <em>{Mixing Our Way Out Of Instance Eval?}[http://hackety.org/2008/10/06/mixingOurWayOutOfInstanceEval.html]</em>, 2008.10.06.
|
data/lib/blockenspiel.rb
CHANGED
@@ -46,7 +46,7 @@ require 'mixology'
|
|
46
46
|
module Blockenspiel
|
47
47
|
|
48
48
|
# Current gem version
|
49
|
-
VERSION_STRING = '0.0
|
49
|
+
VERSION_STRING = '0.1.0'
|
50
50
|
|
51
51
|
|
52
52
|
# Base exception for all exceptions raised by Blockenspiel
|
@@ -470,6 +470,9 @@ module Blockenspiel
|
|
470
470
|
|
471
471
|
def self.invoke(block_, target_=nil, opts_={}, &builder_block_)
|
472
472
|
|
473
|
+
unless block_
|
474
|
+
raise ArgumentError, "Block expected"
|
475
|
+
end
|
473
476
|
parameter_ = opts_[:parameter]
|
474
477
|
parameterless_ = opts_[:parameterless]
|
475
478
|
|
@@ -490,8 +493,8 @@ module Blockenspiel
|
|
490
493
|
end
|
491
494
|
|
492
495
|
# Handle parametered block case
|
493
|
-
if parameter_ != false &&
|
494
|
-
if block_.arity != 1
|
496
|
+
if parameter_ != false && block_.arity == 1 || parameterless_ == false
|
497
|
+
if block_.arity != 1
|
495
498
|
raise Blockenspiel::BlockParameterError, "Block should take exactly one parameter"
|
496
499
|
end
|
497
500
|
return block_.call(target_)
|
data/tests/tc_behaviors.rb
CHANGED
@@ -129,7 +129,59 @@ module Blockenspiel
|
|
129
129
|
assert_equal(2, hash_['b2'])
|
130
130
|
assert_equal(3, hash_['c3'])
|
131
131
|
end
|
132
|
-
|
132
|
+
|
133
|
+
|
134
|
+
# Test parameterless blocks disabled
|
135
|
+
#
|
136
|
+
# * Asserts that an error is raised if sending a no-parameter block in this case.
|
137
|
+
# * Asserts that sending a one-parameter block still works.
|
138
|
+
|
139
|
+
def test_disable_parameterless
|
140
|
+
hash_ = Hash.new
|
141
|
+
block1_ = proc do ||
|
142
|
+
set_value1('a', 1)
|
143
|
+
end
|
144
|
+
block2_ = proc do |target_|
|
145
|
+
target_.set_value1('b', 2)
|
146
|
+
end
|
147
|
+
block3_ = proc do
|
148
|
+
set_value1('c', 3)
|
149
|
+
end
|
150
|
+
assert_raise(Blockenspiel::BlockParameterError) do
|
151
|
+
Blockenspiel.invoke(block1_, Target1.new(hash_), :parameterless => false)
|
152
|
+
end
|
153
|
+
Blockenspiel.invoke(block2_, Target1.new(hash_), :parameterless => false)
|
154
|
+
assert_raise(Blockenspiel::BlockParameterError) do
|
155
|
+
Blockenspiel.invoke(block3_, Target1.new(hash_), :parameterless => false)
|
156
|
+
end
|
157
|
+
assert_equal(2, hash_['b1'])
|
158
|
+
end
|
159
|
+
|
160
|
+
|
161
|
+
# Test parametered blocks disabled
|
162
|
+
#
|
163
|
+
# * Asserts that an error is raised if sending a one-parameter block in this case.
|
164
|
+
# * Asserts that sending a no-parameter block still works.
|
165
|
+
|
166
|
+
def test_disable_parametered
|
167
|
+
hash_ = Hash.new
|
168
|
+
block1_ = proc do ||
|
169
|
+
set_value1('a', 1)
|
170
|
+
end
|
171
|
+
block2_ = proc do |target_|
|
172
|
+
target_.set_value1('b', 2)
|
173
|
+
end
|
174
|
+
block3_ = proc do
|
175
|
+
set_value1('c', 3)
|
176
|
+
end
|
177
|
+
Blockenspiel.invoke(block1_, Target1.new(hash_), :parameter => false)
|
178
|
+
assert_raise(Blockenspiel::BlockParameterError) do
|
179
|
+
Blockenspiel.invoke(block2_, Target1.new(hash_), :parameter => false)
|
180
|
+
end
|
181
|
+
Blockenspiel.invoke(block3_, Target1.new(hash_), :parameter => false)
|
182
|
+
assert_equal(1, hash_['a1'])
|
183
|
+
assert_equal(3, hash_['c1'])
|
184
|
+
end
|
133
185
|
|
134
186
|
end
|
135
187
|
|
data/tests/tc_mixins.rb
CHANGED
@@ -200,6 +200,38 @@ module Blockenspiel
|
|
200
200
|
end
|
201
201
|
|
202
202
|
|
203
|
+
# Test of two threads mixing the same mixin into the same object
|
204
|
+
#
|
205
|
+
# * Asserts that the mixin is removed only after the second thread is done.
|
206
|
+
|
207
|
+
def test_threads_same_mixin
|
208
|
+
hash_ = Hash.new
|
209
|
+
block1_ = proc do
|
210
|
+
set_value('a', 1)
|
211
|
+
sleep(0.5)
|
212
|
+
set_value2('b'){ 2 }
|
213
|
+
end
|
214
|
+
block2_ = proc do
|
215
|
+
set_value('c', 3)
|
216
|
+
sleep(1)
|
217
|
+
set_value2('d'){ 4 }
|
218
|
+
end
|
219
|
+
target_ = Target1.new(hash_)
|
220
|
+
thread1_ = Thread.new do
|
221
|
+
Blockenspiel.invoke(block1_, target_)
|
222
|
+
end
|
223
|
+
thread2_ = Thread.new do
|
224
|
+
Blockenspiel.invoke(block2_, target_)
|
225
|
+
end
|
226
|
+
thread1_.join
|
227
|
+
thread2_.join
|
228
|
+
assert_equal(1, hash_['a1'])
|
229
|
+
assert_equal(2, hash_['b1'])
|
230
|
+
assert_equal(3, hash_['c1'])
|
231
|
+
assert_equal(4, hash_['d1'])
|
232
|
+
end
|
233
|
+
|
234
|
+
|
203
235
|
end
|
204
236
|
|
205
237
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: blockenspiel
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Azuma
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2008-10-
|
12
|
+
date: 2008-10-29 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -30,7 +30,7 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 1.8.
|
33
|
+
version: 1.8.2
|
34
34
|
version:
|
35
35
|
description: Blockenspiel is a helper library designed to make it easy to implement DSL blocks. It is designed to be comprehensive and robust, supporting most common usage patterns, and working correctly in the presence of nested blocks and multithreading.
|
36
36
|
email:
|
@@ -78,7 +78,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
78
78
|
requirements: []
|
79
79
|
|
80
80
|
rubyforge_project: virtuoso
|
81
|
-
rubygems_version: 1.3.
|
81
|
+
rubygems_version: 1.3.1
|
82
82
|
signing_key:
|
83
83
|
specification_version: 2
|
84
84
|
summary: Blockenspiel is a helper library designed to make it easy to implement DSL blocks
|