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.
- checksums.yaml +4 -4
- data/.DS_Store +0 -0
- data/.cursor/rules/cursor-instructions.mdc +6 -0
- data/.rubocop.yml +16 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +31 -1
- data/README.md +72 -25
- data/docs/ai_prompts.md +309 -0
- data/docs/basics/call.md +225 -14
- data/docs/basics/chain.md +271 -0
- data/docs/basics/context.md +232 -33
- data/docs/basics/setup.md +76 -12
- data/docs/callbacks.md +273 -0
- data/docs/configuration.md +158 -28
- data/docs/getting_started.md +134 -22
- data/docs/interruptions/exceptions.md +189 -11
- data/docs/interruptions/faults.md +187 -44
- data/docs/interruptions/halt.md +179 -35
- data/docs/logging.md +194 -53
- data/docs/middlewares.md +735 -0
- data/docs/outcomes/result.md +296 -10
- data/docs/outcomes/states.md +203 -31
- data/docs/outcomes/statuses.md +275 -30
- data/docs/parameters/coercions.md +402 -29
- data/docs/parameters/defaults.md +249 -25
- data/docs/parameters/definitions.md +238 -72
- data/docs/parameters/namespacing.md +250 -27
- data/docs/parameters/validations.md +193 -168
- data/docs/testing.md +550 -0
- data/docs/tips_and_tricks.md +95 -43
- data/docs/workflows.md +319 -0
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/callback.rb +69 -0
- data/lib/cmdx/callback_registry.rb +106 -0
- data/lib/cmdx/chain.rb +190 -0
- data/lib/cmdx/chain_inspector.rb +149 -0
- data/lib/cmdx/chain_serializer.rb +175 -0
- data/lib/cmdx/coercions/array.rb +37 -0
- data/lib/cmdx/coercions/big_decimal.rb +33 -0
- data/lib/cmdx/coercions/boolean.rb +41 -1
- data/lib/cmdx/coercions/complex.rb +31 -0
- data/lib/cmdx/coercions/date.rb +39 -0
- data/lib/cmdx/coercions/date_time.rb +39 -0
- data/lib/cmdx/coercions/float.rb +31 -0
- data/lib/cmdx/coercions/hash.rb +42 -0
- data/lib/cmdx/coercions/integer.rb +32 -0
- data/lib/cmdx/coercions/rational.rb +31 -0
- data/lib/cmdx/coercions/string.rb +31 -0
- data/lib/cmdx/coercions/time.rb +39 -0
- data/lib/cmdx/coercions/virtual.rb +31 -0
- data/lib/cmdx/configuration.rb +217 -9
- data/lib/cmdx/context.rb +173 -2
- data/lib/cmdx/core_ext/hash.rb +72 -0
- data/lib/cmdx/core_ext/module.rb +94 -0
- data/lib/cmdx/core_ext/object.rb +105 -0
- data/lib/cmdx/correlator.rb +217 -0
- data/lib/cmdx/error.rb +210 -8
- data/lib/cmdx/errors.rb +256 -1
- data/lib/cmdx/fault.rb +177 -2
- data/lib/cmdx/faults.rb +158 -2
- data/lib/cmdx/immutator.rb +121 -2
- data/lib/cmdx/lazy_struct.rb +261 -18
- data/lib/cmdx/log_formatters/json.rb +46 -0
- data/lib/cmdx/log_formatters/key_value.rb +46 -0
- data/lib/cmdx/log_formatters/line.rb +54 -0
- data/lib/cmdx/log_formatters/logstash.rb +64 -0
- data/lib/cmdx/log_formatters/pretty_json.rb +57 -0
- data/lib/cmdx/log_formatters/pretty_key_value.rb +51 -0
- data/lib/cmdx/log_formatters/pretty_line.rb +60 -0
- data/lib/cmdx/log_formatters/raw.rb +54 -0
- data/lib/cmdx/logger.rb +85 -0
- data/lib/cmdx/logger_ansi.rb +93 -7
- data/lib/cmdx/logger_serializer.rb +116 -0
- data/lib/cmdx/middleware.rb +74 -0
- data/lib/cmdx/middleware_registry.rb +106 -0
- data/lib/cmdx/middlewares/correlate.rb +266 -0
- data/lib/cmdx/middlewares/timeout.rb +232 -0
- data/lib/cmdx/parameter.rb +228 -1
- data/lib/cmdx/parameter_inspector.rb +61 -0
- data/lib/cmdx/parameter_registry.rb +125 -0
- data/lib/cmdx/parameter_serializer.rb +83 -0
- data/lib/cmdx/parameter_validator.rb +62 -0
- data/lib/cmdx/parameter_value.rb +109 -1
- data/lib/cmdx/parameters_inspector.rb +59 -0
- data/lib/cmdx/parameters_serializer.rb +102 -0
- data/lib/cmdx/railtie.rb +123 -3
- data/lib/cmdx/result.rb +367 -25
- data/lib/cmdx/result_ansi.rb +105 -9
- data/lib/cmdx/result_inspector.rb +76 -0
- data/lib/cmdx/result_logger.rb +90 -3
- data/lib/cmdx/result_serializer.rb +137 -0
- data/lib/cmdx/rspec/result_matchers.rb +917 -0
- data/lib/cmdx/rspec/task_matchers.rb +570 -0
- data/lib/cmdx/task.rb +405 -37
- data/lib/cmdx/task_serializer.rb +74 -2
- data/lib/cmdx/utils/ansi_color.rb +95 -0
- data/lib/cmdx/utils/log_timestamp.rb +48 -0
- data/lib/cmdx/utils/monotonic_runtime.rb +71 -4
- data/lib/cmdx/utils/name_affix.rb +78 -0
- data/lib/cmdx/validators/custom.rb +82 -0
- data/lib/cmdx/validators/exclusion.rb +94 -0
- data/lib/cmdx/validators/format.rb +102 -8
- data/lib/cmdx/validators/inclusion.rb +104 -0
- data/lib/cmdx/validators/length.rb +128 -0
- data/lib/cmdx/validators/numeric.rb +128 -0
- data/lib/cmdx/validators/presence.rb +93 -7
- data/lib/cmdx/version.rb +7 -1
- data/lib/cmdx/workflow.rb +394 -0
- data/lib/cmdx.rb +25 -64
- data/lib/generators/cmdx/install_generator.rb +37 -1
- data/lib/generators/cmdx/task_generator.rb +69 -1
- data/lib/generators/cmdx/templates/install.rb +8 -12
- data/lib/generators/cmdx/workflow_generator.rb +109 -0
- metadata +54 -15
- data/docs/basics/run.md +0 -34
- data/docs/batch.md +0 -53
- data/docs/example.md +0 -82
- data/docs/hooks.md +0 -62
- data/lib/cmdx/batch.rb +0 -43
- data/lib/cmdx/parameters.rb +0 -35
- data/lib/cmdx/run.rb +0 -39
- data/lib/cmdx/run_inspector.rb +0 -26
- data/lib/cmdx/run_serializer.rb +0 -20
- data/lib/cmdx/task_hook.rb +0 -18
- data/lib/generators/cmdx/batch_generator.rb +0 -30
- /data/lib/generators/cmdx/templates/{batch.rb.tt → workflow.rb.tt} +0 -0
data/docs/basics/context.md
CHANGED
@@ -1,67 +1,266 @@
|
|
1
1
|
# Basics - Context
|
2
2
|
|
3
|
-
The task `context` provides
|
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
|
-
##
|
7
|
+
## Table of Contents
|
6
8
|
|
7
|
-
Loading
|
8
|
-
|
9
|
-
|
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
|
-
|
23
|
+
ProcessUserOrderTask.call(
|
24
|
+
user: User.first,
|
25
|
+
order_id: 456,
|
26
|
+
send_notification: true
|
27
|
+
)
|
13
28
|
```
|
14
29
|
|
15
|
-
##
|
30
|
+
## Accessing Data
|
16
31
|
|
17
|
-
|
18
|
-
|
32
|
+
Context provides multiple ways to access stored data with automatic key
|
33
|
+
normalization to symbols:
|
19
34
|
|
20
35
|
```ruby
|
21
|
-
class
|
36
|
+
class ProcessUserOrderTask < CMDx::Task
|
22
37
|
|
23
38
|
def call
|
24
|
-
#
|
25
|
-
context.
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
context
|
31
|
-
|
32
|
-
|
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
|
-
[
|
39
|
-
|
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
|
-
>
|
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
|
-
|
140
|
+
Context objects can be passed between tasks, enabling data flow in complex
|
141
|
+
workflows:
|
45
142
|
|
46
|
-
|
47
|
-
that passes data around as part of a higher level task.
|
143
|
+
### Within Task Composition
|
48
144
|
|
49
145
|
```ruby
|
50
|
-
|
51
|
-
class ProcessOrderTask < CMDx::Task
|
146
|
+
class ProcessUserOrderTask < CMDx::Task
|
52
147
|
|
53
148
|
def call
|
54
|
-
|
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
|
-
#
|
60
|
-
|
61
|
-
|
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](
|
67
|
-
- **Next:** [Basics -
|
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.
|
4
|
-
of
|
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
|
19
|
+
class ProcessUserOrderTask < CMDx::Task
|
8
20
|
|
9
21
|
def call
|
10
|
-
#
|
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
|
-
#
|
29
|
+
# Support methods and business logic
|
16
30
|
|
17
31
|
end
|
18
32
|
```
|
19
33
|
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
27
|
-
|
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](
|
32
|
-
- **Next:** [Basics - Call](
|
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)
|