light-service-ext 0.1.2 → 0.1.3

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: a72952c78aab0b82bbce081bd1c2ab955ef264a9fbb5186a9ead3a429eaa504e
4
- data.tar.gz: 43cb1863bac420a7150573a0e24cc141bd2384e36cfc4440dbd12f2fce276e93
3
+ metadata.gz: 962aaba417524bf102f137c7f62dd622b440701dc106c6afa8372bcb4c2c36ec
4
+ data.tar.gz: 71430081be864aee8f985f67adc936ce46c078a8e19f9e476e4337f5d1d89ca0
5
5
  SHA512:
6
- metadata.gz: cabc217b43f33aa0771de27613eeecd83f425786e93946ed14d716523814a4af7045d464ae16841b6d3e2db20a8b0f4d780190363ad130bf934c100b962ba1f1
7
- data.tar.gz: 5ab3b2151ebb98e80b65a24717a628d7e9f60f206b25c258a05d32e3d57c85e832d99b92905f4883e2d770e9240da85e611771b2497d17f7c87687e670b82838
6
+ metadata.gz: d05157f2a3b4f92591046ef6a51b5fdd55dc9f8d7eeb3fd314529b71a6981bd71bb1d382c58bdd3c3d8bf71cf46095637682b94c35aad37dc9770fba9caef935
7
+ data.tar.gz: 52144674f23c819c5680d4ab53d54260b1919abaf2bd46ed3fe05351bf169056981c8c89ce762f1a4ad837605ba96b9ab12dc2ff9d63f51e074525c2ae728cb2
data/.rubocop.yml CHANGED
@@ -242,3 +242,6 @@ RSpec/FilePath:
242
242
 
243
243
  RSpec/SubjectStub:
244
244
  Enabled: false
245
+
246
+ RSpec/VerifiedDoubles:
247
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -8,6 +8,15 @@
8
8
 
9
9
  - Fixing issue with with use of `relative_path` inside of `light-service-ext.gemspec`
10
10
 
11
- ## [0.1.2] - YYYY-MM-DD
11
+ ## [0.1.2] - 2023-02-24
12
12
 
13
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
@@ -23,77 +23,9 @@ Or install it yourself as:
23
23
 
24
24
  $ gem install light-service-ext
25
25
 
