blockenspiel 0.3.3-java → 0.4.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.
@@ -3,7 +3,7 @@
3
3
  # Blockenspiel implementation
4
4
  #
5
5
  # -----------------------------------------------------------------------------
6
- # Copyright 2008-2009 Daniel Azuma
6
+ # Copyright 2008-2010 Daniel Azuma
7
7
  #
8
8
  # All rights reserved.
9
9
  #
@@ -37,542 +37,13 @@
37
37
  require 'thread'
38
38
 
39
39
 
40
- # == Blockenspiel
41
- #
42
- # The Blockenspiel module provides a namespace for Blockenspiel, as well as
43
- # the main entry point method "invoke".
44
-
45
40
  module Blockenspiel
46
41
 
47
42
 
48
- # Base exception for all exceptions raised by Blockenspiel
49
-
50
- class BlockenspielError < ::RuntimeError
51
- end
52
-
53
-
54
- # This exception is rasied when attempting to use the <tt>:proxy</tt> or
55
- # <tt>:mixin</tt> parameterless behavior with a target that does not have
56
- # the DSL module included. It is an error made by the DSL implementor.
57
-
58
- class DSLMissingError < ::Blockenspiel::BlockenspielError
59
- end
60
-
61
-
62
- # This exception is raised when the block provided does not take the
63
- # expected number of parameters. It is an error made by the caller.
64
-
65
- class BlockParameterError < ::Blockenspiel::BlockenspielError
66
- end
67
-
68
-
69
- # === DSL setup methods
70
- #
71
- # These class methods are available after you have included the
72
- # Blockenspiel::DSL module.
73
- #
74
- # By default, a class that has DSL capability will automatically make
75
- # all public methods available to parameterless blocks, except for the
76
- # +initialize+ method, any methods whose names begin with an underscore,
77
- # and any methods whose names end with an equals sign.
78
- #
79
- # If you want to change this behavior, use the directives defined here to
80
- # control exactly which methods are available to parameterless blocks.
81
-
82
- module DSLSetupMethods
83
-
84
-
85
- # :stopdoc:
86
-
87
- # Called when DSLSetupMethods extends a class.
88
- # This sets up the current class, and adds a hook that causes
89
- # any subclass of the current class also to be set up.
90
-
91
- def self.extended(klass_)
92
- unless klass_.instance_variable_defined?(:@_blockenspiel_module)
93
- _setup_class(klass_)
94
- def klass_.inherited(subklass_)
95
- ::Blockenspiel::DSLSetupMethods._setup_class(subklass_)
96
- super
97
- end
98
- class << klass_
99
- unless private_method_defined?(:_blockenspiel_default_include)
100
- alias_method :_blockenspiel_default_include, :include
101
- alias_method :include, :_blockenspiel_custom_include
102
- end
103
- end
104
- end
105
- end
106
-
107
- # :startdoc:
108
-
109
-
110
- # Set up a class.
111
- # Creates a DSL module for this class, optionally delegating to the superclass's module.
112
- # Also initializes the class's methods hash and active flag.
113
-
114
- def self._setup_class(klass_) # :nodoc:
115
- superclass_ = klass_.superclass
116
- superclass_ = nil unless superclass_.respond_to?(:_get_blockenspiel_module)
117
- mod_ = ::Module.new
118
- if superclass_
119
- mod_.module_eval do
120
- include superclass_._get_blockenspiel_module
121
- end
122
- end
123
- klass_.instance_variable_set(:@_blockenspiel_superclass, superclass_)
124
- klass_.instance_variable_set(:@_blockenspiel_module, mod_)
125
- klass_.instance_variable_set(:@_blockenspiel_methods, ::Hash.new)
126
- klass_.instance_variable_set(:@_blockenspiel_active, nil)
127
- end
128
-
129
-
130
- # Automatically make the given method a DSL method according to the current setting.
131
-
132
- def _blockenspiel_auto_dsl_method(symbol_) # :nodoc:
133
- if @_blockenspiel_active
134
- dsl_method(symbol_)
135
- elsif @_blockenspiel_active.nil?
136
- if symbol_ != :initialize && symbol_.to_s !~ /^_/ && symbol_.to_s !~ /=$/
137
- dsl_method(symbol_)
138
- end
139
- end
140
- end
141
-
142
-
143
- # Hook called when a method is added.
144
- # This calls _blockenspiel_auto_dsl_method to auto-handle the method,
145
- # possibly making it a DSL method according to the current setting.
146
-
147
- def method_added(symbol_) # :nodoc:
148
- _blockenspiel_auto_dsl_method(symbol_)
149
- super
150
- end
151
-
152
-
153
- # Custom include method. Calls the main include implementation, but also
154
- # goes through the public methods of the included module and calls
155
- # _blockenspiel_auto_dsl_method on each to make them DSL methods
156
- # (possibly) according to the current setting.
157
-
158
- def _blockenspiel_custom_include(*modules_) # :nodoc:
159
- _blockenspiel_default_include(*modules_)
160
- modules_.reverse_each do |mod_|
161
- mod_.public_instance_methods.each do |method_|
162
- _blockenspiel_auto_dsl_method(method_)
163
- end
164
- end
165
- end
166
-
167
-
168
- # Get this class's corresponding DSL module
169
-
170
- def _get_blockenspiel_module # :nodoc:
171
- @_blockenspiel_module
172
- end
173
-
174
-
175
- # Get information on the given DSL method name.
176
- # Possible values are the name of the delegate method, false for method disabled,
177
- # or nil for method never defined.
178
-
179
- def _get_blockenspiel_delegate(name_) # :nodoc:
180
- delegate_ = @_blockenspiel_methods[name_]
181
- if delegate_.nil? && @_blockenspiel_superclass
182
- @_blockenspiel_superclass._get_blockenspiel_delegate(name_)
183
- else
184
- delegate_
185
- end
186
- end
187
-
188
-
189
- # Make a particular method available to parameterless DSL blocks.
190
- #
191
- # To explicitly make a method available to parameterless blocks:
192
- # dsl_method :my_method
193
- #
194
- # To explicitly exclude a method from parameterless blocks:
195
- # dsl_method :my_method, false
196
- #
197
- # To explicitly make a method available to parameterless blocks, but
198
- # point it to a method of a different name on the target class:
199
- # dsl_method :my_method, :target_class_method
200
-
201
- def dsl_method(name_, delegate_=nil)
202
- name_ = name_.to_sym
203
- if delegate_
204
- delegate_ = delegate_.to_sym
205
- elsif delegate_.nil?
206
- delegate_ = name_
207
- end
208
- @_blockenspiel_methods[name_] = delegate_
209
- unless @_blockenspiel_module.public_method_defined?(name_)
210
- @_blockenspiel_module.module_eval("def #{name_}(*params_, &block_); val_ = ::Blockenspiel._target_dispatch(self, :#{name_}, params_, block_); ::Blockenspiel::NO_VALUE.equal?(val_) ? super(*params_, &block_) : val_; end\n")
211
- end
212
- end
213
-
214
-
215
- # Control the behavior of methods with respect to parameterless blocks,
216
- # or make a list of methods available to parameterless blocks in bulk.
217
- #
218
- # To enable automatic exporting of methods to parameterless blocks.
219
- # After executing this command, all public methods defined in the class
220
- # will be available on parameterless blocks, until
221
- # <tt>dsl_methods false</tt> is called:
222
- # dsl_methods true
223
- #
224
- # To disable automatic exporting of methods to parameterless blocks.
225
- # After executing this command, methods defined in this class will be
226
- # excluded from parameterless blocks, until <tt>dsl_methods true</tt>
227
- # is called:
228
- # dsl_methods false
229
- #
230
- # To make a list of methods available to parameterless blocks in bulk:
231
- # dsl_methods :my_method1, :my_method2, ...
232
- #
233
- # You can also point dsl methods to a method of a different name on the
234
- # target class, by using a hash syntax, as follows:
235
- # dsl_methods :my_method1 => :target_class_method1,
236
- # :my_method2 => :target_class_method2
237
- #
238
- # You can mix non-renamed and renamed method declarations as long as
239
- # the renamed (hash) methods are at the end. e.g.:
240
- # dsl_methods :my_method1, :my_method2 => :target_class_method2
241
-
242
- def dsl_methods(*names_)
243
- if names_.size == 0 || names_ == [true]
244
- @_blockenspiel_active = true
245
- elsif names_ == [false]
246
- @_blockenspiel_active = false
247
- else
248
- if names_.last.kind_of?(::Hash)
249
- names_.pop.each do |name_, delegate_|
250
- dsl_method(name_, delegate_)
251
- end
252
- end
253
- names_.each do |name_|
254
- dsl_method(name_, name_)
255
- end
256
- end
257
- end
258
-
259
-
260
- # A DSL-friendly attr_accessor.
261
- #
262
- # This creates the usual "name" and "name=" methods in the current
263
- # class that can be used in the usual way. However, its implementation
264
- # of the "name" method (the getter) also takes an optional parameter
265
- # that causes it to behave as a setter. This is done because the usual
266
- # setter syntax cannot be used in a parameterless block, since it is
267
- # syntactically indistinguishable from a local variable assignment.
268
- # The "name" method is exposed as a dsl_method.
269
- #
270
- # For example:
271
- #
272
- # dsl_attr_accessor :foo
273
- #
274
- # enables the following:
275
- #
276
- # my_block do |param|
277
- # param.foo = 1 # Usual setter syntax works
278
- # param.foo 2 # Alternate setter syntax also works
279
- # puts param.foo # Usual getter syntax still works
280
- # end
281
- #
282
- # my_block do
283
- # # foo = 1 # Usual setter syntax does NOT work since it
284
- # # looks like a local variable assignment
285
- # foo 2 # Alternate setter syntax does work
286
- # puts foo # Usual getter syntax still works
287
- # end
288
-
289
- def dsl_attr_accessor(*names_)
290
- names_.each do |name_|
291
- unless name_.kind_of?(::String) || name_.kind_of?(::Symbol)
292
- raise ::TypeError, "#{name_.inspect} is not a symbol"
293
- end
294
- unless name_.to_s =~ /^[_a-zA-Z]\w+$/
295
- raise ::NameError, "invalid attribute name #{name_.inspect}"
296
- end
297
- module_eval("def #{name_}(value_=::Blockenspiel::NO_VALUE); ::Blockenspiel::NO_VALUE.equal?(value_) ? @#{name_} : @#{name_} = value_; end\n")
298
- alias_method("#{name_}=", name_)
299
- dsl_method(name_)
300
- end
301
- end
302
-
303
-
304
- # A DSL-friendly attr_writer.
305
- #
306
- # This creates the usual "name=" method in the current class that can
307
- # be used in the usual way. However, it also creates the method "name",
308
- # which also functions as a setter (but not a getter). This is done
309
- # because the usual setter syntax cannot be used in a parameterless
310
- # block, since it is syntactically indistinguishable from a local
311
- # variable assignment. The "name" method is exposed as a dsl_method.
312
- #
313
- # For example:
314
- #
315
- # dsl_attr_writer :foo
316
- #
317
- # is functionally equivalent to:
318
- #
319
- # attr_writer :foo
320
- # alias_method :foo, :foo=
321
- # dsl_method :foo
322
- #
323
- # which enables the following:
324
- #
325
- # my_block do |param|
326
- # param.foo = 1 # Usual setter syntax works
327
- # param.foo 2 # Alternate setter syntax also works
328
- # end
329
- # my_block do
330
- # # foo = 1 # Usual setter syntax does NOT work since it
331
- # # looks like a local variable assignment
332
- # foo(2) # Alternate setter syntax does work
333
- # end
334
-
335
- def dsl_attr_writer(*names_)
336
- names_.each do |name_|
337
- attr_writer(name_)
338
- alias_method(name_, "#{name_}=")
339
- dsl_method(name_)
340
- end
341
- end
342
-
343
-
344
- end
345
-
346
-
347
- # === DSL activation module
348
- #
349
- # Include this module in a class to mark this class as a DSL class and
350
- # make it possible for its methods to be called from a block that does not
351
- # take a parameter.
352
- #
353
- # After you include this module, you can use the directives defined in
354
- # DSLSetupMethods to control what methods are available to DSL blocks
355
- # that do not take parameters.
356
-
357
- module DSL
358
-
359
- def self.included(klass_) # :nodoc:
360
- unless klass_.kind_of?(::Class)
361
- raise ::Blockenspiel::BlockenspielError, "You cannot include Blockenspiel::DSL in a module (yet)"
362
- end
363
- klass_.extend(::Blockenspiel::DSLSetupMethods)
364
- end
365
-
366
- end
367
-
368
-
369
- # === DSL activation base class
370
- #
371
- # Subclasses of this base class are considered DSL classes.
372
- # Methods of the class can be made available to be called from a block that
373
- # doesn't take an explicit block parameter.
374
- # You may use the directives defined in DSLSetupMethods to control how
375
- # methods of the class are handled in such blocks.
376
- #
377
- # Subclassing this base class is functionally equivalent to simply
378
- # including Blockenspiel::DSL in the class.
379
-
380
- class Base
381
-
382
- include ::Blockenspiel::DSL
383
-
384
- end
385
-
386
-
387
- # Class for proxy delegators.
388
- # The proxy behavior creates one of these delegators, mixes in the dsl
389
- # methods, and uses instance_eval to invoke the block. This class delegates
390
- # non-handled methods to the context object.
391
-
392
- class ProxyDelegator # :nodoc:
393
-
394
- def method_missing(symbol_, *params_, &block_)
395
- ::Blockenspiel._proxy_dispatch(self, symbol_, params_, block_)
396
- end
397
-
398
- end
399
-
400
-
401
- # === Dynamically construct a target
402
- #
403
- # These methods are available in a block passed to Blockenspiel#invoke and
404
- # can be used to dynamically define what methods are available from a block.
405
- # See Blockenspiel#invoke for more information.
406
-
407
- class Builder
408
-
409
- include ::Blockenspiel::DSL
410
-
411
-
412
- # This is a base class for dynamically constructed targets.
413
- # The actual target class is an anonymous subclass of this base class.
414
-
415
- class Target # :nodoc:
416
-
417
- include ::Blockenspiel::DSL
418
-
419
-
420
- # Add a method specification to the subclass.
421
-
422
- def self._add_methodinfo(name_, block_, yields_)
423
- (@_blockenspiel_methodinfo ||= ::Hash.new)[name_] = [block_, yields_]
424
- module_eval("def #{name_}(*params_, &block_); self.class._invoke_methodinfo(:#{name_}, params_, block_); end\n")
425
- end
426
-
427
-
428
- # Attempt to invoke the given method on the subclass.
429
-
430
- def self._invoke_methodinfo(name_, params_, block_)
431
- info_ = @_blockenspiel_methodinfo[name_]
432
- case info_[1]
433
- when :first
434
- params_.unshift(block_)
435
- when :last
436
- params_.push(block_)
437
- end
438
- info_[0].call(*params_, &block_)
439
- end
440
-
441
- end
442
-
443
-
444
- # Sets up the dynamic target class.
445
-
446
- def initialize # :nodoc:
447
- @target_class = ::Class.new(::Blockenspiel::Builder::Target)
448
- @target_class.dsl_methods(false)
449
- end
450
-
451
-
452
- # Creates a new instance of the dynamic target class
453
-
454
- def _create_target # :nodoc:
455
- @target_class.new
456
- end
457
-
458
-
459
- # === Declare a DSL method.
460
- #
461
- # This call creates a method that can be called from the DSL block.
462
- # Provide a name for the method, a block defining the method's
463
- # implementation, and an optional hash of options.
464
- #
465
- # By default, a method of the same name is also made available to
466
- # parameterless blocks. To change the name of the parameterless method,
467
- # provide its name as the value of the <tt>:dsl_method</tt> option.
468
- # To disable this method for parameterless blocks, set the
469
- # <tt>:dsl_method</tt> option to +false+.
470
- #
471
- # The <tt>:mixin</tt> option is a deprecated alias for
472
- # <tt>:dsl_method</tt>.
473
- #
474
- # === Warning about the +return+ keyword
475
- #
476
- # Because you are implementing your method using a block, remember the
477
- # distinction between <tt>Proc.new</tt> and +lambda+. Invoking +return+
478
- # from the former does not return from the block, but returns from the
479
- # surrounding method scope. Since normal blocks passed to methods are
480
- # of the former type, be very careful about using the +return+ keyword:
481
- #
482
- # add_method(:foo) do |param|
483
- # puts "foo called with parameter "+param.inspect
484
- # return "a return value" # DOESN'T WORK LIKE YOU EXPECT!
485
- # end
486
- #
487
- # To return a value from the method you are creating, set the evaluation
488
- # value at the end of the block:
489
- #
490
- # add_method(:foo) do |param|
491
- # puts "foo called with parameter "+param.inspect
492
- # "a return value" # Returns from method foo
493
- # end
494
- #
495
- # If you must use the +return+ keyword, create your block as a lambda
496
- # as in this example:
497
- #
498
- # code = lambda do |param|
499
- # puts "foo called with parameter "+param.inspect
500
- # return "a return value" # Returns from method foo
501
- # end
502
- # add_method(:foo, &code)
503
- #
504
- # === Accepting a block argument
505
- #
506
- # If you want your method to take a block, you have several options
507
- # depending on your Ruby version. If you are running the standard Matz
508
- # Ruby interpreter (MRI) version 1.8.7 or later (including 1.9.x), or a
509
- # compatible interpreter such as JRuby 1.5 or later, you can use the
510
- # standard "&" block argument notation to receive the block.
511
- # Note that you must call the passed block using the +call+ method since
512
- # Ruby doesn't support invoking such a block with +yield+.
513
- # For example, to create a method named "foo" that takes one parameter
514
- # and a block, do this:
515
- #
516
- # add_method(:foo) do |param, &block|
517
- # puts "foo called with parameter "+param.inspect
518
- # puts "the block returned "+block.call.inspect
519
- # end
520
- #
521
- # In your DSL, you can then call:
522
- #
523
- # foo("hello"){ "a value" }
524
- #
525
- # If you are using MRI 1.8.6, or another Ruby interpreter that doesn't
526
- # fully support this syntax (such as JRuby versions older than 1.5),
527
- # Blockenspiel provides an alternative in the form of the <tt>:block</tt>
528
- # option. This option causes blocks provided by the caller to be included
529
- # in the normal parameter list to your method, instead of as a block
530
- # parameter. It can be set to <tt>:first</tt> or <tt>:last</tt> to
531
- # prepend or append, respectively, the block (as a +Proc+ object) to
532
- # the parameter list. If the caller does not include a block when
533
- # calling your DSL method, nil is prepended/appended. For example:
534
- #
535
- # add_method(:foo, :block => :last) do |param, block|
536
- # puts "foo called with parameter "+param.inspect
537
- # if block
538
- # puts "the block returned "+block.call.inspect
539
- # else
540
- # puts "no block passed"
541
- # end
542
- # end
543
- #
544
- # The <tt>:receive_block</tt> option is a deprecated alternative.
545
- # Setting <tt>:receive_block => true</tt> is currently equivalent to
546
- # setting <tt>:block => :last</tt>.
547
-
548
- def add_method(name_, opts_={}, &block_)
549
- receive_block_ = opts_[:receive_block] ? :last : opts_[:block]
550
- receive_block_ = :first if receive_block_ && receive_block_ != :last
551
- @target_class._add_methodinfo(name_, block_, receive_block_)
552
- dsl_method_name_ = opts_[:dsl_method] || opts_[:mixin]
553
- if dsl_method_name_ != false
554
- dsl_method_name_ = name_ if dsl_method_name_.nil? || dsl_method_name_ == true
555
- @target_class.dsl_method(dsl_method_name_, name_)
556
- end
557
- end
558
-
559
- end
560
-
561
-
562
- # :stopdoc:
563
- NO_VALUE = ::Object.new
564
- # :startdoc:
565
-
566
- @_target_stacks = ::Hash.new
567
- @_mixin_counts = ::Hash.new
568
- @_proxy_delegators = ::Hash.new
569
- @_mutex = ::Mutex.new
570
-
571
-
572
- # === Invoke a given block
43
+ # === Invoke a given DSL
573
44
  #
