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.
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 +43 -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 +41 -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 +2 -2
  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
@@ -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
+ ```