axn 0.1.0.pre.alpha.2.2 → 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: dcfb310e364c4fb2c7159a6bdc1ff6766c0025b3b0741a62316d3e18cb5f3bf7
4
- data.tar.gz: 7af1c3d88a69c0d109983c85fb6dc1b5ddf162568ee1e1aa516f15bf1d12a1d2
3
+ metadata.gz: e3313bd117a57aae98551088df5895b6db302830101dda0e2fb95f057f97000f
4
+ data.tar.gz: 9517e1d297998af62a2fc2107d3a002a71d5b767dda250b6ebec4d29958902a6
5
5
  SHA512:
6
- metadata.gz: 13868e68f4a7b9a45bebbcf793bba1855ec2fa524ccd6f24fdaf1364bb143fcd4cfd862f0906d6455d10e61492b723ffbdc2c836a887052b030d509bf18171b0
7
- data.tar.gz: 53014437d7a3972ec584195b2c20955ff9f2c1138dba3eeb35df2e82db8d62ec3b3561ec6b1d870472fc7fcea79dd1c2abe759cf341c46e024ae143d266bfa64
6
+ metadata.gz: e3b80de43c96e2362562dc35d88fd28d7c7b97a0dba2e73168031063fa8bed3d64ae3c46dd4f751af6ea051c17fc4592e6c246f505f9e319e5dd6e87caabfc67
7
+ data.tar.gz: c451f8b99639f94bb38d982ab4913b5e126ee099ed97f31cf2ad70903efbc5acb69bc715e634d39e56aad0720d8bd4007a24794255ea9519307f268b4f312061
data/CHANGELOG.md CHANGED
@@ -1,6 +1,17 @@
1
1
  # Changelog
2
2
 
3
3
  ## UNRELEASED
4
+ * N/A
5
+
6
+ ## 0.1.0-alpha.2.4
7
+ * [FEAT] Adds per-Axn `on_exception` handlers
8
+
9
+ ## 0.1.0-alpha.2.3
10
+ * `expects` / `exposes`: Add `type: :uuid` special case validation
11
+ * [BUGFIX] Allow `hoist_errors` to pass the result through on success (allow access to subactions' exposures)
12
+ * [`Axn::Factory`] Support error_from + rescues
13
+ * `Action::Result.error` spec helper -- creation should NOT trigger global exception handler
14
+ * [CHANGE] `expects` / `exposes`: The `default` key, if a callable, should be evaluated in the _instance_'s context
4
15
 
5
16
  ## 0.1.0-alpha.2.2
6
17
  * Expands `Action::Result.ok` and `Action::Result.error` to better support mocking in specs
@@ -25,6 +25,7 @@ While we _support_ complex interface validations, in practice you usually just w
25
25
  In addition to the [standard ActiveModel validations](https://guides.rubyonrails.org/active_record_validations.html), we also support two additional custom validators:
26
26
  * `type: Foo` - fails unless the provided value `.is_a?(Foo)`
27
27
  * Edge case: use `type: :boolean` to handle a boolean field (since ruby doesn't have a Boolean class to pass in directly)
28
+ * Edge case: use `type: :uuid` to handle a confirming given string is a UUID (with or without `-` chars)
28
29
  * `validate: [callable]` - Support custom validations (fails if any string is returned OR if it raises an exception)
29
30
  * Example:
30
31
  ```ruby
@@ -62,7 +63,7 @@ messages success: "All good!", error: ->(e) { "Bad news: #{e.message}" }
62
63
 
63
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.
64
65
 
65
- `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).
66
67
 
67
68
  ```ruby
68
69
  messages error: "bad"
@@ -74,3 +75,37 @@ rescues ActiveRecord::InvalidRecord => "Invalid params provided"
74
75
  error_for ArgumentError, ->(e) { "Argument error: #{e.message}"
75
76
  error_for -> { name == "bad" }, -> { "was given bad name: #{name}" }
76
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`
@@ -103,7 +103,7 @@ module Action
103
103
  end
104
104
 
105
105
  def error(msg = nil, **exposures, &block)
106
- Axn::Factory.build(exposes: exposures.keys, messages: { error: msg }) do
106
+ Axn::Factory.build(exposes: exposures.keys, rescues: [-> { true }, msg]) do
107
107
  exposures.each do |key, value|
108
108
  expose(key, value)
109
109
  end
@@ -163,11 +163,12 @@ module Action
163
163
  hash[config.field] = config.default
164
164
  end.compact
165
165
 
166
- defaults_mapping.each do |field, default_value|
167
- unless @context.public_send(field)
168
- @context.public_send("#{field}=",
169
- default_value.respond_to?(:call) ? default_value.call : default_value)
170
- end
166
+ defaults_mapping.each do |field, default_value_getter|
167
+ next if @context.public_send(field).present?
168
+
169
+ default_value = default_value_getter.respond_to?(:call) ? instance_exec(&default_value_getter) : default_value_getter
170
+
171
+ @context.public_send("#{field}=", default_value)
171
172
  end
172
173
  end
173
174
 
@@ -54,6 +54,8 @@ module Action
54
54
  record.errors.add attribute, (options[:message] || msg) unless types.any? do |type|
55
55
  if type == :boolean
56
56
  [true, false].include?(value)
57
+ elsif type == :uuid
58
+ value.is_a?(String) && value.match?(/\A[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}\z/i)
57
59
  else
58
60
  value.is_a?(type)
59
61
  end
@@ -36,7 +36,9 @@ module Action
36
36
  "#hoist_errors is expected to wrap an Action call, but it returned a #{result.class.name} instead"
37
37
  end
38
38
 
39
- _handle_hoisted_errors(result, prefix:) unless result.ok?
39
+ return result if result.ok?
40
+
41
+ _handle_hoisted_errors(result, prefix:)
40
42
  end
41
43
 
42
44
  # Separate method to allow overriding in subclasses
@@ -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/factory.rb CHANGED
@@ -14,6 +14,10 @@ module Axn
14
14
  exposes: [],
15
15
  expects: [],
16
16
  messages: {},
17
+ error_from: {},
18
+ rescues: {},
19
+
20
+ # Hooks
17
21
  before: nil,
18
22
  after: nil,
19
23
  around: nil,
@@ -73,6 +77,9 @@ module Axn
73
77
 
74
78
  axn.messages(**messages) if messages.present? && messages.values.any?(&:present?)
75
79
 
80
+ axn.error_from(**_array_to_hash(error_from)) if error_from.present?
81
+ axn.rescues(**_array_to_hash(rescues)) if rescues.present?
82
+
76
83
  # Hooks
77
84
  axn.before(before) if before.present?
78
85
  axn.after(after) if after.present?
@@ -98,6 +105,12 @@ module Axn
98
105
 
99
106
  def _hash_with_default_array = Hash.new { |h, k| h[k] = [] }
100
107
 
108
+ def _array_to_hash(given)
109
+ return given if given.is_a?(Hash)
110
+
111
+ [given].to_h
112
+ end
113
+
101
114
  def _hydrate_hash(given)
102
115
  return given if given.is_a?(Hash)
103
116
 
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.2"
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.2
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-21 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