mulligan 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +249 -0
- data/Rakefile +12 -0
- data/ext/mulligan/extconf.rb +3 -0
- data/ext/mulligan/mulligan.c +203 -0
- data/lib/mulligan/condition.rb +88 -0
- data/lib/mulligan/kernel.rb +4 -0
- data/lib/mulligan/kernel_common.rb +12 -0
- data/lib/mulligan/kernel_pure.rb +31 -0
- data/lib/mulligan/version.rb +3 -0
- data/lib/mulligan.rb +16 -0
- data/mulligan.gemspec +31 -0
- data/plan.markdown +129 -0
- data/spec/mulligan_spec.rb +391 -0
- data/spec/spec_helper.rb +2 -0
- metadata +125 -0
data/lib/mulligan.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require "mulligan/version"
|
2
|
+
require "mulligan/condition"
|
3
|
+
require "mulligan/kernel"
|
4
|
+
|
5
|
+
class Exception
|
6
|
+
include Mulligan::Condition
|
7
|
+
end
|
8
|
+
|
9
|
+
class Object
|
10
|
+
if RUBY_VERSION < "2.0"
|
11
|
+
# ruby 1.9 replaces raise in the extension
|
12
|
+
include Mulligan::Kernel
|
13
|
+
else
|
14
|
+
prepend Mulligan::Kernel
|
15
|
+
end
|
16
|
+
end
|
data/mulligan.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'mulligan/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "mulligan"
|
8
|
+
spec.version = Mulligan::VERSION
|
9
|
+
spec.required_ruby_version='~> 2.0'
|
10
|
+
spec.authors = ["michaeljbishop"]
|
11
|
+
spec.email = ["mbtyke@gmail.com"]
|
12
|
+
spec.summary = %q{Adds restarts to Ruby's Exception class (similar to LISP Conditions)}
|
13
|
+
spec.description = <<__END__
|
14
|
+
Allows you to decouple the code implementing a exception-handling strategy from the code which decides which strategy to use.
|
15
|
+
|
16
|
+
In other words, when you handle a Mulligan::Condition in your rescue clause, you can choose from a set of strategies (called "restarts") exposed by the exception to take the stack back to where #raise was called, execute your strategy, and pretend that the exception was never raised.
|
17
|
+
__END__
|
18
|
+
spec.homepage = "http://michaeljbishop.github.io/mulligan"
|
19
|
+
spec.license = "MIT"
|
20
|
+
|
21
|
+
spec.files = `git ls-files -z`.split("\x0")
|
22
|
+
spec.extensions = %w[ext/mulligan/extconf.rb]
|
23
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
24
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
25
|
+
spec.require_paths = ["lib"]
|
26
|
+
|
27
|
+
spec.add_development_dependency "bundler", "~> 1.5"
|
28
|
+
spec.add_development_dependency "rake"
|
29
|
+
spec.add_development_dependency "rspec"
|
30
|
+
spec.add_development_dependency "rake-compiler"
|
31
|
+
end
|
data/plan.markdown
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
Features
|
2
|
+
========
|
3
|
+
|
4
|
+
Specify restart strategies for a given Exception
|
5
|
+
------------------------------------------------
|
6
|
+
Simply put, this adds "restarts" to the `Exception` class which allows exception raisers to specify multiple strategies for recovering from the exception. The strategies are attached to the Exception instance which travel as the stack unwinds to the enclosing `rescue` clause higher up on the stack. That rescue clause can then invoke a recovery strategy.
|
7
|
+
|
8
|
+
What is special is that when the recovery strategy is invoked, the stack is recreated to the point in time where the Exception was raised and the recovery strategy is invoked in *that* context, as if the exception were never raised to begin with. This allows code higher up in abstraction to inject recovery into low-level code making the low-level code more reusable.
|
9
|
+
|
10
|
+
In pseudocode:
|
11
|
+
|
12
|
+
def inner_function
|
13
|
+
raise
|
14
|
+
<specify some restarts here>
|
15
|
+
end
|
16
|
+
|
17
|
+
def outer_function
|
18
|
+
inner_function # will cause an exception with restarts
|
19
|
+
rescue Exception => e
|
20
|
+
# here we specify a recovery and we are able to repair the lower
|
21
|
+
# level code as if it never threw an exception to begin with
|
22
|
+
e.restart <strategy id>
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
Restart strategies can accept parameters
|
27
|
+
----------------------------------------
|
28
|
+
In addition, restarts can accept parameters to affect their implementation. In this way, when a restart is invoked, some state can be passed to it, affecting its execution.
|
29
|
+
|
30
|
+
Future Ideas
|
31
|
+
============
|
32
|
+
|
33
|
+
Specify restarts using a nice DSL
|
34
|
+
---------------------------------
|
35
|
+
We should be be able to specify restart clauses using a nice, concise language.
|
36
|
+
|
37
|
+
Here are some examples:
|
38
|
+
|
39
|
+
raise Exception, "Could not do the thing!" do
|
40
|
+
restart :ignore do
|
41
|
+
# simply doing nothing ignores the error
|
42
|
+
end
|
43
|
+
restart :replace_entry do |replacement|
|
44
|
+
replacement
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
Be able to catch an exception and add restarts to it before rethrowing it
|
50
|
+
-------------------------------------------------------------------------
|
51
|
+
We should be able to catch an exception from below, and add our own strategies to it
|
52
|
+
|
53
|
+
Here is an example:
|
54
|
+
|
55
|
+
def rethrow
|
56
|
+
should_retry = false
|
57
|
+
inner_call
|
58
|
+
rescue Exception => e
|
59
|
+
e.add_restarts do
|
60
|
+
restart :retry do
|
61
|
+
should_retry = true
|
62
|
+
end
|
63
|
+
end
|
64
|
+
retry if should_retry
|
65
|
+
end
|
66
|
+
|
67
|
+
When overriding a restart, should be able to call previous restart (super?)
|
68
|
+
-------------------------------------------------------------------------
|
69
|
+
|
70
|
+
Here is an example:
|
71
|
+
|
72
|
+
def rethrow
|
73
|
+
inner_call
|
74
|
+
rescue Exception => e
|
75
|
+
e.add_restarts do |super_restarts|
|
76
|
+
restart :fix do
|
77
|
+
super_restarts[:fix].call
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
Restarts can have some metadata associated with them
|
83
|
+
----------------------------------------------------
|
84
|
+
Like LISP, it would be nice to be able to handle these in the debugger by default. The debugger should be able to specify a nice interface to the user by outputting messages from the metatdata associated with a restart strategy (like a description of what it does and what parameters it will take)
|
85
|
+
|
86
|
+
|
87
|
+
`#raise` will return the strategy chosen and the return value of the strategy
|
88
|
+
-----------------------------------------------------------------------------
|
89
|
+
chosen = raise "can't to the thing" do
|
90
|
+
restart :ignore do
|
91
|
+
5
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
puts chosen # => [:ignore, 5]
|
96
|
+
|
97
|
+
|
98
|
+
Exceptions can carry data with them from the raise condition that the rescuers can use
|
99
|
+
--------------------------------------------------------------------------------------
|
100
|
+
I'm not sure if this is totally needed and think it's a separate item, *but* here's what it might look like:
|
101
|
+
|
102
|
+
class Exception
|
103
|
+
attr_reader :restart_data
|
104
|
+
def initialize(m, message = "", c = callback, options={}, &restart_block)
|
105
|
+
@restart_data = options[:restart_data]
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
Smalltalk has some built-in strategies
|
111
|
+
--------------------------------------
|
112
|
+
- #exit, #resume (like ignore, it means just continue as if it hadn't been thrown)
|
113
|
+
- #outer (reraise with the same strategy identifier except that now returns to this point)
|
114
|
+
- #pass (reraise the exception)
|
115
|
+
- #resignalAs:. Raise a different class of exception in place of the current exception, as if the new class of exception had been raised in the first place.
|
116
|
+
- #retry (note: not sure how we can make this a built-in)The try-block associated with the handler (i.e. the receiver of the #on:do: to which it is the last argument) is re-evaluated. Of course it is pointless retrying if the same exception will be raised, and this is an easy way to create an infinite loop (though Ctrl+Break should
|
117
|
+
#retryUsing: Substitute the argument as the new try block, and #retry. This has particular application for operations which have fast implementations for commonly used execution paths, and slower implementations for less common usages. get you out of trouble). - Super-interesting. Wonder if we might try it.
|
118
|
+
|
119
|
+
Dylan has a way of attaching a standard set of recoveries to a given Exception class
|
120
|
+
------------------------------------------------------------------------------------
|
121
|
+
So a given Exception class would have a documented set of recoveries (they refer to it as a protocol)
|
122
|
+
|
123
|
+
You can use a global variable to reference the last restart taken
|
124
|
+
-----------------------------------------------------------------
|
125
|
+
One of the difficult problems to solve is, what should #raise return? Currently, it returns the array of the restart chosen and the result of the restart block. I really like that we can get the result of the restart block, but I really don't like that methods written with raise that are ignored suddenly implicitly return an array. Yet to implement a property retry, it's important to know which restart was executed. I'd like the cleanliness of #raise returning only the return value of the block and the cleanliness of being able to know which restart was chosen.
|
126
|
+
|
127
|
+
One solution is to have #raise return the value straight through and use a thread-local global variable that holds the id of the last invoked restart. So far, the only use case I've seen that requires that information is to do a retry inside a rescue clause.
|
128
|
+
|
129
|
+
|
@@ -0,0 +1,391 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Mulligan do
|
4
|
+
it 'should have a version number' do
|
5
|
+
Mulligan::VERSION.should_not be_nil
|
6
|
+
end
|
7
|
+
|
8
|
+
shared_examples "a Mulligan Condition" do
|
9
|
+
it 'should propagate errors' do
|
10
|
+
expect { outer_test(style) }.to raise_error
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should correctly report missing strategies' do
|
14
|
+
outer_test(style){|e|e.has_recovery?(:aaa)}.should be_false
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should correctly report included strategies' do
|
18
|
+
outer_test(style){|e|e.has_recovery?(:ignore)}.should be_true
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should correctly report the list of recoveries' do
|
22
|
+
outer_test(style){|e|e.recovery_identifiers}.should == [:ignore, :return_param, :return_all_params, :return_param2, :retry]
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should raise a control exception when invoking a non-existent recovery' do
|
26
|
+
expect { outer_test(style){|e|e.recover :aaa} }.to raise_error(Mulligan::ControlException)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should not raise an exception when invoking the ignore recovery' do
|
30
|
+
expect { outer_test(style){|e|e.recover :ignore} }.to_not raise_exception
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'should return the parameter sent when invoking the return_param recovery' do
|
34
|
+
result = outer_test(style){|e|e.recover(:return_param, 5)}
|
35
|
+
result.should be(5)
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should return all parameters sent when invoking the return_all_params recovery' do
|
39
|
+
result1, result2 = outer_test(style){|e|e.recover(:return_all_params, 5, 6)}
|
40
|
+
result1.should eq(5)
|
41
|
+
result2.should eq(6)
|
42
|
+
end
|
43
|
+
|
44
|
+
context "and follows the continutation to the correct raise" do
|
45
|
+
it 'should return the parameter sent when invoking the return_param recovery' do
|
46
|
+
result = outer_test(style){|e|e.recover(:return_param2, 5)}
|
47
|
+
result.should be(25)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should ignore setting a recovery when passed no block" do
|
52
|
+
expect { outer_test(style){|e|e.recover :no_block} }.to raise_error(Mulligan::ControlException)
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should retrieve the network request without an exception" do
|
56
|
+
@count_of_calls_before_failure = 2
|
57
|
+
|
58
|
+
result = nil
|
59
|
+
expect { result = do_network_task }.to_not raise_error
|
60
|
+
expect(result).to eq(
|
61
|
+
{
|
62
|
+
:users => "json_data",
|
63
|
+
:posts => "json_data",
|
64
|
+
:comments => "json_data"
|
65
|
+
}
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "recovery options" do
|
70
|
+
it "should not return the block" do
|
71
|
+
data = nil
|
72
|
+
outer_test(style) do |e|
|
73
|
+
data = e.recovery_options(:return_param)[:block]
|
74
|
+
e.recover(:return_param)
|
75
|
+
end
|
76
|
+
expect(data).to be_nil
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should not return the continuation" do
|
80
|
+
data = nil
|
81
|
+
outer_test(style) do |e|
|
82
|
+
data = e.recovery_options(:return_param)[:continuation]
|
83
|
+
e.recover(:return_param)
|
84
|
+
end
|
85
|
+
expect(data).to be_nil
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should pass data created in set_recovery" do
|
89
|
+
data = nil
|
90
|
+
outer_test(style) do |e|
|
91
|
+
data = e.recovery_options(:return_param)[:data]
|
92
|
+
e.recover(:return_param)
|
93
|
+
end
|
94
|
+
expect(data).to be(5)
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should pass summary created in set_recovery" do
|
98
|
+
data = nil
|
99
|
+
outer_test(style) do |e|
|
100
|
+
data = e.recovery_options(:return_param)[:summary]
|
101
|
+
e.recover(:return_param)
|
102
|
+
end
|
103
|
+
expect(data).to eq("Passes the parameter sent in as the value of the block.")
|
104
|
+
end
|
105
|
+
|
106
|
+
it "should be read-only" do
|
107
|
+
result = outer_test(style) do |e|
|
108
|
+
e.recovery_options(:return_param)[:new_entry] = 5
|
109
|
+
e.recover(:return_param, e)
|
110
|
+
end
|
111
|
+
expect(result.recovery_options(:return_param)[:new_entry]).to be_nil
|
112
|
+
end
|
113
|
+
|
114
|
+
if Exception.method_defined?(:cause)
|
115
|
+
it "should support the `#cause` method in the native extension" do
|
116
|
+
begin
|
117
|
+
raise "test"
|
118
|
+
rescue => e
|
119
|
+
begin
|
120
|
+
raise "test2"
|
121
|
+
rescue =>e
|
122
|
+
expect(e.cause.message).to eq "test"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
it "should support overriding a recovery and calling the inherited recovery"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
context Exception do
|
133
|
+
let(:style){:manual}
|
134
|
+
it_behaves_like "a Mulligan Condition"
|
135
|
+
|
136
|
+
it "shouldn't fail when recovering before raising" do
|
137
|
+
t = Exception.new("Test Exception")
|
138
|
+
t.set_recovery(:ignore) {|p|}
|
139
|
+
expect{t.recover(:ignore)}.to_not raise_error
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
shared_examples "raising exceptions" do
|
144
|
+
it_behaves_like "a Mulligan Condition"
|
145
|
+
|
146
|
+
it "should propgate recoveries when raising a pre-existing exception" do
|
147
|
+
t = Exception.new("Test Exception")
|
148
|
+
t.set_recovery(:ignore) {|p|}
|
149
|
+
begin
|
150
|
+
raise t do |e|
|
151
|
+
e.set_recovery(:return_param){|p|p}
|
152
|
+
end
|
153
|
+
rescue Exception => e
|
154
|
+
expect(e.has_recovery?:ignore).to be_true
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
it "should properly report the line when raising with no block" do
|
159
|
+
begin
|
160
|
+
line = __LINE__ ; raise "Test"
|
161
|
+
rescue => e
|
162
|
+
expect(line_from_stack_string(e.backtrace[0])).to eq(line)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
it "should properly report the line when raising WITH a block" do
|
167
|
+
begin
|
168
|
+
line = __LINE__ ; raise "Test" do |e|
|
169
|
+
false
|
170
|
+
end
|
171
|
+
rescue => e
|
172
|
+
expect(line_from_stack_string(e.backtrace[0])).to eq(line)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
it "should modify variables in the binding of the raiser" do
|
177
|
+
begin
|
178
|
+
result = scope_test
|
179
|
+
rescue => e
|
180
|
+
e.recover :change
|
181
|
+
end
|
182
|
+
expect(result).to eq(7)
|
183
|
+
end
|
184
|
+
|
185
|
+
context "when raise is called with no arguments" do
|
186
|
+
it "should raise $! when $! is not nil" do
|
187
|
+
begin
|
188
|
+
raise Exception, "test"
|
189
|
+
rescue Exception => e
|
190
|
+
expect($!).to be(e)
|
191
|
+
begin
|
192
|
+
raise
|
193
|
+
rescue Exception => e2
|
194
|
+
expect(e2).to be(e)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
it "should raise a RuntimeError when $! is nil" do
|
200
|
+
expect { raise }.to raise_error(RuntimeError)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
it "should raise a RuntimeError with string message when raise is called only with a string" do
|
205
|
+
message = "test"
|
206
|
+
begin
|
207
|
+
raise message
|
208
|
+
rescue RuntimeError => e
|
209
|
+
expect(e.message).to eq(message)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
it "should raise a TypeError when called with two strings" do
|
214
|
+
expect {
|
215
|
+
raise "hello", "world"
|
216
|
+
}.to raise_error(TypeError)
|
217
|
+
end
|
218
|
+
|
219
|
+
it "should, when called with a Exception subclassclass, raise that subclass" do
|
220
|
+
expect {
|
221
|
+
raise CustomException
|
222
|
+
}.to raise_error(CustomException)
|
223
|
+
end
|
224
|
+
|
225
|
+
context "when called with an object instance," do
|
226
|
+
let(:object){CustomObjectReturner.new}
|
227
|
+
|
228
|
+
it "should raise the result of calling object.exception" do
|
229
|
+
expect{ raise object }.to raise_error(CustomException)
|
230
|
+
end
|
231
|
+
|
232
|
+
it "and a string, should raise the result of calling object.exception with a custom message" do
|
233
|
+
begin
|
234
|
+
raise object, "test"
|
235
|
+
rescue CustomException => e
|
236
|
+
expect(e.message).to eq("test")
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
end
|
242
|
+
|
243
|
+
describe "Kernel#raise" do
|
244
|
+
let(:style){:raise}
|
245
|
+
it_behaves_like "raising exceptions"
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
|
250
|
+
class CustomException < Exception
|
251
|
+
end
|
252
|
+
|
253
|
+
class CustomObjectReturner
|
254
|
+
def exception(*args)
|
255
|
+
CustomException.new(*args)
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
|
260
|
+
#=======================
|
261
|
+
# HELPER METHODS
|
262
|
+
#=======================
|
263
|
+
|
264
|
+
def scope_test
|
265
|
+
result = 5
|
266
|
+
raise do |e|
|
267
|
+
e.set_recovery :change do |arg|
|
268
|
+
result = 7
|
269
|
+
end
|
270
|
+
end
|
271
|
+
result
|
272
|
+
end
|
273
|
+
|
274
|
+
|
275
|
+
|
276
|
+
|
277
|
+
|
278
|
+
#=======================
|
279
|
+
# HELPER METHODS
|
280
|
+
#=======================
|
281
|
+
|
282
|
+
# returns the line given a string from Exception#backtrace
|
283
|
+
def line_from_stack_string(s)
|
284
|
+
s.match(/[^:]+:(\d+)/)[1].to_i
|
285
|
+
end
|
286
|
+
|
287
|
+
def core_test(style)
|
288
|
+
t = "Test Exception"
|
289
|
+
t = Exception.new("Test Exception") if style == :manual
|
290
|
+
raise t do |e|
|
291
|
+
e.set_recovery(:ignore){|p|p}
|
292
|
+
e.set_recovery(:no_block)
|
293
|
+
e.set_recovery(:return_param, data: 5, summary: "Passes the parameter sent in as the value of the block."){|p|p}
|
294
|
+
e.set_recovery(:return_all_params){|*p|next *p}
|
295
|
+
e.set_recovery(:return_param2){|p|p}
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
|
300
|
+
def inner_test(style = :manual)
|
301
|
+
core_test(style)
|
302
|
+
rescue Exception => e
|
303
|
+
result = raise(e) do |e|
|
304
|
+
e.set_recovery(:retry){}
|
305
|
+
# here we add 10 so we can ensure we are in fact, overriding
|
306
|
+
# the behavior for the same recovery as defined in core_test
|
307
|
+
e.set_recovery(:return_param2){|p|p+10}
|
308
|
+
end
|
309
|
+
retry if last_recovery == :retry
|
310
|
+
# here we add 10 so we can differentiate retries in `inner_test` from `core_test`
|
311
|
+
# we know we are in inner_test if our result is plus 10
|
312
|
+
result + 10
|
313
|
+
end
|
314
|
+
|
315
|
+
|
316
|
+
def outer_test(style = :manual, &handler)
|
317
|
+
inner_test(style)
|
318
|
+
rescue Exception => e
|
319
|
+
handler.call(e)
|
320
|
+
end
|
321
|
+
|
322
|
+
|
323
|
+
|
324
|
+
#===========================
|
325
|
+
# SIMULATE LOGIN EXPIRE
|
326
|
+
#===========================
|
327
|
+
|
328
|
+
# simple method that logs the user in
|
329
|
+
|
330
|
+
@credentials = "password"
|
331
|
+
def login
|
332
|
+
@credentials = "password"
|
333
|
+
@count_of_calls_before_failure = 2
|
334
|
+
end
|
335
|
+
|
336
|
+
|
337
|
+
class CredentialsExpiredException < Exception ; end
|
338
|
+
|
339
|
+
# at a low level, we will just raise a CredentialsExpiredException if that's
|
340
|
+
# the response we get from the server
|
341
|
+
def rest_get(url)
|
342
|
+
# simulate that the credentials expire after a certain period
|
343
|
+
@credentials = nil if @count_of_calls_before_failure <= 0
|
344
|
+
@count_of_calls_before_failure = @count_of_calls_before_failure - 1
|
345
|
+
|
346
|
+
# Simulate getting a response from the server indicating our credentials
|
347
|
+
# have expired.
|
348
|
+
raise(CredentialsExpiredException, "Credentials expired") if @credentials != "password"
|
349
|
+
|
350
|
+
# Canned data to return
|
351
|
+
"json_data"
|
352
|
+
end
|
353
|
+
|
354
|
+
# this takes a resource, makes an URL for that and returns the raw data provided
|
355
|
+
# from rest_get.
|
356
|
+
# It also specifies a "retry" recovery in case the credentials can be restored by
|
357
|
+
# code at a higher level
|
358
|
+
def request_resource(name)
|
359
|
+
url = "http://site.com/#{name}"
|
360
|
+
rest_get(url)
|
361
|
+
|
362
|
+
rescue CredentialsExpiredException => e
|
363
|
+
# re-raise the exception but add a recovery so if they fix the credentials
|
364
|
+
# we can try again
|
365
|
+
raise (e) do |e|
|
366
|
+
e.set_recovery(:retry){true}
|
367
|
+
end
|
368
|
+
retry if last_recovery == :retry
|
369
|
+
result
|
370
|
+
end
|
371
|
+
|
372
|
+
# This is the method that demonstrates how it all comes together.
|
373
|
+
# We can handle all credential failures from a very high-level.
|
374
|
+
# Because #request_resource offers a retry recovery, any code that calls
|
375
|
+
# #request_resource doesn't have to worry about the state of the user's credentials.
|
376
|
+
# Those exceptions will be thrown and the handling of them will at a very high-level
|
377
|
+
# of abstraction, yet, after they are handled, the program will continue as if the
|
378
|
+
# exception hadn't been thrown to begin with.
|
379
|
+
def do_network_task
|
380
|
+
{
|
381
|
+
:users => request_resource("users"), # This one should work
|
382
|
+
:posts => request_resource("posts"), # This one should work
|
383
|
+
:comments => request_resource("comments") # This one should fail
|
384
|
+
}
|
385
|
+
|
386
|
+
# Here, we handle any requests where credentials fail and we re-login
|
387
|
+
# then we ask to retry the same query
|
388
|
+
rescue CredentialsExpiredException => e
|
389
|
+
login
|
390
|
+
e.recover :retry
|
391
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mulligan
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- michaeljbishop
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-03-12 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.5'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.5'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake-compiler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: |
|
70
|
+
Allows you to decouple the code implementing a exception-handling strategy from the code which decides which strategy to use.
|
71
|
+
|
72
|
+
In other words, when you handle a Mulligan::Condition in your rescue clause, you can choose from a set of strategies (called "restarts") exposed by the exception to take the stack back to where #raise was called, execute your strategy, and pretend that the exception was never raised.
|
73
|
+
email:
|
74
|
+
- mbtyke@gmail.com
|
75
|
+
executables: []
|
76
|
+
extensions:
|
77
|
+
- ext/mulligan/extconf.rb
|
78
|
+
extra_rdoc_files: []
|
79
|
+
files:
|
80
|
+
- .gitignore
|
81
|
+
- .rspec
|
82
|
+
- .travis.yml
|
83
|
+
- Gemfile
|
84
|
+
- LICENSE.txt
|
85
|
+
- README.md
|
86
|
+
- Rakefile
|
87
|
+
- ext/mulligan/extconf.rb
|
88
|
+
- ext/mulligan/mulligan.c
|
89
|
+
- lib/mulligan.rb
|
90
|
+
- lib/mulligan/condition.rb
|
91
|
+
- lib/mulligan/kernel.rb
|
92
|
+
- lib/mulligan/kernel_common.rb
|
93
|
+
- lib/mulligan/kernel_pure.rb
|
94
|
+
- lib/mulligan/version.rb
|
95
|
+
- mulligan.gemspec
|
96
|
+
- plan.markdown
|
97
|
+
- spec/mulligan_spec.rb
|
98
|
+
- spec/spec_helper.rb
|
99
|
+
homepage: http://michaeljbishop.github.io/mulligan
|
100
|
+
licenses:
|
101
|
+
- MIT
|
102
|
+
metadata: {}
|
103
|
+
post_install_message:
|
104
|
+
rdoc_options: []
|
105
|
+
require_paths:
|
106
|
+
- lib
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - ~>
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '2.0'
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - '>='
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
requirements: []
|
118
|
+
rubyforge_project:
|
119
|
+
rubygems_version: 2.1.11
|
120
|
+
signing_key:
|
121
|
+
specification_version: 4
|
122
|
+
summary: Adds restarts to Ruby's Exception class (similar to LISP Conditions)
|
123
|
+
test_files:
|
124
|
+
- spec/mulligan_spec.rb
|
125
|
+
- spec/spec_helper.rb
|