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.
- checksums.yaml +4 -4
- data/.rubocop.yml +11 -5
- data/CHANGELOG.md +10 -0
- data/Rakefile +12 -0
- data/docs/intro/about.md +2 -2
- data/docs/intro/overview.md +18 -0
- data/docs/recipes/rubocop-integration.md +352 -0
- data/docs/reference/action-result.md +1 -1
- data/docs/reference/class.md +110 -2
- data/docs/reference/configuration.md +5 -3
- data/docs/reference/instance.md +0 -52
- data/docs/usage/setup.md +4 -0
- data/docs/usage/steps.md +335 -0
- data/docs/usage/writing.md +67 -0
- data/lib/action/attachable/steps.rb +18 -17
- data/lib/action/attachable/subactions.rb +1 -1
- data/lib/action/context.rb +10 -14
- data/lib/action/core/context/facade.rb +11 -2
- data/lib/action/core/context/facade_inspector.rb +3 -2
- data/lib/action/core/context/internal.rb +3 -11
- data/lib/action/core/contract_validation.rb +1 -1
- data/lib/action/core/flow/callbacks.rb +22 -8
- data/lib/action/core/flow/exception_execution.rb +2 -5
- data/lib/action/core/flow/handlers/{base_handler.rb → base_descriptor.rb} +7 -4
- data/lib/action/core/flow/handlers/descriptors/callback_descriptor.rb +17 -0
- data/lib/action/core/flow/handlers/descriptors/message_descriptor.rb +53 -0
- data/lib/action/core/flow/handlers/matcher.rb +41 -2
- data/lib/action/core/flow/handlers/resolvers/base_resolver.rb +28 -0
- data/lib/action/core/flow/handlers/resolvers/callback_resolver.rb +29 -0
- data/lib/action/core/flow/handlers/resolvers/message_resolver.rb +59 -0
- data/lib/action/core/flow/handlers.rb +7 -4
- data/lib/action/core/flow/messages.rb +15 -41
- data/lib/action/core/nesting_tracking.rb +31 -0
- data/lib/action/core/timing.rb +1 -1
- data/lib/action/core.rb +20 -12
- data/lib/action/exceptions.rb +20 -2
- data/lib/action/result.rb +30 -32
- data/lib/axn/factory.rb +22 -23
- data/lib/axn/rubocop.rb +10 -0
- data/lib/axn/version.rb +1 -1
- data/lib/rubocop/cop/axn/README.md +237 -0
- data/lib/rubocop/cop/axn/unchecked_result.rb +327 -0
- metadata +14 -6
- data/lib/action/core/flow/handlers/callback_handler.rb +0 -21
- data/lib/action/core/flow/handlers/message_handler.rb +0 -27
- data/lib/action/core/hoist_errors.rb +0 -58
data/docs/reference/instance.md
CHANGED
@@ -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
|
|
data/docs/usage/steps.md
ADDED
@@ -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.
|
data/docs/usage/writing.md
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
36
|
-
|
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
|
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
|
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-
|
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)
|
data/lib/action/context.rb
CHANGED
@@ -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 :
|
22
|
+
attr_accessor :elapsed_time
|
23
|
+
attr_reader :exception
|
24
|
+
private :elapsed_time=
|
33
25
|
|
34
|
-
#
|
35
|
-
|
36
|
-
|
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
|
-
|
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
|
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
|
-
|
26
|
-
|
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
|
-
|
9
|
-
def
|
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
|
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
|
|