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
@@ -0,0 +1,327 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Axn
6
+ # This cop enforces that when calling Actions from within other Actions,
7
+ # you must either use `call!` (with the bang) or check `result.ok?`.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # class OuterAction
12
+ # include Action
13
+ # def call
14
+ # InnerAction.call(param: "value") # Missing result check
15
+ # end
16
+ # end
17
+ #
18
+ # # good
19
+ # class OuterAction
20
+ # include Action
21
+ # def call
22
+ # result = InnerAction.call(param: "value")
23
+ # return result unless result.ok?
24
+ # # Process successful result...
25
+ # end
26
+ # end
27
+ #
28
+ # # also good
29
+ # class OuterAction
30
+ # include Action
31
+ # def call
32
+ # InnerAction.call!(param: "value") # Using call! ensures exceptions bubble up
33
+ # end
34
+ # end
35
+ #
36
+ # rubocop:disable Metrics/ClassLength
37
+ class UncheckedResult < RuboCop::Cop::Base
38
+ extend RuboCop::Cop::AutoCorrector
39
+
40
+ MSG = "Use `call!` or check `result.ok?` when calling Actions from within Actions"
41
+
42
+ # Configuration options
43
+ def check_nested?
44
+ cop_config["CheckNested"] != false
45
+ end
46
+
47
+ def check_non_nested?
48
+ cop_config["CheckNonNested"] != false
49
+ end
50
+
51
+ # Track whether we're inside an Action class and its call method
52
+ def_node_search :action_class?, <<~PATTERN
53
+ (class _ (const nil? :Action) ...)
54
+ PATTERN
55
+
56
+ def_node_search :includes_action?, <<~PATTERN
57
+ (send nil? :include (const nil? :Action))
58
+ PATTERN
59
+
60
+ def_node_search :call_method?, <<~PATTERN
61
+ (def :call ...)
62
+ PATTERN
63
+
64
+ def_node_search :action_call?, <<~PATTERN
65
+ (send (const _ _) :call ...)
66
+ PATTERN
67
+
68
+ def_node_search :bang_call?, <<~PATTERN
69
+ (send (const _ _) :call! ...)
70
+ PATTERN
71
+
72
+ def_node_search :result_assignment?, <<~PATTERN
73
+ (lvasgn _ (send (const _ _) :call ...))
74
+ PATTERN
75
+
76
+ def_node_search :result_ok_check?, <<~PATTERN
77
+ (send (send _ :result) :ok?)
78
+ PATTERN
79
+
80
+ def_node_search :result_failed_check?, <<~PATTERN
81
+ (send (send _ :result) :failed?)
82
+ PATTERN
83
+
84
+ def_node_search :result_error_check?, <<~PATTERN
85
+ (send (send _ :result) :error)
86
+ PATTERN
87
+
88
+ def_node_search :result_exception_check?, <<~PATTERN
89
+ (send (send _ :result) :exception)
90
+ PATTERN
91
+
92
+ def_node_search :return_with_result?, <<~PATTERN
93
+ (return (send _ :result))
94
+ PATTERN
95
+
96
+ def_node_search :expose_with_result?, <<~PATTERN
97
+ (send nil? :expose ...)
98
+ PATTERN
99
+
100
+ def_node_search :result_passed_to_method?, <<~PATTERN
101
+ (send nil? :result ...)
102
+ PATTERN
103
+
104
+ def on_send(node)
105
+ return unless action_call?(node)
106
+ return if bang_call?(node)
107
+ return unless inside_action_call_method?(node)
108
+
109
+ # Check if we should process this call based on configuration
110
+ is_inside_action = inside_action_context?(node)
111
+ return unless (is_inside_action && check_nested?) || (!is_inside_action && check_non_nested?)
112
+
113
+ return if result_properly_handled?(node)
114
+
115
+ add_offense(node, message: MSG)
116
+ end
117
+
118
+ private
119
+
120
+ def inside_action_call_method?(node)
121
+ # Check if we're inside a call method of an Action class
122
+ current_node = node
123
+ while current_node.parent
124
+ current_node = current_node.parent
125
+
126
+ # Check if we're inside a def :call
127
+ next unless call_method?(current_node) && current_node.method_name == :call
128
+
129
+ # Now check if this class includes Action
130
+ class_node = find_enclosing_class(current_node)
131
+ return includes_action?(class_node) if class_node
132
+ end
133
+ false
134
+ rescue StandardError => _e
135
+ # If there's any error in the analysis, assume we're not in an Action call method
136
+ # This prevents the cop from crashing on complex or malformed code
137
+ false
138
+ end
139
+
140
+ def inside_action_context?(node)
141
+ # Check if this Action call is inside an Action class's call method
142
+ current_node = node
143
+ while current_node.parent
144
+ current_node = current_node.parent
145
+
146
+ # Check if we're inside a def :call
147
+ next unless call_method?(current_node) && current_node.method_name == :call
148
+
149
+ # Now check if this class includes Action
150
+ class_node = find_enclosing_class(current_node)
151
+ return true if class_node && includes_action?(class_node)
152
+ end
153
+ false
154
+ rescue StandardError => _e
155
+ false
156
+ end
157
+
158
+ def find_enclosing_class(node)
159
+ current_node = node
160
+ while current_node.parent
161
+ current_node = current_node.parent
162
+ return current_node if current_node.type == :class
163
+ end
164
+ nil
165
+ rescue StandardError => _e
166
+ # If there's any error in the analysis, return nil
167
+ nil
168
+ end
169
+
170
+ def result_properly_handled?(node)
171
+ # Check if the result is assigned to a variable
172
+ parent = node.parent
173
+ return false unless parent&.type == :lvasgn
174
+
175
+ result_var = parent.children[0]
176
+
177
+ # Look for proper result handling in the method
178
+ method_body = find_enclosing_method_body(node)
179
+ return false unless method_body
180
+
181
+ # Check if result.ok? is checked
182
+ return true if result_ok_check_in_method?(method_body, result_var)
183
+
184
+ # Check if result.failed? is checked
185
+ return true if result_failed_check_in_method?(method_body, result_var)
186
+
187
+ # Check if result.error is accessed
188
+ return true if result_error_check_in_method?(method_body, result_var)
189
+
190
+ # Check if result.exception is accessed
191
+ return true if result_exception_check_in_method?(method_body, result_var)
192
+
193
+ # Check if result is returned
194
+ return true if result_returned_in_method?(method_body, result_var)
195
+
196
+ # Check if result is used in expose
197
+ return true if result_used_in_expose?(method_body, result_var)
198
+
199
+ # Check if result is passed to another method
200
+ return true if result_passed_to_method?(method_body, result_var)
201
+
202
+ false
203
+ rescue StandardError => _e
204
+ # If there's any error in the analysis, assume the result is not properly handled
205
+ # This prevents the cop from crashing on complex or malformed code
206
+ false
207
+ end
208
+
209
+ def find_enclosing_method_body(node)
210
+ current_node = node
211
+ while current_node.parent
212
+ current_node = current_node.parent
213
+ if current_node.type == :def && current_node.method_name == :call
214
+ return current_node.children[2] # The method body
215
+ end
216
+ end
217
+ nil
218
+ end
219
+
220
+ def result_ok_check_in_method?(method_body, result_var)
221
+ method_body.each_descendant(:send) do |send_node|
222
+ next unless send_node.method_name == :ok?
223
+
224
+ # Check if this is any_variable.ok?
225
+ receiver = send_node.children[0]
226
+ return true if receiver&.type == :lvar && receiver.children[0] == result_var
227
+ end
228
+ false
229
+ end
230
+
231
+ def result_failed_check_in_method?(method_body, result_var)
232
+ method_body.each_descendant(:send) do |send_node|
233
+ next unless send_node.method_name == :failed?
234
+
235
+ receiver = send_node.children[0]
236
+ return true if receiver&.type == :lvar && receiver.children[0] == result_var
237
+ end
238
+ false
239
+ end
240
+
241
+ def result_error_check_in_method?(method_body, result_var)
242
+ method_body.each_descendant(:send) do |send_node|
243
+ next unless send_node.method_name == :error
244
+
245
+ receiver = send_node.children[0]
246
+ return true if receiver&.type == :lvar && receiver.children[0] == result_var
247
+ end
248
+ false
249
+ end
250
+
251
+ def result_exception_check_in_method?(method_body, result_var)
252
+ method_body.each_descendant(:send) do |send_node|
253
+ next unless send_node.method_name == :exception
254
+
255
+ receiver = send_node.children[0]
256
+ return true if receiver&.type == :lvar && receiver.children[0] == result_var
257
+ end
258
+ false
259
+ end
260
+
261
+ def result_returned_in_method?(method_body, result_var)
262
+ # Check for explicit return statements
263
+ method_body.each_descendant(:return) do |return_node|
264
+ return_value = return_node.children[0]
265
+ return true if return_value&.type == :lvar && return_value.children[0] == result_var
266
+ end
267
+
268
+ # Check for implicit return (last statement in method)
269
+ last_statement = if method_body.type == :begin
270
+ method_body.children.last
271
+ else
272
+ method_body
273
+ end
274
+
275
+ return true if last_statement&.type == :lvar && last_statement.children[0] == result_var
276
+
277
+ false
278
+ end
279
+
280
+ def result_used_in_expose?(method_body, result_var)
281
+ method_body.each_descendant(:send) do |send_node|
282
+ next unless send_node.method_name == :expose
283
+
284
+ # Check if any argument references the result variable
285
+ # send_node.children[0] is the receiver (nil for self)
286
+ # send_node.children[1] is the method name (:expose)
287
+ # send_node.children[2..] are the actual arguments
288
+ send_node.children[2..].each do |arg|
289
+ # Handle hash arguments in expose calls
290
+ if arg.type == :hash
291
+ arg.children.each do |pair|
292
+ next unless pair.type == :pair
293
+
294
+ value = pair.children[1]
295
+ return true if value&.type == :lvar && value.children[0] == result_var
296
+ end
297
+ elsif arg&.type == :lvar && arg.children[0] == result_var
298
+ return true
299
+ end
300
+ end
301
+ end
302
+ false
303
+ end
304
+
305
+ def result_passed_to_method?(method_body, result_var)
306
+ method_body.each_descendant(:send) do |send_node|
307
+ next if send_node.method_name == :result # Skip result method calls
308
+ next if send_node.method_name == :ok? # Skip result.ok? calls
309
+ next if send_node.method_name == :failed? # Skip result.failed? calls
310
+ next if send_node.method_name == :error # Skip result.error calls
311
+ next if send_node.method_name == :exception # Skip result.exception calls
312
+
313
+ # Check if result is passed as an argument
314
+ # send_node.children[0] is the receiver
315
+ # send_node.children[1] is the method name
316
+ # send_node.children[2..] are the actual arguments
317
+ send_node.children[2..].each do |arg|
318
+ return true if arg&.type == :lvar && arg.children[0] == result_var
319
+ end
320
+ end
321
+ false
322
+ end
323
+ # rubocop:enable Metrics/ClassLength
324
+ end
325
+ end
326
+ end
327
+ 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.7.1
4
+ version: 0.1.0.pre.alpha.2.8
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-08-15 00:00:00.000000000 Z
11
+ date: 2025-08-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -61,6 +61,7 @@ files:
61
61
  - docs/intro/about.md
