cmdx 0.5.0 → 1.0.0

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/.DS_Store +0 -0
  3. data/.cursor/rules/cursor-instructions.mdc +6 -0
  4. data/.rubocop.yml +16 -1
  5. data/.ruby-version +1 -1
  6. data/CHANGELOG.md +31 -1
  7. data/README.md +72 -25
  8. data/docs/ai_prompts.md +309 -0
  9. data/docs/basics/call.md +225 -14
  10. data/docs/basics/chain.md +271 -0
  11. data/docs/basics/context.md +232 -33
  12. data/docs/basics/setup.md +76 -12
  13. data/docs/callbacks.md +273 -0
  14. data/docs/configuration.md +158 -28
  15. data/docs/getting_started.md +134 -22
  16. data/docs/interruptions/exceptions.md +189 -11
  17. data/docs/interruptions/faults.md +187 -44
  18. data/docs/interruptions/halt.md +179 -35
  19. data/docs/logging.md +194 -53
  20. data/docs/middlewares.md +735 -0
  21. data/docs/outcomes/result.md +296 -10
  22. data/docs/outcomes/states.md +203 -31
  23. data/docs/outcomes/statuses.md +275 -30
  24. data/docs/parameters/coercions.md +402 -29
  25. data/docs/parameters/defaults.md +249 -25
  26. data/docs/parameters/definitions.md +238 -72
  27. data/docs/parameters/namespacing.md +250 -27
  28. data/docs/parameters/validations.md +193 -168
  29. data/docs/testing.md +550 -0
  30. data/docs/tips_and_tricks.md +95 -43
  31. data/docs/workflows.md +319 -0
  32. data/lib/cmdx/.DS_Store +0 -0
  33. data/lib/cmdx/callback.rb +69 -0
  34. data/lib/cmdx/callback_registry.rb +106 -0
  35. data/lib/cmdx/chain.rb +190 -0
  36. data/lib/cmdx/chain_inspector.rb +149 -0
  37. data/lib/cmdx/chain_serializer.rb +175 -0
  38. data/lib/cmdx/coercions/array.rb +37 -0
  39. data/lib/cmdx/coercions/big_decimal.rb +33 -0
  40. data/lib/cmdx/coercions/boolean.rb +41 -1
  41. data/lib/cmdx/coercions/complex.rb +31 -0
  42. data/lib/cmdx/coercions/date.rb +39 -0
  43. data/lib/cmdx/coercions/date_time.rb +39 -0
  44. data/lib/cmdx/coercions/float.rb +31 -0
  45. data/lib/cmdx/coercions/hash.rb +42 -0
  46. data/lib/cmdx/coercions/integer.rb +32 -0
  47. data/lib/cmdx/coercions/rational.rb +31 -0
  48. data/lib/cmdx/coercions/string.rb +31 -0
  49. data/lib/cmdx/coercions/time.rb +39 -0
  50. data/lib/cmdx/coercions/virtual.rb +31 -0
  51. data/lib/cmdx/configuration.rb +217 -9
  52. data/lib/cmdx/context.rb +173 -2
  53. data/lib/cmdx/core_ext/hash.rb +72 -0
  54. data/lib/cmdx/core_ext/module.rb +94 -0
  55. data/lib/cmdx/core_ext/object.rb +105 -0
  56. data/lib/cmdx/correlator.rb +217 -0
  57. data/lib/cmdx/error.rb +210 -8
  58. data/lib/cmdx/errors.rb +256 -1
  59. data/lib/cmdx/fault.rb +177 -2
  60. data/lib/cmdx/faults.rb +158 -2
  61. data/lib/cmdx/immutator.rb +121 -2
  62. data/lib/cmdx/lazy_struct.rb +261 -18
  63. data/lib/cmdx/log_formatters/json.rb +46 -0
  64. data/lib/cmdx/log_formatters/key_value.rb +46 -0
  65. data/lib/cmdx/log_formatters/line.rb +54 -0
  66. data/lib/cmdx/log_formatters/logstash.rb +64 -0
  67. data/lib/cmdx/log_formatters/pretty_json.rb +57 -0
  68. data/lib/cmdx/log_formatters/pretty_key_value.rb +51 -0
  69. data/lib/cmdx/log_formatters/pretty_line.rb +60 -0
  70. data/lib/cmdx/log_formatters/raw.rb +54 -0
  71. data/lib/cmdx/logger.rb +85 -0
  72. data/lib/cmdx/logger_ansi.rb +93 -7
  73. data/lib/cmdx/logger_serializer.rb +116 -0
  74. data/lib/cmdx/middleware.rb +74 -0
  75. data/lib/cmdx/middleware_registry.rb +106 -0
  76. data/lib/cmdx/middlewares/correlate.rb +266 -0
  77. data/lib/cmdx/middlewares/timeout.rb +232 -0
  78. data/lib/cmdx/parameter.rb +228 -1
  79. data/lib/cmdx/parameter_inspector.rb +61 -0
  80. data/lib/cmdx/parameter_registry.rb +125 -0
  81. data/lib/cmdx/parameter_serializer.rb +83 -0
  82. data/lib/cmdx/parameter_validator.rb +62 -0
  83. data/lib/cmdx/parameter_value.rb +109 -1
  84. data/lib/cmdx/parameters_inspector.rb +59 -0
  85. data/lib/cmdx/parameters_serializer.rb +102 -0
  86. data/lib/cmdx/railtie.rb +123 -3
  87. data/lib/cmdx/result.rb +367 -25
  88. data/lib/cmdx/result_ansi.rb +105 -9
  89. data/lib/cmdx/result_inspector.rb +76 -0
  90. data/lib/cmdx/result_logger.rb +90 -3
  91. data/lib/cmdx/result_serializer.rb +137 -0
  92. data/lib/cmdx/rspec/result_matchers.rb +917 -0
  93. data/lib/cmdx/rspec/task_matchers.rb +570 -0
  94. data/lib/cmdx/task.rb +405 -37
  95. data/lib/cmdx/task_serializer.rb +74 -2
  96. data/lib/cmdx/utils/ansi_color.rb +95 -0
  97. data/lib/cmdx/utils/log_timestamp.rb +48 -0
  98. data/lib/cmdx/utils/monotonic_runtime.rb +71 -4
  99. data/lib/cmdx/utils/name_affix.rb +78 -0
  100. data/lib/cmdx/validators/custom.rb +82 -0
  101. data/lib/cmdx/validators/exclusion.rb +94 -0
  102. data/lib/cmdx/validators/format.rb +102 -8
  103. data/lib/cmdx/validators/inclusion.rb +104 -0
  104. data/lib/cmdx/validators/length.rb +128 -0
  105. data/lib/cmdx/validators/numeric.rb +128 -0
  106. data/lib/cmdx/validators/presence.rb +93 -7
  107. data/lib/cmdx/version.rb +7 -1
  108. data/lib/cmdx/workflow.rb +394 -0
  109. data/lib/cmdx.rb +25 -64
  110. data/lib/generators/cmdx/install_generator.rb +37 -1
  111. data/lib/generators/cmdx/task_generator.rb +69 -1
  112. data/lib/generators/cmdx/templates/install.rb +8 -12
  113. data/lib/generators/cmdx/workflow_generator.rb +109 -0
  114. metadata +54 -15
  115. data/docs/basics/run.md +0 -34
  116. data/docs/batch.md +0 -53
  117. data/docs/example.md +0 -82
  118. data/docs/hooks.md +0 -62
  119. data/lib/cmdx/batch.rb +0 -43
  120. data/lib/cmdx/parameters.rb +0 -35
  121. data/lib/cmdx/run.rb +0 -39
  122. data/lib/cmdx/run_inspector.rb +0 -26
  123. data/lib/cmdx/run_serializer.rb +0 -20
  124. data/lib/cmdx/task_hook.rb +0 -18
  125. data/lib/generators/cmdx/batch_generator.rb +0 -30
  126. /data/lib/generators/cmdx/templates/{batch.rb.tt → workflow.rb.tt} +0 -0