26
- ## ApplicationContext
27
-
28
- > Adds useful defaults to the organizer/orchestrator context
29
- - `:input` ~> values originally provided to organizer get moved here for better isolation
30
- - `:params`
31
- - stores values `filtered` and `mapped` from original `input`
32
- - outcomes/return values provided by any action that implements `LightServiceExt::ApplicationAction`
33
- - `:errors`
34
- - validation errors processed by `LightServiceExt::ApplicationValidatorAction` [dry-validation](https://github.com/dry-rb/dry-validation) contract
35
- - manually added by an action e.g. `{ errors: { email: 'not found' } }`
36
- - `:successful_actions` ~> provides a list of actions processed mostly useful for debugging purposes
37
- - `:api_responses` ~> contains a list of external API interactions mostly for recording/debugging purposes
38
- - `: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
39
- - `:outcome` denotes the current status of the organizer with one of the following flags:
40
- - `LightServiceExt::Outcome::COMPLETE`
41
-
42
- Example
43
-
44
- ````ruby
45
- input = { order: order }
46
- overrides = {} # optionally override `params`, `errors` and `allow_raise_on_failure`
47
- LightServiceExt::ApplicationContext.make_with_defaults(input, overrides)
48
-
49
- # => { input: { order: order },
50
- # params: {},
51
- # errors: {},
52
- # successful_actions: [],
53
- # api_responses: [],
54
- # allow_raise_on_failure: true
55
- # }
56
- ````
57
-
58
- #### Useful methods
59
-
60
- - `.add_params(**params)`
61
- - Adds given args to context's `params` field
62
- - e.g. `add_params(user_id: 1) # => { params: { user_id: 1 } }`
63
- - `.add_errors(**errors)`
64
- - Adds given args to to context's `errors` field
65
- - Fails and returns from current action/organizer's context
66
- - e.g. `add_to_errors(email: 'not found') # => { errors: { email: 'not found' } }`
67
-
68
-
69
26
  ## ApplicationOrganizer
70
-
71
27
  > Adds the following support
72
28
 
73
- ### Useful methods
74
-
75
- - `.reduce_if_success(<list of actions>)` prevents execution of action/step in the case of context failure or `:errors` present
76
- - `.with_context(&block)` calls given block with `:ctx` argument
77
- - `.execute_if` ~> Useful if you want the current `Organizer` to act as a `Orchestrator` and call another organizer
78
- - *ONLY* modifies the current organizer/orchestrator's as a result of executing `organizer_or_action_class_or_proc` if manually applied by a given `result_callback` Proc
79
- - Executed `steps` do modify the current organizer/orchestrator's context without the need for manual intervention
80
- - Arguments:
81
- - `condition_block` (required) ~> given block is called with current `context` argument
82
- - `organizer_or_action_class_or_proc` (required) ~> only executed if `condition_block` evaluates to `true`
83
- - must be one of `ApplicationOrganizer`, `ApplicationAction`, `Proc`
84
- - `apply_ctx_transform` (optional)
85
- - given block is called prior to `organizer_or_action_class_or_proc` being executed
86
- - e.g. `apply_ctx_transform: -> (context) { context[:params][:user_id] = record(context)&.id }`
87
- - returned value gets passed to `organizer_or_action_class_or_proc` call
88
- - `result_callback` (optional)
89
- - given block is called after `organizer_or_action_class_or_proc` has been executed
90
- - Useful in the case where you want to augment the current organizer's context based on the context returned from the `organizer_or_action_class_or_proc` call
91
- - e.g. `result_callback: -> (ctx:, result:) { ctx[:params] = result[:params] }`
92
- - `ctx:` represents the main `organizer/orchestrator's` context
93
- - `result:` represents the context returned from the executed `organizer_or_action_class_or_proc`
94
- - `steps` (optional) ~> calls current `organizer/orchestrator's` actions/steps and called once `organizer_or_action_class_or_proc` has been processed
95
- - *PLEASE NOTE* called regardless of the result from the `organizer_or_action_class_or_proc` call unless you *manually* fail the current context or add `:errors`
96
-
97
29
  #### Error Handling
98
30
  > Provided by `.with_error_handler`
99
31
 
@@ -126,25 +58,64 @@ LightServiceExt::ApplicationContext.make_with_defaults(input, overrides)
126
58
  #### Retrieve Record
127
59
  > Allows for a block to be defined on an organizer in order to retrieve the model record
128
60
 
129
- Example
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
130
66
 
131
67
  ```ruby
132
68
  class TaxCalculator < LightServiceExt::ApplicationOrganizer
133
69
  self.retrieve_record = -> (ctx:) { User.find_by(email: ctx.params[:email]) }
134
70
 
135
- def self.call(input:)
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)
136
99
  user = record(ctx: input) # `.record` method executes proc provided to `retrieve_record`
137
100
  input = { user: user }.merge(user: user)
138
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]
139
114
  end
140
115
  end
141
116
  ```
142
117
 
143
- #### Failing The Context
144
- - Prevents further action's been executed in the following scenarios:
145
- - All actions complete determined by organizer's `:outcome` context field set to `LightServiceExt::Outcome::COMPLETE`
146
-
147
- ### ApplicationAction
118
+ ## ApplicationAction
148
119
 
149
120
  #### Useful methods
150
121
  - TODO
@@ -181,6 +152,71 @@ end
181
152
  - `.contract_class` ~> sets the [dry-validation](https://github.com/dry-rb/dry-validation) contract to be applied by the current validator action
182
153
  - `.params_mapper_class` ~> sets the mapper class that must implement `.map_from(context)` and return mapped `:input` values
183
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
+
184
220
  ## ContextError
185
221
 
186
222
  > Provides all the information related to an exception/validation errors captured by the current organizer
@@ -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
@@ -2,40 +2,38 @@
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: [],
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,
17
25
  allow_raise_on_failure: LightServiceExt.config.allow_raise_on_failure?,
18
- internal_only: { error_info: nil } }.freeze
26
+ internal_only: { error_info: nil }
27
+ }.freeze
19
28
  end