62
62
  - docs/intro/overview.md
63
63
  - docs/recipes/memoization.md
64
+ - docs/recipes/rubocop-integration.md
64
65
  - docs/recipes/testing.md
65
66
  - docs/recipes/validating-user-input.md
66
67
  - docs/reference/action-result.md
@@ -70,6 +71,7 @@ files:
70
71
  - docs/strategies/index.md
71
72
  - docs/strategies/transaction.md
72
73
  - docs/usage/setup.md
74
+ - docs/usage/steps.md
73
75
  - docs/usage/using.md
74
76
  - docs/usage/writing.md
75
77
  - lib/action/attachable.rb
@@ -90,16 +92,19 @@ files:
90
92
  - lib/action/core/flow/callbacks.rb
91
93
  - lib/action/core/flow/exception_execution.rb
92
94
  - lib/action/core/flow/handlers.rb
93
- - lib/action/core/flow/handlers/base_handler.rb
94
- - lib/action/core/flow/handlers/callback_handler.rb
95
+ - lib/action/core/flow/handlers/base_descriptor.rb
96
+ - lib/action/core/flow/handlers/descriptors/callback_descriptor.rb
97
+ - lib/action/core/flow/handlers/descriptors/message_descriptor.rb
95
98
  - lib/action/core/flow/handlers/invoker.rb
