axn 0.1.0.pre.alpha.2.7 → 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 +14 -1
- 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 +2 -2
- data/docs/reference/class.md +122 -6
- data/docs/reference/configuration.md +43 -9
- 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 +70 -3
- 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 +27 -12
- 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
@@ -2,7 +2,6 @@
|
|
2
2
|
|
3
3
|
Somewhere at boot (e.g. `config/initializers/actions.rb` in Rails), you can call `Action.configure` to adjust a few global settings.
|
4
4
|
|
5
|
-
|
6
5
|
```ruby
|
7
6
|
Action.configure do |c|
|
8
7
|
c.log_level = :info
|
@@ -22,7 +21,6 @@ By default any swallowed errors are noted in the logs, but it's _highly recommen
|
|
22
21
|
|
23
22
|
For example, if you're using Honeybadger this could look something like:
|
24
23
|
|
25
|
-
|
26
24
|
```ruby
|
27
25
|
Action.configure do |c|
|
28
26
|
c.on_exception = proc do |e, action:, context:|
|
@@ -56,13 +54,10 @@ A couple notes:
|
|
56
54
|
* If your handler raises, the failure will _also_ be swallowed and logged
|
57
55
|
* This handler is global across _all_ Axns. You can also specify per-Action handlers via [the class-level declaration](/reference/class#on-exception).
|
58
56
|
|
59
|
-
|
60
|
-
## `top_level_around_hook`
|
57
|
+
## `wrap_with_trace` and `emit_metrics`
|
61
58
|
|
62
59
|
If you're using an APM provider, observability can be greatly enhanced by adding automatic _tracing_ of Action calls and/or emitting count metrics after each call completes.
|
63
60
|
|
64
|
-
### Tracing and Metrics
|
65
|
-
|
66
61
|
The framework provides two distinct hooks for observability:
|
67
62
|
|
68
63
|
- **`wrap_with_trace`**: An around hook that wraps the entire action execution. You MUST call the provided block to execute the action.
|
@@ -79,7 +74,7 @@ For example, to wire up Datadog:
|
|
79
74
|
end
|
80
75
|
|
81
76
|
c.emit_metrics = proc do |resource, result|
|
82
|
-
TS::Metrics.increment("action.#{resource.underscore}", tags: { outcome: result.outcome, resource: })
|
77
|
+
TS::Metrics.increment("action.#{resource.underscore}", tags: { outcome: result.outcome.to_s, resource: })
|
83
78
|
TS::Metrics.histogram("action.duration", result.elapsed_time, tags: { resource: })
|
84
79
|
end
|
85
80
|
end
|
@@ -88,15 +83,16 @@ For example, to wire up Datadog:
|
|
88
83
|
A couple notes:
|
89
84
|
|
90
85
|
* `Datadog::Tracing` is provided by [the datadog gem](https://rubygems.org/gems/datadog)
|
91
|
-
* `TS::Metrics` is a custom implementation to set a Datadog count metric, but the relevant part to note is that the result object provides access to the outcome (`success
|
86
|
+
* `TS::Metrics` is a custom implementation to set a Datadog count metric, but the relevant part to note is that the result object provides access to the outcome (`result.outcome.success?`, `result.outcome.failure?`, `result.outcome.exception?`) and elapsed time of the action.
|
92
87
|
* The `wrap_with_trace` hook is an around hook - you must call the provided block to execute the action
|
93
88
|
* The `emit_metrics` hook is called after execution with the result - do not call any blocks
|
94
89
|
|
95
|
-
|
96
90
|
## `logger`
|
97
91
|
|
98
92
|
Defaults to `Rails.logger`, if present, otherwise falls back to `Logger.new($stdout)`. But can be set to a custom logger as necessary.
|
99
93
|
|
94
|
+
|
95
|
+
|
100
96
|
## `additional_includes`
|
101
97
|
|
102
98
|
This is much less critical than the preceding options, but on the off chance you want to add additional customization to _all_ your actions you can set additional modules to be included alongside `include Action`.
|
@@ -115,6 +111,10 @@ For a practical example of this in practice, see [our 'memoization' recipe](/rec
|
|
115
111
|
|
116
112
|
Sets the log level used when you call `log "Some message"` in your Action. Note this is read via a `log_level` class method, so you can easily use inheritance to support different log levels for different sets of actions.
|
117
113
|
|
114
|
+
## `env`
|
115
|
+
|
116
|
+
Automatically detects the environment from `RACK_ENV` or `RAILS_ENV`, defaulting to `"development"`. This is used internally for conditional behavior (e.g., more verbose logging in non-production environments).
|
117
|
+
|
118
118
|
## Automatic Logging
|
119
119
|
|
120
120
|
By default, every `action.call` will emit log lines when it is called and after it completes:
|
@@ -148,3 +148,37 @@ end
|
|
148
148
|
```
|
149
149
|
|
150
150
|
The `auto_log` method supports inheritance, so subclasses will inherit the setting from their parent class unless explicitly overridden.
|
151
|
+
|
152
|
+
## Complete Configuration Example
|
153
|
+
|
154
|
+
Here's a complete example showing all available configuration options:
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
Action.configure do |c|
|
158
|
+
# Logging
|
159
|
+
c.log_level = :info
|
160
|
+
c.logger = Rails.logger
|
161
|
+
|
162
|
+
# Exception handling
|
163
|
+
c.on_exception = proc do |e, action:, context:|
|
164
|
+
message = "[#{action.class.name}] Failing due to #{e.class.name}: #{e.message}"
|
165
|
+
Rails.logger.warn(message)
|
166
|
+
Honeybadger.notify(message, context: { axn_context: context })
|
167
|
+
end
|
168
|
+
|
169
|
+
# Observability
|
170
|
+
c.wrap_with_trace = proc do |resource, &action|
|
171
|
+
Datadog::Tracing.trace("Action", resource:) do
|
172
|
+
action.call
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
c.emit_metrics = proc do |resource, result|
|
177
|
+
Datadog::Metrics.increment("action.#{resource.underscore}", tags: { outcome: result.outcome.to_s })
|
178
|
+
Datadog::Metrics.histogram("action.duration", result.elapsed_time, tags: { resource: })
|
179
|
+
end
|
180
|
+
|
181
|
+
# Global includes
|
182
|
+
c.additional_includes = [MyCustomModule]
|
183
|
+
end
|
184
|
+
```
|
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_ `messages :error` 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
@@ -68,7 +68,7 @@ See [the reference doc](/reference/instance) for a few more handy helper methods
|
|
68
68
|
|
69
69
|
The default `error` and `success` message strings ("Something went wrong" / "Action completed successfully", respectively) _are_ technically safe to show users, but you'll often want to set them to something more useful.
|
70
70
|
|
71
|
-
There
|
71
|
+
There are `success` and `error` declarations for that -- you can set strings (most common) or a callable (note for the error case, if you give it a callable that expects a single argument, the exception that was raised will be passed in).
|
72
72
|
|
73
73
|
For instance, configuring the action like this:
|
74
74
|
|
@@ -79,8 +79,8 @@ class Foo
|
|
79
79
|
expects :name, type: String
|
80
80
|
exposes :meaning_of_life
|
81
81
|
|
82
|
-
|
83
|
-
|
82
|
+
success { "Revealed to #{name}: #{result.meaning_of_life}" } # [!code focus:2]
|
83
|
+
error { |e| "No secret of life for you: #{e.message}" }
|
84
84
|
|
85
85
|
def call
|
86
86
|
fail! "Douglas already knows the meaning" if name == "Doug"
|
@@ -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)
|