mulligan 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'mulligan'
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