cmdx 1.0.1 → 1.1.1

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 (170) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/prompts/docs.md +9 -0
  3. data/.cursor/prompts/rspec.md +21 -0
  4. data/.cursor/prompts/yardoc.md +13 -0
  5. data/.rubocop.yml +2 -0
  6. data/CHANGELOG.md +29 -3
  7. data/README.md +2 -1
  8. data/docs/ai_prompts.md +269 -195
  9. data/docs/basics/call.md +126 -60
  10. data/docs/basics/chain.md +190 -160
  11. data/docs/basics/context.md +242 -154
  12. data/docs/basics/setup.md +302 -32
  13. data/docs/callbacks.md +382 -119
  14. data/docs/configuration.md +211 -49
  15. data/docs/deprecation.md +245 -0
  16. data/docs/getting_started.md +161 -39
  17. data/docs/internationalization.md +590 -70
  18. data/docs/interruptions/exceptions.md +135 -118
  19. data/docs/interruptions/faults.md +152 -127
  20. data/docs/interruptions/halt.md +134 -80
  21. data/docs/logging.md +183 -120
  22. data/docs/middlewares.md +165 -392
  23. data/docs/outcomes/result.md +140 -112
  24. data/docs/outcomes/states.md +134 -99
  25. data/docs/outcomes/statuses.md +204 -146
  26. data/docs/parameters/coercions.md +251 -289
  27. data/docs/parameters/defaults.md +224 -169
  28. data/docs/parameters/definitions.md +289 -141
  29. data/docs/parameters/namespacing.md +250 -161
  30. data/docs/parameters/validations.md +247 -159
  31. data/docs/testing.md +196 -203
  32. data/docs/workflows.md +146 -101
  33. data/lib/cmdx/.DS_Store +0 -0
  34. data/lib/cmdx/callback.rb +39 -55
  35. data/lib/cmdx/callback_registry.rb +80 -73
  36. data/lib/cmdx/chain.rb +65 -122
  37. data/lib/cmdx/chain_inspector.rb +23 -116
  38. data/lib/cmdx/chain_serializer.rb +34 -146
  39. data/lib/cmdx/coercion.rb +57 -0
  40. data/lib/cmdx/coercion_registry.rb +113 -0
  41. data/lib/cmdx/coercions/array.rb +18 -36
  42. data/lib/cmdx/coercions/big_decimal.rb +21 -33
  43. data/lib/cmdx/coercions/boolean.rb +21 -40
  44. data/lib/cmdx/coercions/complex.rb +18 -31
  45. data/lib/cmdx/coercions/date.rb +20 -39
  46. data/lib/cmdx/coercions/date_time.rb +22 -39
  47. data/lib/cmdx/coercions/float.rb +19 -32
  48. data/lib/cmdx/coercions/hash.rb +22 -41
  49. data/lib/cmdx/coercions/integer.rb +20 -33
  50. data/lib/cmdx/coercions/rational.rb +20 -32
  51. data/lib/cmdx/coercions/string.rb +23 -31
  52. data/lib/cmdx/coercions/time.rb +24 -40
  53. data/lib/cmdx/coercions/virtual.rb +14 -31
  54. data/lib/cmdx/configuration.rb +101 -162
  55. data/lib/cmdx/context.rb +34 -166
  56. data/lib/cmdx/core_ext/hash.rb +42 -67
  57. data/lib/cmdx/core_ext/module.rb +35 -79
  58. data/lib/cmdx/core_ext/object.rb +63 -98
  59. data/lib/cmdx/correlator.rb +59 -154
  60. data/lib/cmdx/error.rb +37 -202
  61. data/lib/cmdx/errors.rb +153 -216
  62. data/lib/cmdx/fault.rb +68 -150
  63. data/lib/cmdx/faults.rb +26 -137
  64. data/lib/cmdx/immutator.rb +22 -110
  65. data/lib/cmdx/lazy_struct.rb +110 -186
  66. data/lib/cmdx/log_formatters/json.rb +14 -40
  67. data/lib/cmdx/log_formatters/key_value.rb +14 -40
  68. data/lib/cmdx/log_formatters/line.rb +14 -48
  69. data/lib/cmdx/log_formatters/logstash.rb +14 -57
  70. data/lib/cmdx/log_formatters/pretty_json.rb +14 -50
  71. data/lib/cmdx/log_formatters/pretty_key_value.rb +13 -46
  72. data/lib/cmdx/log_formatters/pretty_line.rb +16 -54
  73. data/lib/cmdx/log_formatters/raw.rb +19 -49
  74. data/lib/cmdx/logger.rb +22 -79
  75. data/lib/cmdx/logger_ansi.rb +31 -72
  76. data/lib/cmdx/logger_serializer.rb +74 -103
  77. data/lib/cmdx/middleware.rb +56 -60
  78. data/lib/cmdx/middleware_registry.rb +82 -77
  79. data/lib/cmdx/middlewares/correlate.rb +41 -226
  80. data/lib/cmdx/middlewares/timeout.rb +46 -185
  81. data/lib/cmdx/parameter.rb +167 -183
  82. data/lib/cmdx/parameter_evaluator.rb +231 -0
  83. data/lib/cmdx/parameter_inspector.rb +37 -55
  84. data/lib/cmdx/parameter_registry.rb +65 -84
  85. data/lib/cmdx/parameter_serializer.rb +32 -76
  86. data/lib/cmdx/railtie.rb +24 -107
  87. data/lib/cmdx/result.rb +254 -259
  88. data/lib/cmdx/result_ansi.rb +28 -80
  89. data/lib/cmdx/result_inspector.rb +34 -70
  90. data/lib/cmdx/result_logger.rb +23 -77
  91. data/lib/cmdx/result_serializer.rb +59 -125
  92. data/lib/cmdx/rspec/matchers.rb +28 -0
  93. data/lib/cmdx/rspec/result_matchers/be_executed.rb +42 -0
  94. data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +94 -0
  95. data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +94 -0
  96. data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +59 -0
  97. data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +57 -0
  98. data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +87 -0
  99. data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +51 -0
  100. data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +58 -0
  101. data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +59 -0
  102. data/lib/cmdx/rspec/result_matchers/have_context.rb +86 -0
  103. data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +54 -0
  104. data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +52 -0
  105. data/lib/cmdx/rspec/result_matchers/have_metadata.rb +114 -0
  106. data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +66 -0
  107. data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +64 -0
  108. data/lib/cmdx/rspec/result_matchers/have_runtime.rb +78 -0
  109. data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +76 -0
  110. data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +62 -0
  111. data/lib/cmdx/rspec/task_matchers/have_callback.rb +85 -0
  112. data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +68 -0
  113. data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +92 -0
  114. data/lib/cmdx/rspec/task_matchers/have_middleware.rb +46 -0
  115. data/lib/cmdx/rspec/task_matchers/have_parameter.rb +181 -0
  116. data/lib/cmdx/task.rb +336 -427
  117. data/lib/cmdx/task_deprecator.rb +52 -0
  118. data/lib/cmdx/task_processor.rb +246 -0
  119. data/lib/cmdx/task_serializer.rb +34 -69
  120. data/lib/cmdx/utils/ansi_color.rb +13 -89
  121. data/lib/cmdx/utils/log_timestamp.rb +13 -42
  122. data/lib/cmdx/utils/monotonic_runtime.rb +11 -63
  123. data/lib/cmdx/utils/name_affix.rb +21 -71
  124. data/lib/cmdx/validator.rb +57 -0
  125. data/lib/cmdx/validator_registry.rb +108 -0
  126. data/lib/cmdx/validators/exclusion.rb +55 -94
  127. data/lib/cmdx/validators/format.rb +31 -85
  128. data/lib/cmdx/validators/inclusion.rb +65 -110
  129. data/lib/cmdx/validators/length.rb +117 -133
  130. data/lib/cmdx/validators/numeric.rb +123 -130
  131. data/lib/cmdx/validators/presence.rb +38 -79
  132. data/lib/cmdx/version.rb +1 -7
  133. data/lib/cmdx/workflow.rb +58 -330
  134. data/lib/cmdx.rb +1 -1
  135. data/lib/generators/cmdx/install_generator.rb +14 -31
  136. data/lib/generators/cmdx/task_generator.rb +39 -55
  137. data/lib/generators/cmdx/templates/install.rb +24 -6
  138. data/lib/generators/cmdx/workflow_generator.rb +41 -66
  139. data/lib/locales/ar.yml +0 -1
  140. data/lib/locales/cs.yml +0 -1
  141. data/lib/locales/da.yml +0 -1
  142. data/lib/locales/de.yml +0 -1
  143. data/lib/locales/el.yml +0 -1
  144. data/lib/locales/en.yml +0 -1
  145. data/lib/locales/es.yml +0 -1
  146. data/lib/locales/fi.yml +0 -1
  147. data/lib/locales/fr.yml +0 -1
  148. data/lib/locales/he.yml +0 -1
  149. data/lib/locales/hi.yml +0 -1
  150. data/lib/locales/it.yml +0 -1
  151. data/lib/locales/ja.yml +0 -1
  152. data/lib/locales/ko.yml +0 -1
  153. data/lib/locales/nl.yml +0 -1
  154. data/lib/locales/no.yml +0 -1
  155. data/lib/locales/pl.yml +0 -1
  156. data/lib/locales/pt.yml +0 -1
  157. data/lib/locales/ru.yml +0 -1
  158. data/lib/locales/sv.yml +0 -1
  159. data/lib/locales/th.yml +0 -1
  160. data/lib/locales/tr.yml +0 -1
  161. data/lib/locales/vi.yml +0 -1
  162. data/lib/locales/zh.yml +0 -1
  163. metadata +36 -8
  164. data/lib/cmdx/parameter_validator.rb +0 -81
  165. data/lib/cmdx/parameter_value.rb +0 -244
  166. data/lib/cmdx/parameters_inspector.rb +0 -72
  167. data/lib/cmdx/parameters_serializer.rb +0 -115
  168. data/lib/cmdx/rspec/result_matchers.rb +0 -917
  169. data/lib/cmdx/rspec/task_matchers.rb +0 -570
  170. data/lib/cmdx/validators/custom.rb +0 -102
