light-service-ext 0.1.8 → 0.1.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5af4ef116e875211e34ed7878338e8e6a6e7a4b2b0a1777bcd685a4d33219e18
4
- data.tar.gz: 2cbc4827230fa19f43140d0022a75b308ac516b86eab1fc201358a060b3503c6
3
+ metadata.gz: 587a60d79dffd34dfbdbdbfc28e024c3a2983310209a8169e555e9ae88c8592b
4
+ data.tar.gz: 817889b86ce56ae818070d9bf938a34c8efca969729a51a8c4d37d738d20e821
5
5
  SHA512:
6
- metadata.gz: 640c9a51e52035660e2e2c56ce3573c5d5cf9586897fb8f6e483e6f32ebe3cc54612170d4f851e06837fe4b155f5dd10ce57f100616270f988dafff50ec227d0
7
- data.tar.gz: 1d62d1806e54380ed3a66f54dee065fb76a9d7a91aae12c3191e70544f1c1f1b3e4148f6fa7c787e0b438cc3f96640b002cbcfe323342664fc7b5b7c827eed99
6
+ metadata.gz: 538351e6b9c9050dcc81f91487c4fcd81e48e822589b5b3a77fa39909b733964d383241bd236c9ac199a32b018b8f9c6edf11efa5ef37d4051f1d0d0212810bd
7
+ data.tar.gz: 8ebd87e41ceb3bb2db3982289ded3281fd0c0234a33f255c95f9ff926b0a1a62231ade022b9c251246b1f1af6ee4ff4e0e5afe9b7e452ad8c8f41a3489022dbd
data/.env.example ADDED
@@ -0,0 +1 @@
1
+ export RBENV_VERSION=$(head -n 1 .ruby-version)
data/.gitignore CHANGED
@@ -18,3 +18,5 @@ Gemfile.lock
18
18
 
19
19
  # rspec failure tracking
20
20
  .rspec_status
21
+
22
+ .env
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.2.2
@@ -2,6 +2,8 @@
2
2
 
3
3
  module LightServiceExt
4
4
  class ApplicationContext < LightService::Context
5
+ attr_reader :error_info
6
+
5
7
  class << self
6
8
  def make_with_defaults(input = {}, overrides = {})
7
9
  defaults = default_attrs
@@ -23,7 +25,7 @@ module LightServiceExt
23
25
  api_responses: [],
24
26
  last_failed_context: nil,
25
27
  allow_raise_on_failure: LightServiceExt.config.allow_raise_on_failure?,
26
- internal_only: { error_info: nil }
28
+ internal_only: {}
27
29
  }.freeze
28
30
  end
29
31
  end
@@ -61,6 +63,18 @@ module LightServiceExt
61
63
  add_attrs_to_ctx(:internal_only, **attrs)
62
64
  end
63
65
 
66
+ def record_raised_error(error)
67
+ @error_info = ErrorInfo.new(error)
68
+ error_type = @error_info.type
69
+ error_message = @error_info.message
70
+ add_internal_only(error_info: { organizer: organizer_name,
71
+ action_name: action_name,
72
+ error: { type: error_type, message: error_message, backtrace: @error_info.clean_backtrace } })
73
+ add_errors(base: error_message)
74
+
75
+ LightServiceExt.config.on_raised_error.call(self, error)
76
+ end
77
+
64
78
  def add_to_api_responses(*api_response)
65
79
  add_collection_to_ctx(:api_responses, *api_response)
66
80
  end
@@ -77,6 +91,22 @@ module LightServiceExt
77
91
  !!self[:allow_raise_on_failure]
78
92
  end
79
93
 
94
+ def organizer_name
95
+ return nil if organized_by.nil?
96
+
97
+ organized_by.name.split('::').last
98
+ end
99
+
100
+ def action_name
101
+ return nil if invoked_action.blank?
102
+
103
+ invoked_action.name.split('::').last
104
+ end
105
+
106
+ def formatted_errors
107
+ JSON.pretty_generate(errors.presence || {})
108
+ end
109
+
80
110
  private
