axn 0.1.0.pre.alpha.2.8.1 → 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.
- checksums.yaml +4 -4
- data/.cursor/rules/axn-framework-patterns.mdc +43 -0
- data/.cursor/rules/general-coding-standards.mdc +27 -0
- data/.cursor/rules/spec/testing-patterns.mdc +40 -0
- data/CHANGELOG.md +43 -0
- data/Rakefile +12 -2
- data/docs/.vitepress/config.mjs +8 -3
- data/docs/advanced/conventions.md +2 -2
- data/docs/advanced/mountable.md +562 -0
- data/docs/advanced/profiling.md +355 -0
- data/docs/advanced/rough.md +1 -1
- data/docs/index.md +5 -3
- data/docs/intro/about.md +1 -1
- data/docs/intro/overview.md +5 -5
- data/docs/recipes/memoization.md +2 -2
- data/docs/recipes/rubocop-integration.md +38 -284
- data/docs/recipes/testing.md +14 -14
- data/docs/recipes/validating-user-input.md +1 -1
- data/docs/reference/async.md +160 -0
- data/docs/reference/axn-result.md +107 -0
- data/docs/reference/class.md +123 -25
- data/docs/reference/configuration.md +191 -10
- data/docs/reference/instance.md +14 -29
- data/docs/strategies/index.md +21 -21
- data/docs/strategies/transaction.md +1 -1
- data/docs/usage/setup.md +14 -0
- data/docs/usage/steps.md +7 -7
- data/docs/usage/using.md +23 -12
- data/docs/usage/writing.md +92 -11
- data/lib/axn/async/adapters/active_job.rb +65 -0
- data/lib/axn/async/adapters/disabled.rb +26 -0
- data/lib/axn/async/adapters/sidekiq.rb +74 -0
- data/lib/axn/async/adapters.rb +26 -0
- data/lib/axn/async.rb +61 -0
- data/lib/{action → axn}/configuration.rb +21 -3
- data/lib/{action → axn}/context.rb +21 -4
- data/lib/{action → axn}/core/automatic_logging.rb +6 -6
- data/lib/axn/core/context/facade.rb +69 -0
- data/lib/{action → axn}/core/context/facade_inspector.rb +31 -4
- data/lib/{action → axn}/core/context/internal.rb +5 -5
- data/lib/{action → axn}/core/contract.rb +41 -46
- data/lib/{action → axn}/core/contract_for_subfields.rb +30 -35
- data/lib/{action → axn}/core/contract_validation.rb +16 -6
- data/lib/axn/core/contract_validation_for_subfields.rb +158 -0
- data/lib/axn/core/field_resolvers/extract.rb +32 -0
- data/lib/axn/core/field_resolvers/model.rb +63 -0
- data/lib/axn/core/field_resolvers.rb +24 -0
- data/lib/{action → axn}/core/flow/callbacks.rb +7 -7
- data/lib/{action → axn}/core/flow/exception_execution.rb +4 -13
- data/lib/{action → axn}/core/flow/handlers/base_descriptor.rb +3 -2
- data/lib/{action → axn}/core/flow/handlers/descriptors/callback_descriptor.rb +2 -2
- data/lib/{action → axn}/core/flow/handlers/descriptors/message_descriptor.rb +6 -6
- data/lib/{action → axn}/core/flow/handlers/invoker.rb +2 -2
- data/lib/{action → axn}/core/flow/handlers/matcher.rb +5 -5
- data/lib/{action → axn}/core/flow/handlers/registry.rb +3 -1
- data/lib/{action → axn}/core/flow/handlers/resolvers/base_resolver.rb +1 -1
- data/lib/{action → axn}/core/flow/handlers/resolvers/callback_resolver.rb +2 -2
- data/lib/{action → axn}/core/flow/handlers/resolvers/message_resolver.rb +12 -3
- data/lib/axn/core/flow/handlers.rb +20 -0
- data/lib/{action → axn}/core/flow/messages.rb +7 -7
- data/lib/{action → axn}/core/flow.rb +4 -4
- data/lib/{action → axn}/core/hooks.rb +16 -5
- data/lib/{action → axn}/core/logging.rb +3 -3
- data/lib/{action → axn}/core/nesting_tracking.rb +1 -1
- data/lib/axn/core/profiling.rb +124 -0
- data/lib/{action → axn}/core/timing.rb +1 -1
- data/lib/axn/core/tracing.rb +17 -0
- data/lib/axn/core/use_strategy.rb +29 -0
- data/lib/{action → axn}/core/validation/fields.rb +26 -2
- data/lib/{action → axn}/core/validation/subfields.rb +14 -12
- data/lib/axn/core/validation/validators/model_validator.rb +36 -0
- data/lib/axn/core/validation/validators/type_validator.rb +80 -0
- data/lib/{action → axn}/core/validation/validators/validate_validator.rb +12 -2
- data/lib/axn/core.rb +123 -0
- data/lib/{action → axn}/exceptions.rb +12 -2
- data/lib/axn/factory.rb +102 -34
- data/lib/axn/internal/logging.rb +26 -0
- data/lib/axn/internal/registry.rb +87 -0
- data/lib/axn/mountable/descriptor.rb +76 -0
- data/lib/axn/mountable/helpers/class_builder.rb +162 -0
- data/lib/axn/mountable/helpers/mounter.rb +33 -0
- data/lib/axn/mountable/helpers/namespace_manager.rb +66 -0
- data/lib/axn/mountable/helpers/validator.rb +112 -0
- data/lib/axn/mountable/inherit_profiles.rb +72 -0
- data/lib/axn/mountable/mounting_strategies/_base.rb +83 -0
- data/lib/axn/mountable/mounting_strategies/axn.rb +48 -0
- data/lib/axn/mountable/mounting_strategies/enqueue_all.rb +55 -0
- data/lib/axn/mountable/mounting_strategies/method.rb +95 -0
- data/lib/axn/mountable/mounting_strategies/step.rb +69 -0
- data/lib/axn/mountable/mounting_strategies.rb +32 -0
- data/lib/axn/mountable.rb +85 -0
- data/lib/axn/rails/engine.rb +51 -0
- data/lib/axn/rails/generators/axn_generator.rb +68 -0
- data/lib/axn/rails/generators/templates/action.rb.erb +17 -0
- data/lib/axn/rails/generators/templates/action_spec.rb.erb +25 -0
- data/lib/{action → axn}/result.rb +30 -11
- data/lib/{action → axn}/strategies/transaction.rb +1 -1
- data/lib/axn/strategies.rb +20 -0
- data/lib/axn/testing/spec_helpers.rb +6 -8
- data/lib/axn/util/memoization.rb +20 -0
- data/lib/axn/version.rb +1 -1
- data/lib/axn.rb +17 -16
- data/lib/rubocop/cop/axn/README.md +23 -23
- data/lib/rubocop/cop/axn/unchecked_result.rb +138 -17
- metadata +88 -64
- data/.rspec +0 -3
- data/.rubocop.yml +0 -76
- data/.tool-versions +0 -1
- data/docs/reference/action-result.md +0 -37
- data/lib/action/attachable/base.rb +0 -43
- data/lib/action/attachable/steps.rb +0 -63
- data/lib/action/attachable/subactions.rb +0 -70
- data/lib/action/attachable.rb +0 -17
- data/lib/action/core/context/facade.rb +0 -48
- data/lib/action/core/flow/handlers.rb +0 -20
- data/lib/action/core/tracing.rb +0 -17
- data/lib/action/core/use_strategy.rb +0 -30
- data/lib/action/core/validation/validators/model_validator.rb +0 -34
- data/lib/action/core/validation/validators/type_validator.rb +0 -30
- data/lib/action/core.rb +0 -108
- data/lib/action/enqueueable/via_sidekiq.rb +0 -76
- data/lib/action/enqueueable.rb +0 -13
- data/lib/action/strategies.rb +0 -48
- data/lib/axn/util.rb +0 -24
- data/package.json +0 -10
- data/yarn.lock +0 -1166
@@ -0,0 +1,562 @@
|
|
1
|
+
---
|
2
|
+
outline: deep
|
3
|
+
---
|
4
|
+
|
5
|
+
# Mountable Actions
|
6
|
+
|
7
|
+
The mountable functionality is an advanced feature that allows you to mount actions directly to classes, providing convenient access patterns and reducing boilerplate. This is particularly useful for API clients to automatically wrap bits of logic in full Axn affordances, and for creating batch enqueueing methods that can process multiple items and enqueue them as individual background jobs.
|
8
|
+
|
9
|
+
::: danger ALPHA
|
10
|
+
This is in VERY EXPERIMENTAL use at Teamshares, but the API is still definitely in flux.
|
11
|
+
:::
|
12
|
+
|
13
|
+
## Overview
|
14
|
+
|
15
|
+
When you attach an action to a class, you get multiple ways to access it:
|
16
|
+
|
17
|
+
1. **Direct method calls** on the class (e.g., `SomeClass.foo`), which depend on how you told it to mount
|
18
|
+
3. **Namespace method calls** (e.g., `SomeClass::Axns.foo`) which always call the underlying axn directly (i.e. returning Axn::Result like a normal SomeAxn.call)
|
19
|
+
|
20
|
+
## Attachment Strategies
|
21
|
+
|
22
|
+
### `axn` Strategy
|
23
|
+
|
24
|
+
The `axn` strategy attaches an action that returns an `Axn::Result` object.
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
class UserService
|
28
|
+
include Axn
|
29
|
+
|
30
|
+
mount_axn(:create_user) do |email:, name:|
|
31
|
+
user = User.create!(email: email, name: name)
|
32
|
+
expose :user_id, user.id
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Usage
|
37
|
+
result = UserService.create_user(email: "user@example.com", name: "John")
|
38
|
+
if result.ok?
|
39
|
+
puts "User created with ID: #{result.user_id}"
|
40
|
+
else
|
41
|
+
puts "Error: #{result.error}"
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
**Mounted methods:**
|
46
|
+
- `UserService.create_user(**kwargs)` - Returns `Axn::Result`
|
47
|
+
- `UserService.create_user!(**kwargs)` - Returns `Axn::Result` on success, raises on error
|
48
|
+
- `UserService.create_user_async(**kwargs)` - Executes asynchronously (requires async adapter configuration)
|
49
|
+
|
50
|
+
### `mount_axn_method` Strategy
|
51
|
+
|
52
|
+
The `mount_axn_method` strategy creates methods that automatically extract the return value from the `Axn::Result`. This is a useful shorthand when you have a snippet that needs to return one or zero values, when you don't want to manually check if the result was ok?.
|
53
|
+
|
54
|
+
Note we only attach a bang version to be clear that on failure it'll raise an exception.
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
class Calculator
|
58
|
+
include Axn
|
59
|
+
|
60
|
+
mount_axn_method(:add) do |a:, b:|
|
61
|
+
a + b
|
62
|
+
end
|
63
|
+
|
64
|
+
mount_axn_method(:multiply) do |a:, b:|
|
65
|
+
a * b
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Usage
|
70
|
+
sum = Calculator.add!(a: 5, b: 3) # Returns 8 directly
|
71
|
+
product = Calculator.multiply!(a: 4, b: 6) # Returns 24 directly
|
72
|
+
|
73
|
+
# NOTE: you can still access the underlying Axn on the <wrapping_class>::Axns namespace
|
74
|
+
result = Calculator::Axns.add(a: 5, b: 3) # Returns Axn::Result
|
75
|
+
```
|
76
|
+
|
77
|
+
**Mounted methods:**
|
78
|
+
- `Calculator.add!(**kwargs)` - Returns the extracted value directly, raises on error
|
79
|
+
- `Calculator::Axns.add(**kwargs)` - Returns `Axn::Result`
|
80
|
+
|
81
|
+
### `step` Strategy
|
82
|
+
|
83
|
+
The `step` strategy is designed for composing actions into sequential workflows. Steps are executed as part of a larger action flow.
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
class OrderProcessor
|
87
|
+
include Axn
|
88
|
+
expects :order_data
|
89
|
+
exposes :order_id, :confirmation_number
|
90
|
+
|
91
|
+
step :validate_order, expects: [:order_data], exposes: [:validated_data] do
|
92
|
+
fail! "Invalid order data" if order_data[:items].empty?
|
93
|
+
expose :validated_data, order_data
|
94
|
+
end
|
95
|
+
|
96
|
+
step :create_order, expects: [:validated_data], exposes: [:order_id] do
|
97
|
+
order = Order.create!(validated_data)
|
98
|
+
expose :order_id, order.id
|
99
|
+
end
|
100
|
+
|
101
|
+
step :send_confirmation, expects: [:order_id], exposes: [:confirmation_number] do
|
102
|
+
confirmation = ConfirmationMailer.send_order_confirmation(order_id).deliver_now
|
103
|
+
expose :confirmation_number, confirmation.number
|
104
|
+
end
|
105
|
+
|
106
|
+
# call is automatically defined -- will execute steps in sequence
|
107
|
+
end
|
108
|
+
|
109
|
+
# Usage
|
110
|
+
result = OrderProcessor.call(order_data: { items: [...] })
|
111
|
+
if result.ok?
|
112
|
+
puts "Order #{result.order_id} created with confirmation #{result.confirmation_number}"
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
116
|
+
**Available methods:**
|
117
|
+
- `OrderProcessor.call(**kwargs)` - Executes all steps in sequence
|
118
|
+
|
119
|
+
### `enqueue_all_via`
|
120
|
+
|
121
|
+
The `enqueue_all_via` method is designed for batch processing scenarios where you need to enqueue multiple instances of an action. It creates methods that can process a collection of items and enqueue each as a separate background job.
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
class SyncForCompany
|
125
|
+
include Axn
|
126
|
+
|
127
|
+
async :sidekiq
|
128
|
+
|
129
|
+
expects :company_id
|
130
|
+
|
131
|
+
def call
|
132
|
+
company = Company.find(company_id)
|
133
|
+
puts "Syncing data for company: #{company.name}"
|
134
|
+
# Sync individual company data
|
135
|
+
end
|
136
|
+
|
137
|
+
enqueue_all_via do
|
138
|
+
puts "About to enqueue sync jobs for all companies"
|
139
|
+
|
140
|
+
Company.find_each.map do |company|
|
141
|
+
enqueue(company_id: company.id)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Usage
|
147
|
+
# Enqueue all companies immediately
|
148
|
+
SyncForCompany.enqueue_all
|
149
|
+
|
150
|
+
# Enqueue the enqueue_all action itself as a background job
|
151
|
+
SyncForCompany.enqueue_all_async
|
152
|
+
```
|
153
|
+
|
154
|
+
**Mounted methods:**
|
155
|
+
- `SyncForCompany.enqueue_all` - Executes the block immediately and enqueues individual jobs
|
156
|
+
- `SyncForCompany.enqueue_all_async` - Enqueues the enqueue_all action itself as a background job
|
157
|
+
|
158
|
+
#### Key Features
|
159
|
+
|
160
|
+
- **Inheritance**: Uses `:async_only` mode by default (only inherits async config, nothing else)
|
161
|
+
- **enqueue Shortcut**: Use `enqueue` as syntactic sugar for `ClassName.call_async` within the enqueue_all block
|
162
|
+
|
163
|
+
|
164
|
+
## Async Execution
|
165
|
+
|
166
|
+
Mountable actions automatically support async execution when an async adapter is configured. Each mounted action gets a `_async` method that executes the action in the background.
|
167
|
+
|
168
|
+
### Configuring Async Adapters
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
class DataProcessor
|
172
|
+
include Axn
|
173
|
+
|
174
|
+
# Configure async adapter (e.g., Sidekiq, ActiveJob)
|
175
|
+
async :sidekiq
|
176
|
+
|
177
|
+
mount_axn(:process_data, async: :sidekiq) do |data:|
|
178
|
+
# Processing logic
|
179
|
+
expose :processed_count, data.count
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Usage
|
184
|
+
# Synchronous execution
|
185
|
+
result = DataProcessor.process_data(data: large_dataset)
|
186
|
+
|
187
|
+
# Asynchronous execution
|
188
|
+
DataProcessor.process_data_async(data: large_dataset)
|
189
|
+
```
|
190
|
+
|
191
|
+
### Available Async Methods
|
192
|
+
|
193
|
+
When you attach an action using the `axn` strategy, you automatically get:
|
194
|
+
- `ClassName.action_name(**kwargs)` - Synchronous execution
|
195
|
+
- `ClassName.action_name!(**kwargs)` - Synchronous execution, raises on error
|
196
|
+
- `ClassName.action_name_async(**kwargs)` - Asynchronous execution
|
197
|
+
|
198
|
+
The `_async` methods require an async adapter to be configured. See the [Async Execution documentation](/reference/async) for more details on available adapters and configuration options.
|
199
|
+
|
200
|
+
## Advanced Options
|
201
|
+
|
202
|
+
### Inheritance Behavior
|
203
|
+
|
204
|
+
Mounted actions inherit features from their target class in different ways depending on the mounting strategy. Each strategy has sensible defaults, but you can customize inheritance behavior using the `inherit` parameter.
|
205
|
+
|
206
|
+
#### Default Behavior
|
207
|
+
|
208
|
+
Each mounting strategy has a default inheritance mode that fits its typical use case:
|
209
|
+
|
210
|
+
- **`mount_axn` and `mount_axn_method`**: Use `:lifecycle` mode (inherits hooks, callbacks, messages, and async config, but not fields)
|
211
|
+
- **`step`**: Uses `:none` mode (completely independent to avoid conflicts)
|
212
|
+
- **`enqueue_all_via`**: Uses `:async_only` mode (only inherits async configuration for enqueueing)
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
class UserService
|
216
|
+
include Axn
|
217
|
+
|
218
|
+
before :log_start
|
219
|
+
on_success :track_success
|
220
|
+
error "Parent error occurred"
|
221
|
+
async :sidekiq
|
222
|
+
|
223
|
+
def log_start
|
224
|
+
puts "Starting..."
|
225
|
+
end
|
226
|
+
|
227
|
+
def track_success
|
228
|
+
puts "Success!"
|
229
|
+
end
|
230
|
+
|
231
|
+
# Inherits lifecycle (hooks, callbacks, messages, async) but not fields
|
232
|
+
mount_axn :create_user do
|
233
|
+
# Will run log_start before and track_success after
|
234
|
+
expose :user_id, 123
|
235
|
+
end
|
236
|
+
|
237
|
+
# Completely independent - no inheritance
|
238
|
+
step :validate_user do
|
239
|
+
# Will NOT run log_start or track_success
|
240
|
+
expose :valid, true
|
241
|
+
end
|
242
|
+
|
243
|
+
# Only inherits async config for enqueueing
|
244
|
+
enqueue_all_via do
|
245
|
+
# Can call enqueue (uses inherited async config)
|
246
|
+
# Does NOT inherit hooks, callbacks, or messages
|
247
|
+
User.find_each { |u| enqueue(user_id: u.id) }
|
248
|
+
end
|
249
|
+
end
|
250
|
+
```
|
251
|
+
|
252
|
+
#### Inheritance Profiles
|
253
|
+
|
254
|
+
You can control what gets inherited using predefined profiles:
|
255
|
+
|
256
|
+
##### `:lifecycle` Profile
|
257
|
+
|
258
|
+
Inherits everything except fields. Use this when the mounted action should fully participate in the parent's execution lifecycle:
|
259
|
+
|
260
|
+
```ruby
|
261
|
+
mount_axn :process, inherit: :lifecycle do
|
262
|
+
# Inherits: hooks, callbacks, messages, async config
|
263
|
+
# Does NOT inherit: fields
|
264
|
+
end
|
265
|
+
```
|
266
|
+
|
267
|
+
**What's inherited:**
|
268
|
+
- ✅ Hooks (`before`, `after`, `around`)
|
269
|
+
- ✅ Callbacks (`on_success`, `on_failure`, `on_error`, `on_exception`)
|
270
|
+
- ✅ Messages (`success`, `error`)
|
271
|
+
- ✅ Async configuration (`async :sidekiq`, etc.)
|
272
|
+
- ❌ Fields (`expects`, `exposes`)
|
273
|
+
|
274
|
+
##### `:async_only` Profile
|
275
|
+
|
276
|
+
Only inherits async configuration. Use this for utility methods that need async capability but nothing else:
|
277
|
+
|
278
|
+
```ruby
|
279
|
+
enqueue_all_via inherit: :async_only do
|
280
|
+
# Only inherits async config for enqueueing
|
281
|
+
# Completely independent otherwise
|
282
|
+
end
|
283
|
+
```
|
284
|
+
|
285
|
+
**What's inherited:**
|
286
|
+
- ✅ Async configuration
|
287
|
+
- ❌ Everything else
|
288
|
+
|
289
|
+
##### `:none` Profile
|
290
|
+
|
291
|
+
Completely standalone with no inheritance. Use this when the mounted action should be fully independent:
|
292
|
+
|
293
|
+
```ruby
|
294
|
+
step :independent_step, inherit: :none do
|
295
|
+
# Completely isolated from parent
|
296
|
+
end
|
297
|
+
```
|
298
|
+
|
299
|
+
**What's inherited:**
|
300
|
+
- ❌ Nothing - completely independent
|
301
|
+
|
302
|
+
#### Granular Control
|
303
|
+
|
304
|
+
For advanced use cases, you can use a hash to specify exactly what should be inherited:
|
305
|
+
|
306
|
+
```ruby
|
307
|
+
mount_axn :custom, inherit: {
|
308
|
+
fields: false,
|
309
|
+
hooks: true,
|
310
|
+
callbacks: false,
|
311
|
+
messages: true,
|
312
|
+
async: true
|
313
|
+
} do
|
314
|
+
# Custom inheritance: only hooks, messages, and async
|
315
|
+
end
|
316
|
+
```
|
317
|
+
|
318
|
+
**Available options:**
|
319
|
+
- `fields` - Field declarations (`expects`, `exposes`)
|
320
|
+
- `hooks` - Execution hooks (`before`, `after`, `around`)
|
321
|
+
- `callbacks` - Result callbacks (`on_success`, `on_failure`, `on_error`, `on_exception`)
|
322
|
+
- `messages` - Success and error messages
|
323
|
+
- `async` - Async adapter configuration
|
324
|
+
|
325
|
+
::: info Strategies Always Inherit
|
326
|
+
Strategies (like `use :transaction`) are always inherited as they're part of the class ancestry chain. This cannot be controlled via the `inherit` parameter.
|
327
|
+
:::
|
328
|
+
|
329
|
+
#### Practical Examples
|
330
|
+
|
331
|
+
**Example 1: Step that needs parent's error messages**
|
332
|
+
|
333
|
+
```ruby
|
334
|
+
class DataProcessor
|
335
|
+
include Axn
|
336
|
+
|
337
|
+
error "Data processing failed"
|
338
|
+
|
339
|
+
# Inherit only error messages, nothing else
|
340
|
+
step :validate, inherit: { fields: false, messages: true } do
|
341
|
+
fail! "Invalid data" # Will use parent's error message format
|
342
|
+
end
|
343
|
+
end
|
344
|
+
```
|
345
|
+
|
346
|
+
**Example 2: Mounted action with custom hooks but no callbacks**
|
347
|
+
|
348
|
+
```ruby
|
349
|
+
class ApiClient
|
350
|
+
include Axn
|
351
|
+
|
352
|
+
before :authenticate
|
353
|
+
on_success :log_success
|
354
|
+
|
355
|
+
def authenticate
|
356
|
+
# Auth logic
|
357
|
+
end
|
358
|
+
|
359
|
+
# Inherit hooks but not callbacks
|
360
|
+
mount_axn :fetch_data, inherit: { hooks: true, callbacks: false } do
|
361
|
+
# Will run authenticate before
|
362
|
+
# Will NOT run log_success callback
|
363
|
+
end
|
364
|
+
end
|
365
|
+
```
|
366
|
+
|
367
|
+
**Example 3: Override default for a step**
|
368
|
+
|
369
|
+
```ruby
|
370
|
+
class Workflow
|
371
|
+
include Axn
|
372
|
+
|
373
|
+
before :setup
|
374
|
+
|
375
|
+
# Steps default to :none, but we can override to inherit lifecycle
|
376
|
+
step :special_step, inherit: :lifecycle do
|
377
|
+
# Will run setup hook (unusual for a step)
|
378
|
+
end
|
379
|
+
end
|
380
|
+
```
|
381
|
+
|
382
|
+
### Error Prefixing for Steps
|
383
|
+
|
384
|
+
Steps automatically prefix error messages with the step name:
|
385
|
+
|
386
|
+
```ruby
|
387
|
+
step :validation, expects: [:input] do
|
388
|
+
fail! "Input is invalid"
|
389
|
+
end
|
390
|
+
|
391
|
+
# If this step fails, the error message becomes: "validation: Input is invalid"
|
392
|
+
```
|
393
|
+
|
394
|
+
You can customize the error prefix:
|
395
|
+
|
396
|
+
```ruby
|
397
|
+
step :validation, expects: [:input], error_prefix: "Custom: " do
|
398
|
+
fail! "Input is invalid"
|
399
|
+
end
|
400
|
+
|
401
|
+
# Error message becomes: "Custom: Input is invalid"
|
402
|
+
```
|
403
|
+
|
404
|
+
## Method Naming and Validation
|
405
|
+
|
406
|
+
### Valid Method Names
|
407
|
+
|
408
|
+
Method names must be convertible to valid Ruby constant names:
|
409
|
+
|
410
|
+
```ruby
|
411
|
+
# ✅ Valid names
|
412
|
+
mount_axn(:create_user) # Creates CreateUser constant
|
413
|
+
mount_axn(:process_payment) # Creates ProcessPayment constant
|
414
|
+
mount_axn(:send-email) # Creates SendEmail constant (parameterized)
|
415
|
+
mount_axn(:step_1) # Creates Step1 constant
|
416
|
+
|
417
|
+
# ❌ Invalid names
|
418
|
+
mount_axn(:create_user!) # Cannot contain method suffixes (!?=)
|
419
|
+
mount_axn(:123invalid) # Cannot start with number
|
420
|
+
```
|
421
|
+
|
422
|
+
### Special Character Handling
|
423
|
+
|
424
|
+
The system automatically handles special characters using `parameterize`:
|
425
|
+
|
426
|
+
```ruby
|
427
|
+
mount_axn(:send-email) # Becomes SendEmail constant
|
428
|
+
mount_axn(:step 1) # Becomes Step1 constant
|
429
|
+
mount_axn(:user@domain) # Becomes UserDomain constant
|
430
|
+
```
|
431
|
+
|
432
|
+
## Best Practices
|
433
|
+
|
434
|
+
### 1. Choose the Right Strategy
|
435
|
+
|
436
|
+
- **Use `mount_axn`** when you need full `Axn::Result` objects and error handling
|
437
|
+
- **Use `mount_axn_method`** when you want direct return values for simple operations
|
438
|
+
- **Use `step`** when composing complex workflows with multiple sequential operations
|
439
|
+
- **Use `enqueue_all_via`** when you need to process multiple items and enqueue each as a separate background job
|
440
|
+
|
441
|
+
### 2. Keep Actions Focused
|
442
|
+
|
443
|
+
```ruby
|
444
|
+
# ✅ Good: Focused action
|
445
|
+
mount_axn(:send_welcome_email) do |user_id:|
|
446
|
+
WelcomeMailer.send_welcome(user_id).deliver_now
|
447
|
+
end
|
448
|
+
|
449
|
+
# ❌ Bad: Too many responsibilities - prefer a standalone class
|
450
|
+
mount_axn(:process_user) do |user_data:|
|
451
|
+
user = User.create!(user_data)
|
452
|
+
WelcomeMailer.send_welcome(user.id).deliver_now
|
453
|
+
Analytics.track_user_signup(user.id)
|
454
|
+
# ... more logic
|
455
|
+
end
|
456
|
+
```
|
457
|
+
|
458
|
+
### 3. Use Descriptive Names
|
459
|
+
|
460
|
+
```ruby
|
461
|
+
# ✅ Good: Clear intent
|
462
|
+
mount_axn(:validate_email_format)
|
463
|
+
mount_axn_method(:calculate_tax)
|
464
|
+
step(:send_confirmation_email)
|
465
|
+
|
466
|
+
# ❌ Bad: Unclear purpose
|
467
|
+
mount_axn(:process)
|
468
|
+
mount_axn_method(:do_thing)
|
469
|
+
step(:step1)
|
470
|
+
```
|
471
|
+
|
472
|
+
|
473
|
+
|
474
|
+
## Common Patterns
|
475
|
+
|
476
|
+
### Service Objects
|
477
|
+
|
478
|
+
```ruby
|
479
|
+
class UserService
|
480
|
+
include Axn
|
481
|
+
|
482
|
+
mount_axn(:create) do |email:, name:|
|
483
|
+
user = User.create!(email: email, name: name)
|
484
|
+
expose :user_id, user.id
|
485
|
+
end
|
486
|
+
|
487
|
+
mount_axn_method(:find_by_email) do |email:|
|
488
|
+
User.find_by(email: email)
|
489
|
+
end
|
490
|
+
end
|
491
|
+
|
492
|
+
# Usage
|
493
|
+
result = UserService.create(email: "user@example.com", name: "John")
|
494
|
+
user = UserService.find_by_email!(email: "user@example.com")
|
495
|
+
```
|
496
|
+
|
497
|
+
|
498
|
+
### Workflow Composition
|
499
|
+
|
500
|
+
```ruby
|
501
|
+
class OrderWorkflow
|
502
|
+
include Axn
|
503
|
+
expects :order_data
|
504
|
+
exposes :order_id, :confirmation_number
|
505
|
+
|
506
|
+
step :validate, expects: [:order_data], exposes: [:validated_data] do
|
507
|
+
# Validation logic
|
508
|
+
expose :validated_data, order_data
|
509
|
+
end
|
510
|
+
|
511
|
+
step :create_order, expects: [:validated_data], exposes: [:order_id] do
|
512
|
+
order = Order.create!(validated_data)
|
513
|
+
expose :order_id, order.id
|
514
|
+
end
|
515
|
+
|
516
|
+
step :send_confirmation, expects: [:order_id], exposes: [:confirmation_number] do
|
517
|
+
# Send confirmation logic
|
518
|
+
expose :confirmation_number, "CONF-123"
|
519
|
+
end
|
520
|
+
|
521
|
+
def call
|
522
|
+
# Steps execute automatically
|
523
|
+
end
|
524
|
+
end
|
525
|
+
```
|
526
|
+
|
527
|
+
### Batch Processing
|
528
|
+
|
529
|
+
```ruby
|
530
|
+
class EmailProcessor
|
531
|
+
include Axn
|
532
|
+
|
533
|
+
async :sidekiq
|
534
|
+
|
535
|
+
expects :email_id
|
536
|
+
|
537
|
+
def call
|
538
|
+
email = Email.find(email_id)
|
539
|
+
email.deliver!
|
540
|
+
end
|
541
|
+
|
542
|
+
enqueue_all_via do |email_ids:, priority: :normal|
|
543
|
+
puts "Processing #{email_ids.count} emails with priority: #{priority}"
|
544
|
+
|
545
|
+
email_ids.map do |email_id|
|
546
|
+
enqueue(email_id: email_id)
|
547
|
+
end
|
548
|
+
end
|
549
|
+
end
|
550
|
+
|
551
|
+
# Process all pending emails immediately
|
552
|
+
EmailProcessor.enqueue_all(
|
553
|
+
email_ids: Email.pending.pluck(:id),
|
554
|
+
priority: :high
|
555
|
+
)
|
556
|
+
|
557
|
+
# Or enqueue the batch processing as a background job
|
558
|
+
EmailProcessor.enqueue_all_async(
|
559
|
+
email_ids: Email.pending.pluck(:id),
|
560
|
+
priority: :normal
|
561
|
+
)
|
562
|
+
```
|