axn 0.1.0.pre.alpha.2.3 → 0.1.0.pre.alpha.2.4

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: ac3f240b093cdf14fef16b5eb6993dbb8f6d49ba58c837245128a0efb3558f11
4
- data.tar.gz: 40b631f49349b51811ebb0bbc60cbc7962a3f7d633bab1134b7b234b4f681123
3
+ metadata.gz: e3313bd117a57aae98551088df5895b6db302830101dda0e2fb95f057f97000f
4
+ data.tar.gz: 9517e1d297998af62a2fc2107d3a002a71d5b767dda250b6ebec4d29958902a6
5
5
  SHA512:
6
- metadata.gz: c8ac8be69e70d72d04b0c5be89e4ec198223c4a1771a15d254b82b0a204543c627a80321b3de95d69fa97a244fc56c9c39ca59262600c6b991c363603e0a7beb
7
- data.tar.gz: 4eba5152626d0417ff3be725da9512034276d4b034898f8ad331890499196c1851a1d3dcd1cde880955c42f425090ab6c5fe30cac1496835013c7bd6cda52cdb
6
+ metadata.gz: e3b80de43c96e2362562dc35d88fd28d7c7b97a0dba2e73168031063fa8bed3d64ae3c46dd4f751af6ea051c17fc4592e6c246f505f9e319e5dd6e87caabfc67
7
+ data.tar.gz: c451f8b99639f94bb38d982ab4913b5e126ee099ed97f31cf2ad70903efbc5acb69bc715e634d39e56aad0720d8bd4007a24794255ea9519307f268b4f312061
data/CHANGELOG.md CHANGED
@@ -3,6 +3,9 @@
3
3
  ## UNRELEASED
4
4
  * N/A
5
5
 
6
+ ## 0.1.0-alpha.2.4
7
+ * [FEAT] Adds per-Axn `on_exception` handlers
8
+
6
9
  ## 0.1.0-alpha.2.3
7
10
  * `expects` / `exposes`: Add `type: :uuid` special case validation
8
11
  * [BUGFIX] Allow `hoist_errors` to pass the result through on success (allow access to subactions' exposures)
@@ -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 error handler.
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"
@@ -75,3 +75,37 @@ rescues ActiveRecord::InvalidRecord => "Invalid params provided"
75
75
  error_for ArgumentError, ->(e) { "Argument error: #{e.message}"
76
76
  error_for -> { name == "bad" }, -> { "was given bad name: #{name}" }
77
77
  ```
78
+
79
+ ## `on_exception`
80
+
81
+ Much like the [globally-configured on_exception hook](/reference/configuration#on-exception), you can also specify exception handlers for a _specific_ Axn class:
82
+
83
+ ```ruby
84
+ class Foo
85
+ include Action
86
+
87
+ on_exception do |exception| # [!code focus:3]
88
+ # e.g. trigger a slack error
89
+ end
90
+ end
91
+ ```
92
+
93
+ 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):
94
+
95
+ ```ruby
96
+ class Foo
97
+ include Action
98
+
99
+ on_exception NoMethodError do |exception| # [!code focus]
100
+ # e.g. trigger a slack error
101
+ end
102
+
103
+ on_exception ->(e) { e.is_a?(ZeroDivisionError) } do # [!code focus]
104
+ # e.g. trigger a slack error
105
+ end
106
+ end
107
+ ```
108
+
109
+ If multiple `on_exception` handlers are provided, ALL that match the raised exception will be triggered in the order provided.
110
+
111
+ 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`
@@ -3,8 +3,10 @@
3
3
  module Action
4
4
  module SwallowExceptions
5
5
  CustomErrorInterceptor = Data.define(:matcher, :message, :should_report_error)
6
+ CustomErrorHandler = Data.define(:matcher, :block)
7
+
6
8
  class CustomErrorInterceptor
7
- def matches?(exception:, action:)
9
+ def self.matches?(matcher:, exception:, action:)
8
10
  if matcher.respond_to?(:call)
9
11
  if matcher.arity == 1
10
12
  !!action.instance_exec(exception, &matcher)
@@ -24,12 +26,17 @@ module Action
24
26
  action.warn("Ignoring #{e.class.name} raised while determining matcher: #{e.message}")
25
27
  false
26
28
  end
29
+
30
+ def matches?(exception:, action:)
31
+ self.class.matches?(matcher:, exception:, action:)
32
+ end
27
33
  end
28
34
 
29
35
  def self.included(base)
30
36
  base.class_eval do
31
37
  class_attribute :_success_msg, :_error_msg
32
38
  class_attribute :_custom_error_interceptors, default: []
39
+ class_attribute :_exception_handlers, default: []
33
40
 
34
41
  include InstanceMethods
35
42
  extend ClassMethods
@@ -62,6 +69,10 @@ module Action
62
69
  interceptor = self.class._error_interceptor_for(exception: e, action: self)
63
70
  return if interceptor&.should_report_error == false
64
71
 
72
+ # Call any handlers registered on *this specific action* class
73
+ _on_exception(e)
74
+
75
+ # Call any global handlers
65
76
  Action.config.on_exception(e,
66
77
  action: self,
67
78
  context: respond_to?(:context_for_logging) ? context_for_logging : @context.to_h)
@@ -103,6 +114,12 @@ module Action
103
114
  _register_error_interceptor(matcher, message, should_report_error: false, **match_and_messages)
104
115
  end
105
116
 
117
+ def on_exception(matcher = StandardError, &block)
118
+ raise ArgumentError, "on_exception must be called with a block" unless block_given?
119
+
120
+ self._exception_handlers += [CustomErrorHandler.new(matcher:, block:)]
121
+ end
122
+
106
123
  def default_error = new.internal_context.default_error
107
124
 
108
125
  # Private helpers
@@ -144,6 +161,18 @@ module Action
144
161
  end
145
162
 
146
163
  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
147
176
  end
148
177
  end
149
178
  end
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.3"
4
+ VERSION = "0.1.0-alpha.2.4"
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.3
4
+ version: 0.1.0.pre.alpha.2.4
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-05-22 00:00:00.000000000 Z
11
+ date: 2025-05-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel