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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +11 -5
  3. data/CHANGELOG.md +10 -0
  4. data/Rakefile +12 -0
  5. data/docs/intro/about.md +2 -2
  6. data/docs/intro/overview.md +18 -0
  7. data/docs/recipes/rubocop-integration.md +352 -0
  8. data/docs/reference/action-result.md +1 -1
  9. data/docs/reference/class.md +110 -2
  10. data/docs/reference/configuration.md +5 -3
  11. data/docs/reference/instance.md +0 -52
  12. data/docs/usage/setup.md +4 -0
  13. data/docs/usage/steps.md +335 -0
  14. data/docs/usage/writing.md +67 -0
  15. data/lib/action/attachable/steps.rb +18 -17
  16. data/lib/action/attachable/subactions.rb +1 -1
  17. data/lib/action/context.rb +10 -14
  18. data/lib/action/core/context/facade.rb +11 -2
  19. data/lib/action/core/context/facade_inspector.rb +3 -2
  20. data/lib/action/core/context/internal.rb +3 -11
  21. data/lib/action/core/contract_validation.rb +1 -1
  22. data/lib/action/core/flow/callbacks.rb +22 -8
  23. data/lib/action/core/flow/exception_execution.rb +2 -5
  24. data/lib/action/core/flow/handlers/{base_handler.rb → base_descriptor.rb} +7 -4
  25. data/lib/action/core/flow/handlers/descriptors/callback_descriptor.rb +17 -0
  26. data/lib/action/core/flow/handlers/descriptors/message_descriptor.rb +53 -0
  27. data/lib/action/core/flow/handlers/matcher.rb +41 -2
  28. data/lib/action/core/flow/handlers/resolvers/base_resolver.rb +28 -0
  29. data/lib/action/core/flow/handlers/resolvers/callback_resolver.rb +29 -0
  30. data/lib/action/core/flow/handlers/resolvers/message_resolver.rb +59 -0
  31. data/lib/action/core/flow/handlers.rb +7 -4
  32. data/lib/action/core/flow/messages.rb +15 -41
  33. data/lib/action/core/nesting_tracking.rb +31 -0
  34. data/lib/action/core/timing.rb +1 -1
  35. data/lib/action/core.rb +20 -12
  36. data/lib/action/exceptions.rb +20 -2
  37. data/lib/action/result.rb +30 -32
  38. data/lib/axn/factory.rb +22 -23
  39. data/lib/axn/rubocop.rb +10 -0
  40. data/lib/axn/version.rb +1 -1
  41. data/lib/rubocop/cop/axn/README.md +237 -0
  42. data/lib/rubocop/cop/axn/unchecked_result.rb +327 -0
  43. metadata +14 -6
  44. data/lib/action/core/flow/handlers/callback_handler.rb +0 -21
  45. data/lib/action/core/flow/handlers/message_handler.rb +0 -27
  46. 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.exception = e
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
- [@context.error_prefix, determine_error_message].compact.join(" ").squeeze(" ")
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
- determine_success_message
54
+ _user_provided_success_message || _msg_resolver(:success, exception: nil).resolve_message
57
55
  end
58
56
 
59
- def ok = success
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 = :success,
66
- OUTCOME_FAILURE = :failure,
67
- OUTCOME_EXCEPTION = :exception,
61
+ OUTCOME_SUCCESS = "success",
62
+ OUTCOME_FAILURE = "failure",
63
+ OUTCOME_EXCEPTION = "exception",
68
64
  ].freeze
69
65
 
70
66
  def outcome
71
- return OUTCOME_EXCEPTION if exception
72
- return OUTCOME_FAILURE if @context.failed?
73
-
74
- OUTCOME_SUCCESS
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
- # Elapsed time in milliseconds
78
- def elapsed_time
79
- @context.elapsed_time
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 context_data_source = @context.exposed_data
84
+ def _context_data_source = @context.exposed_data
85
85
 
86
- def determine_error_message
87
- return @context.error_from_user if @context.error_from_user.present?
86
+ # TODO: hook for adding early-return success at some point
87
+ def _user_provided_success_message = nil
88
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
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
- def determine_success_message
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
- apply_message.call(:success, success)
95
- apply_message.call(:error, error)
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.on_success(&on_success) if on_success.present?
104
- axn.on_failure(&on_failure) if on_failure.present?
105
- axn.on_error(&on_error) if on_error.present?
106
- axn.on_exception(&on_exception) if on_exception.present?
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Axn
4
- VERSION = "0.1.0-alpha.2.7.1"
4
+ VERSION = "0.1.0-alpha.2.8"
5
5
  end
@@ -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
+ ```