81
111
 
82
112
  def add_value_to_ctx(key, value)
@@ -3,12 +3,15 @@
3
3
  module LightServiceExt
4
4
  class ApplicationOrganizer
5
5
  extend LightService::Organizer
6
+ extend WithErrorHandler
6
7
 
7
8
  class << self
8
9
  def call(context)
9
- with(ApplicationContext.make_with_defaults(context))
10
- .around_each(RecordActions)
11
- .reduce(all_steps)
10
+ ctx = ApplicationContext.make_with_defaults(context)
11
+
12
+ with_error_handler(ctx: ctx) do
13
+ with(ctx).around_each(RecordActions).reduce(all_steps)
14
+ end
12
15
  end
13
16
 
14
17
  def steps
@@ -7,7 +7,7 @@ module LightServiceExt
7
7
  config_accessor(:allow_raise_on_failure) { true }
8
8
  config_accessor(:non_fatal_error_classes) { [] }
9
9
  config_accessor(:default_non_fatal_error_classes) { ['Rails::ActiveRecordError'.safe_constantize] }
10
- config_accessor(:logger) { (defined? Rails.logger).nil? ? Logger.new($stdout) : Rails.logger }
10
+ config_accessor(:on_raised_error) { proc {|_ctx, _error|} }
11
11
 
12
12
  def allow_raise_on_failure?
13
13
  !!allow_raise_on_failure
@@ -8,7 +8,7 @@ module LightServiceExt
8
8
  @fatal = fatal
9
9
  @error = error
10
10
  @type = error.class.name
11
- @message = message || error&.message
11
+ @message = message || error.try(:message)
12
12
  @title = "#{error.class.name} : #{error&.message}"
13
13
  end
14
14
 
@@ -30,8 +30,8 @@ module LightServiceExt
30
30
  end
31
31
 
32
32
  def errors
33
- model = error && (error.try(:model) || error.try(:record))
34
- return model.errors.messages if model.present?
33
+ model = error.try(:model) || error.try(:record)
34
+ message = model.present? ? model.errors.messages : error.message
35
35
 
36
36
  { base: message }
37
37
  end
@@ -41,7 +41,7 @@ module LightServiceExt
41
41
  type: type,
42
42
  message: message,
43
43
  exception: title,
44
- backtrace: clean_backtrace[0, 3]&.join("\n"),
44
+ backtrace: clean_backtrace[0, 5]&.join("\n"),
45
45
  error: error,
46
46
  fatal_error?: fatal_error?,
47
47
  errors: errors
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LightServiceExt
4
- VERSION = "0.1.8"
4
+ VERSION = "0.1.9"
5
5
  end
@@ -1,25 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # rubocop:disable Metrics/AbcSize
4
-
5
4
  module LightServiceExt
6
5
  module WithErrorHandler
7
6
  def with_error_handler(ctx:)
8
7
  @result = yield || ApplicationContext.make_with_defaults
9
- rescue Rails::ActiveRecordError => e
10
- error_info = ErrorInfo.new(e, fatal: false)
11
- ctx.add_internal_only(error_info: error_info)
12
- ctx.add_errors(**error_info.errors)
13
-
14
- LightServiceExt.config.logger.error(error_info.error_summary)
15
-
8
+ rescue StandardError => e
9
+ ctx.record_raised_error(e)
10
+ ctx.add_status(Status::COMPLETE)
16
11
  ctx.fail!
17
12
  ctx
18
- rescue StandardError => e
19
- error_info = ErrorInfo.new(e, fatal: false)
20
- LightServiceExt.config.logger.error(error_info.error_summary)
21
-
22
- raise
23
13
  end
24
14
  end
25
15
  end
@@ -23,10 +23,10 @@ Gem::Specification.new do |gem|
23
23
  gem.metadata["changelog_uri"] = "#{gem.homepage}/CHANGELOG.md"
24
24
 
25
25
  gem.add_runtime_dependency 'light-service', '~> 0.18', '>= 0.18.0'
