axn 0.1.0.pre.alpha.2.3 → 0.1.0.pre.alpha.2.4.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/.rubocop.yml +2 -2
- data/CHANGELOG.md +6 -0
- data/docs/reference/class.md +63 -2
- data/docs/reference/configuration.md +1 -0
- data/docs/usage/writing.md +10 -12
- data/lib/action/core/event_handlers.rb +64 -0
- data/lib/action/core/exceptions.rb +1 -1
- data/lib/action/core/swallow_exceptions.rb +64 -32
- data/lib/axn/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 19221983bd11ff17620830a4807b4080f8ea35dfb07050e2af2e6c1dac1e1b7d
|
4
|
+
data.tar.gz: 7ac5fc8f9ae99bd9a5a189ad53f6113c4c544181ff760bbe923ca1867f011877
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4115cc144bfac9420ff85de1ff9198efd092f63594811aed1afbf3ac5a07a5b73a9017b9d9665aac78e128c312d79e996d8dc2f8710fb30a8b9825a51caa30e5
|
7
|
+
data.tar.gz: 7528abe56dd99066409a59a57f07d04f9b9ea98fd399b62aab7318a762920027eb0870b7895b5f8bff525023a7d754c67f0d8c484661929c8a4dab1a360caf6f
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -3,6 +3,12 @@
|
|
3
3
|
## UNRELEASED
|
4
4
|
* N/A
|
5
5
|
|
6
|
+
## 0.1.0-alpha.2.4.1
|
7
|
+
* [FEAT] Adds full suite of per-Axn callbacks: `on_exception`, `on_failure`, `on_error`, `on_success`
|
8
|
+
|
9
|
+
## 0.1.0-alpha.2.4
|
10
|
+
* [FEAT] Adds per-Axn `on_exception` handlers
|
11
|
+
|
6
12
|
## 0.1.0-alpha.2.3
|
7
13
|
* `expects` / `exposes`: Add `type: :uuid` special case validation
|
8
14
|
* [BUGFIX] Allow `hoist_errors` to pass the result through on success (allow access to subactions' exposures)
|
data/docs/reference/class.md
CHANGED
@@ -63,7 +63,7 @@ messages success: "All good!", error: ->(e) { "Bad news: #{e.message}" }
|
|
63
63
|
|
64
64
|
While `.messages` sets the _default_ error/success messages and is more commonly used, there are times when you want specific error messages for specific failure cases.
|
65
65
|
|
66
|
-
`error_for` and `rescues` both register a matcher (exception class, exception class name (string), or callable) and a message to use if the matcher succeeds. They act exactly the same, except if a matcher registered with `rescues` succeeds, the exception _will not_ trigger the configured global
|
66
|
+
`error_for` and `rescues` both register a matcher (exception class, exception class name (string), or callable) and a message to use if the matcher succeeds. They act exactly the same, except if a matcher registered with `rescues` succeeds, the exception _will not_ trigger the configured exception handlers (global or specific to this class).
|
67
67
|
|
68
68
|
```ruby
|
69
69
|
messages error: "bad"
|
@@ -72,6 +72,67 @@ messages error: "bad"
|
|
72
72
|
rescues ActiveRecord::InvalidRecord => "Invalid params provided"
|
73
73
|
|
74
74
|
# These WILL trigger error handler (second demonstrates callable matcher AND message)
|
75
|
-
error_for ArgumentError, ->(e) { "Argument error: #{e.message}"
|
75
|
+
error_for ArgumentError, ->(e) { "Argument error: #{e.message}" }
|
76
76
|
error_for -> { name == "bad" }, -> { "was given bad name: #{name}" }
|
77
77
|
```
|
78
|
+
|
79
|
+
## Callbacks
|
80
|
+
|
81
|
+
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.
|
82
|
+
|
83
|
+
::: danger ALPHA
|
84
|
+
* The callbacks themselves are functional. Note the ordering _between_ callbacks is not well defined (currently a side effect of the order they're defined).
|
85
|
+
* Ordering may change at any time so while in alpha DO NOT MAKE ASSUMPTIONS ABOUT THE ORDER OF CALLBACK EXECUTION!
|
86
|
+
:::
|
87
|
+
|
88
|
+
|
89
|
+
::: tip Callbacks vs Hooks
|
90
|
+
* *Hooks* (`before`/`after`) are executed _as part of the `call`_ -- exceptions or `fail!`s here _will_ change a successful action call to a failure (i.e. `result.ok?` will be false)
|
91
|
+
* *Callbacks* (defined below) are executed _after_ the `call` -- exceptions or `fail!`s here will _not_ change `result.ok?`
|
92
|
+
:::
|
93
|
+
|
94
|
+
### `on_success`
|
95
|
+
|
96
|
+
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.
|
97
|
+
|
98
|
+
### `on_error`
|
99
|
+
|
100
|
+
Triggered on ANY error (explicit `fail!` or uncaught exception). Optional filter argument works the same as `on_exception` (documented below).
|
101
|
+
|
102
|
+
### `on_failure`
|
103
|
+
|
104
|
+
Triggered ONLY on explicit `fail!` (i.e. _not_ by an uncaught exception). Optional filter argument works the same as `on_exception` (documented below).
|
105
|
+
|
106
|
+
### `on_exception`
|
107
|
+
|
108
|
+
Much like the [globally-configured on_exception hook](/reference/configuration#on-exception), you can also specify exception handlers for a _specific_ Axn class:
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
class Foo
|
112
|
+
include Action
|
113
|
+
|
114
|
+
on_exception do |exception| # [!code focus:3]
|
115
|
+
# e.g. trigger a slack error
|
116
|
+
end
|
117
|
+
end
|
118
|
+
```
|
119
|
+
|
120
|
+
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 [`error_for` and `rescues`](#error-for-and-rescues):
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
class Foo
|
124
|
+
include Action
|
125
|
+
|
126
|
+
on_exception NoMethodError do |exception| # [!code focus]
|
127
|
+
# e.g. trigger a slack error
|
128
|
+
end
|
129
|
+
|
130
|
+
on_exception ->(e) { e.is_a?(ZeroDivisionError) } do # [!code focus]
|
131
|
+
# e.g. trigger a slack error
|
132
|
+
end
|
133
|
+
end
|
134
|
+
```
|
135
|
+
|
136
|
+
If multiple `on_exception` handlers are provided, ALL that match the raised exception will be triggered in the order provided.
|
137
|
+
|
138
|
+
The _global_ handler will be triggered _after_ all class-specific handlers.
|
@@ -39,6 +39,7 @@ A couple notes:
|
|
39
39
|
|
40
40
|
* `context` will contain the arguments passed to the `action`, _but_ any marked as sensitive (e.g. `expects :foo, sensitive: true`) will be filtered out in the logs.
|
41
41
|
* If your handler raises, the failure will _also_ be swallowed and logged
|
42
|
+
* This handler is global across _all_ Axns. You can also specify per-Action handlers via [the class-level declaration](/reference/class#on-exception).
|
42
43
|
|
43
44
|
|
44
45
|
## `top_level_around_hook`
|
data/docs/usage/writing.md
CHANGED
@@ -104,23 +104,22 @@ Foo.call(name: "Adams").meaning_of_life # => "Hello Adams, the meaning of life i
|
|
104
104
|
|
105
105
|
In addition to `#call`, there are a few additional pieces to be aware of:
|
106
106
|
|
107
|
-
### `#rollback`
|
107
|
+
<!-- ### `#rollback`
|
108
|
+
*** TODO: rollback actually only applies to rolling back *completed* steps of a multi-step Axn chain. Do not document for now -- need to decide if adding a trigger-when-axn-itself-fails rollback path. ***
|
108
109
|
|
109
110
|
::: danger ALPHA
|
110
111
|
* ⚠️ `#rollback` is _expected_ to be added shortly, but is not yet functional!
|
111
112
|
:::
|
112
113
|
|
113
|
-
If you define a `#rollback` method, it'll be called (_before_ returning an `Action::Result` to the caller) whenever your action fails.
|
114
|
+
If you define a `#rollback` method, it'll be called (_before_ returning an `Action::Result` to the caller) whenever your action fails. -->
|
114
115
|
|
115
116
|
### Hooks
|
116
117
|
|
117
|
-
`before` and `after` hooks are
|
118
|
+
`before` and `after` hooks are supported. They can receive a block directly, or the symbol name of a local method.
|
118
119
|
|
119
|
-
Note execution is halted whenever `fail!` is called or an exception is raised (so a `before` block failure won't execute `call` or `after`, while an `after` block failure will make `
|
120
|
+
Note execution is halted whenever `fail!` is called or an exception is raised (so a `before` block failure won't execute `call` or `after`, while an `after` block failure will make `result.ok?` be false even though `call` completed successfully).
|
120
121
|
|
121
|
-
|
122
|
-
|
123
|
-
Given this series of methods and hooks:
|
122
|
+
For instance, given this configuration:
|
124
123
|
|
125
124
|
```ruby
|
126
125
|
class Foo
|
@@ -133,10 +132,6 @@ class Foo
|
|
133
132
|
log("in call")
|
134
133
|
end
|
135
134
|
|
136
|
-
def rollback
|
137
|
-
log("rolling back")
|
138
|
-
end
|
139
|
-
|
140
135
|
private
|
141
136
|
|
142
137
|
def log_after
|
@@ -153,8 +148,11 @@ end
|
|
153
148
|
before hook
|
154
149
|
in call
|
155
150
|
after hook
|
156
|
-
rolling back
|
157
151
|
```
|
158
152
|
|
153
|
+
### Callbacks
|
154
|
+
|
155
|
+
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. See the [Class Interface docs](/reference/class#callbacks) for details.
|
156
|
+
|
159
157
|
## Debugging
|
160
158
|
Remember you can [enable debug logging](/reference/configuration.html#global-debug-logging) to print log lines before and after each action is executed.
|
@@ -0,0 +1,64 @@
|
|
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
|
+
action.warn("Ignoring #{e.class.name} in when evaluating #{self.class.name} handler: #{e.message}")
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class Matcher
|
36
|
+
def initialize(matcher)
|
37
|
+
@matcher = matcher
|
38
|
+
end
|
39
|
+
|
40
|
+
def matches?(exception:, action:)
|
41
|
+
if matcher.respond_to?(:call)
|
42
|
+
if matcher.arity == 1
|
43
|
+
!!action.instance_exec(exception, &matcher)
|
44
|
+
else
|
45
|
+
!!action.instance_exec(&matcher)
|
46
|
+
end
|
47
|
+
elsif matcher.is_a?(String) || matcher.is_a?(Symbol)
|
48
|
+
klass = Object.const_get(matcher.to_s)
|
49
|
+
klass && exception.is_a?(klass)
|
50
|
+
elsif matcher < Exception
|
51
|
+
exception.is_a?(matcher)
|
52
|
+
else
|
53
|
+
action.warn("Ignoring apparently-invalid matcher #{matcher.inspect} -- could not find way to apply it")
|
54
|
+
false
|
55
|
+
end
|
56
|
+
rescue StandardError => e
|
57
|
+
action.warn("Ignoring #{e.class.name} raised while determining matcher: #{e.message}")
|
58
|
+
false
|
59
|
+
end
|
60
|
+
|
61
|
+
private attr_reader :matcher
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -11,7 +11,7 @@ module Action
|
|
11
11
|
@context = context
|
12
12
|
end
|
13
13
|
|
14
|
-
def message = @message.presence || "Execution was halted"
|
14
|
+
def message = @message.presence || @context.error_from_user.presence || "Execution was halted"
|
15
15
|
|
16
16
|
def inspect = "#<#{self.class.name} '#{message}'>"
|
17
17
|
end
|
@@ -1,35 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "event_handlers"
|
4
|
+
|
3
5
|
module Action
|
4
6
|
module SwallowExceptions
|
5
|
-
CustomErrorInterceptor = Data.define(:matcher, :message, :should_report_error)
|
6
|
-
class CustomErrorInterceptor
|
7
|
-
def matches?(exception:, action:)
|
8
|
-
if matcher.respond_to?(:call)
|
9
|
-
if matcher.arity == 1
|
10
|
-
!!action.instance_exec(exception, &matcher)
|
11
|
-
else
|
12
|
-
!!action.instance_exec(&matcher)
|
13
|
-
end
|
14
|
-
elsif matcher.is_a?(String) || matcher.is_a?(Symbol)
|
15
|
-
klass = Object.const_get(matcher.to_s)
|
16
|
-
klass && exception.is_a?(klass)
|
17
|
-
elsif matcher < Exception
|
18
|
-
exception.is_a?(matcher)
|
19
|
-
else
|
20
|
-
action.warn("Ignoring apparently-invalid matcher #{matcher.inspect} -- could not find way to apply it")
|
21
|
-
false
|
22
|
-
end
|
23
|
-
rescue StandardError => e
|
24
|
-
action.warn("Ignoring #{e.class.name} raised while determining matcher: #{e.message}")
|
25
|
-
false
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
7
|
def self.included(base)
|
30
8
|
base.class_eval do
|
31
9
|
class_attribute :_success_msg, :_error_msg
|
32
10
|
class_attribute :_custom_error_interceptors, default: []
|
11
|
+
class_attribute :_error_handlers, default: []
|
12
|
+
class_attribute :_exception_handlers, default: []
|
13
|
+
class_attribute :_failure_handlers, default: []
|
33
14
|
|
34
15
|
include InstanceMethods
|
35
16
|
extend ClassMethods
|
@@ -37,9 +18,22 @@ module Action
|
|
37
18
|
def run_with_exception_swallowing!
|
38
19
|
original_run!
|
39
20
|
rescue StandardError => e
|
40
|
-
|
21
|
+
# on_error handlers run for both unhandled exceptions and fail!
|
22
|
+
self.class._error_handlers.each do |handler|
|
23
|
+
handler.execute_if_matches(exception: e, action: self)
|
24
|
+
end
|
25
|
+
|
26
|
+
# on_failure handlers run ONLY for fail!
|
27
|
+
if e.is_a?(Action::Failure)
|
28
|
+
self.class._failure_handlers.each do |handler|
|
29
|
+
handler.execute_if_matches(exception: e, action: self)
|
30
|
+
end
|
31
|
+
|
32
|
+
# TODO: avoid raising if this was passed along from a child action (esp. if wrapped in hoist_errors)
|
33
|
+
raise e
|
34
|
+
end
|
41
35
|
|
42
|
-
#
|
36
|
+
# on_exception handlers run for ONLY for unhandled exceptions. AND NOTE: may be skipped if the exception is rescued via `rescues`.
|
43
37
|
trigger_on_exception(e)
|
44
38
|
|
45
39
|
@context.exception = e
|
@@ -58,11 +52,17 @@ module Action
|
|
58
52
|
raise if @context.object_id != e.context.object_id
|
59
53
|
end
|
60
54
|
|
61
|
-
def trigger_on_exception(
|
62
|
-
interceptor = self.class._error_interceptor_for(exception
|
55
|
+
def trigger_on_exception(exception)
|
56
|
+
interceptor = self.class._error_interceptor_for(exception:, action: self)
|
63
57
|
return if interceptor&.should_report_error == false
|
64
58
|
|
65
|
-
|
59
|
+
# Call any handlers registered on *this specific action* class
|
60
|
+
self.class._exception_handlers.each do |handler|
|
61
|
+
handler.execute_if_matches(exception:, action: self)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Call any global handlers
|
65
|
+
Action.config.on_exception(exception,
|
66
66
|
action: self,
|
67
67
|
context: respond_to?(:context_for_logging) ? context_for_logging : @context.to_h)
|
68
68
|
rescue StandardError => e
|
@@ -103,6 +103,36 @@ module Action
|
|
103
103
|
_register_error_interceptor(matcher, message, should_report_error: false, **match_and_messages)
|
104
104
|
end
|
105
105
|
|
106
|
+
# ONLY raised exceptions (i.e. NOT fail!). Skipped if exception is rescued via .rescues.
|
107
|
+
def on_exception(matcher = -> { true }, &handler)
|
108
|
+
raise ArgumentError, "on_exception must be called with a block" unless block_given?
|
109
|
+
|
110
|
+
self._exception_handlers += [Action::EventHandlers::ConditionalHandler.new(matcher:, handler:)]
|
111
|
+
end
|
112
|
+
|
113
|
+
# ONLY raised on fail! (i.e. NOT unhandled exceptions).
|
114
|
+
def on_failure(matcher = -> { true }, &handler)
|
115
|
+
raise ArgumentError, "on_failure must be called with a block" unless block_given?
|
116
|
+
|
117
|
+
self._failure_handlers += [Action::EventHandlers::ConditionalHandler.new(matcher:, handler:)]
|
118
|
+
end
|
119
|
+
|
120
|
+
# Handles both fail! and unhandled exceptions... but is NOT affected by .rescues
|
121
|
+
def on_error(matcher = -> { true }, &handler)
|
122
|
+
raise ArgumentError, "on_error must be called with a block" unless block_given?
|
123
|
+
|
124
|
+
self._error_handlers += [Action::EventHandlers::ConditionalHandler.new(matcher:, handler:)]
|
125
|
+
end
|
126
|
+
|
127
|
+
# Syntactic sugar for "after { try" (after, but if it fails do NOT fail the action)
|
128
|
+
def on_success(&block)
|
129
|
+
raise ArgumentError, "on_success must be called with a block" unless block_given?
|
130
|
+
|
131
|
+
after do
|
132
|
+
try { instance_exec(&block) }
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
106
136
|
def default_error = new.internal_context.default_error
|
107
137
|
|
108
138
|
# Private helpers
|
@@ -117,9 +147,11 @@ module Action
|
|
117
147
|
method_name = should_report_error ? "error_from" : "rescues"
|
118
148
|
raise ArgumentError, "#{method_name} must be called with a key/value pair, or else keyword args" if [matcher, message].compact.size == 1
|
119
149
|
|
120
|
-
{ matcher => message }.compact.merge(match_and_messages).
|
121
|
-
|
150
|
+
interceptors = { matcher => message }.compact.merge(match_and_messages).map do |(matcher, message)| # rubocop:disable Lint/ShadowingOuterLocalVariable
|
151
|
+
Action::EventHandlers::CustomErrorInterceptor.new(matcher:, message:, should_report_error:)
|
122
152
|
end
|
153
|
+
|
154
|
+
self._custom_error_interceptors += interceptors
|
123
155
|
end
|
124
156
|
end
|
125
157
|
|
@@ -131,7 +163,7 @@ module Action
|
|
131
163
|
@context.error_from_user = message if message.present?
|
132
164
|
|
133
165
|
# TODO: should we use context_for_logging here? But doublecheck the one place where we're checking object_id on it...
|
134
|
-
raise Action::Failure.new(@context
|
166
|
+
raise Action::Failure.new(@context, message:)
|
135
167
|
end
|
136
168
|
|
137
169
|
def try
|
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.4.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-
|
11
|
+
date: 2025-06-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -93,6 +93,7 @@ files:
|
|
93
93
|
- lib/action/core/contract.rb
|
94
94
|
- lib/action/core/contract_validator.rb
|
95
95
|
- lib/action/core/enqueueable.rb
|
96
|
+
- lib/action/core/event_handlers.rb
|
96
97
|
- lib/action/core/exceptions.rb
|
97
98
|
- lib/action/core/hoist_errors.rb
|
98
99
|
- lib/action/core/logging.rb
|