96
99
  - lib/action/core/flow/handlers/matcher.rb
97
- - lib/action/core/flow/handlers/message_handler.rb
98
100
  - lib/action/core/flow/handlers/registry.rb
101
+ - lib/action/core/flow/handlers/resolvers/base_resolver.rb
102
+ - lib/action/core/flow/handlers/resolvers/callback_resolver.rb
103
+ - lib/action/core/flow/handlers/resolvers/message_resolver.rb
99
104
  - lib/action/core/flow/messages.rb
100
- - lib/action/core/hoist_errors.rb
101
105
  - lib/action/core/hooks.rb
102
106
  - lib/action/core/logging.rb
107
+ - lib/action/core/nesting_tracking.rb
103
108
  - lib/action/core/timing.rb
104
109
  - lib/action/core/tracing.rb
105
110
  - lib/action/core/use_strategy.rb
@@ -116,9 +121,12 @@ files:
116
121
  - lib/action/strategies/transaction.rb
117
122
  - lib/axn.rb
118
123
  - lib/axn/factory.rb
124
+ - lib/axn/rubocop.rb
119
125
  - lib/axn/testing/spec_helpers.rb
120
126
  - lib/axn/util.rb
121
127
  - lib/axn/version.rb
128
+ - lib/rubocop/cop/axn/README.md
129
+ - lib/rubocop/cop/axn/unchecked_result.rb
122
130
  - package.json