574
- # This is the meat of Blockenspiel. Call this function to invoke a block
575
- # provided by the user of your API.
45
+ # This is the entry point for Blockenspiel. Call this function to invoke
46
+ # a set of DSL code provided by the user of your API.
576
47
  #
577
48
  # For example, if you want users of your API to be able to do this:
578
49
  #
@@ -592,21 +63,71 @@ module Blockenspiel
592
63
  # In the above, <tt>create_block_implementation</tt> is a placeholder that
593
64
  # returns an instance of your DSL methods class. This class includes the
594
65
  # Blockenspiel::DSL module and defines the DSL methods +foo+ and +bar+.
66
+ # See Blockenspiel::DSLSetupMethods for a set of tools you can use in your
67
+ # DSL methods class for creating a DSL.
68
+ #
69
+ # === Usage patterns
70
+ #
71
+ # The invoke method has a number of forms, depending on whether the API
72
+ # user's DSL code is provided as a block or a string, and depending on
73
+ # whether the DSL methods are specified statically using a DSL class or
74
+ # dynamically using a block.
75
+ #
76
+ # [<tt>Blockenspiel.invoke(<i>user_block</i>, <i>my_dsl</i>, <i>opts</i>)</tt>]
77
+ # This form takes the user's code as a block, and the DSL itself as an
78
+ # object with DSL methods. The opts hash is optional and provides a
79
+ # set of arguments as described below under "Block DSL options".
80
+ #
81
+ # [<tt>Blockenspiel.invoke(<i>user_block</i>, <i>opts</i>) { ... }</tt>]
82
+ # This form takes the user's code as a block, while the DSL itself is
83
+ # specified in the given block, as described below under "Dynamic
84
+ # target generation". The opts hash is optional and provides a set of
85
+ # arguments as described below under "Block DSL options".
86
+ #
87
+ # [<tt>Blockenspiel.invoke(<i>user_string</i>, <i>my_dsl</i>, <i>opts</i>)</tt>]
88
+ # This form takes the user's code as a string, and the DSL itself as an
89
+ # object with DSL methods. The opts hash is optional and provides a
90
+ # set of arguments as described below under "String DSL options".
91
+ #
92
+ # [<tt>Blockenspiel.invoke(<i>user_string</i>, <i>opts</i>) { ... }</tt>]
93
+ # This form takes the user's code as a block, while the DSL itself is
94
+ # specified in the given block, as described below under "Dynamic
95
+ # target generation". The opts hash is optional and provides a set of
96
+ # arguments as described below under "String DSL options".
97
+ #
98
+ # [<tt>Blockenspiel.invoke(<i>my_dsl</i>, <i>opts</i>)</tt>]
99
+ # This form reads the user's code from a file, and takes the DSL itself
100
+ # as an object with DSL methods. The opts hash is required and provides
101
+ # a set of arguments as described below under "String DSL options". The
102
+ # <tt>:file</tt> option is required.
103
+ #
104
+ # [<tt>Blockenspiel.invoke(<i>opts</i>) { ... }</tt>]
105
+ # This form reads the user's code from a file, while the DSL itself is
106
+ # specified in the given block, as described below under "Dynamic
107
+ # target generation". The opts hash is required and provides a set of
108
+ # arguments as described below under "String DSL options". The
109
+ # <tt>:file</tt> option is required.
595
110
  #