26
- gem.add_runtime_dependency 'dry-struct', '~> 1.6', '< 2'
26
+ gem.add_runtime_dependency 'dry-struct', '~> 1.6'
27
27
  gem.add_runtime_dependency 'dry-validation', '~> 1.10'
28
28
  gem.add_runtime_dependency 'json', '~> 2.6', '>= 2.6.3'
29
- gem.add_runtime_dependency 'activesupport', '~> 6', '>= 6.0.6'
29
+ gem.add_runtime_dependency 'activesupport', '~> 6.0', '>= 6.0.6'
30
30
 
31
31
  gem.add_development_dependency("rake", "~> 13.0.6")
32
32
  gem.add_development_dependency("rspec", "~> 3.12.0")
@@ -7,6 +7,110 @@ module LightServiceExt
7
7
 
8
8
  subject(:ctx) { described_class.make_with_defaults(input) }
9
9
 
10
+ describe '#record_raised_error' do
11
+ let(:error_message) { 'Something went wrong' }
12
+ let(:error) { RuntimeError.new('Something went wrong') }
13
+ let(:backtrace) { ['some-backtrace-line'] }
14
+
15
+ before do
16
+ allow(subject).to receive(:organized_by) { ApplicationOrganizer }
17
+ allow(ctx).to receive(:invoked_action) { ApplicationAction }
18
+
19
+ error.set_backtrace(backtrace)
20
+ end
21
+
22
+ it 'records the error and adds it to the errors hash' do
23
+ subject.record_raised_error(error)
24
+
25
+ expect(subject.errors).to eql({ base: error_message })
26
+ expect(subject.success?).to be_truthy
27
+
28
+ internal_only = subject.internal_only
29
+ expect(internal_only.keys).to eql([:error_info])
30
+
31
+ error_info = internal_only[:error_info]
32
+ expect(error_info.keys).to eql([:organizer, :action_name, :error])
33
+ expect(error_info[:organizer]).to eql('ApplicationOrganizer')
34
+ expect(error_info[:action_name]).to eql('ApplicationAction')
35
+
36
+ raised_error_info = error_info[:error]
37
+ expect(raised_error_info.keys).to eql([:type, :message, :backtrace])
38
+ expect(raised_error_info[:type]).to eql(error.class.name)
39
+ expect(raised_error_info[:message]).to eql(error_message)
40
+ expect(raised_error_info[:backtrace]).to eql(error.backtrace)
41
+ end
42
+
43
+ it 'fails the operation and sets the error info' do
44
+ subject.record_raised_error(error)
45
+
46
+ expect(subject.success?).to be(true)
47
+ end
48
+ end
49
+
50
+ describe '#organizer_name' do
51
+ context 'with organizer' do
52
+ before { allow(subject).to receive(:organized_by) { ApplicationOrganizer } }
53
+
54
+ it 'returns the name of the organizer class' do
55
+ expect(subject.organizer_name).to eq('ApplicationOrganizer')
56
+ end
57
+ end
58
+
59
+ context 'without organizer' do
60
+ before { allow(subject).to receive(:organized_by) { nil } }
61
+
62
+ it 'returns nil' do
63
+ expect(subject.organizer_name).to be(nil)
64
+ end
65
+ end
66
+ end
67
+
68
+ describe '#action_name' do
69
+ context 'with invoked action' do
70
+ before { allow(subject).to receive(:invoked_action) { ApplicationAction } }
71
+
72
+ it 'returns the name of the organizer class' do
73
+ expect(subject.action_name).to eq('ApplicationAction')
74
+ end
75
+ end
76
+
77
+ context 'without invoked action' do
78
+ before { allow(subject).to receive(:invoked_action) { nil } }
79
+
80
+ it 'returns nil' do
81
+ expect(subject.action_name).to be(nil)
82
+ end
83
+ end
84
+ end
85
+
86
+ describe '#formatted_errors' do
87
+ before { allow(subject).to receive(:errors) { errors } }
88
+
89
+ context 'with errors' do
90
+ let(:errors) { { name: ['is required'], email: ['is invalid'] } }
91
+
92
+ it 'returns a JSON string of the errors' do
93
+ expect(subject.formatted_errors).to eq(JSON.pretty_generate(errors))
94
+ end
95
+ end
96
+
97
+ context 'without errors' do
98
+ let(:errors) { {} }
99
+
100
+ it 'returns an empty JSON string if there are no errors' do
101
+ expect(subject.formatted_errors).to eq(JSON.pretty_generate({}))
102
+ end
103
+ end
104
+
105
+ context 'without errors' do
106
+ let(:errors) { nil }
107
+
108
+ it 'returns an empty JSON string if there are no errors' do
109
+ expect(subject.formatted_errors).to eq(JSON.pretty_generate({}))
110
+ end
111
+ end
112
+ end
113
+
10
114
  describe '#add_to_successful_actions' do
