axn 0.1.0.pre.alpha.2.7.1 → 0.1.0.pre.alpha.2.8
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 +11 -5
- data/CHANGELOG.md +10 -0
- data/Rakefile +12 -0
- data/docs/intro/about.md +2 -2
- data/docs/intro/overview.md +18 -0
- data/docs/recipes/rubocop-integration.md +352 -0
- data/docs/reference/action-result.md +1 -1
- data/docs/reference/class.md +110 -2
- data/docs/reference/configuration.md +5 -3
- data/docs/reference/instance.md +0 -52
- data/docs/usage/setup.md +4 -0
- data/docs/usage/steps.md +335 -0
- data/docs/usage/writing.md +67 -0
- data/lib/action/attachable/steps.rb +18 -17
- data/lib/action/attachable/subactions.rb +1 -1
- data/lib/action/context.rb +10 -14
- data/lib/action/core/context/facade.rb +11 -2
- data/lib/action/core/context/facade_inspector.rb +3 -2
- data/lib/action/core/context/internal.rb +3 -11
- data/lib/action/core/contract_validation.rb +1 -1
- data/lib/action/core/flow/callbacks.rb +22 -8
- data/lib/action/core/flow/exception_execution.rb +2 -5
- data/lib/action/core/flow/handlers/{base_handler.rb → base_descriptor.rb} +7 -4
- data/lib/action/core/flow/handlers/descriptors/callback_descriptor.rb +17 -0
- data/lib/action/core/flow/handlers/descriptors/message_descriptor.rb +53 -0
- data/lib/action/core/flow/handlers/matcher.rb +41 -2
- data/lib/action/core/flow/handlers/resolvers/base_resolver.rb +28 -0
- data/lib/action/core/flow/handlers/resolvers/callback_resolver.rb +29 -0
- data/lib/action/core/flow/handlers/resolvers/message_resolver.rb +59 -0
- data/lib/action/core/flow/handlers.rb +7 -4
- data/lib/action/core/flow/messages.rb +15 -41
- data/lib/action/core/nesting_tracking.rb +31 -0
- data/lib/action/core/timing.rb +1 -1
- data/lib/action/core.rb +20 -12
- data/lib/action/exceptions.rb +20 -2
- data/lib/action/result.rb +30 -32
- data/lib/axn/factory.rb +22 -23
- data/lib/axn/rubocop.rb +10 -0
- data/lib/axn/version.rb +1 -1
- data/lib/rubocop/cop/axn/README.md +237 -0
- data/lib/rubocop/cop/axn/unchecked_result.rb +327 -0
- metadata +14 -6
- data/lib/action/core/flow/handlers/callback_handler.rb +0 -21
- data/lib/action/core/flow/handlers/message_handler.rb +0 -27
- data/lib/action/core/hoist_errors.rb +0 -58
data/lib/action/result.rb
CHANGED
@@ -30,70 +30,68 @@ module Action
|
|
30
30
|
block.call
|
31
31
|
rescue StandardError => e
|
32
32
|
# Set the exception directly without triggering on_exception handlers
|
33
|
-
@__context.
|
33
|
+
@__context.__record_exception(e)
|
34
34
|
end
|
35
|
+
else
|
36
|
+
fail! msg
|
35
37
|
end
|
36
|
-
fail!
|
37
38
|
end.call
|
38
39
|
end
|
39
40
|
end
|
40
41
|
|
41
|
-
# Poke some holes for necessary internal control methods
|
42
|
-
delegate :each_pair, to: :context
|
43
|
-
|
44
42
|
# External interface
|
45
|
-
delegate :ok?, :exception, to: :context
|
43
|
+
delegate :ok?, :exception, :elapsed_time, to: :context
|
46
44
|
|
47
45
|
def error
|
48
46
|
return if ok?
|
49
47
|
|
50
|
-
|
48
|
+
_user_provided_error_message || _msg_resolver(:error, exception:).resolve_message
|
51
49
|
end
|
52
50
|
|
53
51
|
def success
|
54
52
|
return unless ok?
|
55
53
|
|
56
|
-
|
54
|
+
_user_provided_success_message || _msg_resolver(:success, exception: nil).resolve_message
|
57
55
|
end
|
58
56
|
|
59
|
-
def
|
60
|
-
|
61
|
-
def message = error || success
|
57
|
+
def message = exception ? error : success
|
62
58
|
|
63
59
|
# Outcome constants for action execution results
|
64
60
|
OUTCOMES = [
|
65
|
-
OUTCOME_SUCCESS =
|
66
|
-
OUTCOME_FAILURE =
|
67
|
-
OUTCOME_EXCEPTION =
|
61
|
+
OUTCOME_SUCCESS = "success",
|
62
|
+
OUTCOME_FAILURE = "failure",
|
63
|
+
OUTCOME_EXCEPTION = "exception",
|
68
64
|
].freeze
|
69
65
|
|
70
66
|
def outcome
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
67
|
+
label = if exception.is_a?(Action::Failure)
|
68
|
+
OUTCOME_FAILURE
|
69
|
+
elsif exception
|
70
|
+
OUTCOME_EXCEPTION
|
71
|
+
else
|
72
|
+
OUTCOME_SUCCESS
|
73
|
+
end
|
74
|
+
|
75
|
+
ActiveSupport::StringInquirer.new(label)
|
75
76
|
end
|
76
77
|
|
77
|
-
#
|
78
|
-
|
79
|
-
|
80
|
-
end
|
78
|
+
# Internal accessor for the action instance
|
79
|
+
# TODO: exposed for errors :from support, but should be private if possible
|
80
|
+
def __action__ = @action
|
81
81
|
|
82
82
|
private
|
83
83
|
|
84
|
-
def
|
84
|
+
def _context_data_source = @context.exposed_data
|
85
85
|
|
86
|
-
|
87
|
-
|
86
|
+
# TODO: hook for adding early-return success at some point
|
87
|
+
def _user_provided_success_message = nil
|
88
88
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
89
|
+
def _user_provided_error_message
|
90
|
+
return unless exception.is_a?(Action::Failure)
|
91
|
+
return if exception.default_message?
|
92
|
+
return if exception.cause # We raised this ourselves from nesting
|
93
93
|
|
94
|
-
|
95
|
-
msg = action.class._message_for(:success, action:, exception: nil)
|
96
|
-
msg.presence || "Action completed successfully"
|
94
|
+
exception.message.presence
|
97
95
|
end
|
98
96
|
|
99
97
|
def method_missing(method_name, ...) # rubocop:disable Style/MissingRespondToMissing (because we're not actually responding to anything additional)
|
data/lib/axn/factory.rb
CHANGED
@@ -72,17 +72,6 @@ module Axn
|
|
72
72
|
expose(expose_return_as => retval) if expose_return_as.present?
|
73
73
|
end
|
74
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
|
-
|
86
75
|
expects.each do |field, opts|
|
87
76
|
axn.expects(field, **opts)
|
88
77
|
end
|
@@ -91,8 +80,9 @@ module Axn
|
|
91
80
|
axn.exposes(field, **opts)
|
92
81
|
end
|
93
82
|
|
94
|
-
|
95
|
-
|
83
|
+
# Apply success and error handlers
|
84
|
+
_apply_handlers(axn, :success, success, Action::Core::Flow::Handlers::Descriptors::MessageDescriptor)
|
85
|
+
_apply_handlers(axn, :error, error, Action::Core::Flow::Handlers::Descriptors::MessageDescriptor)
|
96
86
|
|
97
87
|
# Hooks
|
98
88
|
axn.before(before) if before.present?
|
@@ -100,10 +90,10 @@ module Axn
|
|
100
90
|
axn.around(around) if around.present?
|
101
91
|
|
102
92
|
# Callbacks
|
103
|
-
axn
|
104
|
-
axn
|
105
|
-
axn
|
106
|
-
axn
|
93
|
+
_apply_handlers(axn, :on_success, on_success, Action::Core::Flow::Handlers::Descriptors::CallbackDescriptor)
|
94
|
+
_apply_handlers(axn, :on_failure, on_failure, Action::Core::Flow::Handlers::Descriptors::CallbackDescriptor)
|
95
|
+
_apply_handlers(axn, :on_error, on_error, Action::Core::Flow::Handlers::Descriptors::CallbackDescriptor)
|
96
|
+
_apply_handlers(axn, :on_exception, on_exception, Action::Core::Flow::Handlers::Descriptors::CallbackDescriptor)
|
107
97
|
|
108
98
|
# Strategies
|
109
99
|
Array(use).each do |strategy|
|
@@ -130,12 +120,6 @@ module Axn
|
|
130
120
|
|
131
121
|
def _hash_with_default_array = Hash.new { |h, k| h[k] = [] }
|
132
122
|
|
133
|
-
def _array_to_hash(given)
|
134
|
-
return given if given.is_a?(Hash)
|
135
|
-
|
136
|
-
[given].to_h
|
137
|
-
end
|
138
|
-
|
139
123
|
def _hydrate_hash(given)
|
140
124
|
return given if given.is_a?(Hash)
|
141
125
|
|
@@ -149,6 +133,21 @@ module Axn
|
|
149
133
|
end
|
150
134
|
end
|
151
135
|
end
|
136
|
+
|
137
|
+
def _apply_handlers(axn, method_name, value, _descriptor_class)
|
138
|
+
return unless value.present?
|
139
|
+
|
140
|
+
# Check if the value itself is a hash (this catches the case where someone passes a hash literal)
|
141
|
+
raise Action::UnsupportedArgument, "Cannot pass hash directly to #{method_name} - use descriptor objects for kwargs" if value.is_a?(Hash)
|
142
|
+
|
143
|
+
# Wrap in Array() to handle both single values and arrays
|
144
|
+
Array(value).each do |handler|
|
145
|
+
raise Action::UnsupportedArgument, "Cannot pass hash directly to #{method_name} - use descriptor objects for kwargs" if handler.is_a?(Hash)
|
146
|
+
|
147
|
+
# Both descriptor objects and simple cases (string/proc) can be used directly
|
148
|
+
axn.public_send(method_name, handler)
|
149
|
+
end
|
150
|
+
end
|
152
151
|
end
|
153
152
|
end
|
154
153
|
end
|
data/lib/axn/rubocop.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Axn RuboCop Integration
|
4
|
+
# This file makes Axn's custom RuboCop cops available to downstream consumers
|
5
|
+
#
|
6
|
+
# Usage in .rubocop.yml:
|
7
|
+
# require:
|
8
|
+
# - axn/rubocop
|
9
|
+
|
10
|
+
require_relative "../rubocop/cop/axn/unchecked_result"
|
data/lib/axn/version.rb
CHANGED
@@ -0,0 +1,237 @@
|
|
1
|
+
# Axn RuboCop Cops
|
2
|
+
|
3
|
+
This directory contains custom RuboCop cops specifically designed for the Axn library.
|
4
|
+
|
5
|
+
## Axn/UncheckedResult
|
6
|
+
|
7
|
+
This cop enforces proper result handling when calling Actions. It can be configured to check nested calls, non-nested calls, or both.
|
8
|
+
|
9
|
+
### Why This Rule Exists
|
10
|
+
|
11
|
+
When Actions are nested, proper error handling becomes crucial. Without proper result checking, failures in nested Actions can be silently ignored, leading to:
|
12
|
+
|
13
|
+
- Silent failures that are hard to debug
|
14
|
+
- Inconsistent error handling patterns
|
15
|
+
- Potential data corruption or unexpected behavior
|
16
|
+
|
17
|
+
### Configuration Options
|
18
|
+
|
19
|
+
The cop supports flexible configuration to match your team's needs:
|
20
|
+
|
21
|
+
```yaml
|
22
|
+
Axn/UncheckedResult:
|
23
|
+
Enabled: true
|
24
|
+
CheckNested: true # Check nested Action calls (default: true)
|
25
|
+
CheckNonNested: true # Check non-nested Action calls (default: true)
|
26
|
+
Severity: warning # or error, if you want to enforce it strictly
|
27
|
+
```
|
28
|
+
|
29
|
+
#### Configuration Modes
|
30
|
+
|
31
|
+
1. **Full Enforcement** (default):
|
32
|
+
```yaml
|
33
|
+
CheckNested: true
|
34
|
+
CheckNonNested: true
|
35
|
+
```
|
36
|
+
Checks all Action calls regardless of nesting.
|
37
|
+
|
38
|
+
2. **Nested Only**:
|
39
|
+
```yaml
|
40
|
+
CheckNested: true
|
41
|
+
CheckNonNested: false
|
42
|
+
```
|
43
|
+
Only checks Action calls from within other Actions.
|
44
|
+
|
45
|
+
3. **Non-Nested Only**:
|
46
|
+
```yaml
|
47
|
+
CheckNested: false
|
48
|
+
CheckNonNested: true
|
49
|
+
```
|
50
|
+
Only checks top-level Action calls.
|
51
|
+
|
52
|
+
4. **Disabled**:
|
53
|
+
```yaml
|
54
|
+
CheckNested: false
|
55
|
+
CheckNonNested: false
|
56
|
+
```
|
57
|
+
Effectively disables the cop.
|
58
|
+
|
59
|
+
### Usage Examples
|
60
|
+
|
61
|
+
#### ❌ Bad - Missing Result Check
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
class OuterAction
|
65
|
+
include Action
|
66
|
+
def call
|
67
|
+
InnerAction.call(param: "value") # Missing result check
|
68
|
+
# This will always continue even if InnerAction fails
|
69
|
+
end
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
#### ✅ Good - Using call!
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
class OuterAction
|
77
|
+
include Action
|
78
|
+
def call
|
79
|
+
InnerAction.call!(param: "value") # Using call! ensures exceptions bubble up
|
80
|
+
end
|
81
|
+
end
|
82
|
+
```
|
83
|
+
|
84
|
+
#### ✅ Good - Checking result.ok?
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
class OuterAction
|
88
|
+
include Action
|
89
|
+
def call
|
90
|
+
result = InnerAction.call(param: "value")
|
91
|
+
return result unless result.ok?
|
92
|
+
# Process successful result...
|
93
|
+
end
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
#### ✅ Good - Checking result.failed?
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
class OuterAction
|
101
|
+
include Action
|
102
|
+
def call
|
103
|
+
result = InnerAction.call(param: "value")
|
104
|
+
if result.failed?
|
105
|
+
return result
|
106
|
+
end
|
107
|
+
# Process successful result...
|
108
|
+
end
|
109
|
+
end
|
110
|
+
```
|
111
|
+
|
112
|
+
#### ✅ Good - Accessing result.error
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
class OuterAction
|
116
|
+
include Action
|
117
|
+
def call
|
118
|
+
result = InnerAction.call(param: "value")
|
119
|
+
if result.error
|
120
|
+
return result
|
121
|
+
end
|
122
|
+
# Process successful result...
|
123
|
+
end
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
#### ✅ Good - Returning the result
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
class OuterAction
|
131
|
+
include Action
|
132
|
+
def call
|
133
|
+
result = InnerAction.call(param: "value")
|
134
|
+
result # Result is returned, so it's properly handled
|
135
|
+
end
|
136
|
+
end
|
137
|
+
```
|
138
|
+
|
139
|
+
#### ✅ Good - Using result in expose
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
class OuterAction
|
143
|
+
include Action
|
144
|
+
exposes :nested_result
|
145
|
+
def call
|
146
|
+
result = InnerAction.call(param: "value")
|
147
|
+
expose nested_result: result # Result is used, so it's properly handled
|
148
|
+
end
|
149
|
+
end
|
150
|
+
```
|
151
|
+
|
152
|
+
#### ✅ Good - Passing result to another method
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
class OuterAction
|
156
|
+
include Action
|
157
|
+
def call
|
158
|
+
result = InnerAction.call(param: "value")
|
159
|
+
process_result(result) # Result is used, so it's properly handled
|
160
|
+
end
|
161
|
+
end
|
162
|
+
```
|
163
|
+
|
164
|
+
### What the Cop Checks
|
165
|
+
|
166
|
+
The cop analyzes your code to determine if you're:
|
167
|
+
|
168
|
+
1. **Inside an Action class** - Classes that `include Action`
|
169
|
+
2. **Inside the `call` method** - Only the main execution method
|
170
|
+
3. **Calling another Action** - Using `.call` on Action classes
|
171
|
+
4. **Properly handling the result** - One of the acceptable patterns above
|
172
|
+
|
173
|
+
### What the Cop Ignores
|
174
|
+
|
175
|
+
The cop will NOT report offenses for:
|
176
|
+
|
177
|
+
- Action calls outside of Action classes
|
178
|
+
- Action calls in methods other than `call`
|
179
|
+
- Action calls that use `call!` (bang method)
|
180
|
+
- Action calls where the result is properly handled
|
181
|
+
|
182
|
+
### Configuration
|
183
|
+
|
184
|
+
Enable the cop in your `.rubocop.yml`:
|
185
|
+
|
186
|
+
```yaml
|
187
|
+
require:
|
188
|
+
- ./lib/rubocop/cop/axn/unchecked_result
|
189
|
+
|
190
|
+
Axn/UncheckedResult:
|
191
|
+
Enabled: true
|
192
|
+
CheckNested: true # Check nested Action calls
|
193
|
+
CheckNonNested: true # Check non-nested Action calls
|
194
|
+
Severity: warning # or error, if you want to enforce it strictly
|
195
|
+
```
|
196
|
+
|
197
|
+
### Best Practices
|
198
|
+
|
199
|
+
1. **Prefer `call!` for simple cases** where you want exceptions to bubble up
|
200
|
+
2. **Use result checking for complex logic** where you need to handle different failure modes
|
201
|
+
3. **Always handle results explicitly** - don't let them be silently ignored
|
202
|
+
4. **Return results early** when they indicate failure
|
203
|
+
5. **Use meaningful variable names** for results to make your code more readable
|
204
|
+
|
205
|
+
### Common Patterns
|
206
|
+
|
207
|
+
#### Early Return Pattern
|
208
|
+
```ruby
|
209
|
+
def call
|
210
|
+
result = InnerAction.call(param: "value")
|
211
|
+
return result unless result.ok?
|
212
|
+
|
213
|
+
# Continue with successful result...
|
214
|
+
end
|
215
|
+
```
|
216
|
+
|
217
|
+
#### Conditional Processing Pattern
|
218
|
+
```ruby
|
219
|
+
def call
|
220
|
+
result = InnerAction.call(param: "value")
|
221
|
+
|
222
|
+
if result.ok?
|
223
|
+
process_success(result)
|
224
|
+
else
|
225
|
+
handle_failure(result)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
```
|
229
|
+
|
230
|
+
#### Pass-Through Pattern
|
231
|
+
```ruby
|
232
|
+
def call
|
233
|
+
result = InnerAction.call(param: "value")
|
234
|
+
# Pass the result through to the caller
|
235
|
+
result
|
236
|
+
end
|
237
|
+
```
|