596
- # Normally, this method will check the block's arity to see whether it
597
- # takes a parameter. If so, it will pass the given target to the block.
598
- # If the block takes no parameter, and the given target is an instance of
599
- # a class with DSL capability, the DSL methods are made available on the
600
- # caller's self object so they may be called without a block parameter.
111
+ # === Block DSL options
601
112
  #
602
- # === Recognized options
113
+ # When a user provides DSL code using a block, you simply pass that block
114
+ # as the first parameter to Blockenspiel.invoke. Normally, Blockenspiel
115
+ # will first check the block's arity to see whether it takes a parameter.
116
+ # If so, it will pass the given target to the block. If the block takes
117
+ # no parameter, and the given target is an instance of a class with DSL
118
+ # capability, the DSL methods are made available on the caller's self
119
+ # object so they may be called without a block parameter.
120
+ #
121
+ # Following are the options understood by Blockenspiel when providing
122
+ # code using a block:
603
123
  #
604
124
  # <tt>:parameterless</tt>::
605
125
  # If set to false, disables parameterless blocks and always attempts to
606
126
  # pass a parameter to the block. Otherwise, you may set it to one of
607
127
  # three behaviors for parameterless blocks: <tt>:mixin</tt> (the
608
128
  # default), <tt>:instance</tt>, and <tt>:proxy</tt>. See below for
