blockenspiel 0.2.2-java → 0.3.0-java
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/Blockenspiel.rdoc +374 -0
- data/History.rdoc +9 -0
- data/README.rdoc +26 -252
- data/Rakefile +5 -3
- data/lib/blockenspiel/impl.rb +197 -49
- data/lib/blockenspiel/version.rb +1 -1
- data/lib/blockenspiel_unmixer.jar +0 -0
- data/tests/tc_basic.rb +3 -3
- data/tests/tc_behaviors.rb +8 -8
- data/tests/tc_dsl_attrs.rb +134 -0
- data/tests/tc_dsl_methods.rb +6 -6
- data/tests/tc_dynamic.rb +55 -27
- data/tests/tc_mixins.rb +8 -8
- metadata +7 -3
data/README.rdoc
CHANGED
@@ -5,106 +5,29 @@ blocks. It is designed to be comprehensive and robust, supporting most common
|
|
5
5
|
usage patterns, and working correctly in the presence of nested blocks and
|
6
6
|
multithreading.
|
7
7
|
|
8
|
-
===
|
8
|
+
=== Summary
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
ActionController::Routing::Routes.draw do |map|
|
15
|
-
map.connect ':controller/:action/:id'
|
16
|
-
map.connect ':controller/:action/:id.:format'
|
17
|
-
end
|
18
|
-
|
19
|
-
Some libraries go one step further and eliminate the need for a block
|
20
|
-
parameter. {RSpec}[http://rspec.info/] is a well-known example:
|
21
|
-
|
22
|
-
describe Stack do
|
23
|
-
before(:each) do
|
24
|
-
@stack = Stack.new
|
25
|
-
end
|
26
|
-
describe "(empty)" do
|
27
|
-
it { @stack.should be_empty }
|
28
|
-
it "should complain when sent #peek" do
|
29
|
-
lambda { @stack.peek }.should raise_error(StackUnderflowError)
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
In both cases, the caller provides descriptive information in the block,
|
35
|
-
using a domain-specific language. The second form, which eliminates the block
|
36
|
-
parameter, often appears cleaner; however it is also sometimes less clear
|
37
|
-
what is actually going on.
|
38
|
-
|
39
|
-
=== How does one implement such a beast?
|
40
|
-
|
41
|
-
Implementing the first form is fairly straightforward. You would create a
|
42
|
-
class defining the methods (such as +connect+ in our Rails routing example
|
43
|
-
above) that should be available within the block. When, for example, the
|
44
|
-
<tt>draw</tt> method is called with a block, you instantiate the class and
|
45
|
-
yield it to the block.
|
46
|
-
|
47
|
-
The second form is perhaps more mystifying. Somehow you would need to make
|
48
|
-
the DSL methods available on the "self" object inside the block. There are
|
49
|
-
several plausible ways to do this, such as using <tt>instance_eval</tt>.
|
50
|
-
However, there are many subtle pitfalls in such techniques, and quite a bit
|
51
|
-
of discussion has taken place in the Ruby community regarding how--or
|
52
|
-
whether--to safely implement such a syntax.
|
53
|
-
|
54
|
-
I have included a critical survey of the debate in the document
|
55
|
-
{ImplementingDSLblocks.txt}[link:files/ImplementingDSLblocks\_txt.html] for
|
56
|
-
the curious. Blockenspiel takes what I consider the best of the solutions and
|
57
|
-
implements them in a comprehensive way, shielding you from the complexity of
|
58
|
-
the Ruby metaprogramming while offering a simple way to implement both forms
|
59
|
-
of DSL blocks.
|
60
|
-
|
61
|
-
=== So what _is_ Blockenspiel?
|
62
|
-
|
63
|
-
Blockenspiel operates on the following observations:
|
64
|
-
|
65
|
-
* Implementing a DSL block that takes a parameter is straightforward.
|
66
|
-
* Safely implementing a DSL block that <em>doesn't</em> take a parameter is tricky.
|
67
|
-
|
68
|
-
With that in mind, Blockenspiel provides a set of tools that allow you to
|
69
|
-
take an implementation of the first form of a DSL block, one that takes a
|
70
|
-
parameter, and turn it into an implementation of the second form, one that
|
71
|
-
doesn't take a parameter.
|
72
|
-
|
73
|
-
Suppose you wanted to write a simple DSL block that takes a parameter:
|
10
|
+
Blockenspiel is a helper library providing several different strategies for
|
11
|
+
implementing DSL blocks. It supports both DSLs that take a block parameter
|
12
|
+
and those that do not. For example:
|
74
13
|
|
14
|
+
# Call DSL block with parameter
|
75
15
|
configure_me do |config|
|
76
16
|
config.add_foo(1)
|
77
17
|
config.add_bar(2)
|
78
18
|
end
|
79
|
-
|
80
|
-
You could write this as follows:
|
81
|
-
|
82
|
-
class ConfigMethods
|
83
|
-
def add_foo(value)
|
84
|
-
# do something
|
85
|
-
end
|
86
|
-
def add_bar(value)
|
87
|
-
# do something
|
88
|
-
end
|
89
|
-
end
|
90
19
|
|
91
|
-
|
92
|
-
yield ConfigMethods.new
|
93
|
-
end
|
94
|
-
|
95
|
-
That was easy. However, now suppose you wanted to support usage _without_
|
96
|
-
the "config" parameter. e.g.
|
97
|
-
|
20
|
+
# Call DSL block without parameter
|
98
21
|
configure_me do
|
99
|
-
add_foo(
|
100
|
-
add_bar(
|
22
|
+
add_foo(3)
|
23
|
+
add_bar(4)
|
101
24
|
end
|
102
25
|
|
103
|
-
|
104
|
-
First, tell Blockenspiel that your +ConfigMethods+ class is a DSL.
|
26
|
+
To support the above usage, you can do this:
|
105
27
|
|
28
|
+
# Implement DSL block methods
|
106
29
|
class ConfigMethods
|
107
|
-
include Blockenspiel::DSL
|
30
|
+
include Blockenspiel::DSL
|
108
31
|
def add_foo(value)
|
109
32
|
# do something
|
110
33
|
end
|
@@ -112,179 +35,28 @@ First, tell Blockenspiel that your +ConfigMethods+ class is a DSL.
|
|
112
35
|
# do something
|
113
36
|
end
|
114
37
|
end
|
115
|
-
|
116
|
-
|
117
|
-
|
38
|
+
|
39
|
+
# Implement configure_me method
|
118
40
|
def configure_me(&block)
|
119
41
|
Blockenspiel.invoke(block, ConfigMethods.new)
|
120
42
|
end
|
121
43
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
=== How does that help me? (Or, why not just use instance_eval?)
|
128
|
-
|
129
|
-
As noted earlier, some libraries that provide parameter-less DSL blocks use
|
130
|
-
<tt>instance_eval</tt>, and they could even support both the parameter and
|
131
|
-
parameter-less mechanisms by checking the block arity:
|
132
|
-
|
133
|
-
def configure_me(&block)
|
134
|
-
if block.arity == 1
|
135
|
-
yield ConfigMethods.new
|
136
|
-
else
|
137
|
-
ConfigMethods.new.instance_eval(&block)
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
That seems like a simple and effective technique that doesn't require a
|
142
|
-
separate library, so why use Blockenspiel? Because <tt>instance_eval</tt>
|
143
|
-
introduces a number of surprising problems. I discuss these issues in detail
|
144
|
-
in {ImplementingDSLblocks.txt}[link:files/ImplementingDSLblocks\_txt.html],
|
145
|
-
but just to get your feet wet, suppose the caller wanted to call its own
|
146
|
-
methods inside the block:
|
44
|
+
By default, Blockenspiel uses a mixin technique (proposed by the late Why
|
45
|
+
The Lucky Stiff) to support parameterless blocks without the complications
|
46
|
+
introduced by <tt>instance_eval</tt>. It supports nested blocks and
|
47
|
+
multithreaded access, and provides a variety of tools for handling the
|
48
|
+
typical issues you may encounter when writing DSLs.
|
147
49
|
|
148
|
-
|
149
|
-
|
150
|
-
end
|
151
|
-
|
152
|
-
configure_me do
|
153
|
-
add_foo(1)
|
154
|
-
callers_helper_method # Error! self is now an instance of ConfigMethods
|
155
|
-
# so this will fail with a NameError
|
156
|
-
add_bar(2)
|
157
|
-
end
|
158
|
-
|
159
|
-
Blockenspiel by default does _not_ use the <tt>instance_eval</tt> technique.
|
160
|
-
Instead, it implements a mechanism using mixin modules, a technique first
|
161
|
-
{proposed}[http://hackety.org/2008/10/06/mixingOurWayOutOfInstanceEval.html]
|
162
|
-
by Why. In this technique, the <tt>add_foo</tt> and <tt>add_bar</tt> methods
|
163
|
-
are temporarily mixed into the caller's +self+ object. That is, +self+ does
|
164
|
-
not change, as it would if we used <tt>instance_eval</tt>, so helper methods
|
165
|
-
like <tt>callers_helper_method</tt> still remain available as expected. But,
|
166
|
-
the <tt>add_foo</tt> and <tt>add_bar</tt> methods are also made available
|
167
|
-
temporarily for the duration of the block. When called, they are intercepted
|
168
|
-
and redirected to your +ConfigMethods+ instance just as if you had called
|
169
|
-
them directly via a block parameter. Blockenspiel handles the object
|
170
|
-
redirection behind the scenes so you do not have to think about it. With
|
171
|
-
Blockenspiel, the caller retains access to its helper methods, and even its
|
172
|
-
own instance variables, within the block, because +self+ has not been
|
173
|
-
modified.
|
174
|
-
|
175
|
-
=== Is that it?
|
176
|
-
|
177
|
-
Although the basic usage is very simple, Blockenspiel is designed to be
|
178
|
-
_comprehensive_. It supports all the use cases that I've run into during my
|
179
|
-
own implementation of DSL blocks. Notably:
|
180
|
-
|
181
|
-
By default, Blockenspiel lets the caller choose to use a parametered block
|
182
|
-
or a parameterless block, based on whether or not the block actually takes a
|
183
|
-
parameter. You can also disable one or the other, to force the use of either
|
184
|
-
a parametered or parameterless block.
|
185
|
-
|
186
|
-
You can control wich methods of the class are available from parameterless
|
187
|
-
blocks, and/or make some methods available under different names. Here are
|
188
|
-
a few examples:
|
189
|
-
|
190
|
-
class ConfigMethods
|
191
|
-
include Blockenspiel::DSL
|
192
|
-
|
193
|
-
def add_foo # automatically added to the dsl
|
194
|
-
# do stuff...
|
195
|
-
end
|
196
|
-
|
197
|
-
def my_private_method
|
198
|
-
# do stuff...
|
199
|
-
end
|
200
|
-
dsl_method :my_private_method, false # remove from the dsl
|
201
|
-
|
202
|
-
dsl_methods false # stop automatically adding methods to the dsl
|
203
|
-
|
204
|
-
def another_private_method # not added
|
205
|
-
# do stuff...
|
206
|
-
end
|
207
|
-
|
208
|
-
dsl_methods true # resume automatically adding methods to the dsl
|
209
|
-
|
210
|
-
def add_bar # this method is automatically added
|
211
|
-
# do stuff...
|
212
|
-
end
|
213
|
-
|
214
|
-
def add_baz
|
215
|
-
# do stuff
|
216
|
-
end
|
217
|
-
dsl_method :add_baz_in_dsl, :add_baz # Method named differently
|
218
|
-
# in a parameterless block
|
219
|
-
end
|
220
|
-
|
221
|
-
This is also useful, for example, when you use <tt>attr_writer</tt>.
|
222
|
-
Parameterless blocks do not support <tt>attr_writer</tt> (or, by corollary,
|
223
|
-
<tt>attr_accessor</tt>) well because methods with names of the form
|
224
|
-
"attribute=" are syntactically indistinguishable from variable assignments:
|
225
|
-
|
226
|
-
configure_me do |config|
|
227
|
-
config.foo = 1 # works fine when the block has a parameter
|
228
|
-
end
|
229
|
-
|
230
|
-
configure_me do
|
231
|
-
# foo = 1 # <--- Doesn't work: looks like a variable assignment
|
232
|
-
set_foo(1) # <--- Renamed to this instead
|
233
|
-
end
|
234
|
-
|
235
|
-
# This is implemented like this::
|
236
|
-
class ConfigMethods
|
237
|
-
include Blockenspiel::DSL
|
238
|
-
attr_writer :foo
|
239
|
-
dsl_method :set_foo, :foo= # Make "foo=" available as "set_foo"
|
240
|
-
end
|
241
|
-
|
242
|
-
In some cases, you might want to dynamically generate a DSL object rather
|
243
|
-
than defining a static class. Blockenspiel provides a tool to do just that.
|
244
|
-
Here's an example:
|
245
|
-
|
246
|
-
Blockenspiel.invoke(block) do
|
247
|
-
add_method(:set_foo) do |value|
|
248
|
-
my_foo = value
|
249
|
-
end
|
250
|
-
add_method(:set_things_using_block, :receive_block => true) do |value, blk|
|
251
|
-
my_foo = value
|
252
|
-
my_bar = blk.call
|
253
|
-
end
|
254
|
-
end
|
255
|
-
|
256
|
-
That API is in itself a DSL block, and yes, Blockenspiel uses itself to
|
257
|
-
implement this feature.
|
258
|
-
|
259
|
-
By default Blockenspiel uses mixins, which usually exhibit fairly safe and
|
260
|
-
non-surprising behavior. However, there are a few cases when you might
|
261
|
-
want the <tt>instance_eval</tt> behavior anyway. RSpec is a good example of
|
262
|
-
such a case, since the DSL is being used to construct objects, so it makes
|
263
|
-
sense for instance variables inside the block to belong to the object
|
264
|
-
being constructed. Blockenspiel gives you the option of choosing
|
265
|
-
<tt>instance_eval</tt> in case you need it. Blockenspiel also provides a
|
266
|
-
compromise behavior that uses a proxy to dispatch methods to the DSL object
|
267
|
-
or the block's context.
|
268
|
-
|
269
|
-
Blockenspiel also correctly handles nested blocks. e.g.
|
270
|
-
|
271
|
-
configure_me do
|
272
|
-
set_foo(1)
|
273
|
-
configure_another do # A block within another block
|
274
|
-
set_bar(2)
|
275
|
-
configure_another do # A block within itself
|
276
|
-
set_bar(3)
|
277
|
-
end
|
278
|
-
end
|
279
|
-
end
|
50
|
+
For more detailed usage and examples, see
|
51
|
+
{Blockenspiel.rdoc}[link:Blockenspiel\_rdoc.html].
|
280
52
|
|
281
|
-
|
282
|
-
|
283
|
-
concurrently.
|
53
|
+
For an extended analysis of different ways to implement DSL blocks, see
|
54
|
+
{ImplementingDSLblocks.rdoc}[link:ImplementingDSLblocks\_rdoc.html].
|
284
55
|
|
285
56
|
=== Requirements
|
286
57
|
|
287
|
-
* Ruby 1.8.6 or later (1.8.7 recommended), Ruby 1.9.1 or later, or JRuby 1.2
|
58
|
+
* Ruby 1.8.6 or later (1.8.7 recommended), Ruby 1.9.1 or later, or JRuby 1.2
|
59
|
+
or later (1.4 recommended).
|
288
60
|
|
289
61
|
=== Installation
|
290
62
|
|
@@ -317,6 +89,8 @@ copies or mirrors out there.
|
|
317
89
|
The unmixer code is based on {Mixology}[http://rubyforge.org/projects/mixology],
|
318
90
|
version 0.1 by Patrick Farley, anonymous z, Dan Manges, and Clint Bishop.
|
319
91
|
The code has been stripped down and modified to support MRI 1.9 and JRuby 1.2.
|
92
|
+
I know Mixology 0.2 is available, but I'm keeping the unmixer bundled with
|
93
|
+
Blockenspiel for now, to reduce dependencies.
|
320
94
|
|
321
95
|
=== License
|
322
96
|
|
data/Rakefile
CHANGED
@@ -43,7 +43,7 @@ require ::File.expand_path("#{::File.dirname(__FILE__)}/lib/blockenspiel/version
|
|
43
43
|
|
44
44
|
|
45
45
|
# Configuration
|
46
|
-
extra_rdoc_files_ = ['README.rdoc', 'History.rdoc', 'ImplementingDSLblocks.rdoc']
|
46
|
+
extra_rdoc_files_ = ['README.rdoc', 'Blockenspiel.rdoc', 'History.rdoc', 'ImplementingDSLblocks.rdoc']
|
47
47
|
|
48
48
|
|
49
49
|
# Default task
|
@@ -138,6 +138,8 @@ task :compile_java do
|
|
138
138
|
end
|
139
139
|
end
|
140
140
|
|
141
|
+
task :package => :compile_java
|
142
|
+
|
141
143
|
|
142
144
|
# Publish RDocs
|
143
145
|
desc 'Publishes RDocs to RubyForge'
|
@@ -149,7 +151,7 @@ end
|
|
149
151
|
|
150
152
|
|
151
153
|
# Publish gem
|
152
|
-
task :release_gem_to_rubyforge do |t_|
|
154
|
+
task :release_gem_to_rubyforge => [:package] do |t_|
|
153
155
|
v_ = ::ENV["VERSION"]
|
154
156
|
abort "Must supply VERSION=x.y.z" unless v_
|
155
157
|
if v_ != ::Blockenspiel::VERSION_STRING
|
@@ -202,7 +204,7 @@ task :release => [:release_gem_to_gemcutter, :release_gem_to_rubyforge, :publish
|
|
202
204
|
# Custom task that takes the implementing dsl blocks paper
|
203
205
|
# and converts it from RDoc format to Markdown
|
204
206
|
task :idslb_markdown do
|
205
|
-
::File.open('ImplementingDSLblocks.
|
207
|
+
::File.open('ImplementingDSLblocks.rdoc') do |read_|
|
206
208
|
::File.open('idslb_markdown.txt', 'w') do |write_|
|
207
209
|
linenum_ = 0
|
208
210
|
read_.each do |line_|
|