11
115
  it 'adds successful action name to context' do
12
116
  ctx.add_to_successful_actions(value)
@@ -24,6 +24,9 @@ module LightServiceExt
24
24
 
25
25
  let(:input) { { some_proc: proc {} } }
26
26
 
27
+ before { allow_any_instance_of(ApplicationContext).to receive(:organized_by) { ApplicationOrganizer } }
28
+
29
+
27
30
  it 'adds inputted data as input key value pair' do
28
31
  ctx = subject_class.call(input)
29
32
 
@@ -32,9 +35,11 @@ module LightServiceExt
32
35
  end
33
36
 
34
37
  it 'calls underlying action' do
35
- expect do
36
- subject_class.call(some_proc: proc { raise 'error' })
37
- end.to raise_error RuntimeError, 'error'
38
+ ctx = subject_class.call(some_proc: proc { raise 'error' })
39
+
40
+ expect(ctx.errors).to be_present
41
+ expect(ctx).to be_failure
42
+ expect(ctx.status).to eql(Status::COMPLETE)
38
43
  end
39
44
  end
40
45
  end
@@ -60,6 +60,33 @@ module LightServiceExt
60
60
  end
61
61
  end
62
62
  end
63
+
64
+ describe '#on_raised_error' do
65
+ it 'returns a proc' do
66
+ expect(config.on_raised_error).to be_a(Proc)
67
+ end
68
+
69
+ context 'with configured callback' do
70
+ let(:error) { ArgumentError.new('some-error') }
71
+ let(:callback) { proc { |_ctx, _error| } }
72
+
73
+ before do
74
+ described_class.configure { |config| config.on_raised_error = callback }
75
+ end
76
+
77
+ after(:each) do
78
+ described_class.configure { |config| config.on_raised_error = proc {|_ctx, _error|} }
79
+ end
80
+
81
+
82
+ it 'returns set proc' do
83
+ ctx = ApplicationContext.make_with_defaults
84
+ ctx.record_raised_error(error)
85
+
86
+ expect(described_class.config.on_raised_error).to eql(callback)
87
+ end
88
+ end
89
+ end
63
90
  end
64
91
  end
65
92
  end
@@ -9,8 +9,57 @@ RSpec.describe LightServiceExt::ErrorInfo do
9
9
  let(:error) { StandardError.new(message) }
10
10
  let(:error_info_class) { Class.new(described_class) }
11
11
 
