axn 0.1.0.pre.alpha.2.6.1 → 0.1.0.pre.alpha.2.7.1
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/CHANGELOG.md +13 -2
- data/docs/reference/action-result.md +1 -1
- data/docs/reference/class.md +143 -22
- data/docs/reference/configuration.md +39 -7
- data/docs/reference/instance.md +1 -1
- data/docs/usage/writing.md +3 -3
- data/lib/action/core/context/facade.rb +0 -30
- data/lib/action/core/context/internal.rb +8 -2
- data/lib/action/core/flow/callbacks.rb +26 -25
- data/lib/action/core/flow/exception_execution.rb +5 -19
- data/lib/action/core/flow/handlers/base_handler.rb +32 -0
- data/lib/action/core/flow/handlers/callback_handler.rb +21 -0
- data/lib/action/core/flow/handlers/invoker.rb +73 -0
- data/lib/action/core/flow/handlers/matcher.rb +85 -0
- data/lib/action/core/flow/handlers/message_handler.rb +27 -0
- data/lib/action/core/flow/handlers/registry.rb +40 -0
- data/lib/action/core/flow/handlers.rb +17 -0
- data/lib/action/core/flow/messages.rb +39 -25
- data/lib/action/result.rb +24 -5
- data/lib/axn/factory.rb +15 -7
- data/lib/axn/version.rb +1 -1
- metadata +9 -3
- data/lib/action/core/event_handlers.rb +0 -62
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 57d67670bb1cf60960aa2bb8234a5894a611801b69a89957057bfffd07ace8cd
|
4
|
+
data.tar.gz: 9e0025cbe4bc367e1351ea867d630b9fdb3d706547d676b9cd2162e8fda0ef38
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 70cc7c2851d95956278d43c746190dd5173ddda62a9a931ae9538374cf75b72523cc7f9b3524a6df6b8b8d57c969fb5510c07fad56b36312130ae86c59e48a15
|
7
|
+
data.tar.gz: f3f961781373a05284ddc54072baaf13998d235528110bf482c221e5f27d6142ab261516d8237bd0689d1161887eef6b567e8ecb4688d400a20ec32d1ae33837
|
data/CHANGELOG.md
CHANGED
@@ -1,7 +1,18 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
-
##
|
4
|
-
*
|
3
|
+
## 0.1.0-alpha.2.7.1
|
4
|
+
* [FEAT] Implemented symbol method handler support for callbacks
|
5
|
+
|
6
|
+
## 0.1.0-alpha.2.7
|
7
|
+
* [BREAKING] Replaced `messages` declaration with separate `success` and `error` calls
|
8
|
+
* [BREAKING] Removed `rescues` method (use `error_from` for custom error messages; all exceptions now report to `on_exception` handlers)
|
9
|
+
* [BREAKING] Replaced `error_from` with an optional `if:` argument on `error`
|
10
|
+
* [FEAT] Implemented conditional success message filtering as well
|
11
|
+
* [FEAT] Added block support for `error` and `success`
|
12
|
+
* [FEAT] `if:` now supports symbol predicates referencing instance methods (arity 0, 1, or keyword `exception:`). If the method accepts `exception:` it is passed as a keyword; else if it accepts one positional arg, it is passed positionally; otherwise it is called with no args. If the method is missing, the symbol falls back to constant lookup (e.g., `:ArgumentError`).
|
13
|
+
* [FEAT] `success`/`error` now accept symbol method names (e.g., `success :local_method`). Handlers can receive the exception via keyword (`exception:`) or single positional argument; otherwise they are called with no args.
|
14
|
+
* [BREAKING] Updated callback methods (`on_success`, `on_error`, `on_failure`, `on_exception`) to use consistent `if:` interface (matching messages)
|
15
|
+
* [FEAT] Added `unless:` support to both `success`/`error` messages and callbacks (`on_success`, `on_error`, `on_failure`, `on_exception`)
|
5
16
|
|
6
17
|
## 0.1.0-alpha.2.6.1
|
7
18
|
* [FEAT] Added `elapsed_time` and `outcome` methods to `Action::Result`
|
@@ -13,7 +13,7 @@ Every `call` invocation on an Action will return an `Action::Result` instance, w
|
|
13
13
|
| `elapsed_time` | Execution time in milliseconds (Float)
|
14
14
|
| any `expose`d values | guaranteed to be set if `ok?` (since they have outgoing presence validations by default; any missing would have failed the action)
|
15
15
|
|
16
|
-
NOTE: `success` and `error` (and so implicitly `message`) can be configured per-action via [the `
|
16
|
+
NOTE: `success` and `error` (and so implicitly `message`) can be configured per-action via [the `success` and `error` declarations](/reference/class#success-and-error).
|
17
17
|
|
18
18
|
### Clarification of exposed values
|
19
19
|
|
data/docs/reference/class.md
CHANGED
@@ -85,48 +85,148 @@ expects :date, type: Date, preprocess: ->(d) { d.is_a?(Date) ? d : Date.parse(d)
|
|
85
85
|
|
86
86
|
will succeed if given _either_ an actual Date object _or_ a string that Date.parse can convert into one. If the preprocess callable raises an exception, that'll be swallowed and the action failed.
|
87
87
|
|
88
|
-
## `.
|
88
|
+
## `.success` and `.error`
|
89
89
|
|
90
|
-
The `
|
90
|
+
The `success` and `error` declarations allow you to customize the `error` and `success` messages on the returned result.
|
91
91
|
|
92
|
-
|
92
|
+
Both methods accept a string (returned directly), a symbol (resolved as a local instance method on the action), or a block (evaluated in the action's context, so can access instance methods and variables).
|
93
93
|
|
94
|
-
|
94
|
+
When an exception is available (e.g., during `error`), handlers can receive it in either of two equivalent ways:
|
95
|
+
- Keyword form: accept `exception:` and it will be passed as a keyword
|
96
|
+
- Positional form: if the handler accepts a single positional argument, it will be passed positionally
|
97
|
+
|
98
|
+
This applies to both blocks and symbol-backed instance methods. Choose the style that best fits your codebase (clarity vs concision).
|
99
|
+
|
100
|
+
In callables and symbol-backed methods, you can access:
|
95
101
|
- **Input data**: Use field names directly (e.g., `name`)
|
96
102
|
- **Output data**: Use `result.field` pattern (e.g., `result.greeting`)
|
97
103
|
- **Instance methods and variables**: Direct access
|
98
104
|
|
99
105
|
```ruby
|
100
|
-
|
101
|
-
|
106
|
+
success { "Hello #{name}, your greeting: #{result.greeting}" }
|
107
|
+
error { |e| "Bad news: #{e.message}" }
|
108
|
+
error { |exception:| "Bad news: #{exception.message}" }
|
109
|
+
|
110
|
+
# Using symbol method names
|
111
|
+
success :build_success_message
|
112
|
+
error :build_error_message
|
113
|
+
|
114
|
+
def build_success_message
|
115
|
+
"Hello #{name}, your greeting: #{result.greeting}"
|
116
|
+
end
|
117
|
+
|
118
|
+
def build_error_message(e)
|
119
|
+
"Bad news: #{e.message}"
|
120
|
+
end
|
121
|
+
|
122
|
+
def build_error_message(exception:)
|
123
|
+
"Bad news: #{exception.message}"
|
124
|
+
end
|
102
125
|
```
|
103
126
|
|
104
|
-
##
|
127
|
+
## Conditional messages
|
105
128
|
|
106
|
-
While `.
|
129
|
+
While `.error` and `.success` set the default messages, you can register conditional messages using an optional `if:` or `unless:` matcher. The matcher can be:
|
107
130
|
|
108
|
-
|
131
|
+
- an exception class (e.g., `ArgumentError`)
|
132
|
+
- a class name string (e.g., `"Action::InboundValidationError"`)
|
133
|
+
- a symbol referencing a local instance method predicate (arity 0 or 1, or keyword `exception:`), e.g. `:bad_input?`
|
134
|
+
- a callable (arity 0 or 1, or keyword `exception:`)
|
109
135
|
|
110
|
-
|
136
|
+
Symbols are resolved as methods on the action instance. If the method accepts `exception:` it will be passed as a keyword; otherwise, if it accepts one positional argument, the raised exception is passed positionally; otherwise it is called with no arguments. If the action does not respond to the symbol, we fall back to constant lookup (e.g., `if: :ArgumentError` behaves like `if: ArgumentError`). Symbols are also supported for the message itself (e.g., `success :method_name`), resolved via the same rules.
|
111
137
|
|
112
138
|
```ruby
|
113
|
-
|
139
|
+
error "bad"
|
140
|
+
|
141
|
+
# Custom message with exception class matcher
|
142
|
+
error "Invalid params provided", if: ActiveRecord::InvalidRecord
|
143
|
+
|
144
|
+
# Custom message with callable matcher and message
|
145
|
+
error(if: ArgumentError) { |e| "Argument error: #{e.message}" }
|
146
|
+
error(if: -> { name == "bad" }) { "Bad input #{name}, result: #{result.status}" }
|
147
|
+
|
148
|
+
# Custom message with symbol predicate (arity 0)
|
149
|
+
error "Transient error, please retry", if: :transient_error?
|
150
|
+
|
151
|
+
def transient_error?
|
152
|
+
# local decision based on inputs/outputs
|
153
|
+
name == "temporary"
|
154
|
+
end
|
155
|
+
|
156
|
+
# Symbol predicate (arity 1), receives the exception
|
157
|
+
error(if: :argument_error?) { |e| "Bad argument: #{e.message}" }
|
158
|
+
|
159
|
+
def argument_error?(e)
|
160
|
+
e.is_a?(ArgumentError)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Symbol predicate (keyword), receives the exception via keyword
|
164
|
+
error(if: :argument_error_kw?) { |exception:| "Bad argument: #{exception.message}" }
|
165
|
+
|
166
|
+
def argument_error_kw?(exception:)
|
167
|
+
exception.is_a?(ArgumentError)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Lambda predicate with keyword
|
171
|
+
error "AE", if: ->(exception:) { exception.is_a?(ArgumentError) }
|
172
|
+
|
173
|
+
# Using unless: for inverse logic
|
174
|
+
error "Custom error", unless: :should_skip?
|
175
|
+
|
176
|
+
def should_skip?
|
177
|
+
# local decision based on inputs/outputs
|
178
|
+
name == "temporary"
|
179
|
+
end
|
180
|
+
|
181
|
+
::: warning
|
182
|
+
You cannot use both `if:` and `unless:` for the same message - this will raise an `ArgumentError`.
|
183
|
+
:::
|
184
|
+
|
185
|
+
### Message ordering and inheritance
|
114
186
|
|
115
|
-
|
116
|
-
rescues ActiveRecord::InvalidRecord => "Invalid params provided"
|
187
|
+
Messages are evaluated in **last-defined-first** order, meaning the most recently defined message that matches its conditions will be used. This applies to both success and error messages:
|
117
188
|
|
118
|
-
|
119
|
-
|
120
|
-
|
189
|
+
```ruby
|
190
|
+
class ParentAction
|
191
|
+
include Action
|
192
|
+
|
193
|
+
success "Parent success message"
|
194
|
+
error "Parent error message"
|
195
|
+
end
|
196
|
+
|
197
|
+
class ChildAction < ParentAction
|
198
|
+
success "Child success message" # This will be used when action succeeds
|
199
|
+
error "Child error message" # This will be used when action fails
|
200
|
+
end
|
201
|
+
```
|
202
|
+
|
203
|
+
Within a single class, later definitions override earlier ones:
|
204
|
+
|
205
|
+
```ruby
|
206
|
+
class MyAction
|
207
|
+
include Action
|
208
|
+
|
209
|
+
success "First success message" # Ignored
|
210
|
+
success "Second success message" # Ignored
|
211
|
+
success "Final success message" # This will be used
|
212
|
+
|
213
|
+
error "First error message" # Ignored
|
214
|
+
error "Second error message" # Ignored
|
215
|
+
error "Final error message" # This will be used
|
216
|
+
end
|
217
|
+
```
|
218
|
+
|
219
|
+
When using conditional messages, the system evaluates handlers in the order defined above until it finds one that matches and doesn't raise an exception. If a handler raises an exception, it falls back to the next matching handler, then to static messages, and finally to the default message.
|
121
220
|
```
|
122
221
|
|
123
222
|
## Callbacks
|
124
223
|
|
125
224
|
In addition to the [global exception handler](/reference/configuration#on-exception), a number of custom callback are available for you as well, if you want to take specific actions when a given Axn succeeds or fails.
|
126
225
|
|
127
|
-
:::
|
128
|
-
*
|
129
|
-
|
226
|
+
::: tip Callback Ordering
|
227
|
+
* Callbacks are executed in **last-defined-first** order, similar to messages
|
228
|
+
* Child class callbacks execute before parent class callbacks
|
229
|
+
* Multiple matching callbacks of the same type will *all* execute
|
130
230
|
:::
|
131
231
|
|
132
232
|
|
@@ -135,6 +235,16 @@ In addition to the [global exception handler](/reference/configuration#on-except
|
|
135
235
|
* *Callbacks* (defined below) are executed _after_ the `call` -- exceptions or `fail!`s here will _not_ change `result.ok?`
|
136
236
|
:::
|
137
237
|
|
238
|
+
|
239
|
+
**Note:** Symbol method handlers for all callback types follow the same argument pattern as [message handlers](#conditional-messages):
|
240
|
+
- If the method accepts `exception:` as a keyword, the exception is passed as a keyword
|
241
|
+
- If the method accepts one positional argument, the exception is passed positionally
|
242
|
+
- Otherwise, the method is called with no arguments
|
243
|
+
|
244
|
+
::: warning
|
245
|
+
You cannot use both `if:` and `unless:` for the same callback - this will raise an `ArgumentError`.
|
246
|
+
:::
|
247
|
+
|
138
248
|
### `on_success`
|
139
249
|
|
140
250
|
This is triggered after the Axn completes, if it was successful. Difference from `after`: if the given block raises an error, this WILL be reported to the global exception handler, but will NOT change `ok?` to false.
|
@@ -161,22 +271,33 @@ class Foo
|
|
161
271
|
end
|
162
272
|
```
|
163
273
|
|
164
|
-
Note that by default the `on_exception` block will be applied to _any_ `StandardError` that is raised, but you can specify a matcher using the same logic as for
|
274
|
+
Note that by default the `on_exception` block will be applied to _any_ `StandardError` that is raised, but you can specify a matcher using the same logic as for conditional messages (`if:` or `unless:`):
|
165
275
|
|
166
276
|
```ruby
|
167
277
|
class Foo
|
168
278
|
include Action
|
169
279
|
|
170
|
-
on_exception NoMethodError do |exception| # [!code focus]
|
280
|
+
on_exception(if: NoMethodError) do |exception| # [!code focus]
|
171
281
|
# e.g. trigger a slack error
|
172
282
|
end
|
173
283
|
|
174
|
-
|
284
|
+
on_exception(unless: :transient_error?) do |exception| # [!code focus]
|
285
|
+
# e.g. trigger a slack error for non-transient errors
|
286
|
+
end
|
287
|
+
|
288
|
+
def transient_error?
|
289
|
+
# local decision based on inputs/outputs
|
290
|
+
name == "temporary"
|
291
|
+
end
|
292
|
+
|
293
|
+
on_exception(if: ->(e) { e.is_a?(ZeroDivisionError) }) do # [!code focus]
|
175
294
|
# e.g. trigger a slack error
|
176
295
|
end
|
177
296
|
end
|
178
297
|
```
|
179
298
|
|
299
|
+
|
180
300
|
If multiple `on_exception` handlers are provided, ALL that match the raised exception will be triggered in the order provided.
|
181
301
|
|
182
302
|
The _global_ handler will be triggered _after_ all class-specific handlers.
|
303
|
+
|
@@ -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.
|
@@ -92,7 +87,6 @@ A couple notes:
|
|
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.
|
@@ -115,6 +109,10 @@ For a practical example of this in practice, see [our 'memoization' recipe](/rec
|
|
115
109
|
|
116
110
|
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
111
|
|
112
|
+
## `env`
|
113
|
+
|
114
|
+
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).
|
115
|
+
|
118
116
|
## Automatic Logging
|
119
117
|
|
120
118
|
By default, every `action.call` will emit log lines when it is called and after it completes:
|
@@ -148,3 +146,37 @@ end
|
|
148
146
|
```
|
149
147
|
|
150
148
|
The `auto_log` method supports inheritance, so subclasses will inherit the setting from their parent class unless explicitly overridden.
|
149
|
+
|
150
|
+
## Complete Configuration Example
|
151
|
+
|
152
|
+
Here's a complete example showing all available configuration options:
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
Action.configure do |c|
|
156
|
+
# Logging
|
157
|
+
c.log_level = :info
|
158
|
+
c.logger = Rails.logger
|
159
|
+
|
160
|
+
# Exception handling
|
161
|
+
c.on_exception = proc do |e, action:, context:|
|
162
|
+
message = "[#{action.class.name}] Failing due to #{e.class.name}: #{e.message}"
|
163
|
+
Rails.logger.warn(message)
|
164
|
+
Honeybadger.notify(message, context: { axn_context: context })
|
165
|
+
end
|
166
|
+
|
167
|
+
# Observability
|
168
|
+
c.wrap_with_trace = proc do |resource, &action|
|
169
|
+
Datadog::Tracing.trace("Action", resource:) do
|
170
|
+
action.call
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
c.emit_metrics = proc do |resource, result|
|
175
|
+
Datadog::Metrics.increment("action.#{resource.underscore}", tags: { outcome: result.outcome })
|
176
|
+
Datadog::Metrics.histogram("action.duration", result.elapsed_time, tags: { resource: })
|
177
|
+
end
|
178
|
+
|
179
|
+
# Global includes
|
180
|
+
c.additional_includes = [MyCustomModule]
|
181
|
+
end
|
182
|
+
```
|
data/docs/reference/instance.md
CHANGED
@@ -63,7 +63,7 @@ NOTE: expects a single action call in the block -- if there are multiple calls,
|
|
63
63
|
|
64
64
|
::: tip Versus `call!`
|
65
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_ `
|
66
|
+
* Note this passes _child_ exception into _parent_ `error` message parsing.
|
67
67
|
* If you want _the child's_ `result.error` to become the _parent's_ `result.error` on failure, use `hoist_errors` + `call`
|
68
68
|
:::
|
69
69
|
|
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"
|
@@ -35,35 +35,5 @@ module Action
|
|
35
35
|
def action_name = @action.class.name.presence || "The action"
|
36
36
|
|
37
37
|
def context_data_source = raise NotImplementedError
|
38
|
-
|
39
|
-
def determine_error_message(only_default: false)
|
40
|
-
return @context.error_from_user if @context.error_from_user.present?
|
41
|
-
|
42
|
-
# We need an exception for interceptors, and also in case the messages.error callable expects an argument
|
43
|
-
exception = @context.exception || Action::Failure.new
|
44
|
-
|
45
|
-
msg = action._error_msg
|
46
|
-
|
47
|
-
unless only_default
|
48
|
-
interceptor = action.class._error_interceptor_for(exception:, action:)
|
49
|
-
msg = interceptor.message if interceptor
|
50
|
-
end
|
51
|
-
|
52
|
-
stringified(msg, exception:).presence || "Something went wrong"
|
53
|
-
end
|
54
|
-
|
55
|
-
# Allow for callable OR string messages
|
56
|
-
def stringified(msg, exception: nil)
|
57
|
-
return msg.presence unless msg.respond_to?(:call)
|
58
|
-
|
59
|
-
# The error message callable can take the exception as an argument
|
60
|
-
if exception && msg.arity == 1
|
61
|
-
action.instance_exec(exception, &msg)
|
62
|
-
else
|
63
|
-
action.instance_exec(&msg)
|
64
|
-
end
|
65
|
-
rescue StandardError => e
|
66
|
-
Axn::Util.piping_error("determining message callable", action:, exception: e)
|
67
|
-
end
|
68
38
|
end
|
69
39
|
end
|
@@ -5,9 +5,15 @@ require "action/core/context/facade"
|
|
5
5
|
module Action
|
6
6
|
# Inbound / Internal ContextFacade
|
7
7
|
class InternalContext < ContextFacade
|
8
|
-
#
|
8
|
+
# Available for use from within message callables
|
9
9
|
def default_error
|
10
|
-
|
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"
|
11
17
|
end
|
12
18
|
|
13
19
|
private
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "action/core/
|
3
|
+
require "action/core/flow/handlers"
|
4
4
|
|
5
5
|
module Action
|
6
6
|
module Core
|
@@ -8,44 +8,45 @@ module Action
|
|
8
8
|
module Callbacks
|
9
9
|
def self.included(base)
|
10
10
|
base.class_eval do
|
11
|
-
class_attribute :
|
12
|
-
class_attribute :_exception_handlers, default: []
|
13
|
-
class_attribute :_failure_handlers, default: []
|
14
|
-
class_attribute :_success_handlers, default: []
|
11
|
+
class_attribute :_callbacks_registry, default: Action::Core::Flow::Handlers::Registry.empty
|
15
12
|
|
16
13
|
extend ClassMethods
|
17
14
|
end
|
18
15
|
end
|
19
16
|
|
20
17
|
module ClassMethods
|
21
|
-
#
|
22
|
-
def
|
23
|
-
|
24
|
-
|
25
|
-
|
18
|
+
# Internal dispatcher
|
19
|
+
def _dispatch_callbacks(event_type, action:, exception: nil)
|
20
|
+
_callbacks_registry.for(event_type).each do |handler|
|
21
|
+
handler.apply(action:, exception:)
|
22
|
+
end
|
26
23
|
end
|
27
24
|
|
28
|
-
# ONLY raised
|
29
|
-
def
|
30
|
-
raise ArgumentError, "on_failure must be called with a block" unless block_given?
|
25
|
+
# ONLY raised exceptions (i.e. NOT fail!).
|
26
|
+
def on_exception(handler = nil, **, &block) = _add_callback(:exception, handler:, **, block:)
|
31
27
|
|
32
|
-
|
33
|
-
|
28
|
+
# ONLY raised on fail! (i.e. NOT unhandled exceptions).
|
29
|
+
def on_failure(handler = nil, **, &block) = _add_callback(:failure, handler:, **, block:)
|
34
30
|
|
35
|
-
# Handles both fail! and unhandled exceptions
|
36
|
-
def on_error(
|
37
|
-
raise ArgumentError, "on_error must be called with a block" unless block_given?
|
38
|
-
|
39
|
-
self._error_handlers += [Action::EventHandlers::ConditionalHandler.new(matcher:, handler:)]
|
40
|
-
end
|
31
|
+
# Handles both fail! and unhandled exceptions
|
32
|
+
def on_error(handler = nil, **, &block) = _add_callback(:error, handler:, **, block:)
|
41
33
|
|
42
34
|
# Executes when the action completes successfully (after all after hooks complete successfully)
|
43
35
|
# Runs in child-first order (child handlers before parent handlers)
|
44
|
-
def on_success(&handler)
|
45
|
-
|
36
|
+
def on_success(handler = nil, **, &block) = _add_callback(:success, handler:, **, block:)
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def _add_callback(event_type, handler: nil, block: nil, **kwargs)
|
41
|
+
raise ArgumentError, "on_#{event_type} cannot be called with both :if and :unless" if kwargs.key?(:if) && kwargs.key?(:unless)
|
42
|
+
|
43
|
+
condition = kwargs.key?(:if) ? kwargs[:if] : kwargs[:unless]
|
44
|
+
raise ArgumentError, "on_#{event_type} must be called with a block or symbol" unless block || handler
|
46
45
|
|
47
|
-
|
48
|
-
|
46
|
+
callback_handler = block || handler
|
47
|
+
matcher = condition.nil? ? nil : Action::Core::Flow::Handlers::Matcher.new(condition, invert: kwargs.key?(:unless))
|
48
|
+
entry = Action::Core::Flow::Handlers::CallbackHandler.new(matcher:, handler: callback_handler)
|
49
|
+
self._callbacks_registry = _callbacks_registry.register(event_type:, entry:)
|
49
50
|
end
|
50
51
|
end
|
51
52
|
end
|
@@ -9,13 +9,8 @@ module Action
|
|
9
9
|
include InstanceMethods
|
10
10
|
|
11
11
|
def _trigger_on_exception(exception)
|
12
|
-
interceptor = self.class._error_interceptor_for(exception:, action: self)
|
13
|
-
return if interceptor&.should_report_error == false
|
14
|
-
|
15
12
|
# Call any handlers registered on *this specific action* class
|
16
|
-
self.class.
|
17
|
-
handler.execute_if_matches(exception:, action: self)
|
18
|
-
end
|
13
|
+
self.class._dispatch_callbacks(:exception, action: self, exception:)
|
19
14
|
|
20
15
|
# Call any global handlers
|
21
16
|
Action.config.on_exception(exception, action: self, context: context_for_logging)
|
@@ -27,12 +22,7 @@ module Action
|
|
27
22
|
|
28
23
|
def _trigger_on_success
|
29
24
|
# Call success handlers in child-first order (like after hooks)
|
30
|
-
self.class.
|
31
|
-
instance_exec(&handler)
|
32
|
-
rescue StandardError => e
|
33
|
-
# Log the error but continue with other handlers
|
34
|
-
Axn::Util.piping_error("executing on_success hook", action: self, exception: e)
|
35
|
-
end
|
25
|
+
self.class._dispatch_callbacks(:success, action: self, exception: nil)
|
36
26
|
end
|
37
27
|
end
|
38
28
|
end
|
@@ -44,17 +34,13 @@ module Action
|
|
44
34
|
yield
|
45
35
|
rescue StandardError => e
|
46
36
|
# on_error handlers run for both unhandled exceptions and fail!
|
47
|
-
self.class.
|
48
|
-
handler.execute_if_matches(exception: e, action: self)
|
49
|
-
end
|
37
|
+
self.class._dispatch_callbacks(:error, action: self, exception: e)
|
50
38
|
|
51
39
|
# on_failure handlers run ONLY for fail!
|
52
40
|
if e.is_a?(Action::Failure)
|
53
|
-
self.class.
|
54
|
-
handler.execute_if_matches(exception: e, action: self)
|
55
|
-
end
|
41
|
+
self.class._dispatch_callbacks(:failure, action: self, exception: e)
|
56
42
|
else
|
57
|
-
# on_exception handlers run for ONLY for unhandled exceptions.
|
43
|
+
# on_exception handlers run for ONLY for unhandled exceptions.
|
58
44
|
_trigger_on_exception(e)
|
59
45
|
|
60
46
|
@__context.exception = e
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action/core/flow/handlers/matcher"
|
4
|
+
|
5
|
+
module Action
|
6
|
+
module Core
|
7
|
+
module Flow
|
8
|
+
# "Handlers" doesn't feel like *quite* the right name for this, but basically things in this namespace
|
9
|
+
# relate to conditionally-invoked code blocks (e.g. callbacks, messages, etc.)
|
10
|
+
module Handlers
|
11
|
+
class BaseHandler
|
12
|
+
def initialize(matcher: nil, handler: nil)
|
13
|
+
@matcher = matcher
|
14
|
+
@handler = handler
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :handler
|
18
|
+
|
19
|
+
def static? = @matcher.nil?
|
20
|
+
|
21
|
+
def matches?(action:, exception:)
|
22
|
+
return true if static?
|
23
|
+
|
24
|
+
@matcher.call(exception:, action:)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Subclasses should implement `apply(action:, exception:)`
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action/core/flow/handlers/base_handler"
|
4
|
+
require "action/core/flow/handlers/invoker"
|
5
|
+
|
6
|
+
module Action
|
7
|
+
module Core
|
8
|
+
module Flow
|
9
|
+
module Handlers
|
10
|
+
class CallbackHandler < BaseHandler
|
11
|
+
def apply(action:, exception:)
|
12
|
+
return false unless matches?(action:, exception:)
|
13
|
+
|
14
|
+
Invoker.call(action:, handler:, exception:, operation: "executing handler")
|
15
|
+
true
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Action
|
4
|
+
module Core
|
5
|
+
module Flow
|
6
|
+
module Handlers
|
7
|
+
# Shared block evaluation with consistent arity handling and error piping
|
8
|
+
module Invoker
|
9
|
+
extend self
|
10
|
+
|
11
|
+
def call(action:, handler:, exception: nil, operation: "executing handler")
|
12
|
+
return call_symbol_handler(action:, symbol: handler, exception:) if symbol?(handler)
|
13
|
+
return call_callable_handler(action:, callable: handler, exception:) if callable?(handler)
|
14
|
+
|
15
|
+
literal_value(handler)
|
16
|
+
rescue StandardError => e
|
17
|
+
Axn::Util.piping_error(operation, action:, exception: e)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Shared introspection helpers
|
21
|
+
def accepts_exception_keyword?(callable_or_method)
|
22
|
+
return false unless callable_or_method.respond_to?(:parameters)
|
23
|
+
|
24
|
+
params = callable_or_method.parameters
|
25
|
+
params.any? { |type, name| %i[keyreq key].include?(type) && name == :exception } ||
|
26
|
+
params.any? { |type, _| type == :keyrest }
|
27
|
+
end
|
28
|
+
|
29
|
+
def accepts_positional_exception?(callable_or_method)
|
30
|
+
return false unless callable_or_method.respond_to?(:arity)
|
31
|
+
|
32
|
+
arity = callable_or_method.arity
|
33
|
+
arity == 1 || arity.negative?
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def symbol?(value) = value.is_a?(Symbol)
|
39
|
+
|
40
|
+
def callable?(value) = value.respond_to?(:arity)
|
41
|
+
|
42
|
+
def call_symbol_handler(action:, symbol:, exception: nil)
|
43
|
+
unless action.respond_to?(symbol)
|
44
|
+
action.warn("Ignoring apparently-invalid symbol #{symbol.inspect} -- action does not respond to method")
|
45
|
+
return nil
|
46
|
+
end
|
47
|
+
|
48
|
+
method = action.method(symbol)
|
49
|
+
if exception && accepts_exception_keyword?(method)
|
50
|
+
action.public_send(symbol, exception:)
|
51
|
+
elsif exception && accepts_positional_exception?(method)
|
52
|
+
action.public_send(symbol, exception)
|
53
|
+
else
|
54
|
+
action.public_send(symbol)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def call_callable_handler(action:, callable:, exception: nil)
|
59
|
+
if exception && accepts_exception_keyword?(callable)
|
60
|
+
action.instance_exec(exception:, &callable)
|
61
|
+
elsif exception && accepts_positional_exception?(callable)
|
62
|
+
action.instance_exec(exception, &callable)
|
63
|
+
else
|
64
|
+
action.instance_exec(&callable)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def literal_value(value) = value
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action/core/flow/handlers/invoker"
|
4
|
+
|
5
|
+
module Action
|
6
|
+
module Core
|
7
|
+
module Flow
|
8
|
+
module Handlers
|
9
|
+
class Matcher
|
10
|
+
def initialize(rule, invert: false)
|
11
|
+
@rule = rule
|
12
|
+
@invert = invert
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(exception:, action:)
|
16
|
+
@invert ? !matches?(exception:, action:) : matches?(exception:, action:)
|
17
|
+
rescue StandardError => e
|
18
|
+
Axn::Util.piping_error("determining if handler applies to exception", action:, exception: e)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def matches?(exception:, action:)
|
24
|
+
return apply_callable(action:, exception:) if callable?
|
25
|
+
return apply_symbol(action:, exception:) if symbol?
|
26
|
+
return apply_string(exception:) if string?
|
27
|
+
return apply_exception_class(exception:) if exception_class?
|
28
|
+
|
29
|
+
handle_invalid(action:)
|
30
|
+
end
|
31
|
+
|
32
|
+
def callable? = @rule.respond_to?(:call)
|
33
|
+
def symbol? = @rule.is_a?(Symbol)
|
34
|
+
def string? = @rule.is_a?(String)
|
35
|
+
def exception_class? = @rule.is_a?(Class) && @rule <= Exception
|
36
|
+
|
37
|
+
def apply_callable(action:, exception:)
|
38
|
+
if exception && Invoker.accepts_exception_keyword?(@rule)
|
39
|
+
!!action.instance_exec(exception:, &@rule)
|
40
|
+
elsif exception && Invoker.accepts_positional_exception?(@rule)
|
41
|
+
!!action.instance_exec(exception, &@rule)
|
42
|
+
else
|
43
|
+
!!action.instance_exec(&@rule)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def apply_symbol(action:, exception:)
|
48
|
+
if action.respond_to?(@rule)
|
49
|
+
method = action.method(@rule)
|
50
|
+
if exception && Invoker.accepts_exception_keyword?(method)
|
51
|
+
!!action.public_send(@rule, exception:)
|
52
|
+
elsif exception && Invoker.accepts_positional_exception?(method)
|
53
|
+
!!action.public_send(@rule, exception)
|
54
|
+
else
|
55
|
+
!!action.public_send(@rule)
|
56
|
+
end
|
57
|
+
else
|
58
|
+
begin
|
59
|
+
klass = Object.const_get(@rule.to_s)
|
60
|
+
klass && exception.is_a?(klass)
|
61
|
+
rescue NameError
|
62
|
+
action.warn("Ignoring apparently-invalid matcher #{@rule.inspect} -- neither action method nor constant found")
|
63
|
+
false
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def apply_string(exception:)
|
69
|
+
klass = Object.const_get(@rule.to_s)
|
70
|
+
klass && exception.is_a?(klass)
|
71
|
+
end
|
72
|
+
|
73
|
+
def apply_exception_class(exception:)
|
74
|
+
exception.is_a?(@rule)
|
75
|
+
end
|
76
|
+
|
77
|
+
def handle_invalid(action:)
|
78
|
+
action.warn("Ignoring apparently-invalid matcher #{@rule.inspect} -- could not find way to apply it")
|
79
|
+
false
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action/core/flow/handlers/base_handler"
|
4
|
+
require "action/core/flow/handlers/invoker"
|
5
|
+
|
6
|
+
module Action
|
7
|
+
module Core
|
8
|
+
module Flow
|
9
|
+
module Handlers
|
10
|
+
class MessageHandler < BaseHandler
|
11
|
+
# Returns a string (truthy) when it applies and yields a non-blank message; otherwise nil
|
12
|
+
def apply(action:, exception:)
|
13
|
+
return nil unless matches?(action:, exception:)
|
14
|
+
|
15
|
+
value =
|
16
|
+
if handler.is_a?(Symbol) || handler.respond_to?(:call)
|
17
|
+
Invoker.call(action:, handler:, exception:, operation: "determining message callable")
|
18
|
+
else
|
19
|
+
handler
|
20
|
+
end
|
21
|
+
value.respond_to?(:presence) ? value.presence : value
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Action
|
4
|
+
module Core
|
5
|
+
module Flow
|
6
|
+
module Handlers
|
7
|
+
# Small, immutable, copy-on-write registry keyed by event_type.
|
8
|
+
# Stores arrays of entries (handlers/interceptors) in insertion order.
|
9
|
+
class Registry
|
10
|
+
def self.empty = new({})
|
11
|
+
|
12
|
+
def initialize(index)
|
13
|
+
# Freeze arrays and the index for immutability
|
14
|
+
@index = index.transform_values { |arr| Array(arr).freeze }.freeze
|
15
|
+
end
|
16
|
+
|
17
|
+
# Always register most-recent-first (last-defined wins). Simpler mental model.
|
18
|
+
def register(event_type:, entry:)
|
19
|
+
key = event_type.to_sym
|
20
|
+
existing = Array(@index[key])
|
21
|
+
updated = [entry] + existing
|
22
|
+
self.class.new(@index.merge(key => updated.freeze))
|
23
|
+
end
|
24
|
+
|
25
|
+
def for(event_type)
|
26
|
+
Array(@index[event_type.to_sym])
|
27
|
+
end
|
28
|
+
|
29
|
+
def empty?
|
30
|
+
@index.empty?
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
attr_reader :index
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Action
|
4
|
+
module Core
|
5
|
+
module Flow
|
6
|
+
module Handlers
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
require "action/core/flow/handlers/invoker"
|
13
|
+
require "action/core/flow/handlers/registry"
|
14
|
+
require "action/core/flow/handlers/matcher"
|
15
|
+
require "action/core/flow/handlers/base_handler"
|
16
|
+
require "action/core/flow/handlers/callback_handler"
|
17
|
+
require "action/core/flow/handlers/message_handler"
|
@@ -1,13 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "action/core/flow/handlers"
|
4
|
+
|
3
5
|
module Action
|
4
6
|
module Core
|
5
7
|
module Flow
|
6
8
|
module Messages
|
7
9
|
def self.included(base)
|
8
10
|
base.class_eval do
|
9
|
-
class_attribute :
|
10
|
-
class_attribute :_custom_error_interceptors, default: []
|
11
|
+
class_attribute :_messages_registry, default: Action::Core::Flow::Handlers::Registry.empty
|
11
12
|
|
12
13
|
extend ClassMethods
|
13
14
|
include InstanceMethods
|
@@ -15,45 +16,58 @@ module Action
|
|
15
16
|
end
|
16
17
|
|
17
18
|
module ClassMethods
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
true
|
19
|
+
# Internal: resolve a message for the given event (conditional first, then static)
|
20
|
+
def _message_for(event_type, action:, exception: nil)
|
21
|
+
_conditional_message_for(event_type, action:, exception:) ||
|
22
|
+
_static_message_for(event_type, action:, exception:)
|
23
23
|
end
|
24
24
|
|
25
|
-
def
|
26
|
-
|
25
|
+
def _conditional_message_for(event_type, action:, exception: nil)
|
26
|
+
_messages_registry.for(event_type).each do |handler|
|
27
|
+
next if handler.respond_to?(:static?) && handler.static?
|
28
|
+
|
29
|
+
msg = handler.apply(action:, exception:)
|
30
|
+
return msg if msg.present?
|
31
|
+
end
|
32
|
+
nil
|
27
33
|
end
|
28
34
|
|
29
|
-
def
|
30
|
-
|
35
|
+
def _static_message_for(event_type, action:, exception: nil)
|
36
|
+
_messages_registry.for(event_type).each do |handler|
|
37
|
+
next unless handler.respond_to?(:static?) && handler.static?
|
38
|
+
|
39
|
+
msg = handler.apply(action:, exception:)
|
40
|
+
return msg if msg.present?
|
41
|
+
end
|
42
|
+
nil
|
31
43
|
end
|
32
44
|
|
45
|
+
def success(message = nil, **, &) = _add_message(:success, message:, **, &)
|
46
|
+
def error(message = nil, **, &) = _add_message(:error, message:, **, &)
|
47
|
+
|
33
48
|
def default_error = new.internal_context.default_error
|
49
|
+
def default_success = new.internal_context.default_success
|
34
50
|
|
35
|
-
|
51
|
+
private
|
36
52
|
|
37
|
-
def
|
38
|
-
|
39
|
-
int.matches?(exception:, action:)
|
40
|
-
end
|
41
|
-
end
|
53
|
+
def _add_message(kind, message:, **kwargs, &block)
|
54
|
+
raise ArgumentError, "#{kind} cannot be called with both :if and :unless" if kwargs.key?(:if) && kwargs.key?(:unless)
|
42
55
|
|
43
|
-
|
44
|
-
|
45
|
-
raise ArgumentError, "
|
56
|
+
condition = kwargs.key?(:if) ? kwargs[:if] : kwargs[:unless]
|
57
|
+
raise ArgumentError, "Provide either a message or a block, not both" if message && block_given?
|
58
|
+
raise ArgumentError, "Provide a message or a block" unless message || block_given?
|
46
59
|
|
47
|
-
|
48
|
-
Action::EventHandlers::CustomErrorInterceptor.new(matcher:, message:, should_report_error:)
|
49
|
-
end
|
60
|
+
handler = block_given? ? block : message
|
50
61
|
|
51
|
-
|
62
|
+
matcher = condition.nil? ? nil : Action::Core::Flow::Handlers::Matcher.new(condition, invert: kwargs.key?(:unless))
|
63
|
+
entry = Action::Core::Flow::Handlers::MessageHandler.new(matcher:, handler:)
|
64
|
+
self._messages_registry = _messages_registry.register(event_type: kind, entry:)
|
65
|
+
true
|
52
66
|
end
|
53
67
|
end
|
54
68
|
|
55
69
|
module InstanceMethods
|
56
|
-
delegate :default_error, to: :internal_context
|
70
|
+
delegate :default_error, :default_success, to: :internal_context
|
57
71
|
end
|
58
72
|
end
|
59
73
|
end
|
data/lib/action/result.rb
CHANGED
@@ -11,7 +11,7 @@ module Action
|
|
11
11
|
def ok(msg = nil, **exposures)
|
12
12
|
exposes = exposures.keys.to_h { |key| [key, { allow_blank: true }] }
|
13
13
|
|
14
|
-
Axn::Factory.build(exposes:,
|
14
|
+
Axn::Factory.build(exposes:, success: msg) do
|
15
15
|
exposures.each do |key, value|
|
16
16
|
expose(key, value)
|
17
17
|
end
|
@@ -20,13 +20,19 @@ module Action
|
|
20
20
|
|
21
21
|
def error(msg = nil, **exposures, &block)
|
22
22
|
exposes = exposures.keys.to_h { |key| [key, { allow_blank: true }] }
|
23
|
-
rescues = [-> { true }, msg]
|
24
23
|
|
25
|
-
Axn::Factory.build(exposes:,
|
24
|
+
Axn::Factory.build(exposes:, error: msg) do
|
26
25
|
exposures.each do |key, value|
|
27
26
|
expose(key, value)
|
28
27
|
end
|
29
|
-
|
28
|
+
if block_given?
|
29
|
+
begin
|
30
|
+
block.call
|
31
|
+
rescue StandardError => e
|
32
|
+
# Set the exception directly without triggering on_exception handlers
|
33
|
+
@__context.exception = e
|
34
|
+
end
|
35
|
+
end
|
30
36
|
fail!
|
31
37
|
end.call
|
32
38
|
end
|
@@ -47,7 +53,7 @@ module Action
|
|
47
53
|
def success
|
48
54
|
return unless ok?
|
49
55
|
|
50
|
-
|
56
|
+
determine_success_message
|
51
57
|
end
|
52
58
|
|
53
59
|
def ok = success
|
@@ -77,6 +83,19 @@ module Action
|
|
77
83
|
|
78
84
|
def context_data_source = @context.exposed_data
|
79
85
|
|
86
|
+
def determine_error_message
|
87
|
+
return @context.error_from_user if @context.error_from_user.present?
|
88
|
+
|
89
|
+
exception = @context.exception || Action::Failure.new
|
90
|
+
msg = action.class._message_for(:error, action:, exception:)
|
91
|
+
msg.presence || "Something went wrong"
|
92
|
+
end
|
93
|
+
|
94
|
+
def determine_success_message
|
95
|
+
msg = action.class._message_for(:success, action:, exception: nil)
|
96
|
+
msg.presence || "Action completed successfully"
|
97
|
+
end
|
98
|
+
|
80
99
|
def method_missing(method_name, ...) # rubocop:disable Style/MissingRespondToMissing (because we're not actually responding to anything additional)
|
81
100
|
if @context.__combined_data.key?(method_name.to_sym)
|
82
101
|
msg = <<~MSG
|
data/lib/axn/factory.rb
CHANGED
@@ -13,9 +13,8 @@ module Axn
|
|
13
13
|
# Expose standard class-level options
|
14
14
|
exposes: [],
|
15
15
|
expects: [],
|
16
|
-
|
17
|
-
|
18
|
-
rescues: {},
|
16
|
+
success: nil,
|
17
|
+
error: nil,
|
19
18
|
|
20
19
|
# Hooks
|
21
20
|
before: nil,
|
@@ -73,6 +72,17 @@ module Axn
|
|
73
72
|
expose(expose_return_as => retval) if expose_return_as.present?
|
74
73
|
end
|
75
74
|
end.tap do |axn|
|
75
|
+
apply_message = lambda do |kind, value|
|
76
|
+
return unless value.present?
|
77
|
+
|
78
|
+
if value.is_a?(Array) && value.size == 2
|
79
|
+
matcher, msg = value
|
80
|
+
axn.public_send(kind, msg, if: matcher)
|
81
|
+
else
|
82
|
+
axn.public_send(kind, value)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
76
86
|
expects.each do |field, opts|
|
77
87
|
axn.expects(field, **opts)
|
78
88
|
end
|
@@ -81,10 +91,8 @@ module Axn
|
|
81
91
|
axn.exposes(field, **opts)
|
82
92
|
end
|
83
93
|
|
84
|
-
|
85
|
-
|
86
|
-
axn.error_from(**_array_to_hash(error_from)) if error_from.present?
|
87
|
-
axn.rescues(**_array_to_hash(rescues)) if rescues.present?
|
94
|
+
apply_message.call(:success, success)
|
95
|
+
apply_message.call(:error, error)
|
88
96
|
|
89
97
|
# Hooks
|
90
98
|
axn.before(before) if before.present?
|
data/lib/axn/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: axn
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.0.pre.alpha.2.
|
4
|
+
version: 0.1.0.pre.alpha.2.7.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kali Donovan
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-08-
|
11
|
+
date: 2025-08-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -86,10 +86,16 @@ files:
|
|
86
86
|
- lib/action/core/contract.rb
|
87
87
|
- lib/action/core/contract_for_subfields.rb
|
88
88
|
- lib/action/core/contract_validation.rb
|
89
|
-
- lib/action/core/event_handlers.rb
|
90
89
|
- lib/action/core/flow.rb
|
91
90
|
- lib/action/core/flow/callbacks.rb
|
92
91
|
- lib/action/core/flow/exception_execution.rb
|
92
|
+
- lib/action/core/flow/handlers.rb
|
93
|
+
- lib/action/core/flow/handlers/base_handler.rb
|
94
|
+
- lib/action/core/flow/handlers/callback_handler.rb
|
95
|
+
- lib/action/core/flow/handlers/invoker.rb
|
96
|
+
- lib/action/core/flow/handlers/matcher.rb
|
97
|
+
- lib/action/core/flow/handlers/message_handler.rb
|
98
|
+
- lib/action/core/flow/handlers/registry.rb
|
93
99
|
- lib/action/core/flow/messages.rb
|
94
100
|
- lib/action/core/hoist_errors.rb
|
95
101
|
- lib/action/core/hooks.rb
|
@@ -1,62 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Action
|
4
|
-
module EventHandlers
|
5
|
-
class CustomErrorInterceptor
|
6
|
-
def initialize(matcher:, message:, should_report_error:)
|
7
|
-
@matcher = Matcher.new(matcher)
|
8
|
-
@message = message
|
9
|
-
@should_report_error = should_report_error
|
10
|
-
end
|
11
|
-
|
12
|
-
delegate :matches?, to: :@matcher
|
13
|
-
attr_reader :message, :should_report_error
|
14
|
-
end
|
15
|
-
|
16
|
-
class ConditionalHandler
|
17
|
-
def initialize(matcher:, handler:)
|
18
|
-
@matcher = Matcher.new(matcher)
|
19
|
-
@handler = handler
|
20
|
-
end
|
21
|
-
|
22
|
-
delegate :matches?, to: :@matcher
|
23
|
-
|
24
|
-
def execute_if_matches(action:, exception:)
|
25
|
-
return false unless matches?(exception:, action:)
|
26
|
-
|
27
|
-
action.instance_exec(exception, &@handler)
|
28
|
-
true
|
29
|
-
rescue StandardError => e
|
30
|
-
Axn::Util.piping_error("executing handler", action:, exception: e)
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
class Matcher
|
35
|
-
def initialize(matcher)
|
36
|
-
@matcher = matcher
|
37
|
-
end
|
38
|
-
|
39
|
-
def matches?(exception:, action:)
|
40
|
-
if matcher.respond_to?(:call)
|
41
|
-
if matcher.arity == 1
|
42
|
-
!!action.instance_exec(exception, &matcher)
|
43
|
-
else
|
44
|
-
!!action.instance_exec(&matcher)
|
45
|
-
end
|
46
|
-
elsif matcher.is_a?(String) || matcher.is_a?(Symbol)
|
47
|
-
klass = Object.const_get(matcher.to_s)
|
48
|
-
klass && exception.is_a?(klass)
|
49
|
-
elsif matcher < Exception
|
50
|
-
exception.is_a?(matcher)
|
51
|
-
else
|
52
|
-
action.warn("Ignoring apparently-invalid matcher #{matcher.inspect} -- could not find way to apply it")
|
53
|
-
false
|
54
|
-
end
|
55
|
-
rescue StandardError => e
|
56
|
-
Axn::Util.piping_error("determining if handler applies to exception", action:, exception: e)
|
57
|
-
end
|
58
|
-
|
59
|
-
private attr_reader :matcher
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|