609
- # detailed descriptions of these behaviors.
129
+ # detailed descriptions of these behaviors. This option key is also
130
+ # available as <tt>:behavior</tt>.
610
131
  # <tt>:parameter</tt>::
611
132
  # If set to false, disables blocks with parameters, and always attempts
612
133
  # to use parameterless blocks. Default is true, enabling parameter mode.
@@ -639,6 +160,51 @@ module Blockenspiel
639
160
  # clobbered) in the block, and the transformations specified by
640
161
  # <tt>dsl_method</tt> directives are honored.
641
162
  #
163
+ # === String DSL options
164
+ #
165
+ # When a user provides DSL code using a string (either directly or via a
166
+ # file), Blockenspiel always treats it as a "parameterless" invocation,
167
+ # since there is no way to "pass a parameter" to a string. Thus, the two
168
+ # options recognized for block DSLs, <tt>:parameterless</tt>, and
169
+ # <tt>:parameter</tt>, are meaningless and ignored. However, the
170
+ # following new options are recognized:
171
+ #
172
+ # <tt>:file</tt>::
173
+ # The value of this option should be a string indicating the path to
174
+ # the file from which the user's DSL code is coming. It is passed
175
+ # as the "file" parameter to eval; that is, it is included in the stack
176
+ # trace should an exception be thrown out of the DSL. If no code string
177
+ # is provided directly, this option is required and must be set to the
178
+ # path of the file from which to load the code.
179
+ # <tt>:line</tt>::
180
+ # This option is passed as the "line" parameter to eval; that is, it
181
+ # indicates the starting line number for the code string, and is used
182
+ # to compute line numbers for the stack trace should an exception be
183
+ # thrown out of the DSL. This option is optional and defaults to 1.
184
+ # <tt>:behavior</tt>::
185
+ # Controls how the DSL is called. Recognized values are <tt>:proxy</tt>
186
+ # (the default) and <tt>:instance</tt>. See below for detailed
187
+ # descriptions of these behaviors. Note that <tt>:mixin</tt> is not
188
+ # allowed in this case because its behavior would be indistinguishable
189
+ # from the proxy behavior.
190
+ #
191
+ # The following values are recognized for the <tt>:behavior</tt> option:
192
+ #
193
+ # <tt>:proxy</tt>::
194
+ # This behavior changes +self+ to a proxy object created by applying the
195
+ # DSL methods to an empty object. Thus, the code in the DSL string does
196
+ # not have access to the target object's internal instance variables or
197
+ # private methods. Furthermore, the transformations specified by
198
+ # <tt>dsl_method</tt> directives are honored. This is the default
199
+ # behavior.
200
+ # <tt>:instance</tt>::
201
+ # This behavior actually changes +self+ to the target object using
202
+ # <tt>instance_eval</tt>. Thus, the code in the DSL string gains access
203
+ # to the target object's instance variables and private methods. Also,
204
+ # the target object's methods are not modified: this behavior does not
205
+ # apply any DSL method changes specified using <tt>dsl_method</tt>
206
+ # directives.
207
+ #
642
208
  # === Dynamic target generation