20
29
  end
21
30
 
22
31
  def add_params(**params)
23
- return if params.blank?
24
-
25
- self[:params].merge!(params.dup)
26
- end
27
-
28
- def add_internal_only(**attrs)
29
- return if attrs.blank?
30
-
31
- self[:internal_only].merge!(attrs.dup)
32
+ add_attrs_to_ctx(:params, params)
32
33
  end
33
34
 
34
35
  def add_errors(**errors)
35
- return if errors.blank?
36
-
37
- self[:errors].merge!(errors.dup)
38
- nil
36
+ add_attrs_to_ctx(:errors, errors)
39
37
  end
40
38
 
41
39
  def add_errors!(**errors)
@@ -45,12 +43,34 @@ module LightServiceExt
45
43
  fail_and_return!
46
44
  end
47
45
 
48
- def invoked_action
49
- self[:invoked_action]
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)
50
62
  end
51
63
 
52
- def validation_errors
53
- self[:errors]
64
+ def add_to_api_responses(*api_response)
65
+ add_collection_to_ctx(:api_responses, *api_response)
66
+ end
67
+
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)
54
74
  end
55
75
 
56
76
  def allow_raise_on_failure?
@@ -66,5 +86,26 @@ module LightServiceExt
66
86
  def respond_to_missing?(method_name, include_private = false)
67
87
  key?(method_name) || super
68
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
69
110
  end
70
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
@@ -8,7 +8,7 @@ module LightServiceExt
8
8
  executed do |context|
9
9
  validator = map_and_validate_inputs(context)
10
10
  context.add_params(**validator.to_h)
11
- context.add_errors!(**validator.errors.to_h.transform_values(&:first))
11
+ context.add_errors!(**validator.errors.to_h)
12
12
  end
13
13
 
14
14
  class << self
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LightServiceExt
4
- module Outcome
4
+ module Status
5
5
  COMPLETE = :all_steps_complete
6
+ INCOMPLETE = :steps_incomplete
6
7
  end
7
8
  end
@@ -31,7 +31,7 @@ module LightServiceExt
31
31
 
32
32
  def errors
33
33
  model = error && (error.try(:model) || error.try(:record))
34
- return model.errors.messages.transform_values(&:first) if model.present?
34
+ return model.errors.messages if model.present?
35
35
 
36
36
  { base: message }
37
37
  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.2"
4
+ VERSION = "0.1.3"
5
5
  end
@@ -8,6 +8,7 @@ error_info
8
8
  context_error
9
9
  configuration
10
10
  with_error_handler
11
+ record_actions
11
12
  application_context
12
13
  application_contract
13
14
  around_action_execute_extension
@@ -15,6 +16,7 @@ application_action
15
16
  all_actions_complete_action
16
17
  application_validator_action
17
18
  application_organizer
19
+ application_orchestrator
18
20
  ].each do |filename|
19
21
  require File.expand_path("../light-service-ext/#{filename}", Pathname.new(__FILE__).realpath)
20
22
  end
@@ -23,7 +23,7 @@ module LightServiceExt
23
23
  it 'does not raise error' do
24
24
  expect { executed_ctx }.not_to raise_error
25
25
 
26
- expect(executed_ctx.keys).to include(:outcome)
26
+ expect(executed_ctx.keys).to include(:status)
27
27
  end
28
28
 
29
29
  context 'with raising of errors allowed' do
@@ -7,6 +7,102 @@ module LightServiceExt
7
7
 
8
8
  subject(:ctx) { described_class.make_with_defaults(input) }
9
9
 
