light-service-ext 0.1.1 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +9 -0
  3. data/CHANGELOG.md +18 -1
  4. data/README.md +229 -5
  5. data/dev/setup.rb +5 -3
  6. data/lib/light-service-ext/all_actions_complete_action.rb +1 -1
  7. data/lib/light-service-ext/application_action.rb +0 -21
  8. data/lib/light-service-ext/application_context.rb +78 -9
  9. data/lib/light-service-ext/application_orchestrator.rb +32 -0
  10. data/lib/light-service-ext/application_organizer.rb +21 -5
  11. data/lib/light-service-ext/application_validator_action.rb +2 -2
  12. data/lib/light-service-ext/configuration.rb +28 -0
  13. data/lib/light-service-ext/constants.rb +3 -2
  14. data/lib/light-service-ext/error_info.rb +11 -11
  15. data/lib/light-service-ext/record_actions.rb +41 -0
  16. data/lib/light-service-ext/version.rb +1 -1
  17. data/lib/light-service-ext/with_error_handler.rb +26 -0
  18. data/lib/light-service-ext.rb +37 -13
  19. data/light-service-ext.gemspec +1 -0
  20. data/spec/{light_service_ext → light-service-ext}/all_actions_complete_action_spec.rb +1 -1
  21. data/spec/light-service-ext/application_action_spec.rb +39 -0
  22. data/spec/light-service-ext/application_context_spec.rb +291 -0
  23. data/spec/light-service-ext/application_orchestrator_spec.rb +76 -0
  24. data/spec/{light_service_ext → light-service-ext}/application_organizer_spec.rb +1 -2
  25. data/spec/{light_service_ext → light-service-ext}/application_validator_action_spec.rb +1 -1
  26. data/spec/light-service-ext/configuration_spec.rb +66 -0
  27. data/spec/{light_service_ext → light-service-ext}/error_info_spec.rb +18 -7
  28. data/spec/light-service-ext/record_actions_spec.rb +68 -0
  29. data/spec/light-service-ext/with_error_handler_spec.rb +58 -0
  30. data/spec/light_service_ext_spec.rb +67 -1
  31. data/spec/spec_helper.rb +19 -2
  32. metadata +48 -22
  33. data/spec/light_service_ext/application_action_spec.rb +0 -77
  34. data/spec/light_service_ext/application_context_spec.rb +0 -68
  35. /data/spec/{light_service_ext → light-service-ext}/application_contract_spec.rb +0 -0
  36. /data/spec/{light_service_ext → light-service-ext}/around_action_execute_extension_spec.rb +0 -0
  37. /data/spec/{light_service_ext → light-service-ext}/context_error_spec.rb +0 -0
  38. /data/spec/{light_service_ext → light-service-ext}/regex_spec.rb +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '08cca14ee81b2019a34e6ce11e69a02769a5f91249c075b11e236570734c895c'
4
- data.tar.gz: 3c5fd91b35a3390394fe647cac366c5b8d6ea783894ab62fa877488c3935555c
3
+ metadata.gz: 962aaba417524bf102f137c7f62dd622b440701dc106c6afa8372bcb4c2c36ec
4
+ data.tar.gz: 71430081be864aee8f985f67adc936ce46c078a8e19f9e476e4337f5d1d89ca0
5
5
  SHA512:
6
- metadata.gz: 7c226a4d2d1338c4f83be7ead8bf5c54bb297abb1c20f5d99efd89f5463820a53843ccc3257944a97e3f981e1929847e6fabf14dc489ea26203bfa20335c1570
7
- data.tar.gz: a26b62d93f3ea08babb2a3f1ec371df6df15f7065ac4617ef92462f884a21e5dff67b566b4ad39fd922c852568892ff54864494d20556ac0322aab5a3efb54a9
6
+ metadata.gz: d05157f2a3b4f92591046ef6a51b5fdd55dc9f8d7eeb3fd314529b71a6981bd71bb1d382c58bdd3c3d8bf71cf46095637682b94c35aad37dc9770fba9caef935
7
+ data.tar.gz: 52144674f23c819c5680d4ab53d54260b1919abaf2bd46ed3fe05351bf169056981c8c89ce762f1a4ad837605ba96b9ab12dc2ff9d63f51e074525c2ae728cb2
data/.rubocop.yml CHANGED
@@ -236,3 +236,12 @@ RSpec/MultipleMemoizedHelpers:
236
236
 