12
- before do
13
- error.set_backtrace(backtrace)
12
+ describe '#errors' do
13
+ context 'when error is a StandardError with no associated model or record' do
14
+ let(:error) { StandardError.new(message) }
15
+
16
+ it 'returns a hash with a base error message' do
17
+ expect(instance.errors).to eq({ base: message })
18
+ end
19
+ end
20
+
21
+ context 'with stubbed error' do
22
+ before(:each) do
23
+ allow(error).to receive(:set_backtrace)
24
+ allow(error).to receive(:backtrace) { backtrace }
25
+ end
26
+
27
+ context 'when error is associated with a model with validation errors' do
28
+ let(:model) { double('Model', errors: double(:messages, messages: { field1: ['error1', 'error2'], field2: ['error3'] })) }
29
+ let(:error) { double('Error', model: model, message: nil) }
30
+
31
+ it 'returns a hash with model validation errors' do
32
+ expect(instance.errors).to eq({ base: { field1: ['error1', 'error2'], field2: ['error3'] } })
33
+ end
34
+ end
35
+
36
+ context 'when error is associated with a model with no validation errors' do
37
+ let(:model) { double('Model', errors: double(:messages, messages: {})) }
38
+ let(:error) { double('Error', model: model, message: nil) }
39
+
40
+ it 'returns an empty hash' do
41
+ expect(instance.errors).to eq({base: {}})
42
+ end
43
+ end
44
+
45
+ context 'when error is associated with a record with validation errors' do
46
+ let(:record) { double('Record', errors: double(:messages, messages: { field1: ['error1', 'error2'], field2: ['error3'] })) }
47
+ let(:error) { double('Error', record: record, message: nil) }
48
+
49
+ it 'returns a hash with record validation errors' do
50
+ expect(instance.errors).to eq({ base: { field1: ['error1', 'error2'], field2: ['error3'] } })
51
+ end
52
+ end
53
+
54
+ context 'when error is associated with a record with no validation errors' do
55
+ let(:record) { double('Record', errors: double(:messages, messages: {})) }
56
+ let(:error) { double('Error', record: record, message: nil) }
57
+
58
+ it 'returns an empty hash' do
59
+ expect(instance.errors).to eq({base: {}})
60
+ end
61
+ end
62
+ end
14
63
  end
15
64
 
16
65
  describe '#type' do
@@ -80,6 +129,8 @@ RSpec.describe LightServiceExt::ErrorInfo do
80
129
  end
81
130
 
82
131
  describe '#error_summary' do
132
+ before { error.set_backtrace(backtrace) }
133
+
83
134
  it 'returns summary of error' do