643
209
  #
644
210
  # It is also possible to dynamically generate a target object by passing
@@ -684,32 +250,96 @@ module Blockenspiel
684
250
  #
685
251
  # See the Blockenspiel::Builder class for more details on add_method.
686
252
  #
687
- # When using dynamic target generation, you may pass the options hash as
688
- # the second argument to invoke instead of the third, since you will not
689
- # be passing a target object as the second argument.
690
- #
691
253
  # (And yes, you guessed it: this API is a DSL block, and is itself
692
254
  # implemented using Blockenspiel.)
693
255
 
694
- def self.invoke(block_, target_=nil, opts_={}, &builder_block_)
695
-
696
- unless block_
697
- raise ::ArgumentError, "Block expected"
256
+ def self.invoke(*args_, &builder_block_)
257
+ # This method itself is responsible for parsing the args to invoke,
258
+ # and handling the dynamic target generation. It then passes control
259
+ # to one of the _invoke_with_* methods.
260
+
261
+ # The arguments.
262
+ block_ = nil
263
+ eval_str_ = nil
264
+ target_ = nil
265
+ opts_ = {}
266
+
267
+ # Get the code
268
+ case args_.first
269
+ when ::String
270
+ eval_str_ = args_.shift
271
+ when ::Proc
272
+ block_ = args_.shift
698
273
  end
