axn 0.1.0.pre.alpha.2.4 → 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 +3 -0
- data/docs/reference/class.md +29 -2
- 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 +56 -53
- 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
data/docs/reference/class.md
CHANGED
@@ -72,11 +72,38 @@ 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
78
|
|
79
|
-
##
|
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`
|
80
107
|
|
81
108
|
Much like the [globally-configured on_exception hook](/reference/configuration#on-exception), you can also specify exception handlers for a _specific_ Axn class:
|
82
109
|
|
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,42 +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
|
-
CustomErrorHandler = Data.define(:matcher, :block)
|
7
|
-
|
8
|
-
class CustomErrorInterceptor
|
9
|
-
def self.matches?(matcher:, exception:, action:)
|
10
|
-
if matcher.respond_to?(:call)
|
11
|
-
if matcher.arity == 1
|
12
|
-
!!action.instance_exec(exception, &matcher)
|
13
|
-
else
|
14
|
-
!!action.instance_exec(&matcher)
|
15
|
-
end
|
16
|
-
elsif matcher.is_a?(String) || matcher.is_a?(Symbol)
|
17
|
-
klass = Object.const_get(matcher.to_s)
|
18
|
-
klass && exception.is_a?(klass)
|
19
|
-
elsif matcher < Exception
|
20
|
-
exception.is_a?(matcher)
|
21
|
-
else
|
22
|
-
action.warn("Ignoring apparently-invalid matcher #{matcher.inspect} -- could not find way to apply it")
|
23
|
-
false
|
24
|
-
end
|
25
|
-
rescue StandardError => e
|
26
|
-
action.warn("Ignoring #{e.class.name} raised while determining matcher: #{e.message}")
|
27
|
-
false
|
28
|
-
end
|
29
|
-
|
30
|
-
def matches?(exception:, action:)
|
31
|
-
self.class.matches?(matcher:, exception:, action:)
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
7
|
def self.included(base)
|
36
8
|
base.class_eval do
|
37
9
|
class_attribute :_success_msg, :_error_msg
|
38
10
|
class_attribute :_custom_error_interceptors, default: []
|
11
|
+
class_attribute :_error_handlers, default: []
|
39
12
|
class_attribute :_exception_handlers, default: []
|
13
|
+
class_attribute :_failure_handlers, default: []
|
40
14
|
|
41
15
|
include InstanceMethods
|
42
16
|
extend ClassMethods
|
@@ -44,9 +18,22 @@ module Action
|
|
44
18
|
def run_with_exception_swallowing!
|
45
19
|
original_run!
|
46
20
|
rescue StandardError => e
|
47
|
-
|
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
|
48
35
|
|
49
|
-
#
|
36
|
+
# on_exception handlers run for ONLY for unhandled exceptions. AND NOTE: may be skipped if the exception is rescued via `rescues`.
|
50
37
|
trigger_on_exception(e)
|
51
38
|
|
52
39
|
@context.exception = e
|
@@ -65,15 +52,17 @@ module Action
|
|
65
52
|
raise if @context.object_id != e.context.object_id
|
66
53
|
end
|
67
54
|
|
68
|
-
def trigger_on_exception(
|
69
|
-
interceptor = self.class._error_interceptor_for(exception
|
55
|
+
def trigger_on_exception(exception)
|
56
|
+
interceptor = self.class._error_interceptor_for(exception:, action: self)
|
70
57
|
return if interceptor&.should_report_error == false
|
71
58
|
|
72
59
|
# Call any handlers registered on *this specific action* class
|
73
|
-
|
60
|
+
self.class._exception_handlers.each do |handler|
|
61
|
+
handler.execute_if_matches(exception:, action: self)
|
62
|
+
end
|
74
63
|
|
75
64
|
# Call any global handlers
|
76
|
-
Action.config.on_exception(
|
65
|
+
Action.config.on_exception(exception,
|
77
66
|
action: self,
|
78
67
|
context: respond_to?(:context_for_logging) ? context_for_logging : @context.to_h)
|
79
68
|
rescue StandardError => e
|
@@ -114,10 +103,34 @@ module Action
|
|
114
103
|
_register_error_interceptor(matcher, message, should_report_error: false, **match_and_messages)
|
115
104
|
end
|
116
105
|
|
117
|
-
|
106
|
+
# ONLY raised exceptions (i.e. NOT fail!). Skipped if exception is rescued via .rescues.
|
107
|
+
def on_exception(matcher = -> { true }, &handler)
|
118
108
|
raise ArgumentError, "on_exception must be called with a block" unless block_given?
|
119
109
|
|
120
|
-
self._exception_handlers += [
|
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
|
121
134
|
end
|
122
135
|
|
123
136
|
def default_error = new.internal_context.default_error
|
@@ -134,9 +147,11 @@ module Action
|
|
134
147
|
method_name = should_report_error ? "error_from" : "rescues"
|
135
148
|
raise ArgumentError, "#{method_name} must be called with a key/value pair, or else keyword args" if [matcher, message].compact.size == 1
|
136
149
|
|
137
|
-
{ matcher => message }.compact.merge(match_and_messages).
|
138
|
-
|
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:)
|
139
152
|
end
|
153
|
+
|
154
|
+
self._custom_error_interceptors += interceptors
|
140
155
|
end
|
141
156
|
end
|
142
157
|
|
@@ -148,7 +163,7 @@ module Action
|
|
148
163
|
@context.error_from_user = message if message.present?
|
149
164
|
|
150
165
|
# TODO: should we use context_for_logging here? But doublecheck the one place where we're checking object_id on it...
|
151
|
-
raise Action::Failure.new(@context
|
166
|
+
raise Action::Failure.new(@context, message:)
|
152
167
|
end
|
153
168
|
|
154
169
|
def try
|
@@ -161,18 +176,6 @@ module Action
|
|
161
176
|
end
|
162
177
|
|
163
178
|
delegate :default_error, to: :internal_context
|
164
|
-
|
165
|
-
def _on_exception(exception)
|
166
|
-
handlers = self.class._exception_handlers.select do |this|
|
167
|
-
CustomErrorInterceptor.matches?(matcher: this.matcher, exception:, action: self)
|
168
|
-
end
|
169
|
-
|
170
|
-
handlers.each do |handler|
|
171
|
-
instance_exec(exception, &handler.block)
|
172
|
-
rescue StandardError => e
|
173
|
-
warn("Ignoring #{e.class.name} in on_exception hook: #{e.message}")
|
174
|
-
end
|
175
|
-
end
|
176
179
|
end
|
177
180
|
end
|
178
181
|
end
|
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
|
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
|