mulligan 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 93a82e010fe6e85e2d4258e582448f6eba71aca0
4
+ data.tar.gz: e93698220f48079b7ed8a34889744fb84de4f657
5
+ SHA512:
6
+ metadata.gz: 68fa8ce73ecec03457111bd4acc355b260e7fb5faccff1bb1a9195a9b62d2c761dce7406e53f3078cdf0fd94dae20829a98b90e3074017b1661331ac9cacf801
7
+ data.tar.gz: 172b8934dfdca12793f9931833d10d4a53aac6a831183150926ad591d7e9fbbe0261804692b25cc557a21d2948b36b485a23c07e1a4f03606c1d40a7ab54b319
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3-p484
4
+ - 2.0.0
5
+ - 2.1.0
6
+ - 2.1.1
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in mulligan.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 michaeljbishop
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,249 @@
1
+ [![Build Status](https://travis-ci.org/michaeljbishop/mulligan.png?branch=master)](https://travis-ci.org/michaeljbishop/mulligan)
2
+ # Mulligan
3
+
4
+ "In golf,...a stroke that is replayed from the spot of the previous stroke without penalty, due to an errant shot made on the previous stroke. The result is, as the hole is played and scored, as if the first errant shot had never been made." -- [Wikipedia](http://en.wikipedia.org/wiki/Mulligan_(games)#Mulligan_in_golf)
5
+
6
+ ## Usage
7
+
8
+ When rescuing an exception, the Mulligan gem allows you to execute some recovery code, then continue your program as if the exception had never been thrown in the first place
9
+
10
+ Here's a very simple contrived example:
11
+ ```ruby
12
+ 1 require 'mulligan'
13
+ 2
14
+ 3 def method_that_raises
15
+ 4 puts "RAISING"
16
+ 5 raise "You can ignore this" do |e|
17
+ 6 e.set_recovery :ignore do
18
+ 7 puts "IGNORING"
19
+ 8 end
20
+ 9 end
21
+ 10 puts "AFTER RAISE"
22
+ 11 end
23
+ 12
24
+ 13 def calling_method
25
+ 14 method_that_raises
26
+ 15 "SUCCESS"
27
+ 16 rescue Exception => e
28
+ 17 puts "RESCUED"
29
+ 18 e.recover :ignore
30
+ 19 puts "HANDLED"
31
+ 20 end
32
+ ```
33
+
34
+ Running this at the REPL shows:
35
+
36
+ ```
37
+ 2.0.0-p353 :009 > calling_method
38
+ RAISING
39
+ RESCUED
40
+ IGNORING
41
+ AFTER RAISE
42
+ => "SUCCESS"
43
+ ```
44
+
45
+ ### Yeah... wait, shouldn't we see "HANDLED" in that output?!
46
+
47
+ Here's what happened in detail:
48
+
49
+ 1. `#method_that_raises` is called from `#calling_method` (line 14)
50
+ 2. `#method_that_raises` raises an exception *but* before it is raised, a "recovery" can be added to the exception (line 6) in the block passed to `#raise`. (The exception is the parameter 'e' passed to the `#raise` block)
51
+ 3. The exception is then raised (line 5) and rescued (line 16)
52
+ 4. The "recovery" on the exception is called (line 18) which executes the statement in the recovery block (defined on line 7).
53
+ 5. Since the exception has recovered, control taks us back to the point *immediately after the block passed to* `#raise` (line 10), continuing as if `#raise` hadn't been called in the first place.
54
+ 6. The method exits (line 11) and we return to line 15 as if we never saw the exception.
55
+ 7. We exit the method because there's no exception to rescue (line 20). The last value in the function was "SUCCESS" so that is returned.
56
+
57
+ ### I see what you did there. That's cool, but why should I care?
58
+
59
+ You should care because your `rescue` statement is likely to be far from the `raise` in your program's execution and the further away it is, the harder it is to fix the error intelligently. It's even harder if that `raise` comes from a library you are calling.
60
+
61
+ Specifying recoveries on the exception allows the lower-level code to offer strategies for fixing the exception without the higher-level code needing to know the internals of those strategies.
62
+
63
+ Better yet, it offers the ability to "go back in time" [Groundhog Day](http://en.wikipedia.org/wiki/Groundhog_Day_(film))-style, but this time, your code knows how to play the piano, how to sculpt ice, and how to speak French.
64
+
65
+ Find your favorite chair and read these:
66
+
67
+ - [Dylan Reference Manual - Conditions - Background](http://opendylan.org/books/drm/Conditions_Background)
68
+ - [Beyond Exception Handling: Conditions and Restarts](http://www.gigamonkeys.com/book/beyond-exception-handling-conditions-and-restarts.html) (keep in mind the "restarts" are what we are calling "recoveries").
69
+
70
+ ### This *seems* like a good thing, but what can I do with it?
71
+
72
+ Here are some use cases:
73
+
74
+ #### Fixing network connection errors
75
+
76
+ ```ruby
77
+ def http_post(url, data)
78
+ ... networking code...
79
+ raise CredentialsExpiredException if response == 401
80
+ raise ConnectionFailedException if response == 404
81
+ end
82
+
83
+ def post_resource(object)
84
+ ... assemble url and data...
85
+ http_post(url, data)
86
+ rescue Exception => e
87
+ raise(e){|e|e.set_recovery(:retry){}}
88
+ retry if last_recovery == :retry
89
+ end
90
+
91
+ def save_resources
92
+ post_resource(user)
93
+ post_resource(post)
94
+ post_resource(comment)
95
+
96
+ rescue CredentialsExpiredException => e
97
+ ... fix credentials...
98
+ e.recover :retry
99
+ rescue ConnectionFailedException => e
100
+ ... switch from wifi to cellular...
101
+ e.recover :retry
102
+ end
103
+ ```
104
+
105
+ #### Screen Scraping (in Dylan)
106
+
107
+ [The maling list post](https://groups.google.com/d/msg/comp.lang.dylan/gszO7d7BAok/zqVbQlNDKzAJ)
108
+
109
+ This is going to be inherently messy and for a long-running program like this, potentially painful to restart if the data is found to be incorrect. Much better to just put in some recoveries and choose from them if errors are found.
110
+
111
+ #### Handling errors in parsers
112
+
113
+ You might write a parser to read XML or a log file format and it might encounter malformed entries. You can make that low-level parser code much more reusable if you specify a few recoveries in the raised exceptions. Higher level code will have many more choices to handle errors.
114
+
115
+ BTW, Here's your second chance to read [Beyond Exception Handling: Conditions and Restarts](http://www.gigamonkeys.com/book/beyond-exception-handling-conditions-and-restarts.html). There's a log file parsing example in there.
116
+
117
+ #### Ask your friendly Lisp coder. They've been solving these problems for years.
118
+
119
+ You've always known he (or she) knew Lisp and now you have something to ask him about.
120
+
121
+ ## Some Notes About the Ruby Implementation
122
+ ### Methods
123
+ #### Kernel#raise
124
+ 1. `Kernel#raise` now has a return value! It is the value returned from the recovery block.
125
+ 2. `Kernel#raise` also yields the exception to a block. It does this since it's pretty common to have `Kernel#raise` create an exception for you and without this, you couldn't otherwise attach recoveries.
126
+
127
+ ```ruby
128
+ def test
129
+ # (result is explicit for this example)
130
+ result = raise "Test" do |e| # yields Exception to the block
131
+ e.set_recovery(:test_return){"hello"} # recovery block returns a string
132
+ end
133
+ result
134
+ rescue Exception => e
135
+ e.recover :test_return
136
+ end
137
+ ```
138
+
139
+ returns
140
+
141
+ ```
142
+ 2.0.0-p353 :012 > test
143
+ => "hello"
144
+ ```
145
+
146
+ #### You can pass parameters to Exception#recover
147
+ The first parameter is always the id of the recovery. The rest will be passed directly to the recovery block. Building on the above example:
148
+
149
+ ```ruby
150
+ def test
151
+ # (result is explicit for this example)
152
+ result = raise "Test" do |e|
153
+ e.set_recovery(:test_return){|p|p} # pass back whatever is passed in
154
+ end
155
+ result
156
+ rescue Exception => e
157
+ e.recover :test_return, 5
158
+ end
159
+ ```
160
+
161
+ returns
162
+
163
+ ```
164
+ 2.0.0-p353 :012 > test
165
+ => 5
166
+ ```
167
+
168
+ #### Your recovery can attach data to be read by the rescue clause
169
+ You can pass an options hash to the `rescue` clause that is attached to your recovery. This is handy if you want to attach extra data about the recovery or the circumstances in which it is being raised. Pass them as the second parameter in `Exception#set_recovery`. You can retrieve them with `Exception#recovery_options`. Reserved keys are `:summary`, and `:discussion`
170
+ ```ruby
171
+ raise "Test" do |e|
172
+ summary = "Replaces the misparsed entry with one you specify."
173
+ e.set_recovery(:replace_value, :summary => summary){|p|p}
174
+ end
175
+ ```
176
+
177
+ To demonstrate this, here's a rescue statement. The rescue simply prints out the description which is not really useful as a rescue statement, but it's an example of how a REPL might output to the user a list of recoveries to choose from and the details of what they do.
178
+
179
+ ```ruby
180
+ rescue MisparsedEntryException => e
181
+ $stderr.puts "Choose a recovery:"
182
+ e.recovery_identifiers.each do |id|
183
+ $stderr.puts " #{id}: - #{e.recovery_options(id)[:summary]}"
184
+ end
185
+ ... read choice and execute ...
186
+ ```
187
+
188
+ #### Kernel#last_recovery
189
+ There is a new method: `Kernel#last_recovery` which will return the id of the last recovery invoked for the current thread. So you can do things like this:
190
+
191
+ ```ruby
192
+ begin
193
+ ... some code ...
194
+ rescue Exception => e
195
+ raise(e){|e|e.set_recovery(:retry){}}
196
+ retry if last_recovery == :retry
197
+ end
198
+ ```
199
+
200
+ ### Mulliigan uses #callcc
201
+
202
+ There is more than one way to do this. In the end, I wanted something that would fit very naturally into Ruby's existing Exception mechanism, yet offer as much of the benefits of Lisp's "restart" as I could.
203
+
204
+ However, to make that happen, I had to use the `#callcc` method. I'm not completely sure how supported this is across different Ruby implementations. Additionally, I've read that it can be a rather slow method. It's important to note that if an exception is raised but does not have any attached recoveries, `#callcc` will not be called and the standard exception mechanism is used.
205
+
206
+ ### Ruby Support
207
+
208
+ [![Build Status](https://travis-ci.org/michaeljbishop/mulligan.png?branch=master)](https://travis-ci.org/michaeljbishop/mulligan)
209
+ Mulligan supports MRI versions 1.9.3 -> 2.1.1
210
+
211
+ ### "Recovery"? What's wrong with "Restart"?
212
+
213
+ I had to make a hard choice about naming the thing that allows an exception to be recovered from. "Restart" is the word used in Lisp, but because it is used as a verb and as a noun, it makes it hard to know what a Ruby method named `#restart` would do. Does it return a "restart" or does it execute a restart?
214
+
215
+ Changing the name to a noun subtracts that confusion (though arguably adds some back for those coming from languages where the "restart" name is entrenched).
216
+
217
+ ## Influences
218
+ - [Beyond Exception Handling: Conditions and Restarts](http://www.gigamonkeys.com/book/beyond-exception-handling-conditions-and-restarts.html) -- (from [Practical Common Lisp](http://www.gigamonkeys.com/book/))
219
+ - [Things You Didn't Know About Exceptions](http://avdi.org/talks/rockymtnruby-2011/things-you-didnt-know-about-exceptions.html) (Avdi Grimm)
220
+ - [Restartable Exceptions](http://chneukirchen.org/blog/archive/2005/03/restartable-exceptions.html) (Christian Neukirchen)
221
+ - [Common Lisp conditions](https://www.ruby-forum.com/topic/179474) (Ruby Forum)
222
+
223
+ ### Acknowledgements
224
+ Thanks to [Ryan Angilly](https://twitter.com/angilly) of [Ramen](https://ramen.is) who graciously released the gem name 'mulligan' to be used with this project. If you've got a good software project, consider launching with them.
225
+
226
+ ## Installation
227
+
228
+ Add this line to your application's Gemfile:
229
+
230
+ gem 'mulligan'
231
+
232
+ And then execute:
233
+
234
+ $ bundle
235
+
236
+ Or install it yourself as:
237
+
238
+ $ gem install mulligan
239
+
240
+ ## Contributing
241
+
242
+ 1. Fork it [http://github.com/michaeljbishop/mulligan](http://github.com/michaeljbishop/mulligan)
243
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
244
+ 4. Push to the branch (`git push origin my-new-feature`)
245
+ 5. Create new Pull Request
246
+
247
+ ## Homepage
248
+
249
+ [http://michaeljbishop.github.io/mulligan/](http://michaeljbishop.github.io/mulligan/)
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ require "rake/extensiontask"
5
+
6
+ Rake::ExtensionTask.new "mulligan" do |ext|
7
+ ext.lib_dir = "lib/mulligan"
8
+ end
9
+
10
+ RSpec::Core::RakeTask.new(:spec => :compile)
11
+
12
+ task :default => :spec
@@ -0,0 +1,3 @@
1
+ require "mkmf"
2
+
3
+ create_makefile "mulligan/mulligan"
@@ -0,0 +1,203 @@
1
+ #include <ruby.h>
2
+
3
+
4
+ // -----------------------------------------------------------
5
+ // UTILITIES
6
+ // -----------------------------------------------------------
7
+
8
+ #define ARRAY_SIZE(var) (sizeof(var)/sizeof(var[0]))
9
+
10
+ // -----------------------------------------------------------
11
+ // DECLARATIONS
12
+ // -----------------------------------------------------------
13
+
14
+ static VALUE rb_mulligan_raise(int argc, VALUE *argv, VALUE self);
15
+ static VALUE callcc_block(VALUE c, VALUE in_context, int argc, VALUE argv[]);
16
+ static VALUE __set_continuation__block(VALUE unused, VALUE in_context, int argc, VALUE argv[]);
17
+
18
+ static ID id_recoveries = 0;
19
+ static ID id_empty_ = 0;
20
+ static ID id_send = 0;
21
+ static ID id_callcc = 0;
22
+ static ID id_call = 0;
23
+ static ID id___set_continuation__ = 0;
24
+ static ID id_puts = 0;
25
+
26
+ // ===========================================================
27
+ // MAIN ENTRY POINT
28
+ // ===========================================================
29
+
30
+ void Init_mulligan(void)
31
+ {
32
+ VALUE mMulligan = rb_define_module("Mulligan");
33
+ VALUE mKernel = rb_define_module_under(mMulligan, "Kernel");
34
+ rb_define_method(mKernel, "raise", rb_mulligan_raise, -1);
35
+ rb_define_method(mKernel, "fail", rb_mulligan_raise, -1);
36
+
37
+ #if RUBY_API_VERSION_MAJOR < 2
38
+ // completely replaces Ruby's version of raise.
39
+ // In Ruby 2 we prepend (but don't call super)
40
+ rb_define_global_function("raise", rb_mulligan_raise, -1);
41
+ rb_define_global_function("fail", rb_mulligan_raise, -1);
42
+ #endif
43
+
44
+ rb_require("continuation");
45
+ id_recoveries = rb_intern("recoveries");
46
+ id_empty_ = rb_intern("empty?");
47
+ id_send = rb_intern("send");
48
+ id_callcc = rb_intern("callcc");
49
+ id_call = rb_intern("call");
50
+ id___set_continuation__ = rb_intern("__set_continuation__");
51
+ id_puts = rb_intern("puts");
52
+ }
53
+
54
+
55
+ /* -----------------------------------------------------------
56
+ // Here is the template ruby code that we are approximating
57
+ // in C. Native-only calls are surround by <<>>
58
+ // -----------------------------------------------------------
59
+ def raise(*args)
60
+ e = <<make_exception(*args)>>
61
+ yield e if block_given?
62
+
63
+ # only use callcc if there are restarts otherwise re-raise it
64
+ <<rb_exc_raise(e)>> if e.send(:recoveries).empty?
65
+ should_raise = true
66
+ result = callcc do |c|
67
+ e.send(:__set_continuation__) do |*args,&block|
68
+ should_raise = false
69
+ c.call(*args,&block)
70
+ end
71
+ end
72
+ <<rb_exc_raise(e)>> if should_raise
73
+ result
74
+ end
75
+ // -----------------------------------------------------------
76
+ // There is one big difference which is at all costs, we only
77
+ // call `rb_exc_raise` from the current frame. This is important
78
+ // because we want the stack-trace and current stack-frame to
79
+ // be identical to what they were if we just called the standard
80
+ // #raise method. If we call `rb_exc_raise` within a block or
81
+ // call super, we will lose that stack-frame context.
82
+ // For this reason, you'll see that awkward `should_raise` variable
83
+ // in the source above.
84
+ // -----------------------------------------------------------*/
85
+
86
+
87
+ static VALUE
88
+ rb_mulligan_raise(int argc, VALUE *argv, VALUE self)
89
+ {
90
+ // -----------------------------------------------------------
91
+ // C90-compliance asks us to declare all variables at the top
92
+ // -----------------------------------------------------------
93
+ VALUE recoveries = Qnil;
94
+ VALUE should_raise_ary = Qnil;
95
+ VALUE contextVars[3];
96
+ VALUE context = Qnil;
97
+ VALUE result = Qnil;
98
+ VALUE is_empty = Qnil;
99
+
100
+ // -----------------------------------------------------------
101
+ // Get a reference to the Exception object
102
+ // -----------------------------------------------------------
103
+ VALUE e = rb_make_exception(argc, argv);
104
+
105
+ if (NIL_P(e)) {
106
+ // get whatever is in $!. I'm sure this is slow
107
+ e = rb_eval_string("$!");
108
+ // what I'd like to use reallu like to use
109
+ // e = rb_rubylevel_errinfo(); // internal ruby call, yet necessary
110
+ }
111
+
112
+ if (NIL_P(e)) {
113
+ e = rb_exc_new(rb_eRuntimeError, 0, 0);
114
+ }
115
+
116
+ // -----------------------------------------------------------
117
+ // With the Exception in place, yield to the block
118
+ // -----------------------------------------------------------
119
+ if (rb_block_given_p())
120
+ rb_yield(e);
121
+
122
+ // -----------------------------------------------------------
123
+ // If there are no recoveries, just throw it without a callcc
124
+ // -----------------------------------------------------------
125
+ recoveries = rb_funcall(e, id_send, 1, ID2SYM(id_recoveries));
126
+ is_empty = rb_funcall(recoveries, id_empty_, 0);
127
+ if (RTEST(is_empty))
128
+ goto raise;
129
+
130
+ // -----------------------------------------------------------
131
+ // There are recoveries so we first capture the continuation
132
+ // using callcc
133
+ // -----------------------------------------------------------
134
+
135
+ // A note about should_raise_ary:
136
+ // 2 callbacks from now (in `__set_continuation__block`) we are
137
+ // going to want to change the value of this from true to false. Since Qtrue
138
+ // is not a pointer, we are going to make a ghetto-pointer by putting it in
139
+ // an array of one. That array will be passed to the callback where it will be
140
+ // modified and we can read it still in this context after all the callbacks
141
+ // are complete.
142
+ should_raise_ary = rb_ary_to_ary(Qtrue);
143
+ contextVars[0] = self;
144
+ contextVars[1] = should_raise_ary;
145
+ contextVars[2] = e;
146
+ context = rb_ary_new4(ARRAY_SIZE(contextVars), contextVars);
147
+
148
+ result = rb_block_call(
149
+ self,
150
+ id_callcc,
151
+ 0, // argc
152
+ 0, // argv
153
+ RUBY_METHOD_FUNC(callcc_block),
154
+ context
155
+ );
156
+
157
+ // Here we read our "pointer"
158
+ if (!RTEST(rb_ary_entry(should_raise_ary, 0)))
159
+ return result;
160
+
161
+ raise:
162
+ rb_exc_raise(e);
163
+ // UNREACHABLE;
164
+ }
165
+
166
+ static VALUE
167
+ callcc_block(VALUE c, VALUE in_context, int argc, VALUE argv[])
168
+ {
169
+ VALUE self = rb_ary_entry(in_context, 0);
170
+ VALUE should_raise_ary = rb_ary_entry(in_context, 1);
171
+ VALUE e = rb_ary_entry(in_context, 2);
172
+
173
+ VALUE contextVars[] = {self, should_raise_ary, c};
174
+ VALUE context = rb_ary_new4(ARRAY_SIZE(contextVars), contextVars);
175
+
176
+ return rb_block_call(
177
+ e,
178
+ id___set_continuation__,
179
+ 0, // argc
180
+ 0, // argv
181
+ RUBY_METHOD_FUNC(__set_continuation__block),
182
+ context // data2
183
+ );
184
+ }
185
+
186
+ static VALUE
187
+ __set_continuation__block(VALUE unused, VALUE in_context, int argc, VALUE argv[])
188
+ {
189
+ // VALUE self = rb_ary_entry(in_context, 0);
190
+ VALUE should_raise_ary = rb_ary_entry(in_context, 1);
191
+ VALUE c = rb_ary_entry(in_context, 2);
192
+ VALUE proc = rb_block_proc();
193
+
194
+ // Here we set our `should_raise` pointer back to false
195
+ rb_ary_store(should_raise_ary, 0, Qfalse);
196
+
197
+ return rb_funcall_with_block(c, id_call, argc, argv, proc);
198
+ }
199
+
200
+
201
+
202
+
203
+
@@ -0,0 +1,88 @@
1
+ module Mulligan
2
+
3
+ # An exception that is thrown when invoking a non-existent recovery.
4
+ class ControlException < Exception ; end
5
+
6
+ module Condition
7
+
8
+ # Creates or replaces a recovery strategy.
9
+ #
10
+ # @param [String or Symbol] id the key to reference the recovery later
11
+ # @param [Hash] options specifies a token for this recovery that can later be
12
+ # retrieved. Can only be set once. See {#recovery_options}
13
+ # Reserved Keys are as follows:
14
+ # :data - Use this as a parameter to send a piece of data to rescuers to use
15
+ # as they determine their strategy for recovering.
16
+ # :summary - A short, one-line description of this recovery
17
+ # :discussion - The complete documentation of this recovery. Please include a
18
+ # description of the behavior, the return parameter, and any parameters
19
+ # the recovery can take
20
+ def set_recovery(id, options={}, &block)
21
+ return if block.nil?
22
+ recoveries[id.to_sym] = options.merge(:block => block)
23
+ nil
24
+ end
25
+
26
+ # Checks for the presence of a recovery
27
+ #
28
+ # @param [String or Symbol] id the key for the recovery
29
+ # @return [Boolean] whether or not a recovery exists for this id
30
+ def has_recovery?(id)
31
+ recoveries.has_key?(id.to_sym)
32
+ end
33
+
34
+ # Retrieves the options specified when a recovery was made
35
+ #
36
+ # @param [String or Symbol] id the key for the recovery
37
+ # @return [Hash] The options set on this recovery
38
+ def recovery_options(id)
39
+ return nil unless has_recovery?(id.to_sym)
40
+ recoveries[id.to_sym].dup.reject{|k,v| [:block, :continuation].include? k}
41
+ end
42
+
43
+ # Retrieves all the identifiers for available recoveries
44
+ #
45
+ # @return [Enumerable] The identifiers for all the recoveries
46
+ def recovery_identifiers
47
+ recoveries.keys
48
+ end
49
+
50
+ # Executes the recovery.
51
+ # This actually places the stack back to just after the `#raise` that brought
52
+ # us to this `rescue` clause. Then the recovery block is executed and the program
53
+ # continues on.
54
+ #
55
+ # @param [String or Symbol] id the key for the recovery
56
+ # @param params any additional parameters you want to pass to the recovery block
57
+ # @return This doesn't actually matter because you can't retrieve it
58
+ def recover(id, *params)
59
+ Thread.current[:__last_recovery__] = nil
60
+ raise ControlException unless has_recovery?(id.to_sym)
61
+ data = recoveries[id.to_sym]
62
+ if data[:continuation].nil?
63
+ $stderr.puts "Cannot invoke restart #{id}. Must first raise this exception: '#{self.inspect}'"
64
+ return
65
+ end
66
+ Thread.current[:__last_recovery__] = id
67
+ data[:continuation].call(*data[:block].call(*params))
68
+ end
69
+
70
+ private
71
+
72
+ def recoveries
73
+ @recoveries ||= {}
74
+ end
75
+
76
+ def __set_continuation__
77
+ continuation = Proc.new
78
+ # the the continuation for any recoveries that are not yet assigned one
79
+ # It's important not to overwrite the existing continuations because a recovery
80
+ # should return to the place it was raised, always.
81
+ recoveries.each do |key, r|
82
+ next if r.has_key? :continuation
83
+ r[:continuation] = continuation
84
+ end
85
+ end
86
+ end
87
+ end
88
+
@@ -0,0 +1,4 @@
1
+ require "mulligan/mulligan"
2
+ require_relative 'kernel_common'
3
+
4
+ # raise is implemented by the c-extension
@@ -0,0 +1,12 @@
1
+ require 'continuation'
2
+
3
+ module Mulligan
4
+ module Kernel
5
+
6
+ # Returns the identifier for the last recovery invoked in this thread.
7
+ # @return [Symbol] The identifier of the last recovery invoked in this thread.
8
+ def last_recovery
9
+ Thread.current[:__last_recovery__]
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,31 @@
1
+ require_relative 'kernel_common'
2
+
3
+ module Mulligan
4
+ module Kernel
5
+
6
+ # Raises an Exception.
7
+ # The Exception that is either passed in or generated is yielded to the block
8
+ # where you can specify recoveries on it.
9
+ #
10
+ # @param args the same args you would pass to the normal Kernel#raise
11
+ # @yield [e] Passes the exception-to-be-raised to the block.
12
+ # @return The value returned from the invoked recovery block.
13
+ def raise(*args)
14
+ super
15
+ rescue Exception => e
16
+ yield e if block_given?
17
+
18
+ # only use callcc if there are restarts otherwise re-raise it
19
+ super(e) if e.send(:recoveries).empty?
20
+ should_raise = true
21
+ result = callcc do |c|
22
+ e.send(:__set_continuation__) do |*args,&block|
23
+ should_raise = false
24
+ c.call(*args,&block)
25
+ end
26
+ end
27
+ super(e) if should_raise
28
+ result
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,3 @@
1
+ module Mulligan
2
+ VERSION = "0.4.1"
3
+ end