84
135
  expect(instance.error_summary).to eql(<<~TEXT
85
136
  =========== SERVER ERROR FOUND: StandardError : some-error ===========
@@ -96,6 +147,8 @@ RSpec.describe LightServiceExt::ErrorInfo do
96
147
  describe '#to_h' do
97
148
  subject(:hash) { instance.to_h }
98
149
 
150
+ before { error.set_backtrace(backtrace) }
151
+
99
152
  it 'returns custom key value pairs' do
100
153
  expect(hash.keys).to match_array(%i[type message exception backtrace error errors fatal_error?])
101
154
 
@@ -6,52 +6,28 @@ module LightServiceExt
6
6
  end
7
7
  end
8
8
 
9
- let(:error_message) { 'some-error' }
10
9
  let(:ctx) { ApplicationContext.make_with_defaults }
11
10
 
12
- before { allow(LightServiceExt.config.logger).to receive(:error) }
11
+ before do
12
+ allow(ctx).to receive(:organized_by) { ApplicationOrganizer }
13
+ allow(ctx).to receive(:invoked_action) { ApplicationAction }
14
+ end
13
15
 
14
16
  describe '.with_error_handler' do
15
- subject(:errors_handled_ctx) do
16
- implementor_class.with_error_handler(ctx: ctx, &results_ctx_proc)
17
- end
18
-
19
- context 'with non validation error' do
20
- let(:error) { ArgumentError.new(error_message) }
21
- let(:results_ctx_proc) { -> { raise error } }
17
+ let(:error_message) { 'some-error' }
18
+ let(:error) { RuntimeError.new(error_message) }
19
+ let(:results_ctx_proc) { -> { raise error } }
22
20
 
23
- it 'logs errors' do
24
- expect { errors_handled_ctx }.to raise_error ArgumentError, error_message
25
- expect(LightServiceExt.config.logger).to have_received(:error)
26
- end
21
+ subject(:raised_error_handled_ctx) do
22
+ implementor_class.with_error_handler(ctx: ctx, &results_ctx_proc)
27
23
  end
28
24
 
29
- context 'with active record error' do
30
- let(:key) { :key }
31
- let(:key_error) { 'invalid' }
32
- let(:messages) { { key => [key_error] } }
33
- let(:error) { Rails::ActiveRecordError.new(error_message) }
34
- let(:errors) { double(:errors, messages: messages) }
35
- let(:model) { double(:model, errors: errors) }
36
- let(:results_ctx_proc) { -> { raise error } }
37
-
38
- context 'with error including model' do
39
- before { allow(error).to receive(:model) { model } }
40
-
41
- it 'adds model errors' do
42
- expect(errors_handled_ctx.failure?).to be_truthy
43
- expect(errors_handled_ctx.errors).to eql({ key => [key_error] })
44
- expect(errors_handled_ctx.internal_only[:error_info]).to be_an_instance_of(ErrorInfo)
45
- expect(LightServiceExt.config.logger).to have_received(:error)
46
- end
47
- end
25
+ before { allow(ctx).to receive(:record_raised_error) }
48
26
 
49
- it 'adds errors to context' do
50
- expect(errors_handled_ctx.failure?).to be_truthy
51
- expect(errors_handled_ctx.errors).to eql({ base: error_message })
52
- expect(errors_handled_ctx.internal_only[:error_info]).to be_an_instance_of(ErrorInfo)
53
- expect(LightServiceExt.config.logger).to have_received(:error)
54
- end
27
+ it 'fails context and records error' do
28
+ expect(raised_error_handled_ctx.failure?).to be_truthy
29
+ expect(raised_error_handled_ctx).to have_received(:record_raised_error).with(error)
30
+ expect(raised_error_handled_ctx.status).to eql(Status::COMPLETE)
55
31
  end
56
32
  end
57
33
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: light-service-ext
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Desmond O'Leary
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-08-28 00:00:00.000000000 Z
11
+ date: 2023-09-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: light-service
@@ -37,9 +37,6 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '1.6'
40
- - - "<"
41
- - !ruby/object:Gem::Version
42
- version: '2'
43
40
  type: :runtime
44
41
  prerelease: false
45
42
  version_requirements: !ruby/object:Gem::Requirement
@@ -47,9 +44,6 @@ dependencies:
47
44
  - - "~>"
48
45
  - !ruby/object:Gem::Version
49
46
  version: '1.6'
50
- - - "<"
51
- - !ruby/object:Gem::Version
52
- version: '2'
53
47
  - !ruby/object:Gem::Dependency
54
48
  name: dry-validation
55
49
  requirement: !ruby/object:Gem::Requirement
@@ -90,7 +84,7 @@ dependencies:
90
84
  requirements:
91
85
  - - "~>"
92
86
  - !ruby/object:Gem::Version
93
- version: '6'
87
+ version: '6.0'
94
88
  - - ">="
95
89
  - !ruby/object:Gem::Version
96
90
  version: 6.0.6
@@ -100,7 +94,7 @@ dependencies:
100
94
  requirements:
101
95
  - - "~>"
102
96
  - !ruby/object:Gem::Version
103
- version: '6'
97
+ version: '6.0'
104
98
  - - ">="
105
99
  - !ruby/object:Gem::Version
106
100
  version: 6.0.6
@@ -167,10 +161,12 @@ executables: []
167
161
  extensions: []
168
162
  extra_rdoc_files: []
169
163
  files:
164
+ - ".env.example"
170
165
  - ".github/workflows/main.yml"
171
166
  - ".gitignore"
172
167
  - ".rspec"
173
168
  - ".rubocop.yml"
169
+ - ".ruby-version"
174
170
  - CHANGELOG.md
175
171
  - CODE_OF_CONDUCT.md
176
172
  - Gemfile
@@ -240,7 +236,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
240
236
  - !ruby/object:Gem::Version
241
237
  version: '0'
242
238
  requirements: []
243
- rubygems_version: 3.2.33
239
+ rubygems_version: 3.4.10
244
240
  signing_key:
245
241
  specification_version: 4
246
242
  summary: Extends light-service with opinionated functionality