data/docs/callbacks.md CHANGED
@@ -1,13 +1,10 @@
1
1
  # Callbacks
2
2
 
3
- Callbacks (callbacks) provide precise control over task execution lifecycle, running custom logic at
4
- specific transition points. Callback callables have access to the same context and result information
5
- as the `call` method, enabling rich integration patterns.
3
+ Callbacks provide precise control over task execution lifecycle, running custom logic at specific transition points. Callback callables have access to the same context and result information as the `call` method, enabling rich integration patterns.
6
4
 
7
5
  ## Table of Contents
8
6
 
9
7
  - [TLDR](#tldr)
10
- - [Overview](#overview)
11
8
  - [Callback Declaration](#callback-declaration)
12
9
  - [Callback Classes](#callback-classes)
13
10
  - [Available Callbacks](#available-callbacks)
@@ -18,164 +15,290 @@ as the `call` method, enabling rich integration patterns.
18
15
  - [Outcome Callbacks](#outcome-callbacks)
19
16
  - [Execution Order](#execution-order)
20
17
  - [Conditional Execution](#conditional-execution)
18
+ - [Error Handling](#error-handling)
21
19
  - [Callback Inheritance](#callback-inheritance)
22
20
 
23
21
  ## TLDR
24
22
 
25
- - **Purpose** - Execute custom logic at specific points in task lifecycle
26
- - **Declaration** - Use method names, procs, class instances, or blocks
27
- - **Callback types** - Validation, execution, state, status, and outcome callbacks
28
- - **Execution order** - Runs in precise lifecycle order (before_execution → validation → call → status → after_execution)
29
- - **Conditional** - Support `:if` and `:unless` options for conditional execution
30
- - **Inheritance** - Callbacks are inherited, perfect for global patterns
23
+ ```ruby
24
+ # Method name callbacks
25
+ after_validation :verify_order_data
26
+ on_success :send_notification
31
27
 
32
- > [!TIP]
33
- > Callbacks are inheritable, making them perfect for setting up global logic execution patterns like tracking markers, account plan checks, or logging standards.
28
+ # Proc/lambda callbacks
29
+ on_complete -> { send_telemetry_data }
34
30
 
35
- ## Overview
31
+ # Callback class instances
32
+ before_execution LoggingCallback.new(:debug)
36
33
 
37
- Callbacks can be declared in multiple ways: method names, procs/lambdas, Callback class instances, or blocks.
34
+ # Conditional execution
35
+ on_failed :alert_support, if: :critical_order?
36
+ after_execution :cleanup, unless: :preserve_data?
38
37
 
39
- ```ruby
40
- class ProcessOrderTask < CMDx::Task
41
- # Method name declaration
42
- after_validation :verify_order_data
38
+ # Multiple callbacks for same event
39
+ on_success :increment_counter, :send_notification
40
+ ```
43
41
 
44
- # Proc/lambda declaration
45
- on_complete -> { send_telemetry_data }
42
+ > [!IMPORTANT]
43
+ > Callbacks execute in declaration order (FIFO) and are inherited by subclasses, making them ideal for application-wide patterns.
44
+
45
+ ## Callback Declaration
46
46
 
47
- # Callback class declaration
48
- before_execution LoggingCallback.new(:debug)
49
- on_success NotificationCallback.new([:email, :slack])
47
+ > [!NOTE]
48
+ > Callbacks can be declared using method names, procs/lambdas, Callback class instances, or blocks. All forms have access to the task's context and result.
50
49
 
51
- # Multiple callbacks for same event
52
- on_success :increment_counter, :send_notification
50
+ ### Declaration Methods
53
51
 
54
- # Conditional execution
55
- on_failed :alert_support, if: :critical_order?
56
- after_execution :cleanup_resources, unless: :preserve_data?
52
+ | Method | Description | Example |
53
+ |--------|-------------|---------|
54
+ | Method name | References instance method | `on_success :send_email` |
55
+ | Proc/Lambda | Inline callable | `on_failed -> { alert_team }` |
56
+ | Callback class | Reusable class instance | `before_execution LoggerCallback.new` |
57
+ | Block | Inline block | `on_success { increment_counter }` |
57
58
 
58
- # Block declaration
59
- before_execution do
60
- context.processing_start = Time.now
59
+ ```ruby
60
+ class ProcessOrderTask < CMDx::Task
61
+ # Method name
62
+ before_validation :load_order
63
+ after_validation :verify_inventory
64
+
65
+ # Proc/lambda
66
+ on_executing -> { context.start_time = Time.current }
67
+ on_complete lambda { Metrics.increment('orders.processed') }
68
+
69
+ # Callback class
70
+ before_execution AuditCallback.new(action: :process_order)
71
+ on_success NotificationCallback.new(channels: [:email, :slack])
72
+
73
+ # Block
74
+ on_failed do
75
+ ErrorReporter.notify(
76
+ error: result.metadata[:error],
77
+ order_id: context.order_id,
78
+ user_id: context.user_id
79
+ )
61
80
  end
62
81
 
82
+ # Multiple callbacks
83
+ on_success :update_inventory, :send_confirmation, :log_success
84
+
63
85
  def call
64
- context.order = Order.find(order_id)
86
+ context.order = Order.find(context.order_id)
65
87
  context.order.process!
66
88
  end
67
89
 
68
90
  private
69
91
 
70
- def critical_order?
71
- context.order.value > 10_000
92
+ def load_order
93
+ context.order ||= Order.find(context.order_id)
72
94
  end
73
95
 
74
- def preserve_data?
75
- Rails.env.development?
96
+ def verify_inventory
97
+ raise "Insufficient inventory" unless context.order.items_available?
76
98
  end
77
99
  end
78
100
  ```
79
101
 
80
102
  ## Callback Classes
81
103
 
82
- For complex callback logic or reusable patterns, you can create Callback classes similar to Middleware classes. Callback classes inherit from `CMDx::Callback` and implement the `call(task, callback_type)` method.
83
-
84
- ### Creating Callback Classes
104
+ > [!TIP]
105
+ > Create reusable Callback classes for complex logic or cross-cutting concerns. Callback classes inherit from `CMDx::Callback` and implement `call(task, type)`.
85
106
 
86
107
  ```ruby
108
+ class AuditCallback < CMDx::Callback
109
+ def initialize(action:, level: :info)
110
+ @action = action
111
+ @level = level
112
+ end
113
+
114
+ def call(task, type)
115
+ AuditLogger.log(
116
+ level: @level,
117
+ action: @action,
118
+ task: task.class.name,
119
+ callback_type: type,
120
+ user_id: task.context.current_user&.id,
121
+ timestamp: Time.current
122
+ )
123
+ end
124
+ end
125
+
87
126
  class NotificationCallback < CMDx::Callback
88
- def initialize(channels)
127
+ def initialize(channels:, template: nil)
89
128
  @channels = Array(channels)
129
+ @template = template
90
130
  end
91
131
 
92
- def call(task, callback_type)
93
- return unless callback_type == :on_success
132
+ def call(task, type)
133
+ return unless should_notify?(type)
94
134
 
95
135
  @channels.each do |channel|
96
- NotificationService.send(channel, "Task #{task.class.name} completed")
136
+ NotificationService.send(
137
+ channel: channel,
138
+ template: @template || default_template(type),
139
+ data: extract_notification_data(task)
140
+ )
97
141
  end
98
142
  end
143
+
144
+ private
145
+
146
+ def should_notify?(type)
147
+ %i[on_success on_failed].include?(type)
148
+ end
149
+
150
+ def default_template(type)
151
+ type == :on_success ? :task_success : :task_failure
152
+ end
153
+
154
+ def extract_notification_data(task)
155
+ {
156
+ task_name: task.class.name,
157
+ status: task.result.status,
158
+ runtime: task.result.runtime,
159
+ context: task.context.to_h.except(:sensitive_data)
160
+ }
161
+ end
99
162
  end
100
163
  ```
101
164
 
102
- ### Registering Callback Classes
165
+ ## Available Callbacks
103
166
 
104
- Callback classes can be registered using the `register` class method (recommended) or by directly calling the CallbackRegistry:
167
+ ### Validation Callbacks
105
168
 
106
- ```ruby
107
- class ProcessOrderTask < CMDx::Task
108
- # Recommended: Use the register class method
109
- register :before_execution, LoggingCallback.new(:debug)
110
- register :on_success, NotificationCallback.new([:email, :slack])
111
- register :on_failure, :alert_admin, if: :critical?
169
+ Execute around parameter validation:
112
170
 
113
- # Alternative: Direct CallbackRegistry access (less common)
114
- # cmd_callbacks.register(:after_execution, CleanupCallback.new)
171
+ | Callback | Timing | Description |
172
+ |----------|--------|-------------|
173
+ | `before_validation` | Before validation | Setup validation context |
174
+ | `after_validation` | After successful validation | Post-validation logic |
115
175
 
116
- # Traditional callback definitions still work alongside Callback classes
117
- before_validation :validate_order_data
118
- on_success :update_metrics
176
+ ```ruby
177
+ class CreateUserTask < CMDx::Task
178
+ before_validation :normalize_email
179
+ after_validation :check_user_limits
180
+
181
+ required :email, type: :string
182
+ required :plan, type: :string
119
183
 
120
184
  def call
121
- context.order = Order.find(order_id)
122
- context.order.process!
185
+ User.create!(email: email, plan: plan)
123
186
  end
124
187
 
125
188
  private
126
189
 
127
- def critical?
128
- context.order.value > 10_000
190
+ def normalize_email
191
+ context.email = email.downcase.strip
192
+ end
193
+
194
+ def check_user_limits
195
+ current_users = User.where(plan: plan).count
196
+ plan_limit = Plan.find_by(name: plan).user_limit
197
+
198
+ if current_users >= plan_limit
199
+ throw(:skip, reason: "Plan user limit reached")
200
+ end
129
201
  end
130
202
  end
131
203
  ```
132
204
 
133
- ## Available Callbacks
205
+ ### Execution Callbacks
134
206
 
135
- ### Validation Callbacks
207
+ Execute around task logic:
136
208
 
137
- Execute around parameter validation:
209
+ | Callback | Timing | Description |
210
+ |----------|--------|-------------|
211
+ | `before_execution` | Before `call` method | Setup and preparation |
212
+ | `after_execution` | After `call` completes | Cleanup and finalization |
138
213
 
139
- - `before_validation` - Before parameter validation
140
- - `after_validation` - After successful parameter validation
214
+ ```ruby
215
+ class ProcessPaymentTask < CMDx::Task
216
+ before_execution :acquire_payment_lock
217
+ after_execution :release_payment_lock
141
218
 
142
- ### Execution Callbacks
219
+ def call
220
+ Payment.process!(context.payment_data)
221
+ end
143
222
 
144
- Execute around task logic:
223
+ private
224
+
225
+ def acquire_payment_lock
226
+ context.lock_key = "payment:#{context.payment_id}"
227
+ Redis.current.set(context.lock_key, "locked", ex: 300)
228
+ end
145
229
 
146
- - `before_execution` - Before task logic begins
147
- - `after_execution` - After task logic completes (success or failure)
230
+ def release_payment_lock
231
+ Redis.current.del(context.lock_key) if context.lock_key
232
+ end
233
+ end
234
+ ```
148
235
 
149
236
  ### State Callbacks
150
237
 
151
238
  Execute based on execution state:
152
239
 
153
- - `on_executing` - Task begins running
154
- - `on_complete` - Task completes successfully
155
- - `on_interrupted` - Task is halted (skip/failure)
156
- - `on_executed` - Task finishes (complete or interrupted)
240
+ | Callback | Condition | Description |
241
+ |----------|-----------|-------------|
242
+ | `on_executing` | Task begins running | Track execution start |
243
+ | `on_complete` | Task completes successfully | Handle successful completion |
244
+ | `on_interrupted` | Task is halted (skip/failure) | Handle interruptions |
245
+ | `on_executed` | Task finishes (any outcome) | Post-execution logic |
157
246
 
158
247
  ### Status Callbacks
159
248
 
160
249
  Execute based on execution status:
161
250
 
162
- - `on_success` - Task succeeds
163
- - `on_skipped` - Task is skipped
164
- - `on_failed` - Task fails
251
+ | Callback | Status | Description |
252
+ |----------|--------|-------------|
253
+ | `on_success` | Task succeeds | Handle success |
254
+ | `on_skipped` | Task is skipped | Handle skips |
255
+ | `on_failed` | Task fails | Handle failures |
165
256
 
166
257
  ### Outcome Callbacks
167
258
 
168
259
  Execute based on outcome classification:
169
260
 
170
- - `on_good` - Positive outcomes (success or skipped)
171
- - `on_bad` - Negative outcomes (skipped or failed)
261
+ | Callback | Outcomes | Description |
262
+ |----------|----------|-------------|
263
+ | `on_good` | Success or skipped | Positive outcomes |
264
+ | `on_bad` | Failed | Negative outcomes |
172
265
 
173
- ## Execution Order
266
+ ```ruby
267
+ class EmailCampaignTask < CMDx::Task
268
+ on_executing -> { Metrics.increment('campaigns.started') }
269
+ on_complete :track_completion
270
+ on_interrupted :handle_interruption
174
271
 
175
- Callbacks execute in precise order during task lifecycle:
272
+ on_success :schedule_followup
273
+ on_skipped :log_skip_reason
274
+ on_failed :alert_marketing_team
275
+
276
+ on_good -> { Metrics.increment('campaigns.positive_outcome') }
277
+ on_bad :create_incident_ticket
278
+
279
+ def call
280
+ EmailService.send_campaign(context.campaign_data)
281
+ end
282
+
283
+ private
284
+
285
+ def track_completion
286
+ Campaign.find(context.campaign_id).update!(
287
+ sent_at: Time.current,
288
+ recipient_count: context.recipients.size
289
+ )
290
+ end
291
+
292
+ def handle_interruption
293
+ Campaign.find(context.campaign_id).update!(status: :interrupted)
294
+ end
295
+ end
296
+ ```
297
+
298
+ ## Execution Order
176
299
 
177
300
  > [!IMPORTANT]
178
- > Multiple callbacks of the same type execute in declaration order (FIFO: first in, first out).
301
+ > Callbacks execute in precise lifecycle order. Multiple callbacks of the same type execute in declaration order (FIFO: first in, first out).
179
302
 
180
303
  ```ruby
181
304
  1. before_execution # Setup and preparation
@@ -190,94 +313,234 @@ Callbacks execute in precise order during task lifecycle:
190
313
  10. after_execution # Cleanup and finalization
191
314
  ```
192
315
 
193
- > [!IMPORTANT]
194
- > Multiple callbacks of the same type execute in declaration order (FIFO: first in, first out).
195
-
196
316
  ## Conditional Execution
197
317
 
198
- Callbacks support conditional execution through `:if` and `:unless` options:
318
+ > [!TIP]
319
+ > Use `:if` and `:unless` options for conditional callback execution. Conditions can be method names, procs, or strings.
199
320
 
200
- | Option | Description |
201
- | --------- | ----------- |
202
- | `:if` | Execute callback only if condition is truthy |
203
- | `:unless` | Execute callback only if condition is falsy |
321
+ | Option | Description | Example |
322
+ |--------|-------------|---------|
323
+ | `:if` | Execute if condition is truthy | `if: :production_env?` |
324
+ | `:unless` | Execute if condition is falsy | `unless: :maintenance_mode?` |
204
325
 
205
326
  ```ruby
206
- class ProcessPaymentTask < CMDx::Task
207
- # Method name condition
327
+ class ProcessOrderTask < CMDx::Task
328
+ # Method name conditions
208
329
  on_success :send_receipt, if: :email_enabled?
330
+ on_failed :retry_payment, unless: :max_retries_reached?
209
331
 
210
- # Proc condition
211
- on_failure :retry_payment, if: -> { retry_count < 3 }
332
+ # Proc conditions
333
+ after_execution :log_metrics, if: -> { Rails.env.production? }
334
+ on_success :expensive_operation, unless: -> { SystemStatus.overloaded? }
212
335
 
213
- # String condition (evaluated as method)
214
- after_execution :log_metrics, unless: "Rails.env.test?"
336
+ # String conditions (evaluated as methods)
337
+ on_complete :update_analytics, if: "tracking_enabled?"
215
338
 
216
339
  # Multiple conditions
217
- on_complete :expensive_operation, if: :production_env?, unless: :maintenance_mode?
340
+ on_failed :escalate_to_support, if: :critical_order?, unless: :business_hours?
341
+
342
+ # Complex conditional logic
343
+ on_success :trigger_automation, if: :automation_conditions_met?
344
+
345
+ def call
346
+ Order.process!(context.order_data)
347
+ end
218
348
 
219
349
  private
220
350
 
221
351
  def email_enabled?
222
- context.user.email_notifications?
352
+ context.user.email_notifications? && !context.user.email.blank?
353
+ end
354
+
355
+ def max_retries_reached?
356
+ context.retry_count >= 3
357
+ end
358
+
359
+ def critical_order?
360
+ context.order_value > 10_000 || context.priority == :high
361
+ end
362
+
363
+ def business_hours?
364
+ Time.current.hour.between?(9, 17) && Time.current.weekday?
365
+ end
366
+
367
+ def automation_conditions_met?
368
+ context.order_type == :subscription &&
369
+ context.user.plan.automation_enabled? &&
370
+ !SystemStatus.maintenance_mode?
371
+ end
372
+ end
373
+ ```
374
+
375
+ ## Error Handling
376
+
377
+ > [!WARNING]
378
+ > Callback errors can interrupt task execution. Use proper error handling and consider callback isolation for non-critical operations.
379
+
380
+ ### Callback Error Behavior
381
+
382
+ ```ruby
383
+ class ProcessDataTask < CMDx::Task
384
+ before_execution :critical_setup # Error stops execution
385
+ on_success :send_notification # Error stops callback chain
386
+ after_execution :cleanup_resources # Always runs
387
+
388
+ def call
389
+ ProcessingService.handle(context.data)
390
+ end
391
+
392
+ private
393
+
394
+ def critical_setup
395
+ # Critical callback - let errors bubble up
396
+ context.processor = ProcessorService.initialize_secure_processor
223
397
  end
224
398
 
225
- def production_env?
226
- Rails.env.production?
399
+ def send_notification
400
+ # Non-critical callback - handle errors gracefully
401
+ NotificationService.send(context.notification_data)
402
+ rescue NotificationService::Error => e
403
+ Rails.logger.warn "Notification failed: #{e.message}"
404
+ # Don't re-raise - allow other callbacks to continue
227
405
  end
228
406
 
229
- def maintenance_mode?
230
- SystemStatus.maintenance_mode?
407
+ def cleanup_resources
408
+ # Cleanup callback - always handle errors
409
+ context.processor&.cleanup
410
+ rescue => e
411
+ Rails.logger.error "Cleanup failed: #{e.message}"
412
+ # Log but don't re-raise
413
+ end
414
+ end
415
+ ```
416
+
417
+ ### Isolating Non-Critical Callbacks
418
+
419
+ ```ruby
420
+ class ResilientCallback < CMDx::Callback
421
+ def initialize(callback_proc, isolate: false)
422
+ @callback_proc = callback_proc
423
+ @isolate = isolate
424
+ end
425
+
426
+ def call(task, type)
427
+ if @isolate
428
+ begin
429
+ @callback_proc.call(task, type)
430
+ rescue => e
431
+ Rails.logger.warn "Isolated callback failed: #{e.message}"
432
+ end
433
+ else
434
+ @callback_proc.call(task, type)
435
+ end
436
+ end
437
+ end
438
+
439
+ class ProcessOrderTask < CMDx::Task
440
+ # Critical callback
441
+ before_execution :validate_payment_method
442
+
443
+ # Isolated non-critical callback
444
+ on_success ResilientCallback.new(
445
+ -> (task, type) { AnalyticsService.track_order(task.context.order_id) },
446
+ isolate: true
447
+ )
448
+
449
+ def call
450
+ Order.process!(context.order_data)
231
451
  end
232
452
  end
233
453
  ```
234
454
 
235
455
  ## Callback Inheritance
236
456
 
237
- Callbacks are inherited from parent classes, enabling application-wide patterns:
457
+ > [!NOTE]
458
+ > Callbacks are inherited from parent classes, enabling application-wide patterns. Child classes can add additional callbacks or override inherited behavior.
238
459
 
239
460
  ```ruby
240
461
  class ApplicationTask < CMDx::Task
241
- before_execution :log_task_start # All tasks get execution logging
242
- after_execution :log_task_end # All tasks get completion logging
243
- on_failed :report_failure # All tasks get error reporting
244
- on_success :track_success_metrics # All tasks get success tracking
462
+ # Global logging
463
+ before_execution :log_task_start
464
+ after_execution :log_task_end
465
+
466
+ # Global error handling
467
+ on_failed :report_failure
468
+
469
+ # Global metrics
470
+ on_success :track_success_metrics
471
+ on_executed :track_execution_metrics
245
472
 
246
473
  private
247
474
 
248
475
  def log_task_start
249
- Rails.logger.info "Starting #{self.class.name}"
476
+ Rails.logger.info "Starting #{self.class.name} with context: #{context.to_h.except(:sensitive_data)}"
250
477
  end
251
478
 
252
479
  def log_task_end
253
- Rails.logger.info "Finished #{self.class.name} in #{result.runtime}s"
480
+ Rails.logger.info "Finished #{self.class.name} in #{result.runtime}ms with status: #{result.status}"
254
481
  end
255
482
 
256
483
  def report_failure
257
- ErrorReporter.notify(result.metadata)
484
+ ErrorReporter.notify(
485
+ task: self.class.name,
486
+ error: result.metadata[:reason],
487
+ context: context.to_h.except(:sensitive_data),
488
+ backtrace: result.metadata[:backtrace]
489
+ )
258
490
  end
259
491
 
260
492
  def track_success_metrics
261
493
  Metrics.increment("task.#{self.class.name.underscore}.success")
262
494
  end
495
+
496
+ def track_execution_metrics
497
+ Metrics.histogram("task.#{self.class.name.underscore}.runtime", result.runtime)
498
+ end
263
499
  end
264
500
 
265
- class ProcessOrderTask < ApplicationTask
266
- before_validation :load_order # Specific to order processing
267
- on_success :send_confirmation # Domain-specific success action
268
- on_failed :refund_payment, if: :payment_captured? # Order-specific failure handling
501
+ class ProcessPaymentTask < ApplicationTask
502
+ # Inherits all ApplicationTask callbacks
503
+ # Plus payment-specific callbacks
504
+
505
+ before_validation :load_payment_method
506
+ on_success :send_receipt
507
+ on_failed :refund_payment, if: :payment_captured?
269
508
 
270
509
  def call
271
- # Inherits all ApplicationTask callbacks plus order-specific ones
272
- context.order.process!
510
+ # Inherits global logging, error handling, and metrics
511
+ # Plus payment-specific behavior
512
+ PaymentProcessor.charge(context.payment_data)
513
+ end
514
+
515
+ private
516
+
517
+ def load_payment_method
518
+ context.payment_method = PaymentMethod.find(context.payment_method_id)
519
+ end
520
+
521
+ def send_receipt
522
+ ReceiptService.send(
523
+ user: context.user,
524
+ payment: context.payment,
525
+ template: :payment_success
526
+ )
527
+ end
528
+
529
+ def payment_captured?
530
+ context.payment&.status == :captured
531
+ end
532
+
533
+ def refund_payment
534
+ RefundService.process(
535
+ payment: context.payment,
536
+ reason: :task_failure,
537
+ amount: context.payment.amount
538
+ )
273
539
  end
274
540
  end
275
541
  ```
276
542
 
277
- > [!TIP]
278
- > Callbacks are inherited by subclasses, making them ideal for setting up global lifecycle patterns across all tasks in your application.
279
-
280
543
  ---
281
544
 
282
545
  - **Prev:** [Parameters - Defaults](parameters/defaults.md)
283
- - **Prev:** [Middlewares](middlewares.md)
546
+ - **Next:** [Middlewares](middlewares.md)