699
274
 
700
- # Perform dynamic target generation if requested
275
+ # Get the target, performing dynamic target generation if requested
701
276
  if builder_block_
702
- # Support passing the opts hash as the second argument since we
703
- # aren't passing a target as an argument.
704
- opts_ = target_ || opts_
705
277
  builder_ = ::Blockenspiel::Builder.new
706
278
  invoke(builder_block_, builder_)
707
279
  target_ = builder_._create_target
280
+ args_.shift if args_.first.nil?
281
+ else
282
+ target_ = args_.shift
283
+ unless target_
284
+ raise ::ArgumentError, "No DSL target provided"
285
+ end
286
+ end
287
+
288
+ # Get the options hash
289
+ if args_.first.kind_of?(::Hash)
290
+ opts_ = args_.shift
291
+ end
292
+ if args_.size > 0
293
+ raise ::ArgumentError, "Unexpected arguments"
294
+ end
295
+
296
+ # Invoke
297
+ if block_
298
+ _invoke_with_block(block_, target_, opts_)
299
+ else
300
+ _invoke_with_string(eval_str_, target_, opts_)
301
+ end
302
+ end
303
+
304
+
305
+ # Invoke when the DSL user provides code as a string or file.
306
+ # We open and read the file if need be, and then pass control
307
+ # to the _execute method.
308
+
309
+ def self._invoke_with_string(eval_str_, target_, opts_) # :nodoc:
310
+ # Read options
311
+ file_ = opts_[:file]
312
+ line_ = opts_[:line] || 1
313
+
314
+ # Read file if no string provided directly
315
+ unless eval_str_
316
+ if file_
317
+ eval_str_ = ::File.read(file_)
318
+ else
319
+ raise ::ArgumentError, "No code or file provided."
320
+ end
321
+ else
322
+ file_ ||= "(String passed to Blockenspiel)"
708
323
  end