@@ -1,67 +1,266 @@
1
1
  # Basics - Context
2
2
 
3
- The task `context` provides a form of storage to the task objects themselves.
3
+ The task `context` provides flexible data storage and sharing for task objects.
4
+ Built on `LazyStruct`, context enables dynamic attribute access, parameter
5
+ validation, and seamless data flow between related tasks.
4
6
 
5
- ## Loading
7
+ ## Table of Contents
6
8
 
7
- Loading a task with data is as simple as passing objects in a key value format.
8
- The context object is a custom version of an [OpenStruct](https://github.com/ruby/ostruct)
9
- called a `LazyStruct` with no limitations for what can be stored.
9
+ - [Loading Parameters](#loading-parameters)
10
+ - [Accessing Data](#accessing-data)
11
+ - [Modifying Context](#modifying-context)
12
+ - [Context Features](#context-features)
13
+ - [Data Sharing Between Tasks](#data-sharing-between-tasks)
14
+ - [Result Object Context Passing](#result-object-context-passing)
15
+ - [Context Inspection](#context-inspection)
16
+
17
+ ## Loading Parameters
18
+
19
+ Context is automatically populated when calling tasks with parameters. All
20
+ parameters become accessible as dynamic attributes within the task.
10
21
 
11
22
  ```ruby
12
- ProcessOrderTask.call(email: "bob@bigcorp.com", order: order)
23
+ ProcessUserOrderTask.call(
24
+ user: User.first,
25
+ order_id: 456,
26
+ send_notification: true
27
+ )
13
28
  ```
14
29
 
15
- ## Access
30
+ ## Accessing Data
16
31
 
17
- Tasks with a loaded context can be accessed within a task itself. Read, set and
18
- alter the context attributes anywhere within the task object.
32
+ Context provides multiple ways to access stored data with automatic key
33
+ normalization to symbols:
19
34
 
20
35
  ```ruby
21
- class ProcessOrderTask < CMDx::Task
36
+ class ProcessUserOrderTask < CMDx::Task
22
37
 
23
38
  def call
24
- # Reading from context storage:
25
- context.email #=> "bob@bigcorp.com"
26
- ctx.order #=> <Order #a1b2c3d>
27
- context.idk #=> nil
28
-
29
- # Writing to context storage:
30
- context.first_name = "Bob"
31
- ctx.middle_name ||= "Biggie"
32
- context.merge!(last_name: "Boomer")
39
+ # Method-style access (preferred)
40
+ context.user_id #=> 123
41
+ context.send_notification #=> true
42
+
43
+ # Hash-style access
44
+ context[:order_id] #=> 456
45
+ context["user_id"] #=> 123
46
+
47
+ # Safe access with defaults
48
+ context.fetch!(:priority, "normal") #=> "high"
49
+
50
+ # Deep access for nested data
51
+ context.dig(:metadata, :source) #=> "mobile"
52
+
53
+ # Alias for shorter code
54
+ ctx.user_id #=> 123 (ctx is alias for context)
55
+ end
56
+
57
+ end
58
+ ```
59
+
60
+ ## Modifying Context
61
+
62
+ Context supports dynamic modification during task execution:
63
+
64
+ ```ruby
65
+ class ProcessUserOrderTask < CMDx::Task
66
+
67
+ def call
68
+ # Direct assignment
69
+ context.user = User.find(user_id)
70
+ context.order = Order.find(order_id)
71
+ context.processed_at = Time.now
72
+
73
+ # Hash-style assignment
74
+ context[:status] = "processing"
75
+ context["result_code"] = "SUCCESS"
76
+
77
+ # Conditional assignment
78
+ context.notification_sent ||= false
79
+
80
+ # Batch updates
81
+ context.merge!(
82
+ status: "completed",
83
+ processed_by: current_user.id,
84
+ completion_time: Time.now
85
+ )
86
+
87
+ # Removing data
88
+ context.delete!(:temporary_data)
33
89
  end
34
90
 
35
91
  end
36
92
  ```
37
93
 
38
- [Learn more](https://github.com/drexed/cmdx/blob/main/lib/cmdx/lazy_struct.rb)
39
- about the `LazyStruct` public API for interacting with the context.
94
+ > [!TIP]
95
+ > Use context for both input parameters and intermediate results. This creates
96
+ > a natural data flow through your task execution pipeline.
97
+
98
+ ## Context Features
99
+
100
+ ### Key Normalization
101
+
102
+ All keys are automatically converted to symbols for consistent access:
103
+
104
+ ```ruby
105
+ SomeTask.call("user_id" => 123, :order_id => 456)
106
+
107
+ # Both accessible as symbols
108
+ context.user_id #=> 123
109
+ context.order_id #=> 456
110
+ ```
111
+
112
+ ### Nil Safety
113
+
114
+ Accessing undefined attributes returns `nil` instead of raising errors:
115
+
116
+ ```ruby
117
+ context.undefined_attribute #=> nil
118
+ context[:missing_key] #=> nil
119
+ ```
40
120
 
41
121
  > [!NOTE]
42
- > Attributes that are **NOT** loaded into the context will return `nil`.
122
+ > Context attributes that are **NOT** loaded will return `nil` rather than
123
+ > raising an error. This allows for graceful handling of optional parameters.
124
+
125
+ ### Type Flexibility
126
+
127
+ Context accepts any data type without restrictions:
128
+
129
+ ```ruby
130
+ context.string_value = "Order processed"
131
+ context.numeric_value = 42
132
+ context.array_value = [1, 2, 3]
133
+ context.hash_value = { total: 99.99, currency: "USD" }
134
+ context.object_value = User.find(123)
135
+ context.proc_value = -> { "dynamic value" }
136
+ ```
137
+
138
+ ## Data Sharing Between Tasks
43
139
 
44
- ## Passing
140
+ Context objects can be passed between tasks, enabling data flow in complex
141
+ workflows:
45
142
 
46
- Context objects can be passed to other tasks which allows you to build small tasks
47
- that passes data around as part of a higher level task.
143
+ ### Within Task Composition
48
144
 
49
145
  ```ruby
50
- # Within task:
51
- class ProcessOrderTask < CMDx::Task
146
+ class ProcessUserOrderTask < CMDx::Task
52
147
 
53
148
  def call
54
- SendEmailConfirmationTask.call(context)
149
+ # Subtasks inherit and modify the same context
150
+ validation_result = ValidateUserOrderTask.call(context)
151
+ throw!(validation_result) unless validation_result.success?
152
+
153
+ payment_result = ProcessOrderPaymentTask.call(context)
154
+ throw!(payment_result) unless payment_result.success?
155
+
156
+ # Context now contains data from all subtasks
157
+ context.order_validated #=> true (from ValidateUserOrderTask)
158
+ context.payment_processed #=> true (from ProcessOrderPaymentTask)
55
159
  end
56
160
 
57
161
  end
162
+ ```
163
+
164
+ ### After Task Completion
165
+
166
+ ```ruby
167
+ # Chain task results using context
168
+ validation_result = ValidateUserOrderTask.call(user_id: 123, order_id: 456)
169
+
170
+ if validation_result.success?
171
+ # Pass accumulated context to next task
172
+ process_result = ProcessUserOrderTask.call(validation_result)
58
173
 
59
- # After call:
60
- result = ProcessOrderTask.call(email: "bob@bigcorp.com", order: order)
61
- NotifyPartnerWarehousesTask.call(result.ctx)
174
+ # Continue chain with enriched context
175
+ notification_result = SendOrderNotificationTask.call(process_result.context)
176
+ end
62
177
  ```
63
178
 
179
+ ### Workflow Processing
180
+
181
+ ```ruby
182
+ # Context maintains continuity across workflow operations
183
+ initial_context = {
184
+ user_id: 123,
185
+ action: "bulk_order_processing"
186
+ }
187
+
188
+ results = [
189
+ ValidateOrderDataTask.call(initial_context),
190
+ ProcessOrderPaymentTask.call(initial_context),
191
+ UpdateInventoryTask.call(initial_context)
192
+ ]
193
+
194
+ # All tasks share and modify the same context data
195
+ results.first.context.validation_completed #=> true
196
+ results.last.context.inventory_updated #=> true
197
+ ```
198
+
199
+ ## Result Object Context Passing
200
+
201
+ CMDx supports automatic context extraction when Result objects are passed to task
202
+ `new` or `call` methods. This enables seamless task chaining where the output of
203
+ one task becomes the input for the next, creating powerful workflow compositions.
204
+
205
+ ```ruby
206
+ # Chain tasks by passing Result objects
207
+ extraction_result = ExtractDataTask.call(source_id: 123)
208
+ processing_result = ProcessDataTask.call(extraction_result)
209
+
210
+ # Context flows automatically between tasks
211
+ processing_result.context.source_id #=> 123 (from first task)
212
+ processing_result.context.extracted_data #=> [data...] (from first task)
213
+ processing_result.context.extraction_time #=> 2024-01-01 10:00:00 (from first task)
214
+ processing_result.context.processed_data #=> [processed...] (from second task)
215
+ processing_result.context.processing_time #=> 2024-01-01 10:00:05 (from second task)
216
+ ```
217
+
218
+ ### Error Handling in Chains
219
+
220
+ Result object chaining works seamlessly with both `call` and `call!` methods:
221
+
222
+ ```ruby
223
+ # Non-raising version (returns failed results)
224
+ extraction_result = ExtractDataTask.call(source_id: 123)
225
+ if extraction_result.failed?
226
+ # Handle failure, but can still pass context to error handler
227
+ error_result = HandleErrorTask.call(extraction_result)
228
+ end
229
+
230
+ # Raising version (propagates exceptions)
231
+ begin
232
+ extraction_result = ExtractDataTask.call!(source_id: 123)
233
+ processing_result = ProcessDataTask.call!(extraction_result)
234
+ rescue CMDx::Failed => e
235
+ # Handle any failure in the chain
236
+ error_result = HandleErrorTask.call(e.result)
237
+ end
238
+ ```
239
+
240
+ > [!TIP]
241
+ > Result object chaining is particularly powerful when combined with [Workflows](../workflows.md)
242
+ > processing, where multiple tasks can operate on shared context while maintaining
243
+ > individual result tracking.
244
+
245
+ ## Context Inspection
246
+
247
+ Context provides inspection methods for debugging and logging:
248
+
249
+ ```ruby
250
+ # Hash representation
251
+ context.to_h #=> { user_id: 123, order_id: 456, ... }
252
+
253
+ # Human-readable inspection
254
+ context.inspect #=> "#<CMDx::Context :user_id=123 :order_id=456>"
255
+
256
+ # Iteration
257
+ context.each_pair { |key, value| puts "#{key}: #{value}" }
258
+ ```
259
+
260
+ [Learn more](../../lib/cmdx/lazy_struct.rb)
261
+ about the `LazyStruct` public API that powers context functionality.
262
+
64
263
  ---
65
264
 
66
- - **Prev:** [Basics - Call](https://github.com/drexed/cmdx/blob/main/docs/basics/call.md)
67
- - **Next:** [Basics - Run](https://github.com/drexed/cmdx/blob/main/docs/basics/run.md)
265
+ - **Prev:** [Basics - Call](call.md)
266
+ - **Next:** [Basics - Chain](chain.md)
data/docs/basics/setup.md CHANGED
@@ -1,32 +1,96 @@
1
1
  # Basics - Setup
2
2
 
3
- A task represents a unit of work to execute. While `CMDx` offers a plethora
4
- of features, a `call` method is the only thing required to execute a task.
3
+ A task represents a unit of work to execute. Tasks are the core building blocks
4
+ of CMDx, encapsulating business logic within a structured, reusable object. While
5
+ CMDx offers extensive features like parameter validation, callbacks, and state tracking,
6
+ only a `call` method is required to create a functional task.
7
+
8
+ ## Table of Contents
9
+
10
+ - [Basic Task Structure](#basic-task-structure)
11
+ - [Task Execution](#task-execution)
12
+ - [Inheritance and Application Tasks](#inheritance-and-application-tasks)
13
+ - [Generator](#generator)
14
+ - [Task Lifecycle](#task-lifecycle)
15
+
16
+ ## Basic Task Structure
5
17
 
6
18
  ```ruby
7
- class ProcessOrderTask < CMDx::Task
19
+ class ProcessUserOrderTask < CMDx::Task
8
20
 
9
21
  def call
10
- # Do work...
22
+ # Your business logic here
23
+ context.order = Order.find(context.order_id)
24
+ context.order.process!
11
25
  end
12
26
 
13
27
  private
14
28
 
15
- # Business logic...
29
+ # Support methods and business logic
16
30
 
17
31
  end
18
32
  ```
19
33
 
20
- > [!TIP]
21
- > While complexity designed into a task is up to the engineer, it's
22
- > suggested that tasks be small and composed into higher level tasks.
34
+ ## Task Execution
35
+
36
+ ```ruby
37
+ # Execute a task
38
+ result = ProcessUserOrderTask.call(order_id: 123)
39
+
40
+ # Access the result
41
+ result.success? #=> true
42
+ result.context.order #=> <Order id: 123>
43
+ ```
44
+
45
+ ## Inheritance and Application Tasks
46
+
47
+ In Rails applications, tasks typically inherit from an `ApplicationTask` base class:
48
+
49
+ ```ruby
50
+ # app/tasks/application_task.rb
51
+ class ApplicationTask < CMDx::Task
52
+ # Shared configuration and functionality
53
+ end
54
+
55
+ # app/tasks/process_user_order_task.rb
56
+ class ProcessUserOrderTask < ApplicationTask
57
+ def call
58
+ # Implementation
59
+ end
60
+ end
61
+ ```
23
62
 
24
63
  ## Generator
25
64
 
26
- Run `rails g cmdx:task [NAME]` to create a task template file under `app/cmds`.
27
- Tasks will inherit from `ApplicationTask` if available or fall back to `CMDx::Task`.
65
+ Rails applications can use the built-in generator to create task templates:
66
+
67
+ ```bash
68
+ rails g cmdx:task ProcessUserOrder
69
+ ```
70
+
71
+ This creates `app/tasks/process_user_order_task.rb` with:
72
+ - Proper inheritance from `ApplicationTask` (if available) or `CMDx::Task`
73
+ - Basic structure with parameter definitions
74
+ - Template implementation
75
+
76
+ > [!TIP]
77
+ > Use the generator to maintain consistent task structure and naming conventions across your application.
78
+
79
+ ## Task Lifecycle
80
+
81
+ Every task follows a predictable lifecycle:
82
+
83
+ 1. **Instantiation** - Task object created with context
84
+ 2. **Validation** - Parameters validated against definitions
85
+ 3. **Execution** - The `call` method runs business logic
86
+ 4. **Completion** - Result finalized with state and status
87
+ 5. **Freezing** - Task becomes immutable after execution
88
+
89
+ > [!IMPORTANT]
90
+ > Tasks are single-use objects. Once executed, they are frozen and cannot
91
+ > be called again. Create a new task instance for each execution.
28
92
 
29
93
  ---
30
94
 
31
- - **Prev:** [Configuration](https://github.com/drexed/cmdx/blob/main/docs/configuration.md)
32
- - **Next:** [Basics - Call](https://github.com/drexed/cmdx/blob/main/docs/basics/call.md)
95
+ - **Prev:** [Configuration](../configuration.md)
96
+ - **Next:** [Basics - Call](call.md)
data/docs/callbacks.md ADDED
@@ -0,0 +1,273 @@
1
+ # Callbacks
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.
6
+
7
+ ## Table of Contents
8
+
9
+ - [Overview](#overview)
10
+ - [Callback Declaration](#callback-declaration)
11
+ - [Callback Classes](#callback-classes)
12
+ - [Available Callbacks](#available-callbacks)
13
+ - [Validation Callbacks](#validation-callbacks)
14
+ - [Execution Callbacks](#execution-callbacks)
15
+ - [State Callbacks](#state-callbacks)
16
+ - [Status Callbacks](#status-callbacks)
17
+ - [Outcome Callbacks](#outcome-callbacks)
18
+ - [Execution Order](#execution-order)
19
+ - [Conditional Execution](#conditional-execution)
20
+ - [Callback Inheritance](#callback-inheritance)
21
+
22
+ > [!TIP]
23
+ > Callbacks are inheritable, making them perfect for setting up global logic execution patterns like tracking markers, account plan checks, or logging standards.
24
+
25
+ ## Callback Declaration
26
+
27
+ Callbacks can be declared in multiple ways: method names, procs/lambdas, Callback class instances, or blocks.
28
+
29
+ ```ruby
30
+ class ProcessOrderTask < CMDx::Task
31
+ # Method name declaration
32
+ after_validation :verify_order_data
33
+
34
+ # Proc/lambda declaration
35
+ on_complete -> { send_telemetry_data }
36
+
37
+ # Callback class declaration
38
+ before_execution LoggingCallback.new(:debug)
39
+ on_success NotificationCallback.new([:email, :slack])
40
+
41
+ # Multiple callbacks for same event
42
+ on_success :increment_counter, :send_notification
43
+
44
+ # Conditional execution
45
+ on_failed :alert_support, if: :critical_order?
46
+ after_execution :cleanup_resources, unless: :preserve_data?
47
+
48
+ # Block declaration
49
+ before_execution do
50
+ context.processing_start = Time.now
51
+ end
52
+
53
+ def call
54
+ context.order = Order.find(order_id)
55
+ context.order.process!
56
+ end
57
+
58
+ private
59
+
60
+ def critical_order?
61
+ context.order.value > 10_000
62
+ end
63
+
64
+ def preserve_data?
65
+ Rails.env.development?
66
+ end
67
+ end
68
+ ```
69
+
70
+ ## Callback Classes
71
+
72
+ 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.
73
+
74
+ ### Creating Callback Classes
75
+
76
+ ```ruby
77
+ class NotificationCallback < CMDx::Callback
78
+ def initialize(channels)
79
+ @channels = Array(channels)
80
+ end
81
+
82
+ def call(task, callback_type)
83
+ return unless callback_type == :on_success
84
+
85
+ @channels.each do |channel|
86
+ NotificationService.send(channel, "Task #{task.class.name} completed")
87
+ end
88
+ end
89
+ end
90
+ ```
91
+
92
+ ### Registering Callback Classes
93
+
94
+ Callback classes can be registered using the `register` class method (recommended) or by directly calling the CallbackRegistry:
95
+
96
+ ```ruby
97
+ class ProcessOrderTask < CMDx::Task
98
+ # Recommended: Use the register class method
99
+ register :before_execution, LoggingCallback.new(:debug)
100
+ register :on_success, NotificationCallback.new([:email, :slack])
101
+ register :on_failure, :alert_admin, if: :critical?
102
+
103
+ # Alternative: Direct CallbackRegistry access (less common)
104
+ # cmd_callbacks.register(:after_execution, CleanupCallback.new)
105
+
106
+ # Traditional callback definitions still work alongside Callback classes
107
+ before_validation :validate_order_data
108
+ on_success :update_metrics
109
+
110
+ def call
111
+ context.order = Order.find(order_id)
112
+ context.order.process!
113
+ end
114
+
115
+ private
116
+
117
+ def critical?
118
+ context.order.value > 10_000
119
+ end
120
+ end
121
+ ```
122
+
123
+ ## Available Callbacks
124
+
125
+ ### Validation Callbacks
126
+
127
+ Execute around parameter validation:
128
+
129
+ - `before_validation` - Before parameter validation
130
+ - `after_validation` - After successful parameter validation
131
+
132
+ ### Execution Callbacks
133
+
134
+ Execute around task logic:
135
+
136
+ - `before_execution` - Before task logic begins
137
+ - `after_execution` - After task logic completes (success or failure)
138
+
139
+ ### State Callbacks
140
+
141
+ Execute based on execution state:
142
+
143
+ - `on_executing` - Task begins running
144
+ - `on_complete` - Task completes successfully
145
+ - `on_interrupted` - Task is halted (skip/failure)
146
+ - `on_executed` - Task finishes (complete or interrupted)
147
+
148
+ ### Status Callbacks
149
+
150
+ Execute based on execution status:
151
+
152
+ - `on_success` - Task succeeds
153
+ - `on_skipped` - Task is skipped
154
+ - `on_failed` - Task fails
155
+
156
+ ### Outcome Callbacks
157
+
158
+ Execute based on outcome classification:
159
+
160
+ - `on_good` - Positive outcomes (success or skipped)
161
+ - `on_bad` - Negative outcomes (skipped or failed)
162
+
163
+ ## Execution Order
164
+
165
+ Callbacks execute in precise order during task lifecycle:
166
+
167
+ > [!IMPORTANT]
168
+ > Multiple callbacks of the same type execute in declaration order (FIFO: first in, first out).
169
+
170
+ ```ruby
171
+ 1. before_execution # Setup and preparation
172
+ 2. on_executing # Task begins running
173
+ 3. before_validation # Pre-validation setup
174
+ 4. after_validation # Post-validation logic
175
+ 5. [call method] # Your business logic
176
+ 6. on_[complete|interrupted] # Based on execution state
177
+ 7. on_executed # Task finished (any outcome)
178
+ 8. on_[success|skipped|failed] # Based on execution status
179
+ 9. on_[good|bad] # Based on outcome classification
180
+ 10. after_execution # Cleanup and finalization
181
+ ```
182
+
183
+ > [!IMPORTANT]
184
+ > Multiple callbacks of the same type execute in declaration order (FIFO: first in, first out).
185
+
186
+ ## Conditional Execution
187
+
188
+ Callbacks support conditional execution through `:if` and `:unless` options:
189
+
190
+ | Option | Description |
191
+ | --------- | ----------- |
192
+ | `:if` | Execute callback only if condition is truthy |
193
+ | `:unless` | Execute callback only if condition is falsy |
194
+
195
+ ```ruby
196
+ class ProcessPaymentTask < CMDx::Task
197
+ # Method name condition
198
+ on_success :send_receipt, if: :email_enabled?
199
+
200
+ # Proc condition
201
+ on_failure :retry_payment, if: -> { retry_count < 3 }
202
+
203
+ # String condition (evaluated as method)
204
+ after_execution :log_metrics, unless: "Rails.env.test?"
205
+
206
+ # Multiple conditions
207
+ on_complete :expensive_operation, if: :production_env?, unless: :maintenance_mode?
208
+
209
+ private
210
+
211
+ def email_enabled?
212
+ context.user.email_notifications?
213
+ end
214
+
215
+ def production_env?
216
+ Rails.env.production?
217
+ end
218
+
219
+ def maintenance_mode?
220
+ SystemStatus.maintenance_mode?
221
+ end
222
+ end
223
+ ```
224
+
225
+ ## Callback Inheritance
226
+
227
+ Callbacks are inherited from parent classes, enabling application-wide patterns:
228
+
229
+ ```ruby
230
+ class ApplicationTask < CMDx::Task
231
+ before_execution :log_task_start # All tasks get execution logging
232
+ after_execution :log_task_end # All tasks get completion logging
233
+ on_failed :report_failure # All tasks get error reporting
234
+ on_success :track_success_metrics # All tasks get success tracking
235
+
236
+ private
237
+
238
+ def log_task_start
239
+ Rails.logger.info "Starting #{self.class.name}"
240
+ end
241
+
242
+ def log_task_end
243
+ Rails.logger.info "Finished #{self.class.name} in #{result.runtime}s"
244
+ end
245
+
246
+ def report_failure
247
+ ErrorReporter.notify(result.metadata)
248
+ end
249
+
250
+ def track_success_metrics
251
+ Metrics.increment("task.#{self.class.name.underscore}.success")
252
+ end
253
+ end
254
+
255
+ class ProcessOrderTask < ApplicationTask
256
+ before_validation :load_order # Specific to order processing
257
+ on_success :send_confirmation # Domain-specific success action
258
+ on_failed :refund_payment, if: :payment_captured? # Order-specific failure handling
259
+
260
+ def call
261
+ # Inherits all ApplicationTask callbacks plus order-specific ones
262
+ context.order.process!
263
+ end
264
+ end
265
+ ```
266
+
267
+ > [!TIP]
268
+ > Callbacks are inherited by subclasses, making them ideal for setting up global lifecycle patterns across all tasks in your application.
269
+
270
+ ---
271
+
272
+ - **Prev:** [Parameters - Defaults](parameters/defaults.md)
273
+ - **Prev:** [Middlewares](middlewares.md)