10
+ describe '#add_to_successful_actions' do
11
+ it 'adds successful action name to context' do
12
+ ctx.add_to_successful_actions(value)
13
+
14
+ expect(ctx.successful_actions).to match_array([value])
15
+ end
16
+
17
+ it 'preserves other successful action name' do
18
+ ctx.add_to_successful_actions(value)
19
+
20
+ ctx.add_to_successful_actions('other-value')
21
+
22
+ expect(ctx.successful_actions).to match_array([value, 'other-value'])
23
+ end
24
+ end
25
+
26
+ describe '#add_to_api_responses' do
27
+ it 'adds last api response to context' do
28
+ ctx.add_to_api_responses(value)
29
+
30
+ expect(ctx.api_responses).to match_array([value])
31
+ end
32
+
33
+ it 'preserves other api responses' do
34
+ ctx.add_to_api_responses(value)
35
+
36
+ ctx.add_to_api_responses('other-value')
37
+
38
+ expect(ctx.api_responses).to match_array([value, 'other-value'])
39
+ end
40
+ end
41
+
42
+ describe '#add_last_failed_context' do
43
+ it 'adds last failed context' do
44
+ ctx.add_last_failed_context(value)
45
+
46
+ expect(ctx.last_failed_context).to eql(value)
47
+ end
48
+
49
+ it 'updates older values' do
50
+ ctx.add_last_failed_context(value)
51
+
52
+ ctx.add_last_failed_context('other-value')
53
+
54
+ expect(ctx.last_failed_context).to eql('other-value')
55
+ end
56
+ end
57
+
58
+ describe '#add_status' do
59
+ it 'adds current api response to context' do
60
+ ctx.add_status(value)
61
+
62
+ expect(ctx.status).to eql(value)
63
+ end
64
+
65
+ it 'updates older values' do
66
+ ctx.add_status(value)
67
+
68
+ ctx.add_status('other-value')
69
+
70
+ expect(ctx.status).to eql('other-value')
71
+ end
72
+ end
73
+
74
+ describe '#add_current_api_response' do
75
+ it 'adds current api response to context' do
76
+ ctx.add_current_api_response(value)
77
+
78
+ expect(ctx.current_api_response).to eql(value)
79
+ end
80
+
81
+ it 'updates older values' do
82
+ ctx.add_current_api_response(value)
83
+
84
+ ctx.add_current_api_response('other-value')
85
+
86
+ expect(ctx.current_api_response).to eql('other-value')
87
+ end
88
+ end
89
+
90
+ describe '#add_invoked_action' do
91
+ it 'adds last api response to context' do
92
+ ctx.add_invoked_action(value)
93
+
94
+ expect(ctx.invoked_action).to eql(value)
95
+ end
96
+
97
+ it 'updates older values' do
98
+ ctx.add_invoked_action(value)
99
+
100
+ ctx.add_invoked_action('other-value')
101
+
102
+ expect(ctx.invoked_action).to eql('other-value')
103
+ end
104
+ end
105
+
10
106
  describe '#add_errors!' do
11
107
  before { allow(ctx).to receive(:fail_and_return!) }
12
108
 
@@ -134,13 +230,19 @@ module LightServiceExt
134
230
  end
135
231
 
136
232
  it 'returns context with default attrs' do
137
- expect(ctx_with_defaults.keys).to match_array(%i[input
138
- errors
139
- params
140
- successful_actions
141
- api_responses
142
- allow_raise_on_failure
143
- internal_only])
233
+ expect(ctx_with_defaults.keys).to match_array(%i[
234
+ input
235
+ params
236
+ errors
237
+ status
238
+ successful_actions
239
+ allow_raise_on_failure
240
+ api_responses
241
+ internal_only
242
+ invoked_action
243
+ current_api_response
244
+ last_failed_context
245
+ ])
144
246
 
145
247
  expect(ctx_with_defaults[:input]).to eql(input)
146
248
  end
@@ -160,8 +262,8 @@ module LightServiceExt
160
262
  described_class.make_with_defaults(input, successful_actions: ['some-action-class-name'])
161
263
  end
162
264
 
163
- it 'prevents successful_actions to change from default' do
164
- expect(ctx_with_defaults[:successful_actions]).to be_empty
265
+ it 'allows for overrides' do
266
+ expect(ctx_with_defaults[:successful_actions]).to match_array(['some-action-class-name'])
165
267
  end
166
268
  end
167
269
 
@@ -170,8 +272,8 @@ module LightServiceExt
170
272
  described_class.make_with_defaults(input, api_responses: ['some-api-response'])
171
273
  end
172
274
 