709
324
 
325
+ # Handle instance-eval behavior
326
+ if opts_[:behavior] == :instance
327
+ return target_.instance_eval(eval_str_, file_, line_)
328
+ end
329
+
330
+ # Execute the DSL using the proxy method.
331
+ _execute_dsl(true, nil, eval_str_, target_, file_, line_)
332
+ end
333
+
334
+
335
+ # Invoke when the DSL user provides code as a block. We read the given
336
+ # options hash, handle a few special cases, and then pass control to the
337
+ # _execute method.
338
+
339
+ def self._invoke_with_block(block_, target_, opts_) # :nodoc:
710
340
  # Read options
711
341
  parameter_ = opts_[:parameter]
712
- parameterless_ = opts_[:parameterless]
342
+ parameterless_ = opts_.include?(:behavior) ? opts_[:behavior] : opts_[:parameterless]
713
343
 
714
344
  # Handle no-target behavior
715
345
  if parameter_ == false && parameterless_ == false
@@ -737,17 +367,49 @@ module Blockenspiel
737
367
  return target_.instance_eval(&block_)
738
368
  end
739
369
 
370
+ # Execute the DSL
371
+ _execute_dsl(parameterless_ == :proxy, block_, nil, target_, nil, nil)
372
+ end
373
+
374
+
375
+ # Class for proxy delegators.
376
+ # The proxy behavior creates one of these delegators, mixes in the dsl
377
+ # methods, and uses instance_eval to invoke the block. This class delegates
378
+ # non-handled methods to the context object.
379
+
380
+ class ProxyDelegator # :nodoc:
381
+
382
+ def method_missing(symbol_, *params_, &block_)
383
+ ::Blockenspiel._proxy_dispatch(self, symbol_, params_, block_)
384
+ end
385
+
386
+ end
387
+
388
+
389
+ # :stopdoc:
390
+ NO_VALUE = ::Object.new
391
+ # :startdoc:
392
+
393
+ @_target_stacks = {}
394
+ @_mixin_counts = {}
395
+ @_proxy_delegators = {}
396
+ @_mutex = ::Mutex.new
397
+
398
+
399
+ # This is the "meat" of Blockenspiel, implementing both the proxy and
400
+ # mixin methods.
401
+
402
+ def self._execute_dsl(use_proxy_method_, block_, eval_str_, target_, file_, line_) # :nodoc:
740
403
  # Get the module of dsl methods
741
404
  mod_ = target_.class._get_blockenspiel_module rescue nil
742
405
  unless mod_
743
- raise ::Blockenspiel::DSLMissingError
406
+ raise ::Blockenspiel::DSLMissingError, "Given DSL target does not include Blockenspiel::DSL"
744
407
  end
745
408
 
746
409
  # Get the block's calling context object
747
- object_ = ::Kernel.eval('self', block_.binding)
410
+ context_object_ = block_ ? ::Kernel.eval('self', block_.binding) : nil
748
411
 
749
- # Handle proxy behavior
750
- if parameterless_ == :proxy
412
+ if use_proxy_method_
751
413
 
752
414
  # Create proxy object
753
415
  proxy_ = ::Blockenspiel::ProxyDelegator.new
@@ -755,73 +417,76 @@ module Blockenspiel
755
417
 
756
418
  # Store the target and proxy object so dispatchers can get them
757
419
  proxy_delegator_key_ = proxy_.object_id
758
- target_stack_key_ = [::Thread.current.object_id, proxy_.object_id]
759
- @_proxy_delegators[proxy_delegator_key_] = object_
420
+ target_stack_key_ = [_current_context_id, proxy_.object_id]
421
+ @_proxy_delegators[proxy_delegator_key_] = context_object_ if context_object_
760
422
  @_target_stacks[target_stack_key_] = [target_]
761
423
 
762
424
  begin
763
425
 
764
- # Call the block with the proxy as self
765
- return proxy_.instance_eval(&block_)
426
+ # Evaluate with the proxy as self
427
+ if block_
428
+ return proxy_.instance_eval(&block_)
429
+ else
430
+ return proxy_.instance_eval(eval_str_, file_, line_)
431
+ end
766
432
 
767
433
  ensure
768
434
 
769
435
  # Clean up the dispatcher information
770
- @_proxy_delegators.delete(proxy_delegator_key_)
436
+ @_proxy_delegators.delete(proxy_delegator_key_) if context_object_
771
437
  @_target_stacks.delete(target_stack_key_)
772
438
 
773
439
  end
