cond 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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