axn 0.1.0.pre.alpha.2.8 → 0.1.0.pre.alpha.3

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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/rules/axn-framework-patterns.mdc +43 -0
  3. data/.cursor/rules/general-coding-standards.mdc +27 -0
  4. data/.cursor/rules/spec/testing-patterns.mdc +40 -0
  5. data/CHANGELOG.md +47 -0
  6. data/Rakefile +12 -2
  7. data/docs/.vitepress/config.mjs +8 -3
  8. data/docs/advanced/conventions.md +2 -2
  9. data/docs/advanced/mountable.md +562 -0
  10. data/docs/advanced/profiling.md +355 -0
  11. data/docs/advanced/rough.md +1 -1
  12. data/docs/index.md +5 -3
  13. data/docs/intro/about.md +1 -1
  14. data/docs/intro/overview.md +5 -5
  15. data/docs/recipes/memoization.md +2 -2
  16. data/docs/recipes/rubocop-integration.md +38 -284
  17. data/docs/recipes/testing.md +14 -14
  18. data/docs/recipes/validating-user-input.md +1 -1
  19. data/docs/reference/async.md +160 -0
  20. data/docs/reference/axn-result.md +107 -0
  21. data/docs/reference/class.md +123 -25
  22. data/docs/reference/configuration.md +191 -10
  23. data/docs/reference/instance.md +14 -29
  24. data/docs/strategies/index.md +21 -21
  25. data/docs/strategies/transaction.md +1 -1
  26. data/docs/usage/setup.md +14 -0
  27. data/docs/usage/steps.md +7 -7
  28. data/docs/usage/using.md +23 -12
  29. data/docs/usage/writing.md +92 -11
  30. data/lib/axn/async/adapters/active_job.rb +65 -0
  31. data/lib/axn/async/adapters/disabled.rb +26 -0
  32. data/lib/axn/async/adapters/sidekiq.rb +74 -0
  33. data/lib/axn/async/adapters.rb +26 -0
  34. data/lib/axn/async.rb +61 -0
  35. data/lib/{action → axn}/configuration.rb +21 -3
  36. data/lib/{action → axn}/context.rb +21 -4
  37. data/lib/{action → axn}/core/automatic_logging.rb +6 -6
  38. data/lib/axn/core/context/facade.rb +69 -0
  39. data/lib/{action → axn}/core/context/facade_inspector.rb +31 -4
  40. data/lib/{action → axn}/core/context/internal.rb +5 -5
  41. data/lib/{action → axn}/core/contract.rb +43 -46
  42. data/lib/{action → axn}/core/contract_for_subfields.rb +30 -35
  43. data/lib/{action → axn}/core/contract_validation.rb +16 -6
  44. data/lib/axn/core/contract_validation_for_subfields.rb +158 -0
  45. data/lib/axn/core/field_resolvers/extract.rb +32 -0
  46. data/lib/axn/core/field_resolvers/model.rb +63 -0
  47. data/lib/axn/core/field_resolvers.rb +24 -0
  48. data/lib/{action → axn}/core/flow/callbacks.rb +7 -7
  49. data/lib/{action → axn}/core/flow/exception_execution.rb +4 -13
  50. data/lib/{action → axn}/core/flow/handlers/base_descriptor.rb +3 -2
  51. data/lib/{action → axn}/core/flow/handlers/descriptors/callback_descriptor.rb +2 -2
  52. data/lib/{action → axn}/core/flow/handlers/descriptors/message_descriptor.rb +6 -6
  53. data/lib/{action → axn}/core/flow/handlers/invoker.rb +6 -6
  54. data/lib/{action → axn}/core/flow/handlers/matcher.rb +5 -5
  55. data/lib/{action → axn}/core/flow/handlers/registry.rb +3 -1
  56. data/lib/{action → axn}/core/flow/handlers/resolvers/base_resolver.rb +1 -1
  57. data/lib/{action → axn}/core/flow/handlers/resolvers/callback_resolver.rb +2 -2
  58. data/lib/{action → axn}/core/flow/handlers/resolvers/message_resolver.rb +12 -3
  59. data/lib/axn/core/flow/handlers.rb +20 -0
  60. data/lib/{action → axn}/core/flow/messages.rb +7 -7
  61. data/lib/{action → axn}/core/flow.rb +4 -4
  62. data/lib/{action → axn}/core/hooks.rb +16 -5
  63. data/lib/{action → axn}/core/logging.rb +3 -3
  64. data/lib/{action → axn}/core/nesting_tracking.rb +1 -1
  65. data/lib/axn/core/profiling.rb +124 -0
  66. data/lib/{action → axn}/core/timing.rb +1 -1
  67. data/lib/axn/core/tracing.rb +17 -0
  68. data/lib/axn/core/use_strategy.rb +29 -0
  69. data/lib/{action → axn}/core/validation/fields.rb +26 -2
  70. data/lib/{action → axn}/core/validation/subfields.rb +14 -12
  71. data/lib/axn/core/validation/validators/model_validator.rb +36 -0
  72. data/lib/axn/core/validation/validators/type_validator.rb +80 -0
  73. data/lib/{action → axn}/core/validation/validators/validate_validator.rb +12 -2
  74. data/lib/axn/core.rb +123 -0
  75. data/lib/{action → axn}/exceptions.rb +12 -2
  76. data/lib/axn/factory.rb +102 -34
  77. data/lib/axn/internal/logging.rb +26 -0
  78. data/lib/axn/internal/registry.rb +87 -0
  79. data/lib/axn/mountable/descriptor.rb +76 -0
  80. data/lib/axn/mountable/helpers/class_builder.rb +162 -0
  81. data/lib/axn/mountable/helpers/mounter.rb +33 -0
  82. data/lib/axn/mountable/helpers/namespace_manager.rb +66 -0
  83. data/lib/axn/mountable/helpers/validator.rb +112 -0
  84. data/lib/axn/mountable/inherit_profiles.rb +72 -0
  85. data/lib/axn/mountable/mounting_strategies/_base.rb +83 -0
  86. data/lib/axn/mountable/mounting_strategies/axn.rb +48 -0
  87. data/lib/axn/mountable/mounting_strategies/enqueue_all.rb +55 -0
  88. data/lib/axn/mountable/mounting_strategies/method.rb +95 -0
  89. data/lib/axn/mountable/mounting_strategies/step.rb +69 -0
  90. data/lib/axn/mountable/mounting_strategies.rb +32 -0
  91. data/lib/axn/mountable.rb +85 -0
  92. data/lib/axn/rails/engine.rb +51 -0
  93. data/lib/axn/rails/generators/axn_generator.rb +68 -0
  94. data/lib/axn/rails/generators/templates/action.rb.erb +17 -0
  95. data/lib/axn/rails/generators/templates/action_spec.rb.erb +25 -0
  96. data/lib/{action → axn}/result.rb +30 -11
  97. data/lib/{action → axn}/strategies/transaction.rb +1 -1
  98. data/lib/axn/strategies.rb +20 -0
  99. data/lib/axn/testing/spec_helpers.rb +6 -8
  100. data/lib/axn/util/memoization.rb +20 -0
  101. data/lib/axn/version.rb +1 -1
  102. data/lib/axn.rb +17 -16
  103. data/lib/rubocop/cop/axn/README.md +23 -23
  104. data/lib/rubocop/cop/axn/unchecked_result.rb +138 -17
  105. metadata +88 -64
  106. data/.rspec +0 -3
  107. data/.rubocop.yml +0 -76
  108. data/.tool-versions +0 -1
  109. data/docs/reference/action-result.md +0 -37
  110. data/lib/action/attachable/base.rb +0 -43
  111. data/lib/action/attachable/steps.rb +0 -63
  112. data/lib/action/attachable/subactions.rb +0 -70
  113. data/lib/action/attachable.rb +0 -17
  114. data/lib/action/core/context/facade.rb +0 -48
  115. data/lib/action/core/flow/handlers.rb +0 -20
  116. data/lib/action/core/tracing.rb +0 -17
  117. data/lib/action/core/use_strategy.rb +0 -30
  118. data/lib/action/core/validation/validators/model_validator.rb +0 -34
  119. data/lib/action/core/validation/validators/type_validator.rb +0 -30
  120. data/lib/action/core.rb +0 -108
  121. data/lib/action/enqueueable/via_sidekiq.rb +0 -76
  122. data/lib/action/enqueueable.rb +0 -13
  123. data/lib/action/strategies.rb +0 -48
  124. data/lib/axn/util.rb +0 -24
  125. data/package.json +0 -10
  126. data/yarn.lock +0 -1166
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## `#expose`
4
4
 
5
- Used to set a value on the Action::Result. Remember you can only `expose` keys that you have declared in [the class-level interface](/reference/class).
5
+ Used to set a value on the Axn::Result. Remember you can only `expose` keys that you have declared in [the class-level interface](/reference/class).
6
6
 
7
7
  * Accepts two positional arguments (the key and value to set, respectively): `expose :some_key, 123`
8
8
  * Accepts a hash with one or more key/value pairs: `expose some_key: 123, another: 456`
@@ -12,43 +12,28 @@ Primarily used for its side effects, but it does return a Hash with the key/valu
12
12
 
13
13
  ## `#fail!`
14
14
 
15
- Called with a string, it immediately halts execution and sets `result.error` to the provided string.
15
+ Called with a string, it immediately halts execution and sets `result.error` to the provided string. Can also accept keyword arguments that will be exposed before halting execution.
16
16
 
17
- ## `#log`
18
-
19
- Helper method to log (via the [configurable](/reference/configuration#logger) `Action.config.logger`) the string you provide (prefixed with the Action's class name).
20
-
21
- * First argument (required) is a string message to log
22
- * Also accepts a `level:` keyword argument to change the log level (defaults to `info`)
17
+ * First argument (optional) is a string error message
18
+ * Additional keyword arguments are exposed as data before halting
23
19
 
24
- Primarily used for its side effects; returns whatever the underlying `Action.config.logger` instance returns but it does return a Hash with the key/value pair(s) you exposed.
20
+ ## `#done!`
25
21
 
26
- ## `#try`
22
+ Called with an optional string, it immediately halts execution and sets `result.success` to the provided string (or default success message if none provided). Can also accept keyword arguments that will be exposed before halting execution. Skips `after` hooks and remaining `call` method execution, but allows `around` hooks to complete normally.
27
23
 
28
- Accepts a block. Any exceptions raised within that block will be swallowed, but _they will NOT fail the action_!
24
+ * First argument (optional) is a string success message
25
+ * Additional keyword arguments are exposed as data before halting
29
26
 
30
- A few details:
31
- * An explicit `fail!` call _will_ still fail the action
32
- * Any exceptions swallowed _will_ still be reported via the `on_exception` handler
27
+ **Important:** This method is implemented internally via an exception, so it will roll back manually applied `ActiveRecord::Base.transaction` blocks. Use the [`use :transaction` strategy](/strategies/transaction) instead for transaction-safe early completion.
33
28
 
34
- This is primarily useful in an after block, e.g. trigger notifications after an action has been taken. If the notification fails to send you DO want to log the failure somewhere to investigate, but since the core action has already been taken often you do _not_ want to fail.
35
-
36
- Example:
37
-
38
- ```ruby
39
- class Foo
40
- include Action
29
+ ## `#log`
41
30
 
42
- after do
43
- try { send_slack_notifications } # [!code focus]
44
- end
31
+ Helper method to log (via the [configurable](/reference/configuration#logger) `Axn.config.logger`) the string you provide (prefixed with the Action's class name).
45
32
 
46
- def call = ...
33
+ * First argument (required) is a string message to log
34
+ * Also accepts a `level:` keyword argument to change the log level (defaults to `info`)
47
35
 
48
- private
36
+ Primarily used for its side effects; returns whatever the underlying `Axn.config.logger` instance returns but it does return a Hash with the key/value pair(s) you exposed.
49
37
 
50
- def send_slack_notifications = ...
51
- end
52
- ```
53
38
 
54
39
 
@@ -19,7 +19,7 @@ To use a strategy in your action, call the `use` method with the strategy name:
19
19
 
20
20
  ```ruby
21
21
  class CreateUser
22
- include Action
22
+ include Axn
23
23
 
24
24
  use :transaction
25
25
 
@@ -35,11 +35,11 @@ end
35
35
 
36
36
  ### Using Strategies with Configuration
37
37
 
38
- Some strategies support configuration options. These strategies have a `setup` method that accepts configuration and returns a configured module. As an _imaginary_ example:
38
+ Some strategies support configuration options. These strategies have a `configure` method that accepts configuration and returns a configured module. As an _imaginary_ example:
39
39
 
40
40
  ```ruby
41
41
  class ProcessPayment
42
- include Action
42
+ include Axn
43
43
 
44
44
  use :retry, max_attempts: 3, backoff: :exponential
45
45
 
@@ -55,7 +55,7 @@ end
55
55
 
56
56
  ## Built-in Strategies
57
57
 
58
- The list of built in strategies is available via `Action::Strategies.built_in`.
58
+ The list of built in strategies is available via `Axn::Strategies.built_in`.
59
59
 
60
60
  ## Registering Custom Strategies
61
61
 
@@ -79,14 +79,14 @@ end
79
79
  Then register it with the strategies system:
80
80
 
81
81
  ```ruby
82
- Action::Strategies.register(:my_custom, MyCustomStrategy)
82
+ Axn::Strategies.register(:my_custom, MyCustomStrategy)
83
83
  ```
84
84
 
85
85
  Now you can use it in your actions:
86
86
 
87
87
  ```ruby
88
88
  class MyAction
89
- include Action
89
+ include Axn
90
90
 
91
91
  use :my_custom
92
92
 
@@ -98,13 +98,13 @@ end
98
98
 
99
99
  ### Configurable Strategies
100
100
 
101
- For strategies that need configuration, implement a `setup` method that returns a configured module:
101
+ For strategies that need configuration, implement a `configure` method that returns a configured module:
102
102
 
103
103
  ```ruby
104
104
  module RetryStrategy
105
105
  extend ActiveSupport::Concern
106
106
 
107
- def self.setup(max_attempts: 3, backoff: :linear, &block)
107
+ def self.configure(max_attempts: 3, backoff: :linear, &block)
108
108
  Module.new do
109
109
  extend ActiveSupport::Concern
110
110
 
@@ -142,15 +142,15 @@ module RetryStrategy
142
142
  end
143
143
 
144
144
  # Register the strategy
145
- Action::Strategies.register(:retry, RetryStrategy)
145
+ Axn::Strategies.register(:retry, RetryStrategy)
146
146
  ```
147
147
 
148
148
  ### Strategy Registration Best Practices
149
149
 
150
150
  1. **Register early**: Register custom strategies during application initialization
151
151
  2. **Use descriptive names**: Choose strategy names that clearly indicate their purpose
152
- 3. **Handle configuration validation**: Validate configuration options in your `setup` method
153
- 4. **Return proper modules**: Always return a module from the `setup` method
152
+ 3. **Handle configuration validation**: Validate configuration options in your `configure` method
153
+ 4. **Return proper modules**: Always return a module from the `configure` method
154
154
  5. **Document your strategies**: Include clear documentation for how to use your custom strategies
155
155
 
156
156
  ### Example: Complete Custom Strategy
@@ -161,7 +161,7 @@ Here's a complete example of a custom strategy that adds performance monitoring
161
161
  module PerformanceMonitoringStrategy
162
162
  extend ActiveSupport::Concern
163
163
 
164
- def self.setup(threshold_ms: 1000, notify_slow: false, &block)
164
+ def self.configure(threshold_ms: 1000, notify_slow: false, &block)
165
165
  Module.new do
166
166
  extend ActiveSupport::Concern
167
167
 
@@ -194,11 +194,11 @@ module PerformanceMonitoringStrategy
194
194
  end
195
195
 
196
196
  # Register the strategy
197
- Action::Strategies.register(:performance_monitoring, PerformanceMonitoringStrategy)
197
+ Axn::Strategies.register(:performance_monitoring, PerformanceMonitoringStrategy)
198
198
 
199
199
  # Use it in an action
200
200
  class ExpensiveCalculation
201
- include Action
201
+ include Axn
202
202
 
203
203
  use :performance_monitoring, threshold_ms: 500, notify_slow: true
204
204
 
@@ -227,7 +227,7 @@ end
227
227
  You can inspect all registered strategies:
228
228
 
229
229
  ```ruby
230
- Action::Strategies.all
230
+ Axn::Strategies.all
231
231
  # Returns a hash of strategy names to their modules
232
232
  ```
233
233
 
@@ -236,11 +236,11 @@ Action::Strategies.all
236
236
  To find a specific strategy by name:
237
237
 
238
238
  ```ruby
239
- Action::Strategies.find(:transaction)
239
+ Axn::Strategies.find(:transaction)
240
240
  # Returns the strategy module for the transaction strategy
241
241
 
242
- Action::Strategies.find(:nonexistent)
243
- # Raises Action::StrategyNotFound: Strategy 'nonexistent' not found
242
+ Axn::Strategies.find(:nonexistent)
243
+ # Raises Axn::StrategyNotFound: Strategy 'nonexistent' not found
244
244
  ```
245
245
 
246
246
  The `find` method is useful when you need to programmatically access a strategy module or verify that a strategy exists before using it.
@@ -250,15 +250,15 @@ The `find` method is useful when you need to programmatically access a strategy
250
250
  To reset strategies to only built-in ones (useful in tests):
251
251
 
252
252
  ```ruby
253
- Action::Strategies.clear!
253
+ Axn::Strategies.clear!
254
254
  ```
255
255
 
256
256
  ### Strategy Errors
257
257
 
258
258
  The following errors may be raised when using strategies:
259
259
 
260
- - `Action::StrategyNotFound`: When trying to use a strategy that hasn't been registered
261
- - `Action::DuplicateStrategyError`: When trying to register a strategy with a name that's already taken
260
+ - `Axn::StrategyNotFound`: When trying to use a strategy that hasn't been registered
261
+ - `Axn::DuplicateStrategyError`: When trying to register a strategy with a name that's already taken
262
262
  - `ArgumentError`: When providing configuration to a strategy that doesn't support it
263
263
 
264
264
  ## Best Practices
@@ -4,7 +4,7 @@ The `transaction` strategy wraps your action execution in a database transaction
4
4
 
5
5
  ```ruby
6
6
  class TransferFunds
7
- include Action
7
+ include Axn
8
8
 
9
9
  use :transaction
10
10
 
data/docs/usage/setup.md CHANGED
@@ -21,6 +21,20 @@ By default any swallowed errors are noted in the logs, but it's _highly recommen
21
21
 
22
22
  If you're using an APM provider, observability can be greatly enhanced by [configuring tracing and metrics hooks](/reference/configuration#tracing-and-metrics).
23
23
 
24
+ ### Rails Integration (Optional)
25
+
26
+ When using Axn in a Rails application, you can configure how actions are autoloaded from the `app/actions` directory. By default, actions are loaded without any namespace, but you can configure a namespace to help differentiate them from existing service objects:
27
+
28
+ ```ruby
29
+ # config/initializers/axn.rb
30
+ Axn.configure do |c|
31
+ # Use :Actions namespace to differentiate from existing service objects
32
+ c.rails.app_actions_autoload_namespace = :Actions
33
+ end
34
+ ```
35
+
36
+ This is particularly useful when migrating from existing service object patterns, as it makes it easy to distinguish between new Axn actions and legacy service objects when you see `action.call` in your codebase.
37
+
24
38
  ### Code Quality (Optional)
25
39
 
26
40
  For teams using RuboCop, Axn provides custom cops to enforce best practices. See the [RuboCop Integration guide](/recipes/rubocop-integration) for setup instructions.
data/docs/usage/steps.md CHANGED
@@ -31,7 +31,7 @@ The `step` method allows you to define steps inline with blocks:
31
31
 
32
32
  ```ruby
33
33
  class UserRegistration
34
- include Action
34
+ include Axn
35
35
  expects :email, :password, :name
36
36
  exposes :user_id, :welcome_message
37
37
 
@@ -66,7 +66,7 @@ The `steps` method allows you to compose existing action classes:
66
66
 
67
67
  ```ruby
68
68
  class ValidateInput
69
- include Action
69
+ include Axn
70
70
  expects :email, :password, :name
71
71
  exposes :validated_data
72
72
 
@@ -80,7 +80,7 @@ class ValidateInput
80
80
  end
81
81
 
82
82
  class CreateUser
83
- include Action
83
+ include Axn
84
84
  expects :validated_data
85
85
  exposes :user_id
86
86
 
@@ -91,7 +91,7 @@ class CreateUser
91
91
  end
92
92
 
93
93
  class SendWelcome
94
- include Action
94
+ include Axn
95
95
  expects :user_id, :validated_data
96
96
  exposes :welcome_message
97
97
 
@@ -102,7 +102,7 @@ class SendWelcome
102
102
  end
103
103
 
104
104
  class UserRegistration
105
- include Action
105
+ include Axn
106
106
  expects :email, :password, :name
107
107
  exposes :user_id, :welcome_message
108
108
 
@@ -117,7 +117,7 @@ You can combine both approaches:
117
117
 
118
118
  ```ruby
119
119
  class UserRegistration
120
- include Action
120
+ include Axn
121
121
  expects :email, :password, :name
122
122
  exposes :user_id, :welcome_message
123
123
 
@@ -292,7 +292,7 @@ end
292
292
 
293
293
  ```ruby
294
294
  class ProcessAPIRequest
295
- include Action
295
+ include Axn
296
296
  expects :request_data
297
297
  exposes :response_data
298
298
 
data/docs/usage/using.md CHANGED
@@ -7,9 +7,9 @@ outline: deep
7
7
 
8
8
  ## Common Case
9
9
 
10
- An action executed via `#call` _always_ returns an instance of the `Action::Result` class.
10
+ An action executed via `#call` _always_ returns an instance of the `Axn::Result` class.
11
11
 
12
- This means the result _always_ implements a consistent interface, including `ok?` and `error` (see [full details](/reference/action-result)) as well as any variables that the action `exposes`.
12
+ This means the result _always_ implements a consistent interface, including `ok?` and `error` (see [full details](/reference/axn-result)) as well as any variables that the action `exposes`.
13
13
 
14
14
  As a consumer, you usually want a conditional that surfaces `error` unless the result is `ok?` (remember that any exceptions have been swallowed), and otherwise takes whatever success action is relevant.
15
15
 
@@ -38,20 +38,31 @@ end
38
38
 
39
39
  ### `#call!`
40
40
 
41
- An action executed via `#call!` (note the `!`) does _not_ swallow exceptions -- a _successful_ action will return an `Action::Result` just like `call`, but any exceptions will bubble up uncaught (note: technically they _will_ be caught, your on_exception handler triggered, and then re-raised) and any explicit `fail!` calls will raise an `Action::Failure` exception with your custom message.
41
+ An action executed via `#call!` (note the `!`) does _not_ swallow exceptions -- a _successful_ action will return an `Axn::Result` just like `call`, but any exceptions will bubble up uncaught (note: technically they _will_ be caught, your on_exception handler triggered, and then re-raised) and any explicit `fail!` calls will raise an `Axn::Failure` exception with your custom message.
42
42
 
43
43
  This is a much less common pattern, as you're giving up the benefits of error swallowing and the consistent return interface guarantee, but it can be useful in limited contexts (usually for smaller, one-off scripts where it's easier to just let a failure bubble up rather than worry about adding conditionals for error handling).
44
44
 
45
45
 
46
- ### `#enqueue`
46
+ ### `#call_async`
47
47
 
48
- Before adopting this library, our code was littered with one-line workers whose only job was to fire off a service on a background job. We were able to remove that entire glue layer by directly supporting enqueueing sidekiq jobs from the Action itself.
48
+ Before adopting this library, our code was littered with one-line workers whose only job was to fire off a service on a background job. We were able to remove that entire glue layer by directly supporting async execution via background jobs from the Axn itself.
49
49
 
50
- ::: danger ALPHA
51
- Sidekiq integration is NOT YET TESTED/NOT YET USED IN OUR APP, and naming will VERY LIKELY change to make it clearer which actions will be retried!
52
- :::
50
+ ```ruby
51
+ class ProcessDataAction
52
+ include Axn
53
+
54
+ expects :data
55
+
56
+ def call
57
+ # Process data logic here
58
+ end
59
+ end
60
+
61
+ # Execute synchronously
62
+ result = ProcessDataAction.call(data: large_dataset)
63
+
64
+ # Execute asynchronously
65
+ ProcessDataAction.call_async(data: large_dataset)
66
+ ```
53
67
 
54
- * enqueue vs enqueue!
55
- * enqueue will not retry even if fails
56
- * enqueue! will go through normal sidekiq retries on any failure (including user-facing `fail!`)
57
- * Note implicit GlobalID support (if not serializable, will get ArgumentError at callsite)
68
+ For detailed information about configuring async adapters (Sidekiq, ActiveJob, etc.), see the [Async Execution documentation](/reference/async).
@@ -8,7 +8,7 @@ The core boilerplate is pretty minimal:
8
8
 
9
9
  ```ruby
10
10
  class Foo
11
- include Action
11
+ include Axn
12
12
 
13
13
  def call
14
14
  # ... do some stuff here?
@@ -22,14 +22,15 @@ The first step is to determine what arguments you expect to be passed into `call
22
22
 
23
23
  If you want to expose any results to the caller, declare that via the `exposes` keyword.
24
24
 
25
- Both of these optionally accept `type:`, `allow_nil:`, `allow_blank:`, and any other ActiveModel validation (see: [reference](/reference/class)).
25
+ Both of these optionally accept `type:`, `optional:`, `allow_nil:`, `allow_blank:`, and any other ActiveModel validation (see: [reference](/reference/class)).
26
26
 
27
27
 
28
28
  ```ruby
29
29
  class Foo
30
- include Action
30
+ include Axn
31
31
 
32
32
  expects :name, type: String # [!code focus:2]
33
+ expects :email, type: String, optional: true # [!code focus:2]
33
34
  exposes :meaning_of_life
34
35
 
35
36
  def call
@@ -42,13 +43,15 @@ end
42
43
 
43
44
  Once the interface is defined, you're primarily focused on defining the `call` method.
44
45
 
45
- To abort execution with a specific error message, call `fail!`.
46
+ To abort execution with a specific error message, call `fail!`. You can also provide exposures as keyword arguments.
47
+
48
+ To complete execution early with a success result, call `done!` with an optional success message and exposures as keyword arguments.
46
49
 
47
50
  If you declare that your action `exposes` anything, you need to actually `expose` it.
48
51
 
49
52
  ```ruby
50
53
  class Foo
51
- include Action
54
+ include Axn
52
55
 
53
56
  expects :name, type: String
54
57
  exposes :meaning_of_life
@@ -64,6 +67,84 @@ end
64
67
 
65
68
  See [the reference doc](/reference/instance) for a few more handy helper methods (e.g. `#log`).
66
69
 
70
+ ### Convenient failure with context
71
+
72
+ Both `fail!` and `done!` can accept keyword arguments to expose data before halting execution:
73
+
74
+ ```ruby
75
+ class UserValidator
76
+ include Axn
77
+
78
+ expects :email
79
+ exposes :error_code, :field
80
+
81
+ def call
82
+ if email.blank?
83
+ fail!("Email is required", error_code: 422, field: "email")
84
+ end
85
+
86
+ # ... validation logic
87
+ end
88
+ end
89
+ ```
90
+
91
+ ## Early completion with `done!`
92
+
93
+ The `done!` method allows you to complete an action early with a success result, bypassing the rest of the execution:
94
+
95
+ ```ruby
96
+ class UserLookup
97
+ include Axn
98
+
99
+ expects :user_id
100
+ exposes :user, :cached
101
+
102
+ def call
103
+ # Check cache first
104
+ cached_user = Rails.cache.read("user:#{user_id}")
105
+ if cached_user
106
+ done!("User found in cache", user: cached_user, cached: true) # Early completion with exposures
107
+ end
108
+
109
+ # This won't execute if done! was called above
110
+ user = User.find(user_id)
111
+ expose user: user, cached: false
112
+ end
113
+ end
114
+ ```
115
+
116
+ ### Important behavior notes
117
+
118
+ **Hook execution:**
119
+ - `done!` **skips** any `after` hooks (or `call` method if called from a `before` hook)
120
+ - `around` hooks **will complete** normally, allowing transactions and tracing to finish properly
121
+ - If you want code that executes on both normal AND early success, use an `on_success` callback instead of an `after` hook
122
+
123
+ **Transaction handling:**
124
+ - `done!` is implemented internally via an exception, so it **will roll back** manually applied `ActiveRecord::Base.transaction` blocks
125
+ - Use the [`use :transaction` strategy](/strategies/transaction) instead - transactions applied via this strategy will **NOT** be rolled back by `done!`
126
+ - This ensures database consistency while allowing early completion
127
+
128
+ **Validation:**
129
+ - Outbound validation (required `exposes`) still runs even with early completion
130
+ - If required fields are not provided, the action will fail despite the early completion
131
+
132
+ ```ruby
133
+ class BadExample
134
+ include Axn
135
+
136
+ expects :user_id
137
+ exposes :user # Required field
138
+
139
+ def call
140
+ done!("Early completion") # This will FAIL - user not exposed
141
+ end
142
+ end
143
+
144
+ BadExample.call(user_id: 123).ok? # => false
145
+ BadExample.call(user_id: 123).exception # => Axn::OutboundValidationError
146
+ ```
147
+
67
148
  ## Customizing messages
68
149
 
69
150
  The default `error` and `success` message strings ("Something went wrong" / "Action completed successfully", respectively) _are_ technically safe to show users, but you'll often want to set them to something more useful.
@@ -74,7 +155,7 @@ For instance, configuring the action like this:
74
155
 
75
156
  ```ruby
76
157
  class Foo
77
- include Action
158
+ include Axn
78
159
 
79
160
  expects :name, type: String
80
161
  exposes :meaning_of_life
@@ -106,7 +187,7 @@ You can also use conditional error messages with the `prefix:` keyword and combi
106
187
 
107
188
  ```ruby
108
189
  class ValidationAction
109
- include Action
190
+ include Axn
110
191
 
111
192
  expects :input
112
193
 
@@ -123,7 +204,7 @@ class ValidationAction
123
204
  end
124
205
 
125
206
  class ApiAction
126
- include Action
207
+ include Axn
127
208
 
128
209
  expects :data
129
210
 
@@ -152,7 +233,7 @@ This configuration provides:
152
233
  **Correct order:**
153
234
  ```ruby
154
235
  class Foo
155
- include Action
236
+ include Axn
156
237
 
157
238
  # Static fallback messages first
158
239
  success "Default success message"
@@ -175,13 +256,13 @@ In addition to `#call`, there are a few additional pieces to be aware of:
175
256
 
176
257
  `before`, `after`, and `around` hooks are supported. They can receive a block directly, or the symbol name of a local method.
177
258
 
178
- Note execution is halted whenever `fail!` is called or an exception is raised (so a `before` block failure won't execute `call` or `after`, while an `after` block failure will make `result.ok?` be false even though `call` completed successfully).
259
+ Note execution is halted whenever `fail!` is called, `done!` is called, or an exception is raised (so a `before` block failure won't execute `call` or `after`, while an `after` block failure will make `result.ok?` be false even though `call` completed successfully). The `done!` method specifically skips `after` hooks and any remaining `call` method execution, but allows `around` hooks to complete normally.
179
260
 
180
261
  For instance, given this configuration:
181
262
 
182
263
  ```ruby
183
264
  class Foo
184
- include Action
265
+ include Axn
185
266
 
186
267
  before { log("before hook") } # [!code focus:2]
187
268
  after :log_after
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Axn
4
+ module Async
5
+ class Adapters
6
+ module ActiveJob
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ raise LoadError, "ActiveJob is not available. Please add 'activejob' to your Gemfile." unless defined?(::ActiveJob::Base)
11
+
12
+ # Validate that kwargs are not provided for ActiveJob
13
+ if _async_config&.any?
14
+ raise ArgumentError, "ActiveJob adapter requires a configuration block. Use `async :active_job do ... end` instead of passing keyword arguments."
15
+ end
16
+ end
17
+
18
+ class_methods do
19
+ def call_async(**kwargs)
20
+ job = active_job_proxy_class
21
+
22
+ if kwargs[:_async].is_a?(Hash)
23
+ options = kwargs.delete(:_async)
24
+ if options[:wait_until]
25
+ job = job.set(wait_until: options[:wait_until])
26
+ elsif options[:wait]
27
+ job = job.set(wait: options[:wait])
28
+ end
29
+ end
30
+
31
+ job.perform_later(**kwargs)
32
+ end
33
+
34
+ private
35
+
36
+ def active_job_proxy_class
37
+ @active_job_proxy_class ||= create_active_job_proxy_class
38
+ end
39
+
40
+ def create_active_job_proxy_class
41
+ # Store reference to the original action class
42
+ action_class = self
43
+
44
+ # Create the ActiveJob proxy class
45
+ Class.new(::ActiveJob::Base).tap do |proxy|
46
+ # Give the job class a meaningful name for logging and debugging
47
+ job_name = "#{name}::ActiveJobProxy"
48
+ const_set("ActiveJobProxy", proxy)
49
+ proxy.define_singleton_method(:name) { job_name }
50
+
51
+ # Apply the async configuration block if it exists
52
+ proxy.class_eval(&_async_config_block) if _async_config_block
53
+
54
+ # Define the perform method
55
+ proxy.define_method(:perform) do |job_context = {}|
56
+ # Call the original action class with the job context
57
+ action_class.call!(**job_context)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Axn
4
+ module Async
5
+ class Adapters
6
+ module Disabled
7
+ def self.included(base)
8
+ base.class_eval do
9
+ # Validate that kwargs are not provided for Disabled adapter
10
+ raise ArgumentError, "Disabled adapter does not accept configuration options." if _async_config&.any?
11
+ raise ArgumentError, "Disabled adapter does not accept configuration block." if _async_config_block
12
+
13
+ def self.call_async(**kwargs)
14
+ # Remove _async parameter to avoid confusion in error message
15
+ kwargs.delete(:_async)
16
+
17
+ raise NotImplementedError,
18
+ "Async execution is explicitly disabled for #{name}. " \
19
+ "Use `async :sidekiq` or `async :active_job` to enable background processing."
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end