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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 30c00229f0062bdba5979d0e69de4ffddb6bece3f31413f83b7f7778d3075ef7
4
- data.tar.gz: 92d944caf300ddfc9a134d34ae2cb73e6d235e0af9f7a10a3ee93065ae834690
3
+ metadata.gz: 57d67670bb1cf60960aa2bb8234a5894a611801b69a89957057bfffd07ace8cd
4
+ data.tar.gz: 9e0025cbe4bc367e1351ea867d630b9fdb3d706547d676b9cd2162e8fda0ef38
5
5
  SHA512:
6
- metadata.gz: 600b1572f324b8cbbaf2b5dd48108546db86e63e3c7d49a04e1eb9f0f2e83720eafdaabb94ccf0550ff1c0e63b3ec555950443eb5c064698dbc65cf97c2b1733
7
- data.tar.gz: e9229f25cd10dcc54f1f7c5b83ff81b997d96263296818caaf2cfc245fde93ab24d7f0605700ea042c085ab7456f5fc1b20c2cc643324609bc0f4662ecbdcd21
6
+ metadata.gz: 70cc7c2851d95956278d43c746190dd5173ddda62a9a931ae9538374cf75b72523cc7f9b3524a6df6b8b8d57c969fb5510c07fad56b36312130ae86c59e48a15
7
+ data.tar.gz: f3f961781373a05284ddc54072baaf13998d235528110bf482c221e5f27d6142ab261516d8237bd0689d1161887eef6b567e8ecb4688d400a20ec32d1ae33837
data/CHANGELOG.md CHANGED
@@ -1,7 +1,18 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
4
- * N/A
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 `messages` declaration](/reference/class#messages).
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
 
@@ -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
- ## `.messages`
88
+ ## `.success` and `.error`
89
89
 
90
- The `messages` declaration allows you to customize the `error` and `success` messages on the returned result.
90
+ The `success` and `error` declarations allow you to customize the `error` and `success` messages on the returned result.
91
91
 
92
- Accepts `error` and/or `success` keys. Values can be a string (returned directly) or a callable (evaluated in the action's context, so can access instance methods and variables). If `error` is provided with a callable that expects a positional argument, the exception that was raised will be passed in as that value.
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
- In callables, you can access:
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
- messages success: -> { "Hello #{name}, your greeting: #{result.greeting}" },
101
- error: ->(e) { "Bad news: #{e.message}" }
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
- ## `error_from` and `rescues`
127
+ ## Conditional messages
105
128
 
106
- 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.
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
- `error_from` 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).
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
- Callable matchers and messages follow the same data access patterns as other callables: input fields directly, output fields via `result.field`, instance variables, and methods.
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
- messages error: "bad"
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
- # Note this will NOT trigger Action.config.on_exception
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
- # These WILL trigger error handler (callable matcher + message with data access)
119
- error_from ArgumentError, ->(e) { "Argument error: #{e.message}" }
120
- error_from -> { name == "bad" }, -> { "Bad input #{name}, result: #{result.status}" }
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
- ::: danger ALPHA
128
- * The callbacks themselves are functional. Note the ordering _between_ callbacks is not well defined (currently a side effect of the order they're defined).
129
- * Ordering may change at any time so while in alpha DO NOT MAKE ASSUMPTIONS ABOUT THE ORDER OF CALLBACK EXECUTION!
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 [`error_from` and `rescues`](#error-for-and-rescues):
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
- on_exception ->(e) { e.is_a?(ZeroDivisionError) } do # [!code focus]
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
+ ```
@@ -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_ `messages :error` parsing.
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
 
@@ -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's a `messages` declaration 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).
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
- messages success: -> { "Revealed to #{name}: #{result.meaning_of_life}" }, # [!code focus:2]
83
- error: ->(e) { "No secret of life for you: #{e.message}" }
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
- # So can be referenced from within e.g. rescues callables
8
+ # Available for use from within message callables
9
9
  def default_error
10
- [@context.error_prefix, determine_error_message(only_default: true)].compact.join(" ").squeeze(" ")
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/event_handlers"
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 :_error_handlers, default: []
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
- # ONLY raised exceptions (i.e. NOT fail!). Skipped if exception is rescued via .rescues.
22
- def on_exception(matcher = -> { true }, &handler)
23
- raise ArgumentError, "on_exception must be called with a block" unless block_given?
24
-
25
- self._exception_handlers += [Action::EventHandlers::ConditionalHandler.new(matcher:, handler:)]
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 on fail! (i.e. NOT unhandled exceptions).
29
- def on_failure(matcher = -> { true }, &handler)
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
- self._failure_handlers += [Action::EventHandlers::ConditionalHandler.new(matcher:, handler:)]
33
- end
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... but is NOT affected by .rescues
36
- def on_error(matcher = -> { true }, &handler)
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
- raise ArgumentError, "on_success must be called with a block" unless block_given?
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
- # Prepend like after hooks - child handlers run before parent handlers
48
- self._success_handlers = [handler] + _success_handlers
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._exception_handlers.each do |handler|
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._success_handlers.each do |handler|
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._error_handlers.each do |handler|
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._failure_handlers.each do |handler|
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. AND NOTE: may be skipped if the exception is rescued via `rescues`.
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 :_success_msg, :_error_msg
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
- def messages(success: nil, error: nil)
19
- self._success_msg = success if success.present?
20
- self._error_msg = error if error.present?
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 error_from(matcher = nil, message = nil, **match_and_messages)
26
- _register_error_interceptor(matcher, message, should_report_error: true, **match_and_messages)
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 rescues(matcher = nil, message = nil, **match_and_messages)
30
- _register_error_interceptor(matcher, message, should_report_error: false, **match_and_messages)
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
- # Private helpers
51
+ private
36
52
 
37
- def _error_interceptor_for(exception:, action:)
38
- Array(_custom_error_interceptors).detect do |int|
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
- def _register_error_interceptor(matcher, message, should_report_error:, **match_and_messages)
44
- method_name = should_report_error ? "error_from" : "rescues"
45
- raise ArgumentError, "#{method_name} must be called with a key/value pair, or else keyword args" if [matcher, message].compact.size == 1
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
- interceptors = { matcher => message }.compact.merge(match_and_messages).map do |(matcher, message)| # rubocop:disable Lint/ShadowingOuterLocalVariable
48
- Action::EventHandlers::CustomErrorInterceptor.new(matcher:, message:, should_report_error:)
49
- end
60
+ handler = block_given? ? block : message
50
61
 
51
- self._custom_error_interceptors += interceptors
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:, messages: { success: msg }) do
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:, rescues:) do
24
+ Axn::Factory.build(exposes:, error: msg) do
26
25
  exposures.each do |key, value|
27
26
  expose(key, value)
28
27
  end
29
- block.call if block_given?
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
- stringified(action._success_msg).presence || "Action completed successfully"
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
- messages: {},
17
- error_from: {},
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
- axn.messages(**messages) if messages.present? && messages.values.any?(&:present?)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Axn
4
- VERSION = "0.1.0-alpha.2.6.1"
4
+ VERSION = "0.1.0-alpha.2.7.1"
5
5
  end
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.6.1
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-12 00:00:00.000000000 Z
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