173
- it 'prevents successful_actions to change from default' do
174
- expect(ctx_with_defaults[:api_responses]).to be_empty
275
+ it 'allows for overrides' do
276
+ expect(ctx_with_defaults[:api_responses]).to match_array(['some-api-response'])
175
277
  end
176
278
  end
177
279
 
@@ -0,0 +1,76 @@
1
+ module LightServiceExt
2
+ RSpec.describe ApplicationOrchestrator do
3
+ describe '.call' do
4
+ let(:orchestrator_class) do
5
+ FakeOrganizer = Class.new(ApplicationOrganizer) do
6
+ class << self
7
+ FakeOrganizerAction = Class.new do
8
+ extend LightService::Action
9
+
10
+ expects :input
11
+
12
+ executed do |context|
13
+ context.dig(:input, :organizer_action_proc).call(context)
14
+ end
15
+ end
16
+
17
+ def steps
18
+ [FakeOrganizerAction]
19
+ end
20
+ end
21
+ end
22
+
23
+ Class.new(described_class) do
24
+ class << self
25
+ FakeOrchestratorAction = Class.new do
26
+ extend LightService::Action
27
+
28
+ expects :input
29
+
30
+ executed do |context|
31
+ context.dig(:input, :orchestrator_action_proc).call(context)
32
+ end
33
+ end
34
+
35
+ def organizer_steps
36
+ [FakeOrganizer]
37
+ end
38
+
39
+ def steps
40
+ [FakeOrchestratorAction]
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ let(:input) do
47
+ { organizer_action_proc: ->(context) { context.add_params(x1: 'x1') },
48
+ orchestrator_action_proc: ->(context) { context.add_params(x2: 'x2') } }
49
+ end
50
+
51
+ it 'adds orchestrator params without organizer params' do
52
+ ctx = orchestrator_class.call(input)
53
+
54
+ expect(ctx.keys).to include(:input)
55
+ expect(ctx[:input]).to eql(input)
56
+ expect(ctx[:params]).to eql(x2: 'x2')
57
+ end
58
+
59
+ context 'with each organizer block' do
60
+ let(:organizer_block) do
61
+ lambda { |organizer_ctx, orchestrator_ctx:|
62
+ orchestrator_ctx.add_params(x3: organizer_ctx.params[:x1])
63
+ }
64
+ end
65
+
66
+ it 'adds organizer param to orchestrator' do
67
+ orchestrator_ctx = orchestrator_class.call(input, &organizer_block)
68
+
69
+ expect(orchestrator_ctx.keys).to include(:input)
70
+ expect(orchestrator_ctx[:input]).to eql(input)
71
+ expect(orchestrator_ctx[:params]).to include(x2: 'x2', x3: 'x1')
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -27,15 +27,7 @@ module LightServiceExt
27
27
  it 'adds inputted data as input key value pair' do
28
28
  ctx = subject_class.call(input)
29
29
 
30
- expect(ctx.keys).to match_array(%i[
31
- api_responses
32
- errors
33
- input
34
- params
35
- allow_raise_on_failure
36
- successful_actions
37
- internal_only
38
- ])
30
+ expect(ctx.keys).to include(:input)
39
31
  expect(ctx[:input]).to eql(input)
40
32
  end
41
33
 
@@ -27,7 +27,7 @@ module LightServiceExt
27
27
 
28
28
  it 'returns promised params and filled in errors' do
29
29
  expect(context).to be_failure
30
- expect(context.errors).to eql(key: 'must be filled')
30
+ expect(context.errors).to eql(key: ['must be filled'])
31
31
  end
32
32
  end
33
33
  end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LightServiceExt