237
237
  RSpec/PredicateMatcher:
238
238
  Enabled: false
239
+
240
+ RSpec/FilePath:
241
+ Enabled: false
242
+
243
+ RSpec/SubjectStub:
244
+ Enabled: false
245
+
246
+ RSpec/VerifiedDoubles:
247
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [0.1.0] - 2023-02-07
3
+ ## [0.1.0] - 2023-02-15
4
4
 
5
5
  - Initial release
6
+
7
+ ## [0.1.1] - 2023-02-15
8
+
9
+ - Fixing issue with with use of `relative_path` inside of `light-service-ext.gemspec`
10
+
11
+ ## [0.1.2] - 2023-02-24
12
+
13
+ - Updates `README.md` with detailed information on features provided
14
+
15
+ ## [0.1.3] - 2023-02-25
16
+
17
+ - Adds `Orchestration` of organizers via `ApplicationOrchestrator` functionality to support calling an organizer(s) inside an orchestrator without polluting the orchestrator context from changes to the called organizer
18
+ - Updates `README.md` with detailed information on features provided
19
+ - Records Actions
20
+ - adds better error handling
21
+ - records api_responses to organizer's context from each action filled with `current_api_response`
22
+ - fails the organizer's context if action returned `errors`
data/README.md CHANGED
@@ -1,8 +1,11 @@
1
- # LightServiceExt
1
+ ![LightService](https://raw.githubusercontent.com/adomokos/light-service/master/resources/light-service.png)
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/light-service-ext`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ # Light Service Extensions
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
5
+ Aims to enhance [light-service](https://github.com/adomokos/light-service) to enhance this powerful and flexible service skeleton framework with an emphasis on simplicity
6
+
7
+ ## Console
8
+ run `bin/console` for an interactive prompt.
6
9
 
7
10
  ## Installation
8
11
 
@@ -20,9 +23,230 @@ Or install it yourself as:
20
23
 
21
24
  $ gem install light-service-ext
22
25
 
23
- ## Usage
26
+ ## ApplicationOrganizer
27
+ > Adds the following support
28
+
29
+ #### Error Handling
30
+ > Provided by `.with_error_handler`
31
+
32
+ - Records errors via `issue_error_report!` into context as exemplified below:
33
+ ```ruby
34
+ {
35
+ errors: {
36
+ base: "some-exception-message",
37
+ internal_only: {
38
+ type: 'ArgumentError',
39
+ message: "`user_id` must be a number",
40
+ exception: "ArgumentError : `user_id` must be a number",
41
+ backtrace: [], # filtered backtrace via `[ActiveSupport::BacktraceCleaner](https://api.rubyonrails.org/classes/ActiveSupport/BacktraceCleaner.html)`
42
+ error: original_captured_exception
43
+ }
44
+ }
45
+ }
46
+ ```
47
+
48
+ - Captures `model validation` exceptions and record the messages to the organizer's `:errors` context field
49
+ - Supports the following exceptions by default
50
+ - `ActiveRecord::Errors`
51
+ - `ActiveModel::Errors`
52
+ - Raises any non validation errors up the stack
53
+
54
+ #### API Responses
55
+ - records api responses set by an action's `:api_response` context field
56
+ - Stored inside of the organizer's `:api_responses` field
57
+
58
+ #### Retrieve Record
59
+ > Allows for a block to be defined on an organizer in order to retrieve the model record
60
+
61
+ #### Failing The Context
62
+ - Prevents further action's been executed in the following scenarios:
63
+ - All actions complete determined by organizer's `:outcome` context field set to `LightServiceExt::Outcome::COMPLETE`
64
+
65
+ #### Example
66
+
67
+ ```ruby
68
+ class TaxCalculator < LightServiceExt::ApplicationOrganizer
69
+ self.retrieve_record = -> (ctx:) { User.find_by(email: ctx.params[:email]) }
70
+
71
+ def self.call(input)
72
+ user = record(ctx: input) # `.record` method executes proc provided to `retrieve_record`
73
+ input = { user: user }.merge(input)
74
+
75
+ super(input)
76
+ end
77
+
78
+ def self.steps
79
+ [TaxValidator, CalcuateTaxAction]
80
+ end
81
+ end
82
+ ```
83
+
84
+ ## ApplicationOrchestrator
85
+ > Useful if you want the current `Organizer` to act as a `Orchestrator` and call another organizer
86
+
87
+ - *ONLY* modifies the orchestrator context from executing `organizer_steps` if manually applied via `each_organizer_result` Proc
88
+
89
+ ### method overrides
90
+ - `organizer_steps` ~ must be a list of organizers to be called prior to orchestrator's actions
91
+
92
+ ### Example
93
+
94
+ ```ruby
95
+ class TaxCalculatorReport < LightServiceExt::ApplicationOrchestrator
96
+ self.retrieve_record = -> (ctx:) { User.find_by(email: ctx.params[:email]) }
97
+
98
+ def self.call(input)
99
+ user = record(ctx: input) # `.record` method executes proc provided to `retrieve_record`
100
+ input = { user: user }.merge(user: user)
101
+ reduce_with({ input: input }, steps)
102
+
103
+ super(input.merge({ user: user })) do |current_organizer_ctx, orchestrator_ctx:|
104
+ orchestrator_ctx.add_params(current_organizer_ctx.params.slice(:user_id)) # manually add params from executed organizer(s)
105
+ end
106
+ end
107
+
108
+ def organizer_steps
109
+ [TaxCalculator]
110
+ end
111
+
112
+ def steps
113
+ [TaxReportAction]
114
+ end
115
+ end
116
+ ```
117
+
118
+ ## ApplicationAction
119
+
120
+ #### Useful methods
121
+ - TODO
122
+
123
+ #### Invoked Action
124
+ - *NOTE* Action's `executed` block gets called by the underlying `LightService::Action`
125
+ - this means in order to call your action's methods you need to invoke it from `invoked_action:` instead of `self`
126
+ - `invoked_action:` added to current action's context before it gets executed
127
+ - Consist of an instance of the current action that implements `LightServiceExt::ApplicationAction`
128
+
129
+ ## ApplicationContract
130
+
131
+ - Enhances `Dry::Validation::Contract` with the following methods:
132
+ - `#keys` ~> returns names of params defined
133
+ - `#t` ~> returns translation messages in context with the current organizer
134
+ - Arguments:
135
+ - `key` e.g. :not_found
136
+ - `base_path:` e.g. :user
137
+ - `**opts` options passed into underlying Rails i18n translate call
138
+ - E.g. `t(:not_found, base_path: 'business_create', scope: 'user')` would execute
139
+ - => `I18n.t('business_create.user.not_found', opts.except(:scope))`
140
+
141
+ ## ApplicationValidatorAction
142
+
143
+ > Responsible for mapping, filtering and validating the context `input:` field
144
+
145
+ - `executed` block does the following:
146
+ - Appends `params:` field to the current context with the mapped and filtered values
147
+ - Appends errors returned from a `ApplicationContract` [dry-validation](https://github.com/dry-rb/dry-validation) contract to the current context's `errors:` field
148
+ - *NOTE* fails current context if `errors:` present
149
+
150
+ ##### Useful Accessors
151
+
152
+ - `.contract_class` ~> sets the [dry-validation](https://github.com/dry-rb/dry-validation) contract to be applied by the current validator action
153
+ - `.params_mapper_class` ~> sets the mapper class that must implement `.map_from(context)` and return mapped `:input` values
154
+
155
+
156
+ ## ApplicationContext
157
+
158
+ > Adds useful defaults to the organizer/orchestrator context
159
+ - `:input` ~> values originally provided to organizer get moved here for better isolation
160
+ - `:params`
161
+ - stores values `filtered` and `mapped` from original `input`
162
+ - outcomes/return values provided by any action that implements `LightServiceExt::ApplicationAction`
163
+ - `:errors`
164
+ - validation errors processed by `LightServiceExt::ApplicationValidatorAction` [dry-validation](https://github.com/dry-rb/dry-validation) contract
165
+ - manually added by an action e.g. `{ errors: { email: 'not found' } }`
166
+ - `:successful_actions` ~> provides a list of actions processed mostly useful for debugging purposes e.g. `['SomeActionClassName']`
167
+ - `invoked_action` ~> instance of action to being called.
168
+ - `:current_api_response` ~> action issued api response
169
+ - `:api_responses` ~> contains a list of external API interactions mostly for recording/debugging purposes (**internal only**)
170
+ - `:allow_raise_on_failure` ~> determines whether or not to throw a `RaiseOnContextError` error up the stack in the case of validation errors and/or captured exceptions
171
+ - `:status` denotes the current status of the organizer with one of the following flags:
172
+ - `LightServiceExt::Status::COMPLETE`
173
+ - `LightServiceExt::Status::INCOMPLETE`
174
+ - `:last_failed_context` ~ copy of context that failed e.g. with `errors` field present
175
+ - `internal_only` ~ includes the likes of raised error summary and should never be passed to endpoint responses
176
+ Example
177
+
178
+ ````ruby
179
+ input = { order: order }
180
+ overrides = {} # optionally override `params`, `errors` and `allow_raise_on_failure`
181
+ LightServiceExt::ApplicationContext.make_with_defaults(input, overrides)
182
+
183
+ # => { input: { order: order },
184
+ # errors: { email: ['not found'] },
185
+ # params: { user_id: 1 },
186
+ # status: Status::INCOMPLETE,
187
+ # invoked_action: SomeActionInstance,
188
+ # successful_actions: ['SomeActionClassName'],
189
+ # current_api_response: { user_id: 1, status: 'ACTIVE' },
190
+ # api_responses: [ { user_id: 1, status: 'ACTIVE' } ],
191
+ # last_failed_context: {input: { order: order }, params: {}, ...},
192
+ # allow_raise_on_failure: true,
193
+ # internal_only: { error_info: ErrorInfoInstance }
194
+ # }
195
+ ````
196
+
197
+ #### Useful methods
198
+ - `.add_params(**params)`
199
+ - Adds given args to context's `params` field
200
+ - e.g. `add_params(user_id: 1) # => { params: { user_id: 1 } }`
201
+
202
+ - `add_errors!`
203
+ - Adds given args to to context's `errors` field
204
+ - Fails and returns from current action/organizer's context
205
+ - e.g. `add_to_errors!(email: 'not found') # => { errors: { email: 'not found' } }`
206
+
207
+ - `.add_errors(**errors)`
208
+ - Adds given args to to context's `errors` field
209
+ - DOES NOT fails current context
210
+ - e.g. `add_to_errors(email: 'not found') # => { errors: { email: 'not found' } }`
211
+
212
+ - `.add_status(status)`
213
+ - Should be one of Statuses e.g. `Status::COMPLETE`
214
+ - e.g. `add_status(Status::COMPLETE) # => { status: Status::COMPLETE }`
215
+
216
+ - `.add_internal_only(attrs)`
217
+ - e.g. `add_internal_only(request_id: 54) # => { internal_only: { error_info: nil, request_id: 54 } }`
218
+ - `add_to_successful_actions(action_name_or_names)` ~> adds action names successfully executed
219
+
220
+ ## ContextError
221
+
222
+ > Provides all the information related to an exception/validation errors captured by the current organizer
223
+
224
+ #### Useful methods
225
+ - `#error_info` ~> `ErrorInfo` instance
226
+ - `#context` ~> state of context provided
227
+ - `#error` ~> original exception
228
+ - `#message` ~> summarizes which action failed etc.
229
+
230
+ ## ErrorInfo
231
+ - Summarize captured exception
232
+
233
+ #### Useful accessors
234
+ - `non_fatal_errors` ~> takes a list of error class names considered to be non fatal exceptions
235
+
236
+ #### Useful methods
237
+ - `#error` ~> captured exception
238
+ - `#type` ~> exception class name e.g. `ArgumentError`
239
+ - `#message` ~> error message
240
+ - `title` ~> combined error class name and error message e.g. `ArgumentError : email must be present`
241
+ - `#fatal_error?`
242
+ - `#error_summary` ~> summarizes exception with message and cleaned backtrace via `ActiveSupport::BacktraceCleaner`
243
+
244
+ ## Regex
24
245
 
25
- TODO: Write usage instructions here
246
+ #### Useful methods
247
+ - `.match?(type, value)` e.g. `LightServiceExt::Regex.match?(email:, 'email@domain.com')`
248
+ - supported `type`:
249
+ - :email
26
250
 
27
251
  ## Development
28
252
 
data/dev/setup.rb CHANGED
@@ -5,8 +5,10 @@
5
5
  require 'json'
6
6
  require 'light-service'
7
7
  require 'dry-validation'
8
+ require 'rspec/core'
8
9
 
9
- Dir.glob("lib/**/*.rb").each do |f|
10
- require File.join(__dir__, '..', f)
11
- end
10
+ require 'active_support/core_ext/array'
11
+ require 'active_support/configurable'
12
12
 
13
+ require File.expand_path("../../lib/light-service-ext", Pathname.new(__FILE__).realpath)
14
+ require File.expand_path("../../spec/spec_helper", Pathname.new(__FILE__).realpath)
@@ -9,7 +9,7 @@ module LightServiceExt
9
9
 
10
10
  raise ContextError.new(ctx: context) if raise_error
11
11
 
12
- context[:outcome] = Outcome::COMPLETE
12
+ context.add_status(Status::COMPLETE)
13
13
  end
14
14
  end
15
15
  end
@@ -4,27 +4,6 @@ module LightServiceExt
4
4
  class ApplicationAction
5
5
  extend LightService::Action
6
6
 
7
- class << self
8
- def add_params(ctx, **params)
9
- add_to_context(ctx, :params, **params)
10
- end
11
-
12
- def add_errors(ctx, **errors)
13
- add_to_context(ctx, :errors, **errors)
14
-
15
- ctx.fail_and_return! if ctx[:errors].present?
16
- end
17
-
18
- private
19
-
20
- def add_to_context(ctx, key, **args)
21
- return if ctx.nil?
22
-
23
- ctx[key].merge!(args.dup)
24
- nil
25
- end
26
- end
27
-
28
7
  def self.inherited(base)
29
8
  base.singleton_class.prepend AroundActionExecuteExtension
30
9
  super
@@ -2,27 +2,75 @@
2
2
 
3
3
  module LightServiceExt
4
4
  class ApplicationContext < LightService::Context
5
- OVERRIDABLE_DEFAULT_KEYS = %i[errors params allow_raise_on_failure].freeze
6
-
7
5
  class << self
8
6
  def make_with_defaults(input = {}, overrides = {})
9
- allowed_overrides = overrides.slice(*OVERRIDABLE_DEFAULT_KEYS)
10
- make({ input: input.symbolize_keys }.merge(default_attrs, allowed_overrides))
7
+ defaults = default_attrs
8
+ allowed_override_keys = defaults.keys.excluding(:input)
9
+ allowed_overrides = overrides.slice(*allowed_override_keys)
10
+ make({ input: input.symbolize_keys }.merge(defaults, allowed_overrides))
11
11
  end
12
12
 
13
13
  private
14
14
 
15
15
  def default_attrs
16
- { errors: {}, params: {}, successful_actions: [], api_responses: [], allow_raise_on_failure: true }.freeze
16
+ {
17
+ errors: {},
18
+ params: {},
19
+ status: Status::INCOMPLETE,
20
+ invoked_action: nil,
21
+ successful_actions: [],
22
+ current_api_response: nil,
23
+ api_responses: [],
24
+ last_failed_context: nil,
25
+ allow_raise_on_failure: LightServiceExt.config.allow_raise_on_failure?,
26
+ internal_only: { error_info: nil }
27
+ }.freeze
17
28
  end
18
29
  end
19
30
 
20
- def invoked_action
21
- self[:invoked_action]
31
+ def add_params(**params)
32
+ add_attrs_to_ctx(:params, params)
33
+ end
34
+
35
+ def add_errors(**errors)
36
+ add_attrs_to_ctx(:errors, errors)
37
+ end
38
+
39
+ def add_errors!(**errors)
40
+ return if errors.blank?
41
+
42
+ add_errors(**errors)
43
+ fail_and_return!
44
+ end
45
+
46
+ def add_status(status)
47
+ add_value_to_ctx(:status, status)
48
+ end
49
+
50
+ def add_current_api_response(api_response)
51
+ add_value_to_ctx(:current_api_response, api_response)
52
+ end
53
+
54
+ def add_last_failed_context(failed_context)
55
+ return if failed_context.nil? || failed_context.try(:success?)
56
+
57
+ add_value_to_ctx(:last_failed_context, failed_context)
58
+ end
59
+
60
+ def add_internal_only(**attrs)
61
+ add_attrs_to_ctx(:internal_only, **attrs)
62
+ end
63
+
64
+ def add_to_api_responses(*api_response)
65
+ add_collection_to_ctx(:api_responses, *api_response)
22
66
  end
23
67
 
24
- def validation_errors
25
- self[:errors]
68
+ def add_to_successful_actions(*action_name)
69
+ add_collection_to_ctx(:successful_actions, *action_name)
70
+ end
71
+
72
+ def add_invoked_action(invoked_action)
73
+ add_value_to_ctx(:invoked_action, invoked_action)
26
74
  end
27
75
 
28
76
  def allow_raise_on_failure?
@@ -38,5 +86,26 @@ module LightServiceExt
38
86
  def respond_to_missing?(method_name, include_private = false)
39
87
  key?(method_name) || super
40
88
  end
89
+
90
+ private
91
+
92
+ def add_value_to_ctx(key, value)
93
+ self[key] = value
94
+ nil
95
+ end
96
+
97
+ def add_attrs_to_ctx(key, **attrs)
98
+ return if attrs.blank?
99
+
100
+ self[key].merge!(attrs)
101
+ nil
102
+ end
103
+
104
+ def add_collection_to_ctx(key, *values)
105
+ return if values.empty?
106
+
107
+ self[key] = self[key].concat(values).compact
108
+ nil
109
+ end
41
110
  end
42
111
  end
@@ -0,0 +1,32 @@
1
+ module LightServiceExt
2
+ class ApplicationOrchestrator < ApplicationOrganizer
3
+ class << self
4
+ def call(context, &each_organizer_result)
5
+ orchestrator_ctx = process_each_organizer(ApplicationContext.make_with_defaults(context),
6
+ &each_organizer_result)
7
+ with(orchestrator_ctx).around_each(RecordActions).reduce(all_steps)
8
+ end
9
+
10
+ def organizer_steps
11
+ raise NotImplementedError
12
+ end
13
+
14
+ def process_each_organizer(orchestrator_ctx, &each_organizer_result)
15
+ organizer_steps.each do |step_class|
16
+ exec_method = exec_method_for(step_class)
17
+ current_organizer_ctx = step_class.send(exec_method, orchestrator_ctx.input)
18
+ yield(current_organizer_ctx, orchestrator_ctx: orchestrator_ctx) if each_organizer_result
19
+ end
20
+ orchestrator_ctx
21
+ end
22
+
23
+ private
24
+
25
+ def exec_method_for(klass)
26
+ return :call if klass.is_a?(LightService::Organizer) || klass.is_a?(Proc)
27
+
28
+ :execute
29
+ end
30
+ end
31
+ end
32
+ end
@@ -4,12 +4,28 @@ module LightServiceExt
4
4
  class ApplicationOrganizer
5
5
  extend LightService::Organizer
6
6
 
7
- def self.call(context)
8
- with(ApplicationContext.make_with_defaults(context)).reduce(steps)
9
- end
7
+ class << self
8
+ def call(context)
9
+ with(ApplicationContext.make_with_defaults(context))
10
+ .around_each(RecordActions)
11
+ .reduce(all_steps)
12
+ end
13
+
14
+ def steps
15
+ raise NotImplementedError
16
+ end
17
+
18
+ def reduce_if_success(steps)
19
+ reduce_if(->(ctx) { ctx.success? && ctx[:errors].blank? }, steps)
20
+ end
21
+
22
+ private
23
+
24
+ def all_steps
25
+ return steps.push(AllActionsCompleteAction) if steps.is_a?(Array)
10
26
 
11
- def self.steps
12
- raise NotImplementedError
27
+ [steps].push(AllActionsCompleteAction)
28
+ end
13
29
  end
14
30
  end
15
31
  end
@@ -7,8 +7,8 @@ module LightServiceExt
7
7
 
8
8
  executed do |context|
9
9
  validator = map_and_validate_inputs(context)
10
- add_params(context, **validator.to_h)
11
- add_errors(context, **validator.errors.to_h.transform_values(&:first))
10
+ context.add_params(**validator.to_h)
11
+ context.add_errors!(**validator.errors.to_h)
12
12
  end
13
13
 
14
14
  class << self
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LightServiceExt
4
+ class Configuration
5
+ include ActiveSupport::Configurable
6
+
7
+ config_accessor(:allow_raise_on_failure, default: true)
8
+ config_accessor(:non_fatal_error_classes, default: [])
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 }
11
+
12
+ def allow_raise_on_failure?
13
+ !!allow_raise_on_failure
14
+ end
15
+
16
+ def non_fatal_errors
17
+ (default_non_fatal_error_classes + non_fatal_error_classes).compact.uniq.map(&:to_s).freeze
18
+ end
19
+
20
+ def fatal_error?(exception)
21
+ !non_fatal_errors.exclude?(exception.class.name)
22
+ end
23
+
24
+ def non_fatal_error?(exception)
25
+ non_fatal_errors.include?(exception.class.name)
26
+ end
27
+ end
28
+ end
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LightServiceExt
4
- module Outcome
5
- COMPLETE = "all_steps_complete"
4
+ module Status
5
+ COMPLETE = :all_steps_complete
6
+ INCOMPLETE = :steps_incomplete
6
7
  end
7
8
  end
@@ -29,6 +29,13 @@ module LightServiceExt
29
29
  TEXT
30
30
  end
31
31
 
32
+ def errors
33
+ model = error && (error.try(:model) || error.try(:record))
34
+ return model.errors.messages if model.present?
35
+
36
+ { base: message }
37
+ end
38
+
32
39
  def to_h
33
40
  {
34
41
  type: type,
@@ -36,7 +43,8 @@ module LightServiceExt
36
43
  exception: title,
37
44
  backtrace: clean_backtrace[0, 3]&.join("\n"),
38
45
  error: error,
39
- fatal_error?: fatal_error?
46
+ fatal_error?: fatal_error?,
47
+ errors: errors
40
48
  }
41
49
  end
42
50
 
@@ -45,7 +53,7 @@ module LightServiceExt
45
53
  end
46
54
 
47
55
  def clean_backtrace
48
- @clean_backtrace ||= if defined? Rails
56
+ @clean_backtrace ||= if defined? Rails.backtrace_cleaner
49
57
  Rails.backtrace_cleaner.clean(backtrace || [])
50
58
  else
51
59
  backtrace || []
@@ -53,15 +61,7 @@ module LightServiceExt
53
61
  end
54
62
 
55
63
  def non_fatal_error?
56
- error.nil? || self.class.non_fatal_errors.map(&:to_s).include?(type)
57
- end
58
-
59
- class << self
60
- attr_writer :non_fatal_errors
61
-
62
- def non_fatal_errors
63
- @non_fatal_errors ||= []
64
- end
64
+ error.nil? || LightServiceExt.config.non_fatal_error?(error)
65
65
  end
66
66
  end
67
67
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LightServiceExt
4
+ class RecordActions
5
+ extend WithErrorHandler
6
+
7
+ def self.call(context)
8
+ with_error_handler(ctx: context) do
9
+ result = yield
10
+ return context if outcomes_complete?(ctx: context, result: result)
11
+
12
+ merge_api_responses!(ctx: context, result: result)
13
+ end
14
+ end
15
+
16
+ class << self
17
+ def merge_api_responses!(ctx:, result:)
18
+ invoked_action = result.invoked_action
19
+ return if invoked_action.nil?
20
+
21
+ ctx.add_to_successful_actions(invoked_action.name)
22
+ ctx.add_to_api_responses(result.current_api_response)
23
+ ctx
24
+ end
25
+
26
+ def outcomes_complete?(ctx:, result:)
27
+ if result.status == Status::COMPLETE
28
+ ctx.add_status(result.status)
29
+
30
+ if ctx.errors.present?
31
+ ctx.add_last_failed_context(result.to_h)
32
+ ctx.fail!
33
+ end
34
+ true
35
+ else
36
+ false
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LightServiceExt
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.3"
5
5
  end