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 +4 -4
- data/CHANGELOG.md +11 -0
- data/docs/reference/class.md +36 -1
- data/docs/reference/configuration.md +1 -0
- data/lib/action/core/context_facade.rb +1 -1
- data/lib/action/core/contract.rb +6 -5
- data/lib/action/core/contract_validator.rb +2 -0
- data/lib/action/core/hoist_errors.rb +3 -1
- data/lib/action/core/swallow_exceptions.rb +30 -1
- data/lib/axn/factory.rb +13 -0
- data/lib/axn/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e3313bd117a57aae98551088df5895b6db302830101dda0e2fb95f057f97000f
|
4
|
+
data.tar.gz: 9517e1d297998af62a2fc2107d3a002a71d5b767dda250b6ebec4d29958902a6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/docs/reference/class.md
CHANGED
@@ -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
|
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,
|
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
|
data/lib/action/core/contract.rb
CHANGED
@@ -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,
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
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
|
-
|
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
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
|
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-
|
11
|
+
date: 2025-05-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|