4
+ RSpec.describe RecordActions do
5
+ describe '.call' do
6
+ let(:input) { {} }
7
+ let(:action_name) { 'some-action-name' }
8
+ let(:current_api_response) { nil }
9
+ let(:invoked_action) { nil }
10
+ let(:status) { nil }
11
+ let(:errors) { {} }
12
+ let(:overrides) { { errors: errors } }
13
+ let(:ctx) { ApplicationContext.make_with_defaults(input, overrides) }
14
+ let(:result_ctx_overrides) do
15
+ { status: status, errors: errors, invoked_action: invoked_action, current_api_response: current_api_response }
16
+ end
17
+ let(:result_ctx) { ApplicationContext.make_with_defaults(input, result_ctx_overrides) }
18
+ let(:proc) { -> { result_ctx } }
19
+
20
+ subject(:called_ctx) { described_class.call(ctx, &proc) }
21
+
22
+ it 'returns expected context' do
23
+ expect(called_ctx.success?).to be_truthy
24
+ expect(called_ctx.successful_actions).to match_array([])
25
+ expect(called_ctx.api_responses).to match_array([])
26
+ expect(called_ctx.status).to eql(Status::INCOMPLETE)
27
+ expect(called_ctx[:last_failed_context]).to be_nil
28
+ end
29
+
30
+ context 'with api response attrs' do
31
+ let(:invoked_action) { class_double(ApplicationAction, name: action_name) }
32
+ let(:current_api_response) { 'some-api-response' }
33
+
34
+ it 'adds api_response as last api response' do
35
+ expect(called_ctx.success?).to be_truthy
36
+ expect(called_ctx.successful_actions).to match_array([action_name])
37
+ expect(called_ctx.api_responses).to match_array([current_api_response])
38
+ expect(called_ctx.status).to eql(Status::INCOMPLETE)
39
+ expect(called_ctx[:last_failed_context]).to be_nil
40
+ end
41
+
42
+ context 'with completed status' do
43
+ let(:status) { :all_steps_complete }
44
+
45
+ it 'adds errors and last failed context' do
46
+ expect(called_ctx.success?).to be_truthy
47
+ expect(called_ctx.successful_actions).to be_empty
48
+ expect(called_ctx.api_responses).to be_empty
49
+ expect(called_ctx.status).to eql(status)
50
+ expect(called_ctx[:last_failed_context]).to be_nil
51
+ end
52
+
53
+ context 'with errors' do
54
+ let(:errors) { { base: 'some-message' } }
55
+
56
+ it 'adds last failed context and fails the context' do
57
+ expect(called_ctx.success?).to be_falsey
58
+ expect(called_ctx.successful_actions).to be_empty
59
+ expect(called_ctx.api_responses).to be_empty
60
+ expect(called_ctx.status).to eql(status)
61
+ expect(called_ctx[:last_failed_context]).to be_present
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -1,33 +1,13 @@
1
1
  module LightServiceExt
2
2
  RSpec.describe WithErrorHandler do
3
- unless defined? Rails
4
- module Rails
5
- end
6
- end
7
-
8
- unless defined? Rails::ActiveRecordError
9
- module Rails
10
- class ActiveRecordError < StandardError
11
- def model; end
12
- end
13
- end
14
- end
15
-
16
3
  let(:implementor_class) do
17
4
  Class.new do
18
5
  extend WithErrorHandler
19
6
  end
20
7
  end
21
8
 
22
- let(:key) { :key }
23
- let(:key_error) { 'invalid' }
24
- let(:messages) { { key => [key_error] } }
25
9
  let(:error_message) { 'some-error' }
26
- let(:errors) { OpenStruct.new(messages: messages) }
27
- let(:model) { OpenStruct.new(errors: errors) }
28
- let(:results_ctx_proc) { -> { ApplicationContext.make_with_defaults } }
29
10
  let(:ctx) { ApplicationContext.make_with_defaults }
30
- let(:error) { Rails::ActiveRecordError.new(error_message) }
31
11
 
32
12
  before { allow(LightServiceExt.config.logger).to receive(:error) }
33
13
 
@@ -36,7 +16,23 @@ module LightServiceExt
36
16
  implementor_class.with_error_handler(ctx: ctx, &results_ctx_proc)
37
17
  end
38
18
 
19
+ context 'with non validation error' do
20
+ let(:error) { ArgumentError.new(error_message) }
21
+ let(:results_ctx_proc) { -> { raise error } }
22
+
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
27
+ end
28
+
39
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) }
40
36
  let(:results_ctx_proc) { -> { raise error } }
41
37
 
42
38
  context 'with error including model' do
@@ -44,7 +40,7 @@ module LightServiceExt
44
40
 
45
41
  it 'adds model errors' do
