blockenspiel 0.0.3 → 0.0.4
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 +8 -0
- data/ImplementingDSLblocks.txt +189 -57
- data/Manifest.txt +2 -1
- data/lib/blockenspiel.rb +138 -115
- metadata +4 -3
data/History.txt
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
=== 0.0.4 / 2008-10-24
|
2
|
+
|
3
|
+
* Improvements to the logic for choosing behaviors
|
4
|
+
* Added exception classes and provided better error handling
|
5
|
+
* Actually added the behavior test case to the gem manifest...
|
6
|
+
* Documentation revisions
|
7
|
+
* Revisions to the Implementing DSL Blocks paper
|
8
|
+
|
1
9
|
=== 0.0.3 / 2008-10-23
|
2
10
|
|
3
11
|
* Added :proxy behavior for parameterless blocks
|
data/ImplementingDSLblocks.txt
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
== Implementing DSL Blocks
|
2
2
|
|
3
|
-
by Daniel Azuma,
|
3
|
+
by Daniel Azuma, 28 October 2008
|
4
4
|
|
5
|
-
<em>DSL
|
5
|
+
A <em>DSL block</em> is a construct commonly used in Ruby APIs. In this paper I present an overview of the implementation strategies proposed 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
|
|
@@ -144,7 +144,28 @@ However, some have argued that it is too verbose. Why, in a DSL, is it necessary
|
|
144
144
|
# etc.
|
145
145
|
end
|
146
146
|
|
147
|
-
|
147
|
+
In the next section we will look at this possible syntax improvement more closely. But first, let us summarize our discussion of the "block parameter" implementation.
|
148
|
+
|
149
|
+
*Implementation*:
|
150
|
+
|
151
|
+
* Create a proxy class defining the DSL.
|
152
|
+
* Yield the proxy object to the block as a parameter.
|
153
|
+
|
154
|
+
*Pros*:
|
155
|
+
|
156
|
+
* Easy to implement.
|
157
|
+
* Clear syntax for the caller.
|
158
|
+
* Clear separation between the DSL and surrounding code.
|
159
|
+
|
160
|
+
*Cons*:
|
161
|
+
|
162
|
+
* Requires a block parameter, sometimes resulting in verbose or clumsy syntax.
|
163
|
+
|
164
|
+
<b>Use it when</b>: you want a simple, effective DSL block and don't mind requiring a parameter.
|
165
|
+
|
166
|
+
=== Parameterless block syntax
|
167
|
+
|
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 parameters 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:
|
148
169
|
|
149
170
|
create_container do |container|
|
150
171
|
container.create_subcontainer do |subcontainer1|
|
@@ -178,28 +199,44 @@ That was clunky. Wouldn't it be nice to instead see this?...
|
|
178
199
|
end
|
179
200
|
end
|
180
201
|
|
181
|
-
|
202
|
+
While this is often an improvement, it does come at a cost, and it is important to bear this cost in mind as we delve into implementations of parameterless DSL blocks. 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>...
|
182
203
|
|
183
|
-
|
204
|
+
class ConfigMethods
|
205
|
+
attr_writer :author
|
206
|
+
attr_writer :title
|
207
|
+
end
|
184
208
|
|
185
|
-
|
186
|
-
* Yield the proxy object to the block as a parameter.
|
209
|
+
You might interact with it in a DSL block that uses parameters, like so:
|
187
210
|
|
188
|
-
|
211
|
+
create_paper do |config|
|
212
|
+
config.author = "Daniel Azuma"
|
213
|
+
config.title = "Implementing DSL Blocks"
|
214
|
+
end
|
189
215
|
|
190
|
-
|
191
|
-
* Clear syntax for the caller.
|
192
|
-
* Clear separation between the DSL and surrounding code.
|
216
|
+
However, if you try to eliminate the block parameter, you run into this dilemma:
|
193
217
|
|
194
|
-
|
218
|
+
create_paper do
|
219
|
+
author = "Daniel Azuma" # Whoops! These no longer work because they
|
220
|
+
title = "Implementing DSL Blocks" # look like local variable assignments!
|
221
|
+
end
|
222
|
+
|
223
|
+
You are forced either to explicitly specify, for example, "<tt>self.author=</tt>", or you must provide different names for your DSL methods. Similarly, many operators, notably <tt>[]=</tt>, are syntactically not available unless you use a full method call syntax.
|
195
224
|
|
196
|
-
|
225
|
+
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:
|
197
226
|
|
198
|
-
|
227
|
+
ActionController::Routing::Routes.draw do
|
228
|
+
connect ':controller/:action/:id'
|
229
|
+
connect ':controller/:action/:page/:format'
|
230
|
+
# etc.
|
231
|
+
end
|
232
|
+
|
233
|
+
...we now _assume_ that the +connect+ method is part of the DSL, but that is no longer explicit in the syntax. If, suppose +connect+ was also 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
|
+
|
235
|
+
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.
|
199
236
|
|
200
237
|
=== Implementation strategy 2: instance_eval
|
201
238
|
|
202
|
-
Micah Martin's post[http://blog.8thlight.com/articles/2007/05/20/] describes an
|
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.
|
203
240
|
|
204
241
|
It is perhaps instructive to see an example. Let's create a simple class
|
205
242
|
|
@@ -309,7 +346,7 @@ The problem gets worse. Changing +self+ affects not only how methods are looked
|
|
309
346
|
|
310
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 rein over 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.
|
311
348
|
|
312
|
-
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
|
349
|
+
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.
|
313
350
|
|
314
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.
|
315
352
|
|
@@ -333,15 +370,15 @@ So does this mean we're stuck with block parameters for better or worse? Not qui
|
|
333
370
|
* Surprising lookup behavior for helper methods.
|
334
371
|
* Surprising lookup behavior for instance variables.
|
335
372
|
* Breaks encapuslation of the proxy class.
|
336
|
-
*
|
373
|
+
* Encounters the helper method vs DSL method ambiguity.
|
337
374
|
|
338
|
-
|
375
|
+
<b>Use it when</b>: you are writing a DSL that constructs classes or modifies class internals.
|
339
376
|
|
340
377
|
=== Implementation strategy 3: delegation
|
341
378
|
|
342
379
|
In our discussion of <tt>instance_eval</tt>, a major problem we identified is that helper methods, and indeed all other methods from the calling context, are not available within the block. One way to improve the situation, perhaps, is by redirecting any methods not defined in the DSL (that is, not defined on the proxy object) back to the original context. That way, we still have access to our helper methods--they'll appear to be part of the DSL. This "delegation" approach was proposed by Dan Manges in his {blog}[http://www.dcmanges.com/blog/ruby-dsls-instance-eval-with-delegation].
|
343
380
|
|
344
|
-
The implementation here is not difficult, if we pull out another tool from Ruby's metaprogramming toolbox, <tt>method_missing</tt>. This method is called whenever you call a method that is not explicitly defined on an object's class. It provides a "last ditch" opportunity to handle the method before Ruby bails with a dreaded +NoMethodError+. Again, an example is probably useful here.
|
381
|
+
The basic implementation here is not difficult, if we pull out another tool from Ruby's metaprogramming toolbox, <tt>method_missing</tt>. This method is called whenever you call a method that is not explicitly defined on an object's class. It provides a "last ditch" opportunity to handle the method before Ruby bails with a dreaded +NoMethodError+. Again, an example is probably useful here.
|
345
382
|
|
346
383
|
class MyClass
|
347
384
|
def foo
|
@@ -389,7 +426,7 @@ Going back to our modification of the Rails routing code, let's see what this lo
|
|
389
426
|
|
390
427
|
def draw(&block)
|
391
428
|
clear!
|
392
|
-
original_self = Kernel.eval('self', block.binding) # Get
|
429
|
+
original_self = Kernel.eval('self', block.binding) # Get block's context self
|
393
430
|
map = Mapper.new(self, original_self) # Give it to the proxy
|
394
431
|
map.instance_eval(&block)
|
395
432
|
named_routes.install
|
@@ -400,7 +437,7 @@ Going back to our modification of the Rails routing code, let's see what this lo
|
|
400
437
|
def add_route(path, options = {})
|
401
438
|
# ...
|
402
439
|
|
403
|
-
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.
|
440
|
+
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:
|
404
441
|
|
405
442
|
URL_PREFIX = 'mywebsite/:controller/:action/'
|
406
443
|
def makeurl(*params)
|
@@ -413,7 +450,65 @@ Now people familiar with how Rails is implemented will probably object that +Map
|
|
413
450
|
# etc.
|
414
451
|
end
|
415
452
|
|
416
|
-
While this would appear to have solved the helper method issue, 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.
|
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.
|
454
|
+
|
455
|
+
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
|
+
|
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, byt 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
|
+
|
459
|
+
Here is a sample implementation of these two techniques for handling instance variables:
|
460
|
+
|
461
|
+
class RouteSet
|
462
|
+
|
463
|
+
class Mapper
|
464
|
+
|
465
|
+
@@routeset_map = Hash.new # Global hashes to replace
|
466
|
+
@@original_self_map = Hash.new # Mapper's instance variables
|
467
|
+
|
468
|
+
def initialize(set, original_self)
|
469
|
+
@@routeset_map[self] = set # Add me to global hashes
|
470
|
+
@@original_self_map[self] = original_self
|
471
|
+
original_self.instance_variables.each do |name| # "pull" instance variables
|
472
|
+
instance_variable_set(name, original_self.instance_variable_get(name))
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
def cleanup
|
477
|
+
@@routeset_map.delete(self) # Remove from global hashes
|
478
|
+
original_self = @@original_self_map.delete(self)
|
479
|
+
instance_variables.each do |name| # "push" instance variables
|
480
|
+
original_self.instance_variable_set(name, instance_variable_get(name))
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
484
|
+
def connect(path, options = {})
|
485
|
+
@@routeset_map[self].add_route(path, options) # Lookup set from global hash
|
486
|
+
end
|
487
|
+
|
488
|
+
# ...
|
489
|
+
|
490
|
+
def method_missing(name, *params, &blk) # Lookup original self
|
491
|
+
@@original_self_map[self].send(name, *params, &blk) # from global hash
|
492
|
+
end
|
493
|
+
end
|
494
|
+
|
495
|
+
# ...
|
496
|
+
|
497
|
+
def draw(&block)
|
498
|
+
clear!
|
499
|
+
original_self = Kernel.eval('self', block.binding)
|
500
|
+
map = Mapper.new(self, original_self)
|
501
|
+
map.instance_eval(&block)
|
502
|
+
map.cleanup
|
503
|
+
named_routes.install
|
504
|
+
end
|
505
|
+
|
506
|
+
# ...
|
507
|
+
|
508
|
+
def add_route(path, options = {})
|
509
|
+
# ...
|
510
|
+
|
511
|
+
While these measures seem to handle most of the cases, the implementation is getting more complex, and includes the additional overhead of hash lookups and copying of instance variables. More significantly, the "pull-push" technique does not quite preserve the expected semantics of instance variables. For instance, if you change an instance variable's value inside the block, it will get "pushed" back to the context object after the block is completed, but until then, the context object will not know about the change. So if, in the meantime, you called a helper method that relies on that instance variable, you will get the old value, and this can result in confusion. Using global hashes might be an effective means of protecting the proxy object's internals from the block. However, I find the "pull-push" technique to delegate instance variables to be of questionable value.
|
417
512
|
|
418
513
|
Several variations on the delegation theme have been proposed. One such variation uses a technique proposed by Jim Weirich called {MethodDirector}[http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-core/19153]. In this variation, we create a small object whose sole purpose is to receive methods and delegate them to whatever object it thinks should handle them. Utilizing Jim's +MethodDirector+ implementation rather than adding a <tt>method_missing</tt> to our +Mapper+ proxy, we could rewrite the +draw+ method as follows:
|
419
514
|
|
@@ -428,7 +523,7 @@ Several variations on the delegation theme have been proposed. One such variatio
|
|
428
523
|
|
429
524
|
The upshot is not much different from Manges's delegation technique. Method calls get delegated in approximately the same way (though Weirich speculates that +MethodDirector+'s dispatch process may be slow). Within the block, +self+ now points to the +MethodDirector+ object rather than the +Mapper+ object. This means that we're no longer breaking encapsulation of the mapper proxy (but we are breaking the encapsulation of the +MethodDirector+ itself.) We still cannot access instance variables from the block's context. We no longer clobber +Mapper+'s instance variables, but now we can clobber +MethodDirector+'s. In short, it might be considered a slight improvement, but not much, at a possible performance cost.
|
430
525
|
|
431
|
-
Let's wrap up our discussion of delegation and then delve into
|
526
|
+
Let's wrap up our discussion of delegation and then delve into an entirely different approach.
|
432
527
|
|
433
528
|
*Implementation*:
|
434
529
|
|
@@ -443,12 +538,11 @@ Let's wrap up our discussion of delegation and then delve into some different, a
|
|
443
538
|
|
444
539
|
*Cons*:
|
445
540
|
|
446
|
-
*
|
447
|
-
*
|
448
|
-
* Does nothing to solve the helper method vs DSL method ambiguity.
|
541
|
+
* No complete way to eliminate the surprising lookup behavior for instance variables.
|
542
|
+
* Does not solve the helper method vs DSL method ambiguity.
|
449
543
|
* Harder to implement than a simple <tt>instance_eval</tt>.
|
450
544
|
|
451
|
-
|
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 are worried about helper methods being available.
|
452
546
|
|
453
547
|
=== Implementation strategy 4: arity detection
|
454
548
|
|
@@ -501,8 +595,8 @@ Let us summarize Gray's arity detection technique, and then proceed to an intere
|
|
501
595
|
|
502
596
|
*Pros*:
|
503
597
|
|
504
|
-
*
|
505
|
-
*
|
598
|
+
* Gives the caller the ability to choose which syntax works best.
|
599
|
+
* Solves method lookup ambiguity.
|
506
600
|
* Implementation cost is not significant.
|
507
601
|
|
508
602
|
*Cons*:
|
@@ -510,7 +604,7 @@ Let us summarize Gray's arity detection technique, and then proceed to an intere
|
|
510
604
|
* Not an all-encompassing solution-- either choice still has its own pros and cons.
|
511
605
|
* Possibility of dilution of DSL branding.
|
512
606
|
|
513
|
-
|
607
|
+
<b>Use it when</b>: it is not clear whether block parameters or <tt>instance_eval</tt> is better, or if you need a way to mitigate the method lookup ambiguity.
|
514
608
|
|
515
609
|
=== Implementation strategy 5: mixins
|
516
610
|
|
@@ -563,13 +657,13 @@ Using mixico, we can now write the +draw+ method like this:
|
|
563
657
|
named_routes.install
|
564
658
|
end
|
565
659
|
|
566
|
-
Wow! That was simple. Mixico even handles all the eval-block-binding
|
660
|
+
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.
|
567
661
|
|
568
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.
|
569
663
|
|
570
664
|
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.
|
571
665
|
|
572
|
-
Instead of adding information to the block's context object, it may be plausible simply to stash the information away in a global location, such as a class variable, that can be accessed by the +MapperModule+ from within the block. 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.
|
666
|
+
Instead of adding information to the block's context object, it may be plausible simply to 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.
|
573
667
|
|
574
668
|
A third approach involves dynamically generating a singleton module, "hard coding" a reference to the +RouteSet+ in the module. For example:
|
575
669
|
|
@@ -588,7 +682,7 @@ A third approach involves dynamically generating a singleton module, "hard codin
|
|
588
682
|
|
589
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 a dynamically generated DSL depending on the context.
|
590
684
|
|
591
|
-
One more issue with the mixin strategy is that, like all implementations that drop the block parameter, there
|
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 called.
|
592
686
|
|
593
687
|
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:
|
594
688
|
|
@@ -658,13 +752,13 @@ As we have seen, the mixin idea seems like it may be a compelling solution, part
|
|
658
752
|
* Implementation is complicated and error-prone.
|
659
753
|
* The helper method vs DSL method ambiguity remains, exhibiting surprising behavior in the presence of singleton methods.
|
660
754
|
|
661
|
-
|
755
|
+
<b>Use it when</b>: parameterless blocks are desired and the method lookup ambiguity can be mitigated, as long as a library is available to handle the details of the implementation.
|
662
756
|
|
663
757
|
=== Blockenspiel: a comprehensive implementation
|
664
758
|
|
665
|
-
|
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. It could be a useful implementation if a library were present to handle the details.
|
666
760
|
|
667
|
-
Blockenspiel was written to be that library. It 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 <tt>instance_eval</tt
|
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 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.
|
668
762
|
|
669
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 Blockenspiel, and it will do the rest.
|
670
764
|
|
@@ -700,56 +794,90 @@ Our Rails routing example implemented using Blockenspiel might look like this:
|
|
700
794
|
|
701
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. Blockenspiel 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.
|
702
796
|
|
703
|
-
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
|
797
|
+
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:
|
704
798
|
|
705
799
|
class ConfigMethods
|
706
800
|
include Blockenspiel::DSL
|
707
801
|
attr_writer :author
|
708
802
|
attr_writer :title
|
803
|
+
dsl_method :set_author, :author= # Make the methods available in parameterless
|
804
|
+
dsl_method :set_title, :title= # blocks under these alternate names.
|
709
805
|
end
|
710
806
|
|
711
|
-
|
807
|
+
Now, when we use block parameters, we use the methods of the original ConfigMethods class:
|
712
808
|
|
713
809
|
create_paper do |config|
|
714
810
|
config.author = "Daniel Azuma"
|
715
811
|
config.title = "Implementing DSL Blocks"
|
716
812
|
end
|
717
813
|
|
718
|
-
|
814
|
+
And omitting the parameter, the alternate method names are mixed in:
|
719
815
|
|
720
816
|
create_paper do
|
721
|
-
|
722
|
-
|
817
|
+
set_author "Daniel Azuma"
|
818
|
+
set_title "Implementing DSL Blocks"
|
723
819
|
end
|
724
820
|
|
725
|
-
|
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 Blockenspiel library, leaving you free to focus on your API.
|
726
822
|
|
727
|
-
|
728
|
-
|
729
|
-
|
823
|
+
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, for example because we want to make the name "connect" available for named routes. That is, suppose we wanted to provide this behavior:
|
824
|
+
|
825
|
+
ActionController::Routing::Routes.draw(:method => :myconnect) do |map|
|
826
|
+
map.myconnect ':controller/:action/:id'
|
827
|
+
map.myconnect ':controller/:action/:page/:format'
|
828
|
+
# etc.
|
730
829
|
end
|
731
830
|
|
732
|
-
Blockenspiel
|
831
|
+
This can be implemented by using Blockenspiel to dynamically generate the proxy class, as follows:
|
733
832
|
|
734
|
-
class
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
833
|
+
class RouteSet
|
834
|
+
|
835
|
+
# We don't define a Mapper class anymore!
|
836
|
+
|
837
|
+
def draw(options={}, &block)
|
838
|
+
clear!
|
839
|
+
method_name = options[:method] || :connect # The method name for the DSL to use
|
840
|
+
save_self = self # Save a reference to the RouteSet
|
841
|
+
Blockenspiel.invoke(block) do # Dynamically create a "mapper" object
|
842
|
+
add_method(method_name) do |path, *args| # Dynamically add the method
|
843
|
+
save_self.add_route(path, *args) # Call back to the RouteSet
|
844
|
+
end
|
845
|
+
end
|
846
|
+
named_routes.install
|
847
|
+
end
|
848
|
+
|
849
|
+
# ...
|
850
|
+
|
851
|
+
def add_route(path, options = {})
|
852
|
+
# ...
|
741
853
|
|
742
|
-
|
854
|
+
The observant reader will notice two features of the above code. First, Blockenspiel's API for dynamically generating a DSL, itself uses a DSL block, and indeed Blockenspiel uses itself to implement this feature. The <tt>add_method</tt> call is part of Blockenspiel's DSL-generation DSL. And second, this code bears a remarkable resemblance to one of the approaches to implementing the proxy class in our discussion on mixins. Indeed, this is Blockenspiel's way of supporting that implementation strategy.
|
743
855
|
|
744
|
-
Blockenspiel is available as a gem
|
856
|
+
Blockenspiel is available now as a gem for MRI 1.8.x
|
745
857
|
|
746
858
|
gem install blockenspiel
|
747
859
|
|
748
|
-
|
860
|
+
More information is available on Blockenspiel's Rubyforge page at http://virtuoso.rubyforge.org/blockenspiel
|
861
|
+
|
862
|
+
Source code is available on Github at http://github.com/dazuma/blockenspiel
|
749
863
|
|
750
864
|
=== Summary
|
751
865
|
|
752
|
-
DSL blocks are a valuable and ubiquitous pattern for designing Ruby APIs. A flurry of discussion has recently
|
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 the need for parameters to the block. We have discussed several different strategies for DSL block implementation, each with its own advantages and disadvantages.
|
867
|
+
|
868
|
+
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
|
+
|
870
|
+
Parameterless blocks inherently pose some syntactic issues. First, it may be ambiguous whether a method is meant to be directed to the DSL or to the block's surrounding context. Second, certain constructions, such as those created by <tt>attr_writer</tt>, are syntactically not allowed and must be renamed.
|
871
|
+
|
872
|
+
The simplest way to eliminate the block parameter is to change +self+ inside the block using <tt>instance_eval</tt>. This has the side effects of opening the implementation of the proxy object, and cutting off access to the context's helper methods and instance variables.
|
873
|
+
|
874
|
+
It is possible to mitigate these side effects by delegating methods, and partially delegating instance variables, back to the context object. These are not foolproof mechanisms and are subject to a few cases of surprising behavior.
|
875
|
+
|
876
|
+
The mixin strategy takes a different approach to parameterless blocks by temporarily "mixing" the DSL methods into the context object itself. This eliminates the side effects of changing the +self+ reference, but requires a more complex implementation, and somewhat exacerbates the method lookup ambiguity.
|
877
|
+
|
878
|
+
Since the question of whether or not to take a block parameter may be best answered by the caller, it is often useful for an implementation to check the block's arity to determine whether to use a block parameter or a parameterless implementation. However, it is possible for this step to lead to dilution of the DSL's branding.
|
879
|
+
|
880
|
+
The Blockenspiel library provides a concrete and robust implementation of DSL blocks, based on the best of these ideas. It hides the implementation complexity while providing a number of features useful for writing DSL blocks.
|
753
881
|
|
754
882
|
=== References
|
755
883
|
|
@@ -778,3 +906,7 @@ DSL blocks are a valuable and ubiquitous pattern for designing Ruby APIs. A flur
|
|
778
906
|
{Why The Lucky Stiff}[http://whytheluckystiff.net/], <em>{Mixico}[http://github.com/why/mixico/tree/master]</em> (Ruby library), 2008.
|
779
907
|
|
780
908
|
{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.
|
909
|
+
|
910
|
+
=== About the author
|
911
|
+
|
912
|
+
Daniel Azuma is Chief Software Architect at Zoodango. He has been working with Ruby for about three years, and finds the language generally pleasant to work with, though he thinks the scoping rules could use some improvement. His home page is at http://www.daniel-azuma.com/.
|
data/Manifest.txt
CHANGED
data/lib/blockenspiel.rb
CHANGED
@@ -46,7 +46,28 @@ require 'mixology'
|
|
46
46
|
module Blockenspiel
|
47
47
|
|
48
48
|
# Current gem version
|
49
|
-
VERSION_STRING = '0.0.
|
49
|
+
VERSION_STRING = '0.0.4'
|
50
|
+
|
51
|
+
|
52
|
+
# Base exception for all exceptions raised by Blockenspiel
|
53
|
+
|
54
|
+
class BlockenspielError < RuntimeError
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
# This exception is rasied when attempting to use the <tt>:proxy</tt> or
|
59
|
+
# <tt>:mixin</tt> parameterless behavior with a target that does not have
|
60
|
+
# the DSL module included. It is an error made by the DSL implementor.
|
61
|
+
|
62
|
+
class DSLMissingError < BlockenspielError
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
# This exception is raised when the block provided does not take the
|
67
|
+
# expected number of parameters. It is an error made by the caller.
|
68
|
+
|
69
|
+
class BlockParameterError < BlockenspielError
|
70
|
+
end
|
50
71
|
|
51
72
|
|
52
73
|
# === DSL setup methods
|
@@ -365,8 +386,8 @@ module Blockenspiel
|
|
365
386
|
# If set to false, disables parameterless blocks and always attempts to
|
366
387
|
# pass a parameter to the block. Otherwise, you may set it to one of
|
367
388
|
# three behaviors for parameterless blocks: <tt>:mixin</tt> (the
|
368
|
-
# default), <tt>:
|
369
|
-
#
|
389
|
+
# default), <tt>:instance</tt>, and <tt>:proxy</tt>. See below for
|
390
|
+
# detailed descriptions of these behaviors.
|
370
391
|
# <tt>:parameter</tt>::
|
371
392
|
# If set to false, disables blocks with parameters, and always attempts
|
372
393
|
# to use parameterless blocks. Default is true, enabling parameter mode.
|
@@ -376,16 +397,17 @@ module Blockenspiel
|
|
376
397
|
#
|
377
398
|
# <tt>:mixin</tt>::
|
378
399
|
# This is the default behavior. DSL methods from the target are
|
379
|
-
# temporarily overlayed on the caller's self object, but self
|
380
|
-
#
|
381
|
-
# caller's closure remain available. The DSL methods
|
382
|
-
# the block completes.
|
400
|
+
# temporarily overlayed on the caller's +self+ object, but +self+ still
|
401
|
+
# points to the same object, so the helper methods and instance
|
402
|
+
# variables from the caller's closure remain available. The DSL methods
|
403
|
+
# are removed when the block completes.
|
383
404
|
# <tt>:instance</tt>::
|
384
405
|
# This behavior actually changes +self+ to the target object using
|
385
406
|
# <tt>instance_eval</tt>. Thus, the caller loses access to its own
|
386
407
|
# helper methods and instance variables, and instead gains access to the
|
387
|
-
# target object's instance variables.
|
388
|
-
#
|
408
|
+
# target object's instance variables. The target object's methods are
|
409
|
+
# not modified: this behavior does not apply any DSL method changes
|
410
|
+
# specified using <tt>dsl_method</tt> directives.
|
389
411
|
# <tt>:proxy</tt>::
|
390
412
|
# This behavior changes +self+ to a proxy object created by applying the
|
391
413
|
# DSL methods to an empty object, whose <tt>method_missing</tt> points
|
@@ -448,10 +470,18 @@ module Blockenspiel
|
|
448
470
|
|
449
471
|
def self.invoke(block_, target_=nil, opts_={}, &builder_block_)
|
450
472
|
|
451
|
-
|
452
|
-
|
473
|
+
parameter_ = opts_[:parameter]
|
474
|
+
parameterless_ = opts_[:parameterless]
|
475
|
+
|
476
|
+
# Handle no-target behavior
|
477
|
+
if parameter_ == false && parameterless_ == false
|
478
|
+
if block_.arity != 0 && block_.arity != -1
|
479
|
+
raise Blockenspiel::BlockParameterError, "Block should not take parameters"
|
480
|
+
end
|
481
|
+
return block_.call
|
482
|
+
end
|
453
483
|
|
454
|
-
#
|
484
|
+
# Perform dynamic target generation if requested
|
455
485
|
if builder_block_
|
456
486
|
opts_ = target_ || opts_
|
457
487
|
builder_ = Blockenspiel::Builder.new
|
@@ -459,115 +489,108 @@ module Blockenspiel
|
|
459
489
|
target_ = builder_._create_target
|
460
490
|
end
|
461
491
|
|
462
|
-
#
|
463
|
-
parameterless_
|
464
|
-
|
465
|
-
|
492
|
+
# Handle parametered block case
|
493
|
+
if parameter_ != false && (block_.arity == 1 || block_.arity == -2) || parameterless_ == false
|
494
|
+
if block_.arity != 1 && block_.arity != -1 && block_.arity != -2
|
495
|
+
raise Blockenspiel::BlockParameterError, "Block should take exactly one parameter"
|
496
|
+
end
|
497
|
+
return block_.call(target_)
|
498
|
+
end
|
499
|
+
|
500
|
+
# Check arity for parameterless case
|
501
|
+
if block_.arity != 0 && block_.arity != -1
|
502
|
+
raise Blockenspiel::BlockParameterError, "Block should not take parameters"
|
503
|
+
end
|
504
|
+
|
505
|
+
# Handle instance-eval behavior
|
506
|
+
if parameterless_ == :instance
|
507
|
+
return target_.instance_eval(&block_)
|
508
|
+
end
|
509
|
+
|
510
|
+
# Get the module of dsl methods
|
511
|
+
mod_ = target_.class._get_blockenspiel_module rescue nil
|
512
|
+
unless mod_
|
513
|
+
raise Blockenspiel::DSLMissingError
|
514
|
+
end
|
515
|
+
|
516
|
+
# Get the block's calling context object
|
517
|
+
object_ = Kernel.eval('self', block_.binding)
|
518
|
+
|
519
|
+
# Handle proxy behavior
|
520
|
+
if parameterless_ == :proxy
|
521
|
+
|
522
|
+
# Create proxy object
|
523
|
+
proxy_ = Blockenspiel::ProxyDelegator.new
|
524
|
+
proxy_.extend(mod_)
|
525
|
+
|
526
|
+
# Store the target and proxy object so dispatchers can get them
|
527
|
+
proxy_delegator_key_ = proxy_.object_id
|
528
|
+
target_stack_key_ = [Thread.current.object_id, proxy_.object_id]
|
529
|
+
@_proxy_delegators[proxy_delegator_key_] = object_
|
530
|
+
@_target_stacks[target_stack_key_] = [target_]
|
531
|
+
|
532
|
+
begin
|
466
533
|
|
467
|
-
#
|
468
|
-
return
|
534
|
+
# Call the block with the proxy as self
|
535
|
+
return proxy_.instance_eval(&block_)
|
469
536
|
|
470
|
-
|
537
|
+
ensure
|
471
538
|
|
472
|
-
#
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
# Get the block's calling context object
|
477
|
-
object_ = Kernel.eval('self', block_.binding)
|
478
|
-
|
479
|
-
if parameterless_ == :proxy
|
480
|
-
|
481
|
-
# Proxy behavior:
|
482
|
-
# Create proxy object
|
483
|
-
proxy_ = ProxyDelegator.new
|
484
|
-
proxy_.extend(mod_)
|
485
|
-
|
486
|
-
# Store the target and proxy object so dispatchers can get them
|
487
|
-
proxy_delegator_key_ = proxy_.object_id
|
488
|
-
target_stack_key_ = [Thread.current.object_id, proxy_.object_id]
|
489
|
-
@_proxy_delegators[proxy_delegator_key_] = object_
|
490
|
-
@_target_stacks[target_stack_key_] = [target_]
|
491
|
-
|
492
|
-
begin
|
493
|
-
|
494
|
-
# Call the block with the proxy as self
|
495
|
-
return proxy_.instance_eval(&block_)
|
496
|
-
|
497
|
-
ensure
|
498
|
-
|
499
|
-
# Clean up the dispatcher information
|
500
|
-
@_proxy_delegators.delete(proxy_delegator_key_)
|
501
|
-
@_target_stacks.delete(target_stack_key_)
|
502
|
-
|
503
|
-
end
|
504
|
-
|
505
|
-
else
|
506
|
-
|
507
|
-
# Mixin behavior:
|
508
|
-
# Create hash keys
|
509
|
-
mixin_count_key_ = [object_.object_id, mod_.object_id]
|
510
|
-
target_stack_key_ = [Thread.current.object_id, object_.object_id]
|
511
|
-
|
512
|
-
# Store the target for inheriting.
|
513
|
-
# We maintain a target call stack per thread.
|
514
|
-
target_stack_ = @_target_stacks[target_stack_key_] ||= Array.new
|
515
|
-
target_stack_.push(target_)
|
516
|
-
|
517
|
-
# Mix this module into the object, if required.
|
518
|
-
# This ensures that we keep track of the number of requests to
|
519
|
-
# mix this module in, from nested blocks and possibly multiple threads.
|
520
|
-
@_mutex.synchronize do
|
521
|
-
count_ = @_mixin_counts[mixin_count_key_]
|
522
|
-
if count_
|
523
|
-
@_mixin_counts[mixin_count_key_] = count_ + 1
|
524
|
-
else
|
525
|
-
@_mixin_counts[mixin_count_key_] = 1
|
526
|
-
object_.mixin(mod_)
|
527
|
-
end
|
528
|
-
end
|
529
|
-
|
530
|
-
begin
|
531
|
-
|
532
|
-
# Now call the block
|
533
|
-
return block_.call
|
534
|
-
|
535
|
-
ensure
|
536
|
-
|
537
|
-
# Clean up the target stack
|
538
|
-
target_stack_.pop
|
539
|
-
@_target_stacks.delete(target_stack_key_) if target_stack_.size == 0
|
540
|
-
|
541
|
-
# Remove the mixin from the object, if required.
|
542
|
-
@_mutex.synchronize do
|
543
|
-
count_ = @_mixin_counts[mixin_count_key_]
|
544
|
-
if count_ == 1
|
545
|
-
@_mixin_counts.delete(mixin_count_key_)
|
546
|
-
object_.unmix(mod_)
|
547
|
-
else
|
548
|
-
@_mixin_counts[mixin_count_key_] = count_ - 1
|
549
|
-
end
|
550
|
-
end
|
551
|
-
|
552
|
-
end
|
553
|
-
# End mixin behavior
|
554
|
-
|
555
|
-
end
|
556
|
-
|
557
|
-
end
|
558
|
-
# End use of dsl methods module
|
539
|
+
# Clean up the dispatcher information
|
540
|
+
@_proxy_delegators.delete(proxy_delegator_key_)
|
541
|
+
@_target_stacks.delete(target_stack_key_)
|
559
542
|
|
560
543
|
end
|
544
|
+
|
561
545
|
end
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
546
|
+
|
547
|
+
# Handle mixin behavior (default)
|
548
|
+
|
549
|
+
# Create hash keys
|
550
|
+
mixin_count_key_ = [object_.object_id, mod_.object_id]
|
551
|
+
target_stack_key_ = [Thread.current.object_id, object_.object_id]
|
552
|
+
|
553
|
+
# Store the target for inheriting.
|
554
|
+
# We maintain a target call stack per thread.
|
555
|
+
target_stack_ = @_target_stacks[target_stack_key_] ||= Array.new
|
556
|
+
target_stack_.push(target_)
|
557
|
+
|
558
|
+
# Mix this module into the object, if required.
|
559
|
+
# This ensures that we keep track of the number of requests to
|
560
|
+
# mix this module in, from nested blocks and possibly multiple threads.
|
561
|
+
@_mutex.synchronize do
|
562
|
+
count_ = @_mixin_counts[mixin_count_key_]
|
563
|
+
if count_
|
564
|
+
@_mixin_counts[mixin_count_key_] = count_ + 1
|
565
|
+
else
|
566
|
+
@_mixin_counts[mixin_count_key_] = 1
|
567
|
+
object_.mixin(mod_)
|
568
|
+
end
|
567
569
|
end
|
568
570
|
|
569
|
-
|
570
|
-
|
571
|
+
begin
|
572
|
+
|
573
|
+
# Now call the block
|
574
|
+
return block_.call
|
575
|
+
|
576
|
+
ensure
|
577
|
+
|
578
|
+
# Clean up the target stack
|
579
|
+
target_stack_.pop
|
580
|
+
@_target_stacks.delete(target_stack_key_) if target_stack_.size == 0
|
581
|
+
|
582
|
+
# Remove the mixin from the object, if required.
|
583
|
+
@_mutex.synchronize do
|
584
|
+
count_ = @_mixin_counts[mixin_count_key_]
|
585
|
+
if count_ == 1
|
586
|
+
@_mixin_counts.delete(mixin_count_key_)
|
587
|
+
object_.unmix(mod_)
|
588
|
+
else
|
589
|
+
@_mixin_counts[mixin_count_key_] = count_ - 1
|
590
|
+
end
|
591
|
+
end
|
592
|
+
|
593
|
+
end
|
571
594
|
|
572
595
|
end
|
573
596
|
|
@@ -579,7 +602,7 @@ module Blockenspiel
|
|
579
602
|
|
580
603
|
def self._target_dispatch(object_, name_, params_, block_) # :nodoc:
|
581
604
|
target_stack_ = @_target_stacks[[Thread.current.object_id, object_.object_id]]
|
582
|
-
return TARGET_MISMATCH unless target_stack_
|
605
|
+
return Blockenspiel::TARGET_MISMATCH unless target_stack_
|
583
606
|
target_stack_.reverse_each do |target_|
|
584
607
|
target_class_ = target_.class
|
585
608
|
delegate_ = target_class_._get_blockenspiel_delegate(name_)
|
@@ -587,7 +610,7 @@ module Blockenspiel
|
|
587
610
|
return target_.send(delegate_, *params_, &block_)
|
588
611
|
end
|
589
612
|
end
|
590
|
-
return TARGET_MISMATCH
|
613
|
+
return Blockenspiel::TARGET_MISMATCH
|
591
614
|
end
|
592
615
|
|
593
616
|
|
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.0.4
|
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-24 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -52,8 +52,9 @@ files:
|
|
52
52
|
- Rakefile
|
53
53
|
- lib/blockenspiel.rb
|
54
54
|
- tests/tc_basic.rb
|
55
|
-
- tests/
|
55
|
+
- tests/tc_behaviors.rb
|
56
56
|
- tests/tc_dsl_methods.rb
|
57
|
+
- tests/tc_mixins.rb
|
57
58
|
has_rdoc: true
|
58
59
|
homepage: http://virtuoso.rubyforge.org/blockenspiel
|
59
60
|
post_install_message:
|