123
131
  - yarn.lock
124
132
  homepage: https://github.com/teamshares/axn
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "action/core/flow/handlers/base_handler"
4
- require "action/core/flow/handlers/invoker"
5
-
6
- module Action
7
- module Core
8
- module Flow
9
- module Handlers
10
- class CallbackHandler < BaseHandler
11
- def apply(action:, exception:)
12
- return false unless matches?(action:, exception:)
13
-
14
- Invoker.call(action:, handler:, exception:, operation: "executing handler")
15
- true
16
- end
17
- end
18
- end
19
- end
20
- end
21
- end
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "action/core/flow/handlers/base_handler"
4
- require "action/core/flow/handlers/invoker"
5
-
6
- module Action
7
- module Core
8
- module Flow
9
- module Handlers
10
- class MessageHandler < BaseHandler
11
- # Returns a string (truthy) when it applies and yields a non-blank message; otherwise nil
12
- def apply(action:, exception:)
13
- return nil unless matches?(action:, exception:)
14
-
15
- value =
16
- if handler.is_a?(Symbol) || handler.respond_to?(:call)
17
- Invoker.call(action:, handler:, exception:, operation: "determining message callable")
18
- else
19
- handler
20
- end
21
- value.respond_to?(:presence) ? value.presence : value
22
- end
23
- end
24
- end
25
- end
26
- end
27
- end
@@ -1,58 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Action
4
- module Core
5
- module HoistErrors
6
- def self.included(base)
7
- base.class_eval do
8
- include InstanceMethods
9
- end
10
- end
11
-
12
- module InstanceMethods
13
- private
14
-
15
- MinimalFailedResult = Data.define(:error, :exception) do
16
- def ok? = false
17
- end
18
-
19
- # This method is used to ensure that the result of a block is successful before proceeding.
20
- #
21
- # Assumes success unless the block raises an exception or returns a failed result.
22
- # (i.e. if you wrap logic that is NOT an action call, it'll be successful unless it raises an exception)
23
- def hoist_errors(prefix: nil)
24
- raise ArgumentError, "#hoist_errors must be given a block to execute" unless block_given?
25
-
26
- result = begin
27
- yield
28
- rescue StandardError => e
29
- log "hoist_errors block transforming a #{e.class.name} exception: #{e.message}"
30
- MinimalFailedResult.new(error: nil, exception: e)
31
- end
32
-
33
- # This ensures the last line of hoist_errors was an Action call
34
- #
35
- # CAUTION: if there are multiple calls per block, only the last one will be checked!
36
- #
37
- unless result.respond_to?(:ok?)
38
- raise ArgumentError,
39
- "#hoist_errors is expected to wrap an Action call, but it returned a #{result.class.name} instead"
40
- end
41
-
42
- return result if result.ok?
43
-
44
- _handle_hoisted_errors(result, prefix:)
45
- end
46
-
47
- # Separate method to allow overriding in subclasses
48
- def _handle_hoisted_errors(result, prefix: nil)
49
- @__context.exception = result.exception if result.exception.present?
50
- @__context.error_prefix = prefix if prefix.present?
51
-
52
- error = result.exception.is_a?(Action::Failure) ? result.exception.message : result.error
53
- fail! error
54
- end
55
- end
56
- end
57
- end
58
- end