recoverable 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7a004cdad4e2c84084abd80d9f768e76e68ef82e
4
- data.tar.gz: 619f7e3dfb8ecb873afaa6f63c1daab89afa022c
3
+ metadata.gz: 8dea1219bb101ee0ff89ca0794c8a4c2648275c0
4
+ data.tar.gz: a82f71d6ade61164f07701c5c1bd26b375e235cb
5
5
  SHA512:
6
- metadata.gz: 27fb6b8a72f506a7f19af2bafd2508a59bc61dd0582af89ab24105f7c36105c57ba42bcbbf8647d4104ad3a93ebb9cd90eef5e37094cdf8e9df0258cf90680f8
7
- data.tar.gz: 0fb7fafc2453bbdbe8427acb02d904708c072fd78fe3c202e5ea587f4f064920640ae94bfccfaad1d2641ff37ddc27e3be9fc0a9dc978f943501ca60c359f6b8
6
+ metadata.gz: a425e9704da44b71696b128c71f4ff4b42934433335a214befc5467d887fb28ffd32f4cdd7757311ed7264a79707ec5a1b12ac2270be2378e9c8f85f6e54205a
7
+ data.tar.gz: affcc22857a7fffba3ddc96b326b9347429fd78e021ffca345ac99ca5b98f7bd69e393a443a282076b99b76503555746c14154d7f7831b58171da60eaed6b850
data/README.md CHANGED
@@ -1,3 +1,8 @@
1
+ [![Version ](https://img.shields.io/gem/v/recoverable.svg?maxAge=2592000)](https://rubygems.org/gems/recoverable)
2
+ [![Build Status ](https://travis-ci.com/Benjaminpjacobs/ship_station.svg)](https://travis-ci.com/Benjaminpjacobs/recoverable)
3
+ [![Maintainability](https://api.codeclimate.com/v1/badges/dd436c45c8a52dc8c13c/maintainability)](https://codeclimate.com/github/Benjaminpjacobs/recoverable/maintainability)
4
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/dd436c45c8a52dc8c13c/test_coverage)](https://codeclimate.com/github/Benjaminpjacobs/recoverable/test_coverage)
5
+
1
6
  ## Recoverbale
2
7
 
3
8
 
@@ -1,46 +1,69 @@
1
- module Retryable
2
- RetryCountExceeded = Class.new(StandardError)
3
-
4
- def recover(method, times, on: [StandardError], sleep: 1, handler: nil)
1
+ module Recoverable
2
+ def recover(method_name, times: 1, on: StandardError, sleep: nil, custom_handler: nil, custom_exception: RetryCountExceeded)
5
3
  recoverable = Array.wrap(on)
6
- proxy = create_proxy(method, times, recoverable, sleep, handler)
4
+ proxy = create_proxy(method_name: method_name, times: times, recoverable: recoverable, sleep: sleep, custom_handler: custom_handler, custom_exception: custom_exception)
7
5
  self.prepend proxy
8
6
  end
9
7
 
10
- def create_proxy(method, times, recoverable, sleep, handler)
8
+ def create_proxy(method_name:, times:, recoverable:, sleep:, custom_handler:, custom_exception:)
11
9
  Module.new do
12
- define_method(method) do |*args|
13
- retries = times.dup
10
+ define_method(method_name) do |*args|
14
11
  begin
15
12
  super *args
16
13
  rescue *recoverable => error
17
- begin
18
- sleep(sleep)
19
- retries.next && retry
20
- rescue StopIteration
21
- if handler
22
- handler_args = {}
23
- unbound = self.class.instance_method(handler)
24
- req_params = unbound.parameters.map{|p| p[1] if p[0] == :keyreq }.compact
25
-
26
- return send(handler) if req_params.empty?
27
-
28
- local_args = args.map(&:to_a).flatten(1).to_h
29
- ivs = instance_values.keys.map(&:to_sym)
30
-
31
- req_params.each do |key|
32
- handler_args[key] ||= eval(key.to_s) if (ivs + public_methods + local_variables).include?(key)
33
- handler_args[key] ||= local_args[key] if local_args.keys.include?(key)
34
- end
35
-
36
- send(handler, handler_args)
37
- end
38
- raise RetryCountExceeded, [error.class, error.message]
39
- end
14
+ sleep(sleep) if sleep
15
+ retry if (times -= 1) > 0
16
+ self.class.handle_exception(instance: self, custom_handler: custom_handler, args: args, error: error, custom_exception: custom_exception)
40
17
  end
41
18
  end
42
19
  end
43
20
  end
21
+
22
+ def handle_exception(instance:, custom_handler:, args:, error:, custom_exception: )
23
+ raise custom_exception.new(error) unless custom_handler
24
+ req_params = retrieve_required_parameters(instance, custom_handler)
25
+
26
+ return instance.send(custom_handler) if req_params.empty?
27
+
28
+ custom_handler_args = fetch_handler_args(args, instance, req_params, error)
29
+ instance.send(custom_handler, custom_handler_args)
30
+ end
31
+
32
+ def retrieve_required_parameters(instance, custom_handler)
33
+ unbound_method = instance.class.instance_method(custom_handler)
34
+ unbound_method.parameters.map{|p| p[1] if p[0] == :keyreq }.compact
35
+ end
36
+
37
+ def fetch_handler_args(args, instance, req_params, error)
38
+ custom_handler_args = {error: error}
39
+ local_args = fetch_local_args(args)
40
+ evaluateables = evaluateable_keys_on(instance)
41
+ generate_custom_handler_args(req_params: req_params, evaluateables: evaluateables, local_args: local_args, instance: instance, custom_handler_args: custom_handler_args)
42
+ end
43
+
44
+ def generate_custom_handler_args(req_params:, evaluateables:, local_args:, instance:, custom_handler_args:)
45
+ req_params.each do |key|
46
+ custom_handler_args[key] = instance.send(:eval, key.to_s) if evaluateables.include?(key)
47
+ custom_handler_args[key] ||= local_args[key] if local_args.keys.include?(key)
48
+ end
49
+
50
+ custom_handler_args
51
+ end
52
+
53
+ def fetch_instance_variables(instance)
54
+ instance.instance_values.keys.map(&:to_sym)
55
+ end
56
+
57
+ def fetch_local_args(args)
58
+ args.map(&:to_a).flatten(1).to_h
59
+ end
60
+
61
+ def evaluateable_keys_on(instance)
62
+ fetch_instance_variables(instance) +
63
+ instance.send(:public_methods) +
64
+ instance.send(:local_variables)
65
+ end
66
+
44
67
  end
45
68
 
46
69
 
@@ -0,0 +1,7 @@
1
+ module Recoverable
2
+ class RetryCountExceeded < StandardError
3
+ def initialize(error)
4
+ super error
5
+ end
6
+ end
7
+ end
@@ -1,3 +1,3 @@
1
1
  module Recoverable
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
data/lib/recoverable.rb CHANGED
@@ -3,6 +3,7 @@ require 'active_support/all'
3
3
  require 'logger'
4
4
 
5
5
  # App
6
+ require 'recoverable/errors'
6
7
  require 'recoverable/base'
7
8
 
8
9
  # Models
@@ -0,0 +1 @@
1
+ class CustomError < StandardError; end
data/spec/mocks/foo.rb ADDED
@@ -0,0 +1,10 @@
1
+ class Foo
2
+ extend Recoverable
3
+
4
+ def bar
5
+ baz
6
+ end
7
+
8
+ def baz
9
+ end
10
+ end
@@ -0,0 +1 @@
1
+ class UnrecoveredError < StandardError; end
@@ -0,0 +1,178 @@
1
+ require 'spec_helper'
2
+ RSpec.describe Recoverable do
3
+ let!(:instance) { self.class::TestClass.new }
4
+ subject{ instance.bar }
5
+ context "passing no errors" do
6
+ class self::TestClass
7
+ extend Recoverable
8
+ recover :bar, times: 2
9
+
10
+ def bar; baz; end
11
+ def baz; end
12
+
13
+ end
14
+
15
+ it "defaults to standard error and no sleep" do
16
+ expect_any_instance_of(Kernel).to receive(:sleep).never
17
+ allow_any_instance_of(self.class::TestClass).to receive(:baz).and_raise(StandardError)
18
+ expect{ subject }.to raise_error(Recoverable::RetryCountExceeded)
19
+ end
20
+
21
+ it "recovers from inheritors of standard error" do
22
+ allow_any_instance_of(self.class::TestClass).to receive(:baz).and_raise(CustomError)
23
+ expect{ subject }.to raise_error(Recoverable::RetryCountExceeded)
24
+ end
25
+
26
+ end
27
+
28
+ context "passing specific errors" do
29
+ class self::TestClass
30
+ extend Recoverable
31
+ recover :bar, times: 2, on: CustomError
32
+
33
+ def bar; baz; end
34
+ def baz; end
35
+
36
+ end
37
+
38
+ it "recovers from specific error" do
39
+ allow_any_instance_of(self.class::TestClass).to receive(:baz).and_raise(CustomError)
40
+ expect{ subject }.to raise_error(Recoverable::RetryCountExceeded)
41
+ end
42
+
43
+ it "does not recover from different raised error" do
44
+ allow_any_instance_of(self.class::TestClass).to receive(:baz).and_raise(UnrecoveredError)
45
+ expect{ subject }.to raise_error(UnrecoveredError)
46
+ end
47
+
48
+ it "does not recover from standard error" do
49
+ allow_any_instance_of(self.class::TestClass).to receive(:baz).and_raise(StandardError)
50
+ expect{ subject }.to raise_error(StandardError)
51
+ end
52
+
53
+ end
54
+
55
+ context "passing specific sleep" do
56
+ class self::TestClass
57
+ extend Recoverable
58
+ recover :bar, times: 2, on: CustomError, sleep: 3
59
+
60
+ def bar; baz; end
61
+ def baz; end
62
+
63
+ end
64
+
65
+ it "recovers from specific error" do
66
+ expect_any_instance_of(Kernel).to receive(:sleep).with(3).exactly(2).times
67
+ allow_any_instance_of(self.class::TestClass).to receive(:baz).and_raise(CustomError)
68
+ expect{ subject }.to raise_error(Recoverable::RetryCountExceeded)
69
+ end
70
+ end
71
+
72
+ context "passing custom error handler" do
73
+ class self::TestClass
74
+ extend Recoverable
75
+ recover :bar, times: 2, on: CustomError, custom_handler: :handle_error
76
+
77
+ def bar; baz; end
78
+ def baz; end
79
+ def handle_error
80
+ "Handled"
81
+ end
82
+
83
+ end
84
+
85
+ it "recovers from specific error" do
86
+ allow_any_instance_of(self.class::TestClass).to receive(:baz).and_raise(CustomError)
87
+ expect{ subject }.to_not raise_error(Recoverable::RetryCountExceeded)
88
+ expect(subject).to eq("Handled")
89
+ end
90
+ end
91
+
92
+ context "passing custom error handler with error message" do
93
+ class self::TestClass
94
+ extend Recoverable
95
+ recover :bar, times: 2, on: CustomError, custom_handler: :handle_error
96
+
97
+ def bar(arg:nil)
98
+ baz
99
+ end
100
+
101
+ def baz; end
102
+
103
+ def handle_error(error:)
104
+ error.message
105
+ end
106
+
107
+ end
108
+
109
+ subject { instance.bar(arg: "I'm a keyword Arg")}
110
+
111
+ it "recovers from specific error" do
112
+ allow_any_instance_of(self.class::TestClass).to receive(:baz).and_raise(CustomError.new("Custom Error!"))
113
+ expect{ subject }.to_not raise_error(Recoverable::RetryCountExceeded)
114
+ expect(subject).to eq("Custom Error!")
115
+ end
116
+ end
117
+
118
+ context "passing custom error handler with arg" do
119
+ class self::TestClass
120
+ extend Recoverable
121
+ recover :bar, times: 2, on: CustomError, custom_handler: :handle_error
122
+
123
+ def bar(arg:nil)
124
+ baz
125
+ end
126
+
127
+ def baz; end
128
+
129
+ def handle_error(error:, arg:)
130
+ arg
131
+ end
132
+
133
+ end
134
+
135
+ subject { instance.bar(arg: "I'm a keyword Arg")}
136
+
137
+ it "recovers from specific error" do
138
+ allow_any_instance_of(self.class::TestClass).to receive(:baz).and_raise(CustomError)
139
+ expect{ subject }.to_not raise_error(Recoverable::RetryCountExceeded)
140
+ expect(subject).to eq("I'm a keyword Arg")
141
+ end
142
+ end
143
+
144
+ context "passing custom error handler with a method call" do
145
+ class self::TestClass
146
+ extend Recoverable
147
+ recover :bar, times: 2, on: CustomError, custom_handler: :handle_error
148
+
149
+ def bar(arg:nil)
150
+ baz
151
+ end
152
+
153
+ def baz; end
154
+
155
+ def handle_error(error:)
156
+ "#{method_call}, #{private_method_call}"
157
+ end
158
+
159
+ def method_call
160
+ "I'm a method call"
161
+ end
162
+ private
163
+
164
+ def private_method_call
165
+ "I'm a private method call"
166
+ end
167
+
168
+ end
169
+
170
+ subject { instance.bar(arg: "I'm a keyword Arg")}
171
+
172
+ it "recovers from specific error" do
173
+ allow_any_instance_of(self.class::TestClass).to receive(:baz).and_raise(CustomError)
174
+ expect{ subject }.to_not raise_error(Recoverable::RetryCountExceeded)
175
+ expect(subject).to eq( "I'm a method call, I'm a private method call")
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,33 @@
1
+ require 'bundler/setup'
2
+ require 'simplecov'
3
+ require 'pry'
4
+
5
+ SimpleCov.start do
6
+ add_filter '/spec'
7
+ end
8
+
9
+ require 'recoverable'
10
+ require 'mocks/foo'
11
+ require 'mocks/custom_error'
12
+ require 'mocks/unrecovered_error'
13
+ require 'factory_bot'
14
+
15
+ RSpec.configure do |config|
16
+ # Enable flags like --only-failures and --next-failure
17
+ config.example_status_persistence_file_path = ".rspec_status"
18
+
19
+ # Disable RSpec exposing methods globally on `Module` and `main`
20
+ config.disable_monkey_patching!
21
+
22
+ config.expect_with :rspec do |c|
23
+ c.syntax = :expect
24
+ end
25
+ config.include FactoryBot::Syntax::Methods
26
+
27
+ config.before(:suite) do
28
+ FactoryBot.find_definitions
29
+ end
30
+
31
+ end
32
+
33
+ RSpec::Expectations.configuration.on_potential_false_positives = :nothing
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: recoverable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Jacobs
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: '3.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: factory_bot
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '4.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '4.0'
97
111
  description: Class Level retry DSL for ruby
98
112
  email:
99
113
  - benjaminpjacobs@gmail.com
@@ -105,7 +119,13 @@ files:
105
119
  - README.md
106
120
  - lib/recoverable.rb
107
121
  - lib/recoverable/base.rb
122
+ - lib/recoverable/errors.rb
108
123
  - lib/recoverable/version.rb
124
+ - spec/mocks/custom_error.rb
125
+ - spec/mocks/foo.rb
126
+ - spec/mocks/unrecovered_error.rb
127
+ - spec/recoverable_spec.rb
128
+ - spec/spec_helper.rb
109
129
  homepage:
110
130
  licenses: []
111
131
  metadata:
@@ -130,4 +150,9 @@ rubygems_version: 2.6.14.1
130
150
  signing_key:
131
151
  specification_version: 4
132
152
  summary: Class Level retry DSL for ruby
133
- test_files: []
153
+ test_files:
154
+ - spec/mocks/foo.rb
155
+ - spec/mocks/custom_error.rb
156
+ - spec/mocks/unrecovered_error.rb
157
+ - spec/spec_helper.rb
158
+ - spec/recoverable_spec.rb