axn 0.1.0.pre.alpha.2.7.1 → 0.1.0.pre.alpha.2.8

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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +11 -5
  3. data/CHANGELOG.md +10 -0
  4. data/Rakefile +12 -0
  5. data/docs/intro/about.md +2 -2
  6. data/docs/intro/overview.md +18 -0
  7. data/docs/recipes/rubocop-integration.md +352 -0
  8. data/docs/reference/action-result.md +1 -1
  9. data/docs/reference/class.md +110 -2
  10. data/docs/reference/configuration.md +5 -3
  11. data/docs/reference/instance.md +0 -52
  12. data/docs/usage/setup.md +4 -0
  13. data/docs/usage/steps.md +335 -0
  14. data/docs/usage/writing.md +67 -0
  15. data/lib/action/attachable/steps.rb +18 -17
  16. data/lib/action/attachable/subactions.rb +1 -1
  17. data/lib/action/context.rb +10 -14
  18. data/lib/action/core/context/facade.rb +11 -2
  19. data/lib/action/core/context/facade_inspector.rb +3 -2
  20. data/lib/action/core/context/internal.rb +3 -11
  21. data/lib/action/core/contract_validation.rb +1 -1
  22. data/lib/action/core/flow/callbacks.rb +22 -8
  23. data/lib/action/core/flow/exception_execution.rb +2 -5
  24. data/lib/action/core/flow/handlers/{base_handler.rb → base_descriptor.rb} +7 -4
  25. data/lib/action/core/flow/handlers/descriptors/callback_descriptor.rb +17 -0
  26. data/lib/action/core/flow/handlers/descriptors/message_descriptor.rb +53 -0
  27. data/lib/action/core/flow/handlers/matcher.rb +41 -2
  28. data/lib/action/core/flow/handlers/resolvers/base_resolver.rb +28 -0
  29. data/lib/action/core/flow/handlers/resolvers/callback_resolver.rb +29 -0
  30. data/lib/action/core/flow/handlers/resolvers/message_resolver.rb +59 -0
  31. data/lib/action/core/flow/handlers.rb +7 -4
  32. data/lib/action/core/flow/messages.rb +15 -41
  33. data/lib/action/core/nesting_tracking.rb +31 -0
  34. data/lib/action/core/timing.rb +1 -1
  35. data/lib/action/core.rb +20 -12
  36. data/lib/action/exceptions.rb +20 -2
  37. data/lib/action/result.rb +30 -32
  38. data/lib/axn/factory.rb +22 -23
  39. data/lib/axn/rubocop.rb +10 -0
  40. data/lib/axn/version.rb +1 -1
  41. data/lib/rubocop/cop/axn/README.md +237 -0
  42. data/lib/rubocop/cop/axn/unchecked_result.rb +327 -0
  43. metadata +14 -6
  44. data/lib/action/core/flow/handlers/callback_handler.rb +0 -21
  45. data/lib/action/core/flow/handlers/message_handler.rb +0 -27
  46. data/lib/action/core/hoist_errors.rb +0 -58
@@ -51,56 +51,4 @@ class Foo
51
51
  end
52
52
  ```
53
53
 
54
- ## `#hoist_errors`
55
54
 