46
42
  expect(errors_handled_ctx.failure?).to be_truthy
47
- expect(errors_handled_ctx.errors).to eql({ key => key_error })
43
+ expect(errors_handled_ctx.errors).to eql({ key => [key_error] })
48
44
  expect(errors_handled_ctx.internal_only[:error_info]).to be_an_instance_of(ErrorInfo)
49
45
  expect(LightServiceExt.config.logger).to have_received(:error)
50
46
  end
data/spec/spec_helper.rb CHANGED
@@ -5,6 +5,19 @@ require Pathname.new(__dir__).realpath.join('coverage_helper').to_s
5
5
 
6
6
  require 'light-service/testing'
7
7
 
8
+ unless defined? Rails
9
+ module Rails
10
+ end
11
+ end
12
+
13
+ unless defined? Rails::ActiveRecordError
14
+ module Rails
15
+ class ActiveRecordError < StandardError
16
+ def model; end
17
+ end
18
+ end
19
+ end
20
+
8
21
  RSpec.configure do |config|
9
22
  config.before do
10
23
  # rubocop:disable Style/ClassVars
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.2
4
+ version: 0.1.3
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-02-24 00:00:00.000000000 Z
11
+ date: 2023-02-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: light-service
@@ -175,6 +175,7 @@ files:
175
175
  - lib/light-service-ext/application_action.rb
176
176
  - lib/light-service-ext/application_context.rb
177
177
  - lib/light-service-ext/application_contract.rb
178
+ - lib/light-service-ext/application_orchestrator.rb
178
179
  - lib/light-service-ext/application_organizer.rb
179
180
  - lib/light-service-ext/application_validator_action.rb
180
181
  - lib/light-service-ext/around_action_execute_extension.rb
@@ -182,6 +183,7 @@ files:
182
183
  - lib/light-service-ext/constants.rb
183
184
  - lib/light-service-ext/context_error.rb
184
185
  - lib/light-service-ext/error_info.rb
186
+ - lib/light-service-ext/record_actions.rb
185
187
  - lib/light-service-ext/regex.rb
186
188
  - lib/light-service-ext/version.rb
187
189
  - lib/light-service-ext/with_error_handler.rb
@@ -192,12 +194,14 @@ files:
192
194
  - spec/light-service-ext/application_action_spec.rb
193
195
  - spec/light-service-ext/application_context_spec.rb
194
196
  - spec/light-service-ext/application_contract_spec.rb
197
+ - spec/light-service-ext/application_orchestrator_spec.rb
195
198
  - spec/light-service-ext/application_organizer_spec.rb
196
199
  - spec/light-service-ext/application_validator_action_spec.rb
197
200
  - spec/light-service-ext/around_action_execute_extension_spec.rb
198
201
  - spec/light-service-ext/configuration_spec.rb
199
202
  - spec/light-service-ext/context_error_spec.rb
200
203
  - spec/light-service-ext/error_info_spec.rb
204
+ - spec/light-service-ext/record_actions_spec.rb
201
205
  - spec/light-service-ext/regex_spec.rb
202
206
  - spec/light-service-ext/with_error_handler_spec.rb
203
207
  - spec/light_service_ext_spec.rb
@@ -234,12 +238,14 @@ test_files:
234
238
  - spec/light-service-ext/application_action_spec.rb
235
239
  - spec/light-service-ext/application_context_spec.rb
236
240
  - spec/light-service-ext/application_contract_spec.rb
241
+ - spec/light-service-ext/application_orchestrator_spec.rb
237
242
  - spec/light-service-ext/application_organizer_spec.rb
238
243
  - spec/light-service-ext/application_validator_action_spec.rb
239
244
  - spec/light-service-ext/around_action_execute_extension_spec.rb
240
245
  - spec/light-service-ext/configuration_spec.rb
241
246
  - spec/light-service-ext/context_error_spec.rb
242
247
  - spec/light-service-ext/error_info_spec.rb
248
+ - spec/light-service-ext/record_actions_spec.rb
243
249
  - spec/light-service-ext/regex_spec.rb
244
250
  - spec/light-service-ext/with_error_handler_spec.rb
245
251
  - spec/light_service_ext_spec.rb