cond 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/cond.gemspec ADDED
@@ -0,0 +1,37 @@
1
+
2
+ Gem::Specification.new { |t|
3
+ t.author = "James M. Lawrence"
4
+ t.email = "quixoticsycophant@gmail.com"
5
+ t.summary = "Resolve errors without unwinding the stack."
6
+ t.name = "cond"
7
+ t.rubyforge_project = t.name
8
+ t.homepage = "#{t.name}.rubyforge.org"
9
+ t.version = "0.2.0"
10
+ t.description = <<-EOS
11
+ +Cond+ allows errors to be handled at the place where they occur.
12
+ You decide whether or not the stack should be unwound, depending on
13
+ the circumstance and the error.
14
+ EOS
15
+ t.files = (
16
+ %W[README #{t.name}.gemspec] +
17
+ Dir["./**/*.rb"] +
18
+ Dir["./**/Rakefile"]
19
+ )
20
+ rdoc_exclude = %w[
21
+ spec
22
+ examples
23
+ readmes
24
+ support
25
+ lib/cond/cond_private
26
+ ]
27
+ t.has_rdoc = true
28
+ t.extra_rdoc_files = %w[README]
29
+ t.rdoc_options += [
30
+ "--main",
31
+ "README",
32
+ "--title",
33
+ "#{t.name}: #{t.summary}",
34
+ ] + rdoc_exclude.inject(Array.new) { |acc, pattern|
35
+ acc + ["--exclude", pattern]
36
+ }
37
+ }
@@ -0,0 +1,51 @@
1
+ require File.dirname(__FILE__) + "/../spec/common"
2
+
3
+ #
4
+ # This is a bad example because a handler should re-raise if no
5
+ # restart is provided. All bets are off if code directly after a
6
+ # 'raise' gets executed. But for fun let's see what it looks like.
7
+ #
8
+
9
+ include Cond
10
+
11
+ module BadExample
12
+ A, B = (1..2).map { Class.new RuntimeError }
13
+
14
+ memo = []
15
+
16
+ describe "bad example" do
17
+ it "should demonstrate how not to use Cond" do
18
+ handling do
19
+ handle A do
20
+ memo.push :ignore_a
21
+ end
22
+
23
+ handle B do
24
+ memo.push :reraise_b
25
+ raise
26
+ end
27
+
28
+ raise A
29
+ # ... still going!
30
+
31
+ handling do
32
+ handle B do
33
+ memo.push :ignore_b
34
+ end
35
+ raise B
36
+ # ... !
37
+ end
38
+
39
+ begin
40
+ raise B, "should not be ignored"
41
+ rescue B => e
42
+ if e.message == "should not be ignored"
43
+ memo.push :rescued_b
44
+ end
45
+ end
46
+ end
47
+ memo.should == [:ignore_a, :ignore_b, :reraise_b, :rescued_b]
48
+ end
49
+ end
50
+ end
51
+
@@ -0,0 +1,84 @@
1
+ require File.dirname(__FILE__) + "/../spec/common"
2
+
3
+ include Cond
4
+
5
+ class DivergedError < StandardError
6
+ attr_reader :epsilon
7
+
8
+ def initialize(epsilon)
9
+ super()
10
+ @epsilon = epsilon
11
+ end
12
+
13
+ def message
14
+ "Failed to converge with epsilon #{@epsilon}"
15
+ end
16
+ end
17
+
18
+ def calc(x, y, epsilon)
19
+ restartable do
20
+ restart :change_epsilon do |new_epsilon|
21
+ epsilon = new_epsilon
22
+ again
23
+ end
24
+ restart :give_up do
25
+ leave
26
+ end
27
+ # ...
28
+ # ... some calculation
29
+ # ...
30
+ if epsilon < 0.01
31
+ raise DivergedError.new(epsilon)
32
+ end
33
+ 42
34
+ end
35
+ end
36
+
37
+ describe "A calculation which can raise a divergent error," do
38
+ describe "with a handler which increases epsilon" do
39
+ before :all do
40
+ handling do
41
+ @memo = []
42
+ @result = nil
43
+ epsilon = 0.0005
44
+ handle DivergedError do
45
+ epsilon += 0.001
46
+ @memo.push :increase
47
+ invoke_restart :change_epsilon, epsilon
48
+ end
49
+ @result = calc(3, 4, epsilon)
50
+ end
51
+ end
52
+
53
+ it "should converge after repeated epsilon increases" do
54
+ @memo.should == (1..10).map { :increase }
55
+ end
56
+
57
+ it "should obtain a result" do
58
+ @result.should == 42
59
+ end
60
+ end
61
+
62
+ describe "with a give-up handler and a too-small epsilon" do
63
+ before :all do
64
+ handling do
65
+ @result = 9999
66
+ @memo = []
67
+ epsilon = 1e-10
68
+ handle DivergedError do
69
+ @memo.push :give_up
70
+ invoke_restart :give_up
71
+ end
72
+ @result = calc(3, 4, epsilon)
73
+ end
74
+ end
75
+
76
+ it "should give up" do
77
+ @memo.should == [:give_up]
78
+ end
79
+
80
+ it "should obtain a nil result" do
81
+ @result.should == nil
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,27 @@
1
+ require File.dirname(__FILE__) + "/../spec/common"
2
+
3
+ require 'quix/ruby'
4
+
5
+ root = Pathname(__FILE__).dirname + ".."
6
+ file = root + "README"
7
+ lib = root + "lib"
8
+
9
+ describe file do
10
+ ["Synopsis",
11
+ "Raw Form",
12
+ "Synopsis 2.0",
13
+ ].each { |section|
14
+ it "#{section} should run as claimed" do
15
+ contents = file.read
16
+
17
+ code = %{
18
+ $LOAD_PATH.unshift "#{lib.expand_path}"
19
+ require 'cond'
20
+ include Cond
21
+ } + contents.match(%r!== #{section}.*?\n(.*?)^\S!m)[1]
22
+
23
+ expected = code.scan(%r!\# => (.*?)\n!).flatten.join("\n")
24
+ pipe_to_ruby(code).chomp.should == expected
25
+ end
26
+ }
27
+ end
@@ -0,0 +1,26 @@
1
+ here = File.dirname(__FILE__)
2
+ require here + "/../spec/common"
3
+
4
+ RESTARTS_FILE = here + "/../readmes/restarts.rb"
5
+
6
+ def run_restarts(input_string)
7
+ capture(input_string) {
8
+ load RESTARTS_FILE
9
+ }
10
+ end
11
+
12
+ describe RESTARTS_FILE do
13
+ it "should fetch with with alternate hash" do
14
+ hash = { "mango" => "mangoish fruit" }
15
+ re = %r!#{hash.values.first}!
16
+ run_restarts(%{2\n#{hash.inspect}\n}).should match(re)
17
+ end
18
+
19
+ it "should fetch with alternate value" do
20
+ class Cond::Restart
21
+ # coverage hack
22
+ undef :message
23
+ end
24
+ run_restarts(%{3\n"apple"\n}).should match(%r!value: "fruit"!)
25
+ end
26
+ end
@@ -0,0 +1,18 @@
1
+ here = File.dirname(__FILE__)
2
+ require here + "/../spec/common"
3
+
4
+ seibel_file = here + "/../readmes/seibel_pcl.rb"
5
+
6
+ include Cond
7
+
8
+ if RUBY_VERSION > "1.8.6"
9
+ describe seibel_file do
10
+ it "should run" do
11
+ lambda {
12
+ capture("0\n") {
13
+ load seibel_file
14
+ }
15
+ }.should_not raise_error
16
+ end
17
+ end
18
+ end
data/install.rb ADDED
@@ -0,0 +1,3 @@
1
+ $LOAD_PATH.unshift "./support"
2
+ require 'quix/simple_installer'
3
+ Quix::SimpleInstaller.new.run
data/lib/cond.rb ADDED
@@ -0,0 +1,456 @@
1
+
2
+ require 'cond/cond_private/thread_local'
3
+ require 'cond/cond_private/symbol_generator'
4
+ require 'cond/cond_private/defaults'
5
+
6
+ #
7
+ # Resolve errors without unwinding the stack.
8
+ #
9
+ module Cond
10
+ module CondPrivate
11
+ class MessageProc < Proc
12
+ def initialize(message = "", &block)
13
+ @message = message
14
+ end
15
+
16
+ def message
17
+ @message
18
+ end
19
+ end
20
+ end
21
+
22
+ #
23
+ # A restart. Use of this class is optional: you could pass lambdas
24
+ # to Cond.with_restarts, but you'll miss the description string
25
+ # shown inside Cond.default_handler.
26
+ #
27
+ class Restart < CondPrivate::MessageProc
28
+ end
29
+
30
+ #
31
+ # A handler. Use of this class is optional: you could pass lambdas
32
+ # to Cond.with_handlers, but you'll miss the description string
33
+ # shown by whichever tools might use it (currently none).
34
+ #
35
+ class Handler < CondPrivate::MessageProc
36
+ end
37
+
38
+ ######################################################################
39
+ # errors
40
+
41
+ #
42
+ # Cond.invoke_restart was called with an unknown restart.
43
+ #
44
+ class NoRestartError < StandardError
45
+ end
46
+
47
+ #
48
+ # `handle', `restart', `leave', or `again' called out of context.
49
+ #
50
+ class ContextError < StandardError
51
+ end
52
+
53
+ ######################################################################
54
+ # singleton methods
55
+
56
+ class << self
57
+ #
58
+ # Register a set of handlers. The given hash is merged with the
59
+ # set of current handlers.
60
+ #
61
+ # When the block exits, the previous set of handlers (if any) are
62
+ # restored.
63
+ #
64
+ def with_handlers(handlers)
65
+ # note: leave unfactored due to notable yield vs &block performance
66
+ handlers_stack.push(handlers_stack.last.merge(handlers))
67
+ begin
68
+ yield
69
+ ensure
70
+ handlers_stack.pop
71
+ end
72
+ end
73
+
74
+ #
75
+ # Register a set of restarts. The given hash is merged with the
76
+ # set of current restarts.
77
+ #
78
+ # When the block exits, the previous set of restarts (if any) are
79
+ # restored.
80
+ #
81
+ def with_restarts(restarts)
82
+ # note: leave unfactored due to notable yield vs &block performance
83
+ restarts_stack.push(restarts_stack.last.merge(restarts))
84
+ begin
85
+ yield
86
+ ensure
87
+ restarts_stack.pop
88
+ end
89
+ end
90
+
91
+ #
92
+ # A default handler is provided which runs a simple
93
+ # choose-a-restart input loop when +raise+ is called.
94
+ #
95
+ def with_default_handlers
96
+ # note: leave unfactored due to notable yield vs &block performance
97
+ with_handlers(defaults.handlers) {
98
+ yield
99
+ }
100
+ end
101
+
102
+ #
103
+ # The current set of restarts which have been registered.
104
+ #
105
+ def available_restarts
106
+ restarts_stack.last
107
+ end
108
+
109
+ #
110
+ # Find the closest-matching handler for the given Exception.
111
+ #
112
+ def find_handler(target) #:nodoc:
113
+ find_handler_from(handlers_stack.last, target)
114
+ end
115
+
116
+ def find_handler_from(handlers, target) #:nodoc:
117
+ handlers.fetch(target) {
118
+ found = handlers.inject(Array.new) { |acc, (klass, func)|
119
+ index = target.ancestors.index(klass)
120
+ if index
121
+ acc << [index, func]
122
+ else
123
+ acc
124
+ end
125
+ }.sort_by { |t| t.first }.first
126
+ found and found[1]
127
+ }
128
+ end
129
+
130
+ def run_code_section(klass, &block) #:nodoc:
131
+ section = klass.new(&block)
132
+ Cond.code_section_stack.push(section)
133
+ begin
134
+ section.instance_eval { run }
135
+ ensure
136
+ Cond.code_section_stack.pop
137
+ end
138
+ end
139
+
140
+ def check_context(keyword) #:nodoc:
141
+ section = Cond.code_section_stack.last
142
+ case keyword
143
+ when :restart
144
+ unless section.is_a? CondPrivate::RestartableSection
145
+ Cond.original_raise(
146
+ ContextError,
147
+ "`#{keyword}' called outside of `restartable' block"
148
+ )
149
+ end
150
+ when :handle
151
+ unless section.is_a? CondPrivate::HandlingSection
152
+ Cond.original_raise(
153
+ ContextError,
154
+ "`#{keyword}' called outside of `handling' block"
155
+ )
156
+ end
157
+ when :leave, :again
158
+ unless section
159
+ Cond.original_raise(
160
+ ContextError,
161
+ "`#{keyword}' called outside of `handling' or `restartable' block"
162
+ )
163
+ end
164
+ end
165
+ end
166
+
167
+ ###############################################
168
+ # wrapping
169
+
170
+ #
171
+ # Allow handlers to be called from C code by wrapping a method with
172
+ # begin/rescue. Returns the aliased name of the original method.
173
+ #
174
+ # See the README.
175
+ #
176
+ # Example:
177
+ #
178
+ # Cond.wrap_instance_method(Fixnum, :/)
179
+ #
180
+ def wrap_instance_method(mod, method)
181
+ original = "cond_original_#{mod.inspect}_#{method.inspect}"
182
+ # TODO: jettison 1.8.6, remove eval and use |&block|
183
+ # TODO: fix rcov bug -- does not see %{}
184
+ mod.module_eval <<-eval_end
185
+ alias_method :'#{original}', :'#{method}'
186
+ def #{method}(*args, &block)
187
+ begin
188
+ send(:'#{original}', *args, &block)
189
+ rescue Exception => e
190
+ raise e
191
+ end
192
+ end
193
+ eval_end
194
+ original
195
+ end
196
+
197
+ #
198
+ # Allow handlers to be called from C code by wrapping a method with
199
+ # begin/rescue. Returns the aliased name of the original method.
200
+ #
201
+ # See the README.
202
+ #
203
+ # Example:
204
+ #
205
+ # Cond.wrap_singleton_method(IO, :read)
206
+ #
207
+ def wrap_singleton_method(mod, method)
208
+ singleton_class = class << mod ; self ; end
209
+ wrap_instance_method(singleton_class, method)
210
+ end
211
+
212
+ ###############################################
213
+ # original raise
214
+
215
+ define_method :original_raise, Kernel.instance_method(:raise)
216
+
217
+ ######################################################################
218
+ # data -- all data is per-thread and fetched from the singleton class
219
+ #
220
+ # Cond.defaults contains the default handlers. To replace it,
221
+ # call
222
+ #
223
+ # Cond.defaults.clear(&block)
224
+ #
225
+ # where &block creates a new instance of your class which
226
+ # implements the method 'handlers'.
227
+ #
228
+ # Note that &block should return a brand new instance. Otherwise
229
+ # the returned object will be shared across threads.
230
+ #
231
+
232
+ stack_0 = lambda { Array.new }
233
+ stack_1 = lambda { Array.new.push(Hash.new) }
234
+ defaults = lambda { CondPrivate::Defaults.new }
235
+ {
236
+ :code_section_stack => stack_0,
237
+ :exception_stack => stack_0,
238
+ :handlers_stack => stack_1,
239
+ :restarts_stack => stack_1,
240
+ :defaults => defaults,
241
+ }.each_pair { |name, create|
242
+ include CondPrivate::ThreadLocal.reader_module(name) {
243
+ create.call
244
+ }
245
+ }
246
+
247
+ include CondPrivate::ThreadLocal.accessor_module(:reraise_count) { 0 }
248
+ end
249
+
250
+ ######################################################################
251
+
252
+ module CondPrivate
253
+ class CodeSection #:nodoc:
254
+ include SymbolGenerator
255
+
256
+ def initialize(with, &block)
257
+ @with = with
258
+ @block = block
259
+ @again_args = []
260
+ @leave, @again = gensym, gensym
261
+ SymbolGenerator.track(self, [@leave, @again])
262
+ end
263
+
264
+ def again(*args)
265
+ @again_args = (
266
+ case args.size
267
+ when 0
268
+ []
269
+ when 1
270
+ args.first
271
+ else
272
+ args
273
+ end
274
+ )
275
+ throw @again
276
+ end
277
+
278
+ def leave(*args)
279
+ case args.size
280
+ when 0
281
+ throw @leave
282
+ when 1
283
+ throw @leave, args.first
284
+ else
285
+ throw @leave, args
286
+ end
287
+ end
288
+
289
+ def run
290
+ catch(@leave) {
291
+ while true
292
+ catch(@again) {
293
+ Cond.send(@with, Hash.new) {
294
+ throw @leave, @block.call(*@again_args)
295
+ }
296
+ }
297
+ end
298
+ }
299
+ end
300
+ end
301
+
302
+ class RestartableSection < CodeSection #:nodoc:
303
+ def initialize(&block)
304
+ super(:with_restarts, &block)
305
+ end
306
+
307
+ def restart(sym, message, &block)
308
+ Cond.restarts_stack.last[sym] = Restart.new(message, &block)
309
+ end
310
+ end
311
+
312
+ class HandlingSection < CodeSection #:nodoc:
313
+ def initialize(&block)
314
+ super(:with_handlers, &block)
315
+ end
316
+
317
+ def handle(sym, message, &block)
318
+ Cond.handlers_stack.last[sym] = Handler.new(message, &block)
319
+ end
320
+ end
321
+ end
322
+
323
+ ######################################################################
324
+ # shiny exterior
325
+
326
+ module_function
327
+
328
+ #
329
+ # Begin a handling block. Inside this block, a matching handler
330
+ # gets called when +raise+ gets called.
331
+ #
332
+ def handling(&block)
333
+ Cond.run_code_section(CondPrivate::HandlingSection, &block)
334
+ end
335
+
336
+ #
337
+ # Begin a restartable block. A handler may transfer control to one
338
+ # of the restarts in this block.
339
+ #
340
+ def restartable(&block)
341
+ Cond.run_code_section(CondPrivate::RestartableSection, &block)
342
+ end
343
+
344
+ #
345
+ # Define a handler.
346
+ #
347
+ # The exception instance is passed to the block.
348
+ #
349
+ def handle(arg, message = "", &block)
350
+ Cond.check_context(:handle)
351
+ Cond.code_section_stack.last.handle(arg, message, &block)
352
+ end
353
+
354
+ #
355
+ # Define a restart.
356
+ #
357
+ # When a handler calls invoke_restart, it may pass additional
358
+ # arguments which are in turn passed to &block.
359
+ #
360
+ def restart(arg, message = "", &block)
361
+ Cond.check_context(:restart)
362
+ Cond.code_section_stack.last.restart(arg, message, &block)
363
+ end
364
+
365
+ #
366
+ # Leave the current handling or restartable block, optionally
367
+ # providing a value for the block.
368
+ #
369
+ # The semantics are the same as 'return'. When given multiple
370
+ # arguments, it returns an array. When given one argument, it
371
+ # returns only that argument (not an array).
372
+ #
373
+ def leave(*args)
374
+ Cond.check_context(:leave)
375
+ Cond.code_section_stack.last.leave(*args)
376
+ end
377
+
378
+ #
379
+ # Run the handling or restartable block again.
380
+ #
381
+ # Optionally pass arguments which are given to the block.
382
+ #
383
+ def again(*args)
384
+ Cond.check_context(:again)
385
+ Cond.code_section_stack.last.again(*args)
386
+ end
387
+
388
+ #
389
+ # Call a restart from a handler; optionally pass it some arguments.
390
+ #
391
+ def invoke_restart(name, *args, &block)
392
+ Cond.available_restarts.fetch(name) {
393
+ raise NoRestartError,
394
+ "Did not find `#{name.inspect}' in available restarts"
395
+ }.call(*args, &block)
396
+ end
397
+ end
398
+
399
+ module Kernel
400
+ remove_method :raise
401
+ def raise(*args)
402
+ if Cond.handlers_stack.last.empty?
403
+ # not using Cond
404
+ Cond.original_raise(*args)
405
+ else
406
+ last_exception, current_handler = Cond.exception_stack.last
407
+ exception = (
408
+ if last_exception and args.empty?
409
+ last_exception
410
+ else
411
+ begin
412
+ Cond.original_raise(*args)
413
+ rescue Exception => e
414
+ e
415
+ end
416
+ end
417
+ )
418
+ if current_handler
419
+ # inside a handler
420
+ handler = loop {
421
+ Cond.reraise_count += 1
422
+ handlers = Cond.handlers_stack[-1 - Cond.reraise_count]
423
+ if handlers.nil?
424
+ break nil
425
+ end
426
+ found = Cond.find_handler_from(handlers, exception.class)
427
+ if found and found != current_handler
428
+ break found
429
+ end
430
+ }
431
+ if handler
432
+ handler.call(exception)
433
+ else
434
+ Cond.reraise_count = 0
435
+ Cond.original_raise(exception)
436
+ end
437
+ else
438
+ # not inside a handler
439
+ Cond.reraise_count = 0
440
+ handler = Cond.find_handler(exception.class)
441
+ if handler
442
+ Cond.exception_stack.push([exception, handler])
443
+ begin
444
+ handler.call(exception)
445
+ ensure
446
+ Cond.exception_stack.pop
447
+ end
448
+ else
449
+ Cond.original_raise(exception)
450
+ end
451
+ end
452
+ end
453
+ end
454
+ remove_method :fail
455
+ alias_method :fail, :raise
456
+ end