56
- Useful when calling one Action from within another. By default the nested action call will return an Action::Result, but it's up to you to check if the result is `ok?` and to handle potential failure modes... and in practice this is easy to miss.
57
-
58
- By wrapping your nested call in `hoist_errors`, it will _automatically_ fail the parent action if the nested call fails.
59
-
60
- Accepts a `prefix` keyword argument -- when set, prefixes the `error` message from any failures in the block (useful to return different error messages for each if you're calling multiple sub-actions in a single service).
61
-
62
- NOTE: expects a single action call in the block -- if there are multiple calls, only the last one will be checked for `ok?` (although anything _raised_ in the block will still be handled).
63
-
64
- ::: tip Versus `call!`
65
- * If you just want to make sure your action fails if the subaction fails: call subaction via `call!` (any failures will raise, which will fail the parent).
66
- * Note this passes _child_ exception into _parent_ `error` message parsing.
67
- * If you want _the child's_ `result.error` to become the _parent's_ `result.error` on failure, use `hoist_errors` + `call`
68
- :::
69
-
70
- ### Example
71
-
72
- ```ruby
73
- class SubAction
74
- include Action
75
-
76
- def call
77
- fail! "bad news"
78
- end
79
- end
80
-
81
- class MainAction
82
- include Action
83
-
84
- def call
85
- SubAction.call
86
- end
87
- end
88
- ```
89
-
90
- _Without_ `hoist_errors`, `MainAction.call` returns an `ok?` result, even though `SubAction.call` always fails, because we haven't explicitly handled the nested call.
91
-
92
- By adding `hoist_errors`, though:
93
-
94
- ```ruby
95
- class MainAction
96
- include Action
97
-
98
- def call
99
- hoist_errors(prefix: "From subaction:") do
100
- SubAction.call
101
- end
102
- end
103
- end
104
- ```
105
-
106
- `MainAction.call` now returns a _failed_ result, and `result.error` is "From subaction: bad news".
data/docs/usage/setup.md CHANGED
@@ -21,4 +21,8 @@ By default any swallowed errors are noted in the logs, but it's _highly recommen
21
21
 
22
22
  If you're using an APM provider, observability can be greatly enhanced by [configuring tracing and metrics hooks](/reference/configuration#tracing-and-metrics).
23
23
 
24
+ ### Code Quality (Optional)
25
+
26
+ For teams using RuboCop, Axn provides custom cops to enforce best practices. See the [RuboCop Integration guide](/recipes/rubocop-integration) for setup instructions.
27
+
24
28
 
@@ -0,0 +1,335 @@
1
+ ---
2
+ outline: deep
3
+ ---
4
+
5
+ # Using Steps in Actions
6
+
7
+ The steps functionality allows you to compose complex actions by breaking them down into sequential, reusable steps. Each step can expect data from the parent context or previous steps, and expose data for subsequent steps.
8
+
9
+ ## Basic Concepts
10
+
11
+ ### What are Steps?
12
+
13
+ Steps are a way to organize action logic into smaller, focused pieces that:
14
+ - Execute in a defined order
15
+ - Can share data between each other
16
+ - Handle failures gracefully with error prefixing
17
+ - Can be reused across different actions
18
+
19
+ ### How Steps Work
20
+
21
+ 1. **Step Definition**: Define steps using the `step` class method
22
+ 2. **Execution Order**: Steps execute sequentially in the order they're defined
23
+ 3. **Data Flow**: Each step can expect and expose data
24
+ 4. **Error Handling**: Step failures are caught and can trigger error handlers
25
+
26
+ ## Defining Steps
27
+
28
+ ### Using the `step` Method
29
+
30
+ The `step` method allows you to define steps inline with blocks:
31
+
32
+ ```ruby
33
+ class UserRegistration
34
+ include Action
35
+ expects :email, :password, :name
36
+ exposes :user_id, :welcome_message
37
+
38
+ step :validate_input, expects: [:email, :password, :name], exposes: [:validated_data] do
39
+ # Validation logic
40
+ fail! "Email is invalid" unless email.match?(/\A[^@\s]+@[^@\s]+\z/)
41
+ fail! "Password too short" if password.length < 8
42
+ fail! "Name is required" if name.blank?
43
+
44
+ expose :validated_data, { email: email.downcase, password: password, name: name.strip }
45
+ end
46
+
47
+ step :create_user, expects: [:validated_data], exposes: [:user_id] do
48
+ user = User.create!(validated_data)
49
+ expose :user_id, user.id
50
+ end
51
+
52
+ step :send_welcome, expects: [:user_id, :validated_data], exposes: [:welcome_message] do
53
+ WelcomeMailer.send_welcome(user_id, validated_data[:email]).deliver_now
54
+ expose :welcome_message, "Welcome #{validated_data[:name]}!"
55
+ end
56
+
57
+ def call
58
+ # Steps handle execution automatically
59
+ end
60
+ end
61
+ ```
62
+
63
+ ### Using the `steps` Method
64
+
65
+ The `steps` method allows you to compose existing action classes:
66
+
67
+ ```ruby
68
+ class ValidateInput
69
+ include Action
70
+ expects :email, :password, :name
71
+ exposes :validated_data
72
+
73
+ def call
74
+ fail! "Email is invalid" unless email.match?(/\A[^@\s]+@[^@\s]+\z/)
75
+ fail! "Password too short" if password.length < 8
76
+ fail! "Name is required" if name.blank?
77
+
78
+ expose :validated_data, { email: email.downcase, password: password, name: name.strip }
79
+ end
80
+ end
81
+
82
+ class CreateUser
83
+ include Action
84
+ expects :validated_data
85
+ exposes :user_id
86
+
87
+ def call
88
+ user = User.create!(validated_data)
89
+ expose :user_id, user.id
90
+ end
91
+ end
92
+
93
+ class SendWelcome
94
+ include Action
95
+ expects :user_id, :validated_data
96
+ exposes :welcome_message
97
+
98
+ def call
99
+ WelcomeMailer.send_welcome(user_id, validated_data[:email]).deliver_now
100
+ expose :welcome_message, "Welcome #{validated_data[:name]}!"
101
+ end
102
+ end
103
+
104
+ class UserRegistration
105
+ include Action
106
+ expects :email, :password, :name
107
+ exposes :user_id, :welcome_message
108
+
109
+ # Use existing action classes as steps
110
+ steps(ValidateInput, CreateUser, SendWelcome)
111
+ end
112
+ ```
113
+
114
+ ### Mixed Approach
115
+
116
+ You can combine both approaches:
117
+
118
+ ```ruby
119
+ class UserRegistration
120
+ include Action
121
+ expects :email, :password, :name
122
+ exposes :user_id, :welcome_message
123
+
124
+ # Use existing action for validation
125
+ steps(ValidateInput)
126
+
127
+ # Define custom step for user creation
128
+ step :create_user, expects: [:validated_data], exposes: [:user_id] do
129
+ user = User.create!(validated_data)
130
+ expose :user_id, user.id
131
+ end
132
+
133
+ # Use existing action for welcome email
134
+ steps(SendWelcome)
135
+ end
136
+ ```
137
+
138
+ ## Data Flow Between Steps
139
+
140
+ ### Expecting Data
141
+
142
+ Steps can expect data from:
143
+ - **Parent context**: Data passed to the parent action
144
+ - **Previous steps**: Data exposed by earlier steps
145
+
146
+ ```ruby
147
+ step :step1, expects: [:input], exposes: [:processed_data] do
148
+ expose :processed_data, input.upcase
149
+ end
150
+
151
+ step :step2, expects: [:processed_data], exposes: [:final_result] do
152
+ # This step can access both 'input' (from parent) and 'processed_data' (from step1)
153
+ expose :final_result, "Result: #{processed_data}"
154
+ end
155
+ ```
156
+
157
+ ### Exposing Data
158
+
159
+ Steps expose data using the `expose` method:
160
+
161
+ ```ruby
162
+ step :calculation, expects: [:base_value], exposes: [:doubled_value, :final_result] do
163
+ doubled = base_value * 2
164
+ expose :doubled_value, doubled
165
+ expose :final_result, doubled + 10
166
+ end
167
+ ```
168
+
169
+ ### Using `expose_return_as`
170
+
171
+ For simple calculations, you can use `expose_return_as`:
172
+
173
+ ```ruby
174
+ step :calculation, expects: [:input], expose_return_as: :result do
175
+ input * 2 + 10 # Return value is automatically exposed as 'result'
176
+ end
177
+ ```
178
+
179
+ ## Error Handling
180
+
181
+ ### Automatic Error Prefixing
182
+
183
+ When a step fails, error messages are automatically prefixed with the step name:
184
+
185
+ ```ruby
186
+ step :validation, expects: [:input] do
187
+ fail! "Input too short"
188
+ end
189
+
190
+ # If this step fails, the error message becomes: "validation step: Input too short"
191
+ ```
192
+
193
+ ### Step Failure Propagation
194
+
195
+ When a step fails:
196
+ 1. The step's exception is caught
197
+ 2. The parent action fails with the prefixed error message
198
+ 3. The `on_exception` handlers are triggered appropriately
199
+
200
+ ### Exception Handling
201
+
202
+ Steps can raise exceptions that will be caught and handled:
203
+
204
+ ```ruby
205
+ step :risky_operation, expects: [:input] do
206
+ raise StandardError, "Something went wrong with #{input}"
207
+ end
208
+
209
+ # The exception is caught and the error message becomes: "risky_operation step: Something went wrong with [input]"
210
+ ```
211
+
212
+
213
+
214
+ ## Best Practices
215
+
216
+ ### 1. Keep Steps Focused
217
+
218
+ Each step should have a single responsibility:
219
+
220
+ ```ruby
221
+ # ❌ Bad: Step does too many things
222
+ step :process_user, expects: [:user_data], exposes: [:user_id, :welcome_sent] do
223
+ user = User.create!(user_data)
224
+ WelcomeMailer.send_welcome(user.id).deliver_now
225
+ expose :user_id, user.id
226
+ expose :welcome_sent, true
227
+ end
228
+
229
+ # ✅ Good: Steps are focused
230
+ step :create_user, expects: [:user_data], exposes: [:user_id] do
231
+ user = User.create!(user_data)
232
+ expose :user_id, user.id
233
+ end
234
+
235
+ step :send_welcome, expects: [:user_id], exposes: [:welcome_sent] do
236
+ WelcomeMailer.send_welcome(user_id).deliver_now
237
+ expose :welcome_sent, true
238
+ end
239
+ ```
240
+
241
+ ### 2. Use Descriptive Step Names
242
+
243
+ Step names should clearly indicate what the step does:
244
+
245
+ ```ruby
246
+ # ❌ Bad: Unclear names
247
+ step :step1, expects: [:input] do
248
+ # ...
249
+ end
250
+
251
+ # ✅ Good: Descriptive names
252
+ step :validate_email_format, expects: [:input] do
253
+ # ...
254
+ end
255
+ ```
256
+
257
+ ### 3. Handle Failures Gracefully
258
+
259
+ Use `fail!` for expected failures and raise exceptions for unexpected errors:
260
+
261
+ ```ruby
262
+ step :validation, expects: [:input] do
263
+ # Expected failure - use fail!
264
+ fail! "Input too short" if input.length < 3
265
+
266
+ # Unexpected error - raise exception
267
+ raise StandardError, "Database connection failed" if database_unavailable?
268
+ end
269
+ ```
270
+
271
+ ### 4. Expose Only Necessary Data
272
+
273
+ Only expose data that subsequent steps actually need:
274
+
275
+ ```ruby
276
+ # ❌ Bad: Exposing unnecessary data
277
+ step :validation, expects: [:input], exposes: [:input, :validated, :timestamp] do
278
+ expose :input, input
279
+ expose :validated, true
280
+ expose :timestamp, Time.current
281
+ end
282
+
283
+ # ✅ Good: Only exposing what's needed
284
+ step :validation, expects: [:input], exposes: [:validated_input] do
285
+ expose :validated_input, input.strip
286
+ end
287
+ ```
288
+
289
+ ## Common Use Cases
290
+
291
+ ### API Request Processing
292
+
293
+ ```ruby
294
+ class ProcessAPIRequest
295
+ include Action
296
+ expects :request_data
297
+ exposes :response_data
298
+
299
+ step :authenticate, expects: [:request_data], exposes: [:authenticated_user] do
300
+ # Authentication logic
301
+ expose :authenticated_user, authenticate_user(request_data[:token])
302
+ end
303
+
304
+ step :authorize, expects: [:authenticated_user, :request_data], exposes: [:authorized] do
305
+ # Authorization logic
306
+ fail! "Access denied" unless authorized_user?(authenticated_user, request_data[:action])
307
+ expose :authorized, true
308
+ end
309
+
310
+ step :process_request, expects: [:request_data, :authenticated_user], exposes: [:response_data] do
311
+ # Process the actual request
312
+ expose :response_data, process_user_request(request_data, authenticated_user)
313
+ end
314
+ end
315
+ ```
316
+
317
+
318
+
319
+ ## Troubleshooting
320
+
321
+ ### Common Issues
322
+
323
+ 1. **Steps not executing**: Ensure the Steps module is properly included
324
+ 2. **Data not flowing**: Check that step names match between `expects` and `exposes`
325
+ 3. **Error messages unclear**: Verify step names are descriptive
326
+
327
+ ### Debugging Tips
328
+
329
+ - Use descriptive step names for better error messages
330
+ - Check that data is properly exposed between steps
331
+ - Verify that step dependencies are correctly specified
332
+
333
+ ## Summary
334
+
335
+ The steps functionality provides a powerful way to compose complex actions from smaller, focused pieces. By following the patterns and best practices outlined here, you can create maintainable, testable, and reusable action compositions.
@@ -100,6 +100,71 @@ Foo.call(name: "Adams").success # => "Revealed the secret of life to Adams"
100
100
  Foo.call(name: "Adams").meaning_of_life # => "Hello Adams, the meaning of life is 42"
101
101
  ```
102
102
 
103
+ ### Advanced Error Message Configuration
104
+
105
+ You can also use conditional error messages with the `prefix:` keyword and combine them with the `from:` parameter for nested actions:
106
+
107
+ ```ruby
108
+ class ValidationAction
109
+ include Action
110
+
111
+ expects :input
112
+
113
+ error if: ArgumentError, prefix: "Validation Error: " do |e|
114
+ "Invalid input: #{e.message}"
115
+ end
116
+
117
+ error if: StandardError, prefix: "System Error: "
118
+
119
+ def call
120
+ raise ArgumentError, "input too short" if input.length < 3
121
+ raise StandardError, "unexpected error" if input == "error"
122
+ end
123
+ end
124
+
125
+ class ApiAction
126
+ include Action
127
+
128
+ expects :data
129
+
130
+ # Combine prefix with from for consistent error formatting
131
+ error from: ValidationAction, prefix: "API Error: " do |e|
132
+ "Request validation failed: #{e.message}"
133
+ end
134
+
135
+ # Or use prefix only (falls back to exception message)
136
+ error from: ValidationAction, prefix: "API Error: "
137
+
138
+ def call
139
+ ValidationAction.call!(input: data)
140
+ end
141
+ end
142
+ ```
143
+
144
+ This configuration provides:
145
+ - Consistent error message formatting with prefixes
146
+ - Automatic fallback to exception messages when no custom message is provided
147
+ - Proper error message inheritance from nested actions
148
+
149
+ ::: warning Message Ordering
150
+ **Important**: When using conditional messages, always define your static fallback messages **first** in your class, before any conditional messages. This ensures proper fallback behavior.
151
+
152
+ **Correct order:**
153
+ ```ruby
154
+ class Foo
155
+ include Action
156
+
157
+ # Static fallback messages first
158
+ success "Default success message"
159
+ error "Default error message"
160
+
161
+ # Then conditional messages
162
+ success "Special success", if: :special_condition?
163
+ error "Special error", if: ArgumentError
164
+ end
165
+ ```
166
+ :::
167
+
103
168
  ## Lifecycle methods
104
169
 
105
170
  In addition to `#call`, there are a few additional pieces to be aware of:
@@ -156,3 +221,5 @@ A number of custom callback are available for you as well, if you want to take s
156
221
 
157
222
  ## Strategies
158
223
  A number of [Strategies](/strategies/index), which are <abbr title="Don't Repeat Yourself">DRY</abbr>ed bits of commonly-used configuration, are available for your use as well.
224
+
225
+ ```
@@ -9,14 +9,16 @@ module Action
9
9
  class_attribute :_axn_steps, default: []
10
10
  end
11
11
 
12
- Entry = Data.define(:label, :axn)
13
-
14
12
  class_methods do
15
13
  def steps(*steps)
16
- self._axn_steps += Array(steps).compact
14
+ Array(steps).compact.each do |step|
15
+ raise ArgumentError, "Step #{step} must include Action module" if step.is_a?(Class) && !step.included_modules.include?(Action) && !step < Action
16
+
17
+ step("Step #{_axn_steps.length + 1}", step)
18
+ end
17
19
  end
18
20
 
19
- def step(name, axn_klass = nil, **kwargs, &block)
21
+ def step(name, axn_klass = nil, error_prefix: nil, **kwargs, &block)
20
22
  axn_klass = axn_for_attachment(
21
23
  name:,
22
24
  axn_klass:,
@@ -27,32 +29,31 @@ module Action
27
29
  )
28
30
 
29
31
  # Add the step to the list of steps
30
- steps Entry.new(label: name, axn: axn_klass)
32
+ _axn_steps << axn_klass
33
+
34
+ # Set up error handling for steps without explicit labels
35
+ error_prefix ||= "#{name}: "
36
+ error from: axn_klass do |e|
37
+ "#{error_prefix}#{e.message}"
38
+ end
31
39
  end
32
40
  end
33
41
 
42
+ # Execute steps automatically when the action is called
34
43
  def call
35
- self.class._axn_steps.each_with_index do |step, idx|
36
- # Set a default label if we were just given an array of unlabeled steps
37
- # TODO: should Axn have a default label passed in already that we could pull out?
38
- step = Entry.new(label: "Step #{idx + 1}", axn: step) if step.is_a?(Class)
39
-
40
- hoist_errors(prefix: "#{step.label} step") do
41
- step.axn.call(**merged_context_data).tap do |step_result|
42
- merge_step_exposures!(step_result)
43
- end
44
- end
44
+ _axn_steps.each do |axn|
45
+ _merge_step_exposures!(axn.call!(**_merged_context_data))
45
46
  end
46
47
  end
47
48
 
48
49
  private
49
50
 
50
- def merged_context_data
51
+ def _merged_context_data
51
52
  @__context.__combined_data
52
53
  end
53
54
 
54
55
  # Each step can expect the data exposed from the previous steps
55
- def merge_step_exposures!(step_result)
56
+ def _merge_step_exposures!(step_result)
56
57
  step_result.declared_fields.each do |field|
57
58
  @__context.exposed_data[field] = step_result.public_send(field)
58
59
  end
@@ -40,7 +40,7 @@ module Action
40
40
  axn_klass.call(**kwargs)
41
41
  end
42
42
 
43
- # TODO: do we also need an instance-level version that auto-wraps in hoist_errors(label: name)?
43
+ # TODO: do we also need an instance-level version that auto-creates the appropriate `error from:` to prefix with the name?
44
44
 
45
45
  define_singleton_method("#{name}!") do |**kwargs|
46
46
  axn_klass.call!(**kwargs)
@@ -11,28 +11,24 @@ module Action
11
11
  # Framework-managed fields
12
12
  @failure = false
13
13
  @exception = nil
14
- @error_from_user = nil
15
- @error_prefix = nil
16
14
  @elapsed_time = nil
17
15
  end
18
16
 
19
- def fail!(message = nil)
20
- @error_from_user = message if message.present?
21
- raise Action::Failure, message
22
- end
23
-
24
- # INTERNAL: base for further filtering (for logging) or providing user with usage hints
25
- def __combined_data = @provided_data.merge(@exposed_data)
26
-
27
17
  # Framework state methods
28
18
  def ok? = !@failure
29
19
  def failed? = @failure || false
30
20
 
31
21
  # Framework field accessors
32
- attr_accessor :exception, :error_from_user, :error_prefix, :elapsed_time
22
+ attr_accessor :elapsed_time
23
+ attr_reader :exception
24
+ private :elapsed_time=
33
25
 
34
- # Internal failure state setter (for framework use)
35
- attr_writer :failure
36
- private :failure=
26
+ # INTERNAL: base for further filtering (for logging) or providing user with usage hints
27
+ def __combined_data = @provided_data.merge(@exposed_data)
28
+
29
+ def __record_exception(e)
30
+ @exception = e
31
+ @failure = true
32
+ end
37
33
  end
38
34
  end
@@ -15,7 +15,7 @@ module Action
15
15
 
16
16
  (@declared_fields + Array(implicitly_allowed_fields)).each do |field|
17
17
  singleton_class.define_method(field) do
18
- context_data_source[field]
18
+ _context_data_source[field]
19
19
  end
20
20
  end
21
21
  end
@@ -34,6 +34,15 @@ module Action
34
34
 
35
35
  def action_name = @action.class.name.presence || "The action"
36
36
 
37
- def context_data_source = raise NotImplementedError
37
+ def _context_data_source = raise NotImplementedError
38
+
39
+ def _msg_resolver(event_type, exception:)
40
+ Action::Core::Flow::Handlers::Resolvers::MessageResolver.new(
41
+ action._messages_registry,
42
+ event_type,
43
+ action:,
44
+ exception:,
45
+ )
46
+ end
38
47
  end
39
48
  end
@@ -22,8 +22,9 @@ module Action
22
22
  return unless facade.is_a?(Action::Result)
23
23
 
24
24
  return "[OK]" if context.ok?
25
- unless context.exception
26
- return context.error_from_user.present? ? "[failed with '#{context.error_from_user}']" : "[failed]"
25
+
26
+ if context.exception.is_a?(Action::Failure)
27
+ return context.exception.message.present? ? "[failed with '#{context.exception.message}']" : "[failed]"
27
28
  end
28
29
 
29
30
  %([failed with #{context.exception.class.name}: '#{context.exception.message}'])
@@ -5,20 +5,12 @@ require "action/core/context/facade"
5
5
  module Action
6
6
  # Inbound / Internal ContextFacade
7
7
  class InternalContext < ContextFacade
8
- # Available for use from within message callables
9
- def default_error
10
- msg = action.class._static_message_for(:error, action:, exception: @context.exception || Action::Failure.new)
11
- [@context.error_prefix, msg.presence || "Something went wrong"].compact.join(" ").squeeze(" ")
12
- end
13
-
14
- def default_success
15
- msg = action.class._static_message_for(:success, action:, exception: nil)
16
- msg.presence || "Action completed successfully"
17
- end
8
+ def default_error = _msg_resolver(:error, exception: Action::Failure.new).resolve_default_message
9
+ def default_success = _msg_resolver(:success, exception: nil).resolve_default_message
18
10
 
19
11
  private
20
12
 
21
- def context_data_source = @context.provided_data
13
+ def _context_data_source = @context.provided_data
22
14
 
23
15
  def method_missing(method_name, ...) # rubocop:disable Style/MissingRespondToMissing (because we're not actually responding to anything additional)
24
16
  if @context.__combined_data.key?(method_name.to_sym)
@@ -13,7 +13,7 @@ module Action
13
13
  new_value = config.preprocess.call(initial_value)
14
14
  @__context.provided_data[config.field] = new_value
15
15
  rescue StandardError => e
16
- raise Action::ContractViolation::PreprocessingError, "Error preprocessing field '#{config.field}': #{e.message}"
16
+ raise Action::ContractViolation::PreprocessingError, "Error preprocessing field '#{config.field}': #{e.message}", cause: e
17
17
  end
18
18
  end
19
19