light-service-ext 0.1.8 → 0.1.9

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
  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