774
440
 
775
- end
776
-
777
- # Handle mixin behavior (default)
778
-
779
- # Create hash keys
780
- mixin_count_key_ = [object_.object_id, mod_.object_id]
781
- target_stack_key_ = [::Thread.current.object_id, object_.object_id]
782
-
783
- # Store the target for inheriting.
784
- # We maintain a target call stack per thread.
785
- target_stack_ = @_target_stacks[target_stack_key_] ||= ::Array.new
786
- target_stack_.push(target_)
787
-
788
- # Mix this module into the object, if required.
789
- # This ensures that we keep track of the number of requests to
790
- # mix this module in, from nested blocks and possibly multiple threads.
791
- @_mutex.synchronize do
792
- count_ = @_mixin_counts[mixin_count_key_]
793
- if count_
794
- @_mixin_counts[mixin_count_key_] = count_ + 1
795
- else
796
- @_mixin_counts[mixin_count_key_] = 1
797
- object_.extend(mod_)
798
- end
799
- end
800
-
801
- begin
802
-
803
- # Now call the block
804
- return block_.call
441
+ else
805
442
 
806
- ensure
443
+ # Create hash keys
444
+ mixin_count_key_ = [context_object_.object_id, mod_.object_id]
445
+ target_stack_key_ = [_current_context_id, context_object_.object_id]
807
446
 
808
- # Clean up the target stack
809
- target_stack_.pop
810
- @_target_stacks.delete(target_stack_key_) if target_stack_.size == 0
447
+ # Store the target for inheriting.
448
+ # We maintain a target call stack per thread.
449
+ target_stack_ = @_target_stacks[target_stack_key_] ||= []
450
+ target_stack_.push(target_)
811
451
 
812
- # Remove the mixin from the object, if required.
452
+ # Mix this module into the object, if required.
453
+ # This ensures that we keep track of the number of requests to
454
+ # mix this module in, from nested blocks and possibly multiple threads.
813
455
  @_mutex.synchronize do
814
456
  count_ = @_mixin_counts[mixin_count_key_]
815
- if count_ == 1
816
- @_mixin_counts.delete(mixin_count_key_)
817
- ::Blockenspiel::Unmixer.unmix(object_, mod_)
457
+ if count_
458
+ @_mixin_counts[mixin_count_key_] = count_ + 1
818
459
  else
819
- @_mixin_counts[mixin_count_key_] = count_ - 1
460
+ @_mixin_counts[mixin_count_key_] = 1
461
+ context_object_.extend(mod_)
820
462
  end
821
463
  end
822
464
 
465
+ begin
466
+
467
+ # Now call the block
468
+ return block_.call
469
+
470
+ ensure
471
+
472
+ # Clean up the target stack
473
+ target_stack_.pop
474
+ @_target_stacks.delete(target_stack_key_) if target_stack_.size == 0
475
+
476
+ # Remove the mixin from the object, if required.
477
+ @_mutex.synchronize do
478
+ count_ = @_mixin_counts[mixin_count_key_]
479
+ if count_ == 1
480
+ @_mixin_counts.delete(mixin_count_key_)
481
+ ::Blockenspiel::Unmixer.unmix(context_object_, mod_)
482
+ else
483
+ @_mixin_counts[mixin_count_key_] = count_ - 1
484
+ end
485
+ end
486
+
487
+ end
488
+
823
489
  end
824
-
825
490
  end
826
491
 
827
492
 
@@ -831,7 +496,7 @@ module Blockenspiel
831
496
  # If we can't find an appropriate method to call, return the special value NO_VALUE.
832
497
 
833
498
  def self._target_dispatch(object_, name_, params_, block_) # :nodoc:
834
- target_stack_ = @_target_stacks[[::Thread.current.object_id, object_.object_id]]
499
+ target_stack_ = @_target_stacks[[_current_context_id, object_.object_id]]
835
500
  return ::Blockenspiel::NO_VALUE unless target_stack_
836
501
  target_stack_.reverse_each do |target_|
837
502
  target_class_ = target_.class
@@ -848,7 +513,28 @@ module Blockenspiel
848
513
  # We look up the context object, and call the given method on that object.
849
514
 
850
515
  def self._proxy_dispatch(proxy_, name_, params_, block_) # :nodoc:
851
- @_proxy_delegators[proxy_.object_id].send(name_, *params_, &block_)
516
+ delegate_ = @_proxy_delegators[proxy_.object_id]
517
+ if delegate_
518
+ delegate_.send(name_, *params_, &block_)
519
+ else
520
+ raise ::NoMethodError, "undefined method `#{name_}' in DSL"
521
+ end
522
+ end
523
+
524
+
525
+ # This returns a current context ID. If fibers are available in the ruby
526
+ # implementation, this will be the current fiber's object_id. Otherwise,
527
+ # it is the current thread's object_id.
528
+
529
+ begin
530
+ require 'fiber'
531
+ def self._current_context_id # :nodoc:
532
+ ::Fiber.current.object_id
533
+ end
534
+ rescue ::LoadError
535
+ def self._current_context_id # :nodoc:
536
+ ::Thread.current.object_id
537
+ end
852
538
  end
853
539
 
854
540