axn 0.1.0.pre.alpha.2.5.1.1 → 0.1.0.pre.alpha.2.5.2
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 +6 -0
- data/CHANGELOG.md +12 -0
- data/docs/.vitepress/config.mjs +7 -0
- data/docs/strategies/index.md +272 -0
- data/docs/strategies/transaction.md +29 -0
- data/docs/usage/using.md +0 -30
- data/docs/usage/writing.md +2 -2
- data/lib/action/core/contract.rb +12 -1
- data/lib/action/core/contract_for_subfields.rb +1 -12
- data/lib/action/core/use_strategy.rb +28 -0
- data/lib/action/enqueueable.rb +0 -2
- data/lib/action/strategies/transaction.rb +19 -0
- data/lib/action/strategies.rb +48 -0
- data/lib/axn/testing/spec_helpers.rb +23 -0
- data/lib/axn/version.rb +1 -1
- data/lib/axn.rb +4 -2
- metadata +8 -4
- data/lib/action/enqueueable/enqueue_all_in_background.rb +0 -17
- data/lib/action/enqueueable/enqueue_all_worker.rb +0 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6902996eda6bf8911d2f90f73217d56a0905ddf0772ebfee3a9c2bbf8072279e
|
4
|
+
data.tar.gz: 3615981ef31731e017b2b9399b485290041b1a79cc0ccc231c189399ddb4e5f4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1f9ae2c8d65defac13be5abded734bdcf1057e8d880878d8cf4c57c58ef537d257a7a96208fe83616ce8ddc89344c60042b9eadbe8575b52d14fa9fa9981489e
|
7
|
+
data.tar.gz: bcbf95ea1e179a44076cb5b451f0ab1f94001bccb30bd78fd3fbbd025472fb08bed4cda805cf934ab6f3328f4aced08a96ff4983142aeed67c7121ecddd11e2b
|
data/.rubocop.yml
CHANGED
@@ -27,12 +27,18 @@ Style/TrailingCommaInArrayLiteral:
|
|
27
27
|
Style/TrailingCommaInHashLiteral:
|
28
28
|
EnforcedStyleForMultiline: comma
|
29
29
|
|
30
|
+
Style/ClassAndModuleChildren:
|
31
|
+
Enabled: false
|
32
|
+
|
30
33
|
Style/DoubleNegation:
|
31
34
|
Enabled: false
|
32
35
|
|
33
36
|
Metrics/BlockLength:
|
34
37
|
Enabled: false
|
35
38
|
|
39
|
+
Metrics/ModuleLength:
|
40
|
+
Enabled: false
|
41
|
+
|
36
42
|
Metrics/MethodLength:
|
37
43
|
Max: 60
|
38
44
|
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,17 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## Unreleased
|
4
|
+
* N/A
|
5
|
+
|
6
|
+
## 0.1.0-alpha.2.5.2
|
7
|
+
* [BREAKING] Removing `EnqueueAllInBackground` + `EnqueueAllWorker` - better + simply solved at application level
|
8
|
+
* [TEST] Expose spec helpers to consumers (add `require "axn/testing/spec_helpers"` to your `spec_helper.rb`)
|
9
|
+
# [FEAT] Added ability to use custom Strategies (via e.g. `use :transaction`)
|
10
|
+
|
11
|
+
## 0.1.0-alpha.2.5.1.2
|
12
|
+
* [BUGFIX] Subfield expectations: now support hashes with string keys (using with_indifferent_access)
|
13
|
+
* [BUGFIX] Subfield expectations: Model reader fields now cache initial value (otherwise get fresh instance each call, cannot make in-memory changes)
|
14
|
+
|
3
15
|
## 0.1.0-alpha.2.5.1.1
|
4
16
|
* [BUGFIX] TypeValidator must handle anonymous classes when determining if given argument is an RSpec mock
|
5
17
|
|
data/docs/.vitepress/config.mjs
CHANGED
@@ -47,6 +47,13 @@ export default defineConfig({
|
|
47
47
|
{ text: 'Testing Actions', link: '/recipes/testing' },
|
48
48
|
]
|
49
49
|
},
|
50
|
+
{
|
51
|
+
text: 'Strategies',
|
52
|
+
items: [
|
53
|
+
{ text: 'Overview', link: '/strategies/index' },
|
54
|
+
{ text: 'Transaction', link: '/strategies/transaction' },
|
55
|
+
]
|
56
|
+
},
|
50
57
|
{
|
51
58
|
text: 'Additional Notes',
|
52
59
|
items: [
|
@@ -0,0 +1,272 @@
|
|
1
|
+
# Strategies
|
2
|
+
|
3
|
+
Strategies in Axn are reusable modules that provide common functionality and configuration patterns for your actions. They allow you to DRY up your code by encapsulating frequently used behaviors into named, configurable modules.
|
4
|
+
|
5
|
+
## What are Strategies?
|
6
|
+
|
7
|
+
Strategies are Ruby modules that can be included into your actions to add specific functionality. They're designed to be:
|
8
|
+
|
9
|
+
- **Reusable**: Once defined, they can be used across multiple actions
|
10
|
+
- **Configurable**: Many strategies support configuration options
|
11
|
+
- **Composable**: You can use multiple strategies in a single action
|
12
|
+
- **Discoverable**: Built-in strategies are automatically loaded, and custom ones can be registered
|
13
|
+
|
14
|
+
## How to Use Strategies
|
15
|
+
|
16
|
+
### Basic Usage
|
17
|
+
|
18
|
+
To use a strategy in your action, call the `use` method with the strategy name:
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
class CreateUser
|
22
|
+
include Action
|
23
|
+
|
24
|
+
use :transaction
|
25
|
+
|
26
|
+
expects :email, :name
|
27
|
+
|
28
|
+
def call
|
29
|
+
# This action will now run within a database transaction (including before/after hooks)
|
30
|
+
user = User.create!(email: email, name: name)
|
31
|
+
expose :user, user
|
32
|
+
end
|
33
|
+
end
|
34
|
+
```
|
35
|
+
|
36
|
+
### Using Strategies with Configuration
|
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:
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
class ProcessPayment
|
42
|
+
include Action
|
43
|
+
|
44
|
+
use :retry, max_attempts: 3, backoff: :exponential
|
45
|
+
|
46
|
+
expects :amount, :card_token
|
47
|
+
|
48
|
+
def call
|
49
|
+
# This action will retry up to 3 times with exponential backoff
|
50
|
+
result = PaymentProcessor.charge(amount, card_token)
|
51
|
+
expose :transaction_id, result.id
|
52
|
+
end
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
## Built-in Strategies
|
57
|
+
|
58
|
+
The list of built in strategies is available via `Action::Strategies.built_in`.
|
59
|
+
|
60
|
+
## Registering Custom Strategies
|
61
|
+
|
62
|
+
### Simple Strategies
|
63
|
+
|
64
|
+
To create a custom strategy, define a module that extends `ActiveSupport::Concern`:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
module MyCustomStrategy
|
68
|
+
extend ActiveSupport::Concern
|
69
|
+
|
70
|
+
included do
|
71
|
+
# Add your strategy behavior here
|
72
|
+
# For example, add hooks, validations, or other functionality
|
73
|
+
before { log("Custom strategy before hook") }
|
74
|
+
after { log("Custom strategy after hook") }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
```
|
78
|
+
|
79
|
+
Then register it with the strategies system:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
Action::Strategies.register(:my_custom, MyCustomStrategy)
|
83
|
+
```
|
84
|
+
|
85
|
+
Now you can use it in your actions:
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
class MyAction
|
89
|
+
include Action
|
90
|
+
|
91
|
+
use :my_custom
|
92
|
+
|
93
|
+
def call
|
94
|
+
# Your action implementation
|
95
|
+
end
|
96
|
+
end
|
97
|
+
```
|
98
|
+
|
99
|
+
### Configurable Strategies
|
100
|
+
|
101
|
+
For strategies that need configuration, implement a `setup` method that returns a configured module:
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
module RetryStrategy
|
105
|
+
extend ActiveSupport::Concern
|
106
|
+
|
107
|
+
def self.setup(max_attempts: 3, backoff: :linear, &block)
|
108
|
+
Module.new do
|
109
|
+
extend ActiveSupport::Concern
|
110
|
+
|
111
|
+
included do
|
112
|
+
around do |hooked|
|
113
|
+
attempts = 0
|
114
|
+
begin
|
115
|
+
attempts += 1
|
116
|
+
hooked.call
|
117
|
+
rescue StandardError => e
|
118
|
+
if attempts < max_attempts
|
119
|
+
sleep(backoff_delay(attempts, backoff))
|
120
|
+
retry
|
121
|
+
else
|
122
|
+
raise e
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def backoff_delay(attempt, type)
|
131
|
+
case type
|
132
|
+
when :linear
|
133
|
+
attempt * 0.1
|
134
|
+
when :exponential
|
135
|
+
0.1 * (2 ** (attempt - 1))
|
136
|
+
else
|
137
|
+
0.1
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Register the strategy
|
145
|
+
Action::Strategies.register(:retry, RetryStrategy)
|
146
|
+
```
|
147
|
+
|
148
|
+
### Strategy Registration Best Practices
|
149
|
+
|
150
|
+
1. **Register early**: Register custom strategies during application initialization
|
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
|
154
|
+
5. **Document your strategies**: Include clear documentation for how to use your custom strategies
|
155
|
+
|
156
|
+
### Example: Complete Custom Strategy
|
157
|
+
|
158
|
+
Here's a complete example of a custom strategy that adds performance monitoring:
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
module PerformanceMonitoringStrategy
|
162
|
+
extend ActiveSupport::Concern
|
163
|
+
|
164
|
+
def self.setup(threshold_ms: 1000, notify_slow: false, &block)
|
165
|
+
Module.new do
|
166
|
+
extend ActiveSupport::Concern
|
167
|
+
|
168
|
+
included do
|
169
|
+
around do |hooked|
|
170
|
+
start_time = Time.current
|
171
|
+
result = hooked.call
|
172
|
+
duration = ((Time.current - start_time) * 1000).round(2)
|
173
|
+
|
174
|
+
if duration > threshold_ms
|
175
|
+
log("Action took #{duration}ms (threshold: #{threshold_ms}ms)", level: :warn)
|
176
|
+
notify_slow_action(duration) if notify_slow
|
177
|
+
else
|
178
|
+
log("Action completed in #{duration}ms", level: :info)
|
179
|
+
end
|
180
|
+
|
181
|
+
result
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
private
|
186
|
+
|
187
|
+
def notify_slow_action(duration)
|
188
|
+
# In a real implementation, this might send to a monitoring service
|
189
|
+
# like New Relic, DataDog, or a custom alerting system
|
190
|
+
Rails.logger.warn("SLOW ACTION ALERT: #{self.class.name} took #{duration}ms")
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# Register the strategy
|
197
|
+
Action::Strategies.register(:performance_monitoring, PerformanceMonitoringStrategy)
|
198
|
+
|
199
|
+
# Use it in an action
|
200
|
+
class ExpensiveCalculation
|
201
|
+
include Action
|
202
|
+
|
203
|
+
use :performance_monitoring, threshold_ms: 500, notify_slow: true
|
204
|
+
|
205
|
+
expects :data
|
206
|
+
|
207
|
+
def call
|
208
|
+
# This action will be monitored for performance
|
209
|
+
result = perform_expensive_calculation(data)
|
210
|
+
expose :result, result
|
211
|
+
end
|
212
|
+
|
213
|
+
private
|
214
|
+
|
215
|
+
def perform_expensive_calculation(data)
|
216
|
+
# Simulate expensive operation
|
217
|
+
sleep(0.1)
|
218
|
+
data.map { |item| item * 2 }
|
219
|
+
end
|
220
|
+
end
|
221
|
+
```
|
222
|
+
|
223
|
+
## Strategy Management
|
224
|
+
|
225
|
+
### Viewing Available Strategies
|
226
|
+
|
227
|
+
You can inspect all registered strategies:
|
228
|
+
|
229
|
+
```ruby
|
230
|
+
Action::Strategies.all
|
231
|
+
# Returns a hash of strategy names to their modules
|
232
|
+
```
|
233
|
+
|
234
|
+
### Finding Specific Strategies
|
235
|
+
|
236
|
+
To find a specific strategy by name:
|
237
|
+
|
238
|
+
```ruby
|
239
|
+
Action::Strategies.find(:transaction)
|
240
|
+
# Returns the strategy module for the transaction strategy
|
241
|
+
|
242
|
+
Action::Strategies.find(:nonexistent)
|
243
|
+
# Raises Action::StrategyNotFound: Strategy 'nonexistent' not found
|
244
|
+
```
|
245
|
+
|
246
|
+
The `find` method is useful when you need to programmatically access a strategy module or verify that a strategy exists before using it.
|
247
|
+
|
248
|
+
### Clearing Strategies
|
249
|
+
|
250
|
+
To reset strategies to only built-in ones (useful in tests):
|
251
|
+
|
252
|
+
```ruby
|
253
|
+
Action::Strategies.clear!
|
254
|
+
```
|
255
|
+
|
256
|
+
### Strategy Errors
|
257
|
+
|
258
|
+
The following errors may be raised when using strategies:
|
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
|
262
|
+
- `ArgumentError`: When providing configuration to a strategy that doesn't support it
|
263
|
+
|
264
|
+
## Best Practices
|
265
|
+
|
266
|
+
1. **Keep strategies focused**: Each strategy should have a single, well-defined responsibility
|
267
|
+
2. **Use meaningful names**: Strategy names should clearly indicate their purpose
|
268
|
+
3. **Document configuration**: If your strategy accepts configuration, document all available options
|
269
|
+
4. **Test your strategies**: Write tests for your custom strategies to ensure they work correctly
|
270
|
+
5. **Consider composition**: Design strategies to work well together when used in combination
|
271
|
+
|
272
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# Transaction Strategy
|
2
|
+
|
3
|
+
The `transaction` strategy wraps your action execution in a database transaction:
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
class TransferFunds
|
7
|
+
include Action
|
8
|
+
|
9
|
+
use :transaction
|
10
|
+
|
11
|
+
expects :from_account, :to_account, :amount
|
12
|
+
|
13
|
+
def call
|
14
|
+
from_account.withdraw!(amount)
|
15
|
+
to_account.deposit!(amount)
|
16
|
+
expose :transfer_id, SecureRandom.uuid
|
17
|
+
end
|
18
|
+
end
|
19
|
+
```
|
20
|
+
|
21
|
+
**Important**: The transaction wraps the entire action execution, including:
|
22
|
+
- `before` hooks
|
23
|
+
- The main `call` method
|
24
|
+
- `after` hooks
|
25
|
+
- Success/failure callbacks (`on_success`, `on_failure`, etc.)
|
26
|
+
|
27
|
+
This means that if any part of the action (including hooks or callbacks) raises an exception or calls `fail!`, the entire transaction will be rolled back.
|
28
|
+
|
29
|
+
**Requirements**: Requires ActiveRecord to be available in your application.
|
data/docs/usage/using.md
CHANGED
@@ -55,33 +55,3 @@ Sidekiq integration is NOT YET TESTED/NOT YET USED IN OUR APP, and naming will V
|
|
55
55
|
* enqueue will not retry even if fails
|
56
56
|
* enqueue! will go through normal sidekiq retries on any failure (including user-facing `fail!`)
|
57
57
|
* Note implicit GlobalID support (if not serializable, will get ArgumentError at callsite)
|
58
|
-
|
59
|
-
|
60
|
-
### `.enqueue_all_in_background`
|
61
|
-
|
62
|
-
In practice it's fairly common to need to enqueue a bunch of sidekiq jobs from a clock process.
|
63
|
-
|
64
|
-
One approach is to define a class-level `.enqueue_all` method on your Action... but that ends up executing the enqueue_all logic directly from the clock process, which is undesirable.
|
65
|
-
|
66
|
-
|
67
|
-
::: danger ALPHA
|
68
|
-
We are actively testing this pattern -- not yet certain we'll keep it past beta.
|
69
|
-
:::
|
70
|
-
|
71
|
-
Therefore we've added an `.enqueue_all_in_background` method that will automatically call your `.enqueue_all` _from a background job_ rather than directly on the active process.
|
72
|
-
|
73
|
-
```ruby
|
74
|
-
class Foo
|
75
|
-
include Action
|
76
|
-
|
77
|
-
def self.enqueue_all
|
78
|
-
SomeModel.some_scope.find_each do |record|
|
79
|
-
enqueue(record:)
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
...
|
84
|
-
end
|
85
|
-
|
86
|
-
Foo.enqueue_all # works, but `SomeModel.some_scope.find_each` is executed in the current context
|
87
|
-
Foo.enqueue_all_in_background # same, but runs in the background (via Action::Enqueueable::EnqueueAllWorker)
|
data/docs/usage/writing.md
CHANGED
@@ -154,5 +154,5 @@ after hook
|
|
154
154
|
|
155
155
|
A number of custom callback are available for you as well, if you want to take specific actions when a given Axn succeeds or fails. See the [Class Interface docs](/reference/class#callbacks) for details.
|
156
156
|
|
157
|
-
##
|
158
|
-
|
157
|
+
## Strategies
|
158
|
+
A number of [Strategies](/strategies), which are <abbr title="Don't Repeat Yourself">DRY</abbr>ed bits of commonly-used configuration, are available for your use as well.
|
data/lib/action/core/contract.rb
CHANGED
@@ -109,6 +109,17 @@ module Action
|
|
109
109
|
end
|
110
110
|
end
|
111
111
|
|
112
|
+
def define_memoized_reader_method(field, &block)
|
113
|
+
define_method(field) do
|
114
|
+
ivar = :"@_memoized_reader_#{field}"
|
115
|
+
cached_val = instance_variable_get(ivar)
|
116
|
+
return cached_val if cached_val.present?
|
117
|
+
|
118
|
+
value = instance_exec(&block)
|
119
|
+
instance_variable_set(ivar, value)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
112
123
|
def _define_field_reader(field)
|
113
124
|
# Allow local access to explicitly-expected fields -- even externally-expected needs to be available locally
|
114
125
|
# (e.g. to allow success message callable to reference exposed fields)
|
@@ -120,7 +131,7 @@ module Action
|
|
120
131
|
raise ArgumentError, "Model validation expects to be given a field ending in _id (given: #{field})" unless field.to_s.end_with?("_id")
|
121
132
|
raise ArgumentError, "Failed to define model reader - #{name} is already defined" if method_defined?(name)
|
122
133
|
|
123
|
-
|
134
|
+
define_memoized_reader_method(name) do
|
124
135
|
Validators::ModelValidator.instance_for(field:, klass:, id: public_send(field))
|
125
136
|
end
|
126
137
|
end
|
@@ -82,22 +82,11 @@ module Action
|
|
82
82
|
raise ArgumentError, "expects does not support duplicate sub-keys (i.e. `#{field}` is already defined)" if method_defined?(field)
|
83
83
|
|
84
84
|
define_memoized_reader_method(field) do
|
85
|
-
public_send(on)
|
85
|
+
Action::Validation::Subfields.extract(field, public_send(on))
|
86
86
|
end
|
87
87
|
|
88
88
|
_define_model_reader(field, validations[:model]) if validations.key?(:model)
|
89
89
|
end
|
90
|
-
|
91
|
-
def define_memoized_reader_method(field, &block)
|
92
|
-
define_method(field) do
|
93
|
-
ivar = :"@_memoized_reader_#{field}"
|
94
|
-
cached_val = instance_variable_get(ivar)
|
95
|
-
return cached_val if cached_val.present?
|
96
|
-
|
97
|
-
value = instance_exec(&block)
|
98
|
-
instance_variable_set(ivar, value)
|
99
|
-
end
|
100
|
-
end
|
101
90
|
end
|
102
91
|
|
103
92
|
module InstanceMethods
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Action
|
4
|
+
module UseStrategy
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
class_methods do
|
8
|
+
def use(strategy_name, **config, &block)
|
9
|
+
strategy = Action::Strategies.all[strategy_name.to_sym]
|
10
|
+
raise StrategyNotFound, "Strategy #{strategy_name} not found" if strategy.blank?
|
11
|
+
raise ArgumentError, "Strategy #{strategy_name} does not support config" if config.any? && !strategy.respond_to?(:setup)
|
12
|
+
|
13
|
+
# Allow dynamic setup of strategy (i.e. dynamically define module before returning)
|
14
|
+
if strategy.respond_to?(:setup)
|
15
|
+
configured = strategy.setup(**config, &block)
|
16
|
+
raise ArgumentError, "Strategy #{strategy_name} setup method must return a module" unless configured.is_a?(Module)
|
17
|
+
|
18
|
+
strategy = configured
|
19
|
+
else
|
20
|
+
raise ArgumentError, "Strategy #{strategy_name} does not support config (define #setup method)" if config.any?
|
21
|
+
raise ArgumentError, "Strategy #{strategy_name} does not support blocks (define #setup method)" if block_given?
|
22
|
+
end
|
23
|
+
|
24
|
+
include strategy
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/action/enqueueable.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "enqueueable/via_sidekiq"
|
4
|
-
require_relative "enqueueable/enqueue_all_in_background"
|
5
4
|
|
6
5
|
module Action
|
7
6
|
module Enqueueable
|
@@ -9,7 +8,6 @@ module Action
|
|
9
8
|
|
10
9
|
included do
|
11
10
|
include ViaSidekiq
|
12
|
-
include EnqueueAllInBackground
|
13
11
|
end
|
14
12
|
end
|
15
13
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Action
|
4
|
+
class Strategies
|
5
|
+
module Transaction
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
raise NotImplementedError, "Transaction strategy requires ActiveRecord" unless defined?(ActiveRecord)
|
10
|
+
|
11
|
+
around do |hooked|
|
12
|
+
ActiveRecord::Base.transaction do
|
13
|
+
hooked.call
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Action
|
4
|
+
class StrategyNotFound < StandardError; end
|
5
|
+
class DuplicateStrategyError < StandardError; end
|
6
|
+
|
7
|
+
class Strategies
|
8
|
+
# rubocop:disable Style/ClassVars
|
9
|
+
class << self
|
10
|
+
def built_in
|
11
|
+
return @@built_in if defined?(@@built_in)
|
12
|
+
|
13
|
+
strategy_files = Dir[File.join(__dir__, "strategies", "*.rb")]
|
14
|
+
strategy_files.each { |file| require file }
|
15
|
+
|
16
|
+
constants = Action::Strategies.constants.map { |const| Action::Strategies.const_get(const) }
|
17
|
+
mods = constants.select { |const| const.is_a?(Module) }
|
18
|
+
|
19
|
+
@@built_in = mods.to_h { |mod| [mod.name.split("::").last.downcase.to_sym, mod] }
|
20
|
+
end
|
21
|
+
|
22
|
+
def register(name, strategy)
|
23
|
+
all # ensure built_in is initialized
|
24
|
+
key = name.to_sym
|
25
|
+
raise DuplicateStrategyError, "Strategy #{name} already registered" if @@strategies.key?(key)
|
26
|
+
|
27
|
+
@@strategies[key] = strategy
|
28
|
+
@@strategies
|
29
|
+
end
|
30
|
+
|
31
|
+
def all
|
32
|
+
@@strategies ||= built_in.dup
|
33
|
+
end
|
34
|
+
|
35
|
+
def clear!
|
36
|
+
@@strategies = built_in.dup
|
37
|
+
end
|
38
|
+
|
39
|
+
def find(name)
|
40
|
+
raise StrategyNotFound, "Strategy name cannot be nil" if name.nil?
|
41
|
+
raise StrategyNotFound, "Strategy name cannot be empty" if name.to_s.strip.empty?
|
42
|
+
|
43
|
+
all[name.to_sym] or raise StrategyNotFound, "Strategy '#{name}' not found"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
# rubocop:enable Style/ClassVars
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rspec"
|
4
|
+
|
5
|
+
module Axn
|
6
|
+
module Testing
|
7
|
+
module SpecHelpers
|
8
|
+
def build_action(&block)
|
9
|
+
action = Class.new.send(:include, Action)
|
10
|
+
action.class_eval(&block) if block
|
11
|
+
action
|
12
|
+
end
|
13
|
+
|
14
|
+
def build_axn(**, &)
|
15
|
+
Axn::Factory.build(**, &)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
RSpec.configure do |config|
|
22
|
+
config.include Axn::Testing::SpecHelpers
|
23
|
+
end
|
data/lib/axn/version.rb
CHANGED
data/lib/axn.rb
CHANGED
@@ -18,11 +18,13 @@ require_relative "action/core/contract"
|
|
18
18
|
require_relative "action/core/contract_for_subfields"
|
19
19
|
require_relative "action/core/swallow_exceptions"
|
20
20
|
require_relative "action/core/hoist_errors"
|
21
|
+
require_relative "action/core/use_strategy"
|
21
22
|
|
22
23
|
require_relative "axn/factory"
|
23
24
|
|
24
25
|
require_relative "action/attachable"
|
25
26
|
require_relative "action/enqueueable"
|
27
|
+
require_relative "action/strategies"
|
26
28
|
|
27
29
|
def Axn(callable, **) # rubocop:disable Naming/MethodName
|
28
30
|
return callable if callable.is_a?(Class) && callable < Action
|
@@ -48,6 +50,8 @@ module Action
|
|
48
50
|
|
49
51
|
include HoistErrors
|
50
52
|
|
53
|
+
include UseStrategy
|
54
|
+
|
51
55
|
# --- Extensions ---
|
52
56
|
include Attachable
|
53
57
|
include Enqueueable
|
@@ -67,5 +71,3 @@ module Action
|
|
67
71
|
end
|
68
72
|
end
|
69
73
|
end
|
70
|
-
|
71
|
-
require "action/enqueueable/enqueue_all_worker"
|
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.5.
|
4
|
+
version: 0.1.0.pre.alpha.2.5.2
|
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-
|
11
|
+
date: 2025-07-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -81,6 +81,8 @@ files:
|
|
81
81
|
- docs/reference/class.md
|
82
82
|
- docs/reference/configuration.md
|
83
83
|
- docs/reference/instance.md
|
84
|
+
- docs/strategies/index.md
|
85
|
+
- docs/strategies/transaction.md
|
84
86
|
- docs/usage/setup.md
|
85
87
|
- docs/usage/using.md
|
86
88
|
- docs/usage/writing.md
|
@@ -98,17 +100,19 @@ files:
|
|
98
100
|
- lib/action/core/logging.rb
|
99
101
|
- lib/action/core/swallow_exceptions.rb
|
100
102
|
- lib/action/core/top_level_around_hook.rb
|
103
|
+
- lib/action/core/use_strategy.rb
|
101
104
|
- lib/action/core/validation/fields.rb
|
102
105
|
- lib/action/core/validation/subfields.rb
|
103
106
|
- lib/action/core/validation/validators/model_validator.rb
|
104
107
|
- lib/action/core/validation/validators/type_validator.rb
|
105
108
|
- lib/action/core/validation/validators/validate_validator.rb
|
106
109
|
- lib/action/enqueueable.rb
|
107
|
-
- lib/action/enqueueable/enqueue_all_in_background.rb
|
108
|
-
- lib/action/enqueueable/enqueue_all_worker.rb
|
109
110
|
- lib/action/enqueueable/via_sidekiq.rb
|
111
|
+
- lib/action/strategies.rb
|
112
|
+
- lib/action/strategies/transaction.rb
|
110
113
|
- lib/axn.rb
|
111
114
|
- lib/axn/factory.rb
|
115
|
+
- lib/axn/testing/spec_helpers.rb
|
112
116
|
- lib/axn/version.rb
|
113
117
|
- package.json
|
114
118
|
- yarn.lock
|
@@ -1,17 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Action
|
4
|
-
module Enqueueable
|
5
|
-
module EnqueueAllInBackground
|
6
|
-
extend ActiveSupport::Concern
|
7
|
-
|
8
|
-
module ClassMethods
|
9
|
-
def enqueue_all_in_background
|
10
|
-
raise NotImplementedError, "#{name} must implement a .enqueue_all method in order to use .enqueue_all_in_background" unless respond_to?(:enqueue_all)
|
11
|
-
|
12
|
-
::Action::Enqueueable::EnqueueAllWorker.enqueue(klass_name: name)
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
@@ -1,21 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# NOTE: this is a standalone worker for enqueueing all instances of a class.
|
4
|
-
# Unlike the other files in the folder, it is NOT included in the Action stack.
|
5
|
-
|
6
|
-
# Note it uses Axn-native enqueueing, so will automatically support additional
|
7
|
-
# backends as they are added (initially, just Sidekiq)
|
8
|
-
|
9
|
-
module Action
|
10
|
-
module Enqueueable
|
11
|
-
class EnqueueAllWorker
|
12
|
-
include Action
|
13
|
-
|
14
|
-
expects :klass_name, type: String
|
15
|
-
|
16
|
-
def call
|
17
|
-
klass_name.constantize.enqueue_all
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|