File: Blockenspiel.rdoc

package info (click to toggle)
ruby-blockenspiel 0.4.5-1
  • links: PTS, VCS
  • area: main
  • in suites: jessie, jessie-kfreebsd
  • size: 352 kB
  • ctags: 202
  • sloc: ruby: 1,467; ansic: 38; makefile: 6
file content (390 lines) | stat: -rw-r--r-- 14,402 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
== Blockenspiel

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.

This is an introduction to DSL blocks and the features of Blockenspiel.

=== What's a DSL block?

A DSL block is an API pattern in which a method call takes a block that can
provide further configuration for the call. A classic example is the
{Rails}[http://www.rubyonrails.org/] route definition:

 ActionController::Routing::Routes.draw do |map|
   map.connect ':controller/:action/:id'
   map.connect ':controller/:action/:id.:format'
 end

Some libraries go one step further and eliminate the need for a block
parameter. {RSpec}[http://rspec.info/] is a well-known example:

 describe Stack do
   before(:each) do
     @stack = Stack.new
   end
   describe "(empty)" do
     it { @stack.should be_empty }
     it "should complain when sent #peek" do
       lambda { @stack.peek }.should raise_error(StackUnderflowError)
     end
   end
 end

In both cases, the caller provides descriptive information in the block,
using a domain-specific language. The second form, which eliminates the
block parameter, often appears cleaner; however it is also sometimes less
clear what is actually going on.

=== How does one implement such a beast?

Implementing the first form is fairly straightforward. You would create a
class defining the methods (such as +connect+ in our Rails routing example
above) that should be available within the block. When, for example, the
<tt>draw</tt> method is called with a block, you instantiate the class and
yield it to the block.

The second form is perhaps more mystifying. Somehow you would need to make
the DSL methods available on the "self" object inside the block. There are
several plausible ways to do this, such as using <tt>instance_eval</tt>.
However, there are many subtle pitfalls in such techniques, and quite a bit
of discussion has taken place in the Ruby community regarding how--or
whether--to safely implement such a syntax.

I have included a critical survey of the discussion in the document
{ImplementingDSLblocks.rdoc}[link:ImplementingDSLblocks\_rdoc.html] for
the curious. Blockenspiel takes what I consider the best of the solutions
and implements them in a comprehensive way, shielding you from the
complexity of the Ruby metaprogramming while offering a simple way to
implement both forms of DSL blocks.

=== So what _is_ Blockenspiel?

Blockenspiel operates on the following observations:

* Implementing a DSL block that takes a parameter is straightforward.
* Safely implementing a DSL block that <em>doesn't</em> take a parameter is
  tricky.

With that in mind, Blockenspiel provides a set of tools that allow you to
take an implementation of the first form of a DSL block, one that takes a
parameter, and turn it into an implementation of the second form, one that
doesn't take a parameter.

Suppose you wanted to write a simple DSL block that takes a parameter:

 configure_me do |config|
   config.add_foo(1)
   config.add_bar(2)
 end

You could write this as follows:

 class ConfigMethods
   def add_foo(value)
     # do something
   end
   def add_bar(value)
     # do something
   end
 end
 
 def configure_me
   yield ConfigMethods.new
 end

That was easy. However, now suppose you wanted to support usage _without_
the "config" parameter. e.g.

 configure_me do
   add_foo(1)
   add_bar(2)
 end

With Blockenspiel, you can do this in two quick steps.
First, tell Blockenspiel that your +ConfigMethods+ class is a DSL.

 class ConfigMethods
   include Blockenspiel::DSL   # <--- Add this line
   def add_foo(value)
     # do something
   end
   def add_bar(value)
     # do something
   end
 end

Next, write your <tt>configure_me</tt> method using Blockenspiel:

 def configure_me(&block)
   Blockenspiel.invoke(block, ConfigMethods.new)
 end

Now, your <tt>configure_me</tt> method supports _both_ DSL block forms. A
caller can opt to use the first form, with a parameter, simply by providing
a block that takes a parameter. Or, if the caller provides a block that
doesn't take a parameter, the second form without a parameter is used.

=== How does that help me? (Or, why not just use instance_eval?)

As noted earlier, some libraries that provide parameter-less DSL blocks use
<tt>instance_eval</tt>, and they could even support both the parameter and
parameter-less mechanisms by checking the block arity:

 def configure_me(&block)
   if block.arity == 1
     yield ConfigMethods.new
   else
     ConfigMethods.new.instance_eval(&block)
   end
 end

That seems like a simple and effective technique that doesn't require a
separate library, so why use Blockenspiel? Because <tt>instance_eval</tt>
introduces a number of surprising problems. I discuss these issues in detail
in {ImplementingDSLblocks.rdoc}[link:ImplementingDSLblocks\_rdoc.html],
but just to get your feet wet, suppose the caller wanted to call its own
methods inside the block:

 def callers_helper_method
   # ...
 end
 
 configure_me do
   add_foo(1)
   callers_helper_method  # Error! self is now an instance of ConfigMethods
                          # so this will fail with a NameError
   add_bar(2)
 end

Blockenspiel by default does _not_ use the <tt>instance_eval</tt> technique.
Instead, it implements a mechanism using mixin modules, a technique proposed
by the late {Why}[http://en.wikipedia.org/wiki/Why_the_lucky_stiff]. In this
technique, the <tt>add_foo</tt> and <tt>add_bar</tt> methods are temporarily
mixed into the caller's +self+ object. That is, +self+ does not change, as
it would if we used <tt>instance_eval</tt>, so helper methods like
<tt>callers_helper_method</tt> still remain available as expected. But, the
<tt>add_foo</tt> and <tt>add_bar</tt> methods are also made available
temporarily for the duration of the block. When called, they are intercepted
and redirected to your +ConfigMethods+ instance just as if you had called
them directly via a block parameter. Blockenspiel handles the object
redirection behind the scenes so you do not have to think about it. With
Blockenspiel, the caller retains access to its helper methods, and even its
own instance variables, within the block, because +self+ has not been
modified.

=== Is that it?

Although the basic usage is very simple, Blockenspiel is designed to be
_comprehensive_. It supports all the use cases that I've run into during my
own implementation of DSL blocks. Notably:

By default, Blockenspiel lets the caller choose to use a parametered block
or a parameterless block, based on whether or not the block actually takes a
parameter. You can also disable one or the other, to force the use of either
a parametered or parameterless block.

You can also let the caller use your DSL by passing you a string or a file
rather than a block. That is, you can create file-based DSLs such as the
Rails routes file.

You can control wich methods of the class are available from parameterless
blocks, and/or make some methods available under different names. Here are
a few examples:

 class ConfigMethods
   include Blockenspiel::DSL
   
   def add_foo         # automatically added to the dsl
     # do stuff...
   end
   
   def my_private_method
     # do stuff...
   end
   dsl_method :my_private_method, false   # remove from the dsl
   
   dsl_methods false   # stop automatically adding methods to the dsl
   
   def another_private_method  # not added
     # do stuff...
   end
   
   dsl_methods true    # resume automatically adding methods to the dsl
   
   def add_bar         # this method is automatically added
     # do stuff...
   end
   
   def add_baz
     # do stuff
   end
   dsl_method :add_baz_in_dsl, :add_baz  # Method named differently
                                         # in a parameterless block
 end

This is also useful, for example, when you use <tt>attr_writer</tt>.
Parameterless blocks do not support <tt>attr_writer</tt> (or, by corollary,
<tt>attr_accessor</tt>) well because methods with names of the form
"attribute=" are syntactically indistinguishable from variable assignments:

 configure_me do |config|
   config.foo = 1    # works fine when the block has a parameter
 end
 
 configure_me do
   # foo = 1     # <--- Doesn't work: looks like a variable assignment
   set_foo(1)    # <--- Fix it by renaming to this instead
 end
 
 # This is implemented like this::
 class ConfigMethods
   include Blockenspiel::DSL
   attr_writer :foo
   dsl_method :set_foo, :foo=    # Make "foo=" available as "set_foo"
 end

This is in fact a common enough case that Blockenspiel includes
conveninence tools for a DSL-friendly attr_writer and attr_accessor,
providing an alternate syntax for setting attributes within a
parameterless block:

 configure_me do
   # foo = 1             # This syntax wouldn't work, but
   foo 1                 # this syntax is now supported.
   puts "foo is #{foo}"  # The getter still works.
 end
 
 # This is implemented like this::
 class ConfigMethods
   include Blockenspiel::DSL
   dsl_attr_accessor :foo     # DSL-friendly attr_accessor
 end

In some cases, you might want to dynamically generate a DSL object rather
than defining a static class. Blockenspiel provides a tool to do just that.
Here's an example:

 Blockenspiel.invoke(block) do
   add_method(:set_foo) do |value|
     my_foo = value
   end
   add_method(:set_things_using_block) do |value, &blk|
     my_foo = value
     my_bar = blk.call
   end
 end

That API is itself a DSL block, and yes, Blockenspiel uses itself to
implement this feature.

By default Blockenspiel uses mixins, which usually exhibit fairly safe and
non-surprising behavior. However, there are a few cases when you might
want the <tt>instance_eval</tt> behavior anyway. RSpec is a good example of
such a case, since the DSL is being used to construct objects, so it makes
sense for instance variables inside the block to belong to the object
being constructed. Blockenspiel gives you the option of choosing
<tt>instance_eval</tt> in case you need it. Blockenspiel also provides a
compromise behavior that uses a proxy to dispatch methods to the DSL object
or the block's context.

Blockenspiel also correctly handles nested blocks. e.g.

 configure_me do
   set_foo(1)
   configure_another do     # A block within another block
     set_bar(2)
     configure_another do   # A block within itself
       set_bar(3)
     end
   end
 end

Finally, it is thread safe, correctly handling, for example, the case of
multiple threads trying to mix methods into the same object concurrently.

=== Requirements

* Ruby 1.8.7, Ruby 1.9.1 or later, JRuby 1.5 or later, or Rubinius 1.0 or later.

=== Installation

 gem install blockenspiel

=== Known issues and to-do items

* Implementing wildcard DSL methods using <tt>method_missing</tt> doesn't
  work. I haven't yet decided on the right semantics for this case, or
  whether it is even a reasonable feature at all.
* Including Blockenspiel::DSL in a module (rather than a class) is not yet
  supported, but this is planned for a future release.
* Installing on Windows may be a challenge because blockenspiel includes a
  native extension. I'm considering evaluating Luis Lavena's rake-compiler
  to simplify this process.

=== Development and support

Documentation is available at http://dazuma.github.com/blockenspiel/rdoc

Source code is hosted on Github at http://github.com/dazuma/blockenspiel

Contributions are welcome. Fork the project on Github.

Build status: {<img src="https://secure.travis-ci.org/dazuma/blockenspiel.png" />}[http://travis-ci.org/dazuma/blockenspiel]

Report bugs on Github issues at http://github.org/dazuma/blockenspiel/issues

Contact the author at dazuma at gmail dot com.

=== Author / Credits

Blockenspiel is written by Daniel Azuma (http://www.daniel-azuma.com/).

The mixin implementation is based on a concept by the late Why The Lucky
Stiff, documented in his 6 October 2008 blog posting entitled "Mixing Our
Way Out Of Instance Eval?". The original link is gone, but you may find
copies or mirrors out there.

The unmixer code is based on {Mixology}[http://rubyforge.org/projects/mixology],
version by Patrick Farley, anonymous z, Dan Manges, and Clint Bishop.
The MRI C extension and the JRuby code were adapted from Mixology 0.1, and
have been stripped down and modified to support Ruby 1.9 and JRuby >= 1.2.
The Rubinius code was adapted from unreleased code in the Mixology source
tree and modified to support Rubinius 1.0. I know Mixology 0.2 is now
available, but its Rubinius support is not active, and I'd rather keep the
unmixer bundled with Blockenspiel for now to reduce dependencies.

The dsl_attr_writer and dsl_attr_accessor feature came from a suggestion by
Luis Lavena.

=== License

Copyright 2008-2011 Daniel Azuma.

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice,
  this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
  this list of conditions and the following disclaimer in the documentation
  and/or other materials provided with the distribution.
* Neither the name of the copyright holder, nor the names of any other
  contributors to this software, may be used to endorse or promote products
  derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.