cmdx 1.1.1 → 1.5.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/prompts/docs.md +4 -1
- data/.cursor/prompts/llms.md +20 -0
- data/.cursor/prompts/rspec.md +4 -1
- data/.cursor/prompts/yardoc.md +3 -2
- data/.cursor/rules/cursor-instructions.mdc +56 -1
- data/.irbrc +6 -0
- data/.rubocop.yml +29 -18
- data/.ruby-version +1 -1
- data/CHANGELOG.md +6 -128
- data/LLM.md +3317 -0
- data/README.md +68 -44
- data/docs/attributes/coercions.md +162 -0
- data/docs/attributes/defaults.md +90 -0
- data/docs/attributes/definitions.md +281 -0
- data/docs/attributes/naming.md +78 -0
- data/docs/attributes/validations.md +309 -0
- data/docs/basics/chain.md +56 -249
- data/docs/basics/context.md +56 -289
- data/docs/basics/execution.md +114 -0
- data/docs/basics/setup.md +37 -334
- data/docs/callbacks.md +89 -467
- data/docs/deprecation.md +91 -174
- data/docs/getting_started.md +212 -202
- data/docs/internationalization.md +11 -647
- data/docs/interruptions/exceptions.md +23 -198
- data/docs/interruptions/faults.md +71 -151
- data/docs/interruptions/halt.md +109 -186
- data/docs/logging.md +44 -256
- data/docs/middlewares.md +113 -426
- data/docs/outcomes/result.md +81 -228
- data/docs/outcomes/states.md +33 -221
- data/docs/outcomes/statuses.md +21 -311
- data/docs/tips_and_tricks.md +120 -70
- data/docs/workflows.md +99 -283
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/attribute.rb +229 -0
- data/lib/cmdx/attribute_registry.rb +94 -0
- data/lib/cmdx/attribute_value.rb +193 -0
- data/lib/cmdx/callback_registry.rb +69 -77
- data/lib/cmdx/chain.rb +56 -73
- data/lib/cmdx/coercion_registry.rb +52 -68
- data/lib/cmdx/coercions/array.rb +19 -18
- data/lib/cmdx/coercions/big_decimal.rb +20 -24
- data/lib/cmdx/coercions/boolean.rb +26 -25
- data/lib/cmdx/coercions/complex.rb +21 -22
- data/lib/cmdx/coercions/date.rb +25 -23
- data/lib/cmdx/coercions/date_time.rb +24 -25
- data/lib/cmdx/coercions/float.rb +25 -22
- data/lib/cmdx/coercions/hash.rb +31 -32
- data/lib/cmdx/coercions/integer.rb +30 -24
- data/lib/cmdx/coercions/rational.rb +29 -24
- data/lib/cmdx/coercions/string.rb +19 -22
- data/lib/cmdx/coercions/symbol.rb +37 -0
- data/lib/cmdx/coercions/time.rb +26 -25
- data/lib/cmdx/configuration.rb +49 -108
- data/lib/cmdx/context.rb +222 -44
- data/lib/cmdx/deprecator.rb +61 -0
- data/lib/cmdx/errors.rb +42 -252
- data/lib/cmdx/exceptions.rb +39 -0
- data/lib/cmdx/faults.rb +78 -39
- data/lib/cmdx/freezer.rb +51 -0
- data/lib/cmdx/identifier.rb +30 -0
- data/lib/cmdx/locale.rb +52 -0
- data/lib/cmdx/log_formatters/json.rb +21 -22
- data/lib/cmdx/log_formatters/key_value.rb +20 -22
- data/lib/cmdx/log_formatters/line.rb +15 -22
- data/lib/cmdx/log_formatters/logstash.rb +22 -23
- data/lib/cmdx/log_formatters/raw.rb +16 -22
- data/lib/cmdx/middleware_registry.rb +70 -74
- data/lib/cmdx/middlewares/correlate.rb +90 -54
- data/lib/cmdx/middlewares/runtime.rb +58 -0
- data/lib/cmdx/middlewares/timeout.rb +48 -68
- data/lib/cmdx/railtie.rb +12 -45
- data/lib/cmdx/result.rb +229 -314
- data/lib/cmdx/task.rb +194 -366
- data/lib/cmdx/utils/call.rb +49 -0
- data/lib/cmdx/utils/condition.rb +71 -0
- data/lib/cmdx/utils/format.rb +61 -0
- data/lib/cmdx/validator_registry.rb +63 -72
- data/lib/cmdx/validators/exclusion.rb +38 -67
- data/lib/cmdx/validators/format.rb +48 -49
- data/lib/cmdx/validators/inclusion.rb +43 -74
- data/lib/cmdx/validators/length.rb +91 -154
- data/lib/cmdx/validators/numeric.rb +87 -162
- data/lib/cmdx/validators/presence.rb +37 -50
- data/lib/cmdx/version.rb +1 -1
- data/lib/cmdx/worker.rb +178 -0
- data/lib/cmdx/workflow.rb +85 -81
- data/lib/cmdx.rb +19 -13
- data/lib/generators/cmdx/install_generator.rb +14 -13
- data/lib/generators/cmdx/task_generator.rb +25 -50
- data/lib/generators/cmdx/templates/install.rb +11 -46
- data/lib/generators/cmdx/templates/task.rb.tt +3 -2
- data/lib/locales/en.yml +18 -4
- data/src/cmdx-logo.png +0 -0
- metadata +32 -116
- data/docs/ai_prompts.md +0 -393
- data/docs/basics/call.md +0 -317
- data/docs/configuration.md +0 -344
- data/docs/parameters/coercions.md +0 -396
- data/docs/parameters/defaults.md +0 -335
- data/docs/parameters/definitions.md +0 -446
- data/docs/parameters/namespacing.md +0 -378
- data/docs/parameters/validations.md +0 -405
- data/docs/testing.md +0 -553
- data/lib/cmdx/callback.rb +0 -53
- data/lib/cmdx/chain_inspector.rb +0 -56
- data/lib/cmdx/chain_serializer.rb +0 -63
- data/lib/cmdx/coercion.rb +0 -57
- data/lib/cmdx/coercions/virtual.rb +0 -29
- data/lib/cmdx/core_ext/hash.rb +0 -83
- data/lib/cmdx/core_ext/module.rb +0 -98
- data/lib/cmdx/core_ext/object.rb +0 -125
- data/lib/cmdx/correlator.rb +0 -122
- data/lib/cmdx/error.rb +0 -60
- data/lib/cmdx/fault.rb +0 -140
- data/lib/cmdx/immutator.rb +0 -52
- data/lib/cmdx/lazy_struct.rb +0 -246
- data/lib/cmdx/log_formatters/pretty_json.rb +0 -40
- data/lib/cmdx/log_formatters/pretty_key_value.rb +0 -38
- data/lib/cmdx/log_formatters/pretty_line.rb +0 -41
- data/lib/cmdx/logger.rb +0 -49
- data/lib/cmdx/logger_ansi.rb +0 -68
- data/lib/cmdx/logger_serializer.rb +0 -116
- data/lib/cmdx/middleware.rb +0 -70
- data/lib/cmdx/parameter.rb +0 -312
- data/lib/cmdx/parameter_evaluator.rb +0 -231
- data/lib/cmdx/parameter_inspector.rb +0 -66
- data/lib/cmdx/parameter_registry.rb +0 -106
- data/lib/cmdx/parameter_serializer.rb +0 -59
- data/lib/cmdx/result_ansi.rb +0 -71
- data/lib/cmdx/result_inspector.rb +0 -71
- data/lib/cmdx/result_logger.rb +0 -59
- data/lib/cmdx/result_serializer.rb +0 -104
- data/lib/cmdx/rspec/matchers.rb +0 -28
- data/lib/cmdx/rspec/result_matchers/be_executed.rb +0 -42
- data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +0 -94
- data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +0 -94
- data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +0 -59
- data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +0 -57
- data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +0 -87
- data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +0 -51
- data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +0 -58
- data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +0 -59
- data/lib/cmdx/rspec/result_matchers/have_context.rb +0 -86
- data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +0 -54
- data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +0 -52
- data/lib/cmdx/rspec/result_matchers/have_metadata.rb +0 -114
- data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +0 -66
- data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +0 -64
- data/lib/cmdx/rspec/result_matchers/have_runtime.rb +0 -78
- data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +0 -76
- data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +0 -62
- data/lib/cmdx/rspec/task_matchers/have_callback.rb +0 -85
- data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +0 -68
- data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +0 -92
- data/lib/cmdx/rspec/task_matchers/have_middleware.rb +0 -46
- data/lib/cmdx/rspec/task_matchers/have_parameter.rb +0 -181
- data/lib/cmdx/task_deprecator.rb +0 -52
- data/lib/cmdx/task_processor.rb +0 -246
- data/lib/cmdx/task_serializer.rb +0 -57
- data/lib/cmdx/utils/ansi_color.rb +0 -73
- data/lib/cmdx/utils/log_timestamp.rb +0 -36
- data/lib/cmdx/utils/monotonic_runtime.rb +0 -34
- data/lib/cmdx/utils/name_affix.rb +0 -52
- data/lib/cmdx/validator.rb +0 -57
- data/lib/generators/cmdx/templates/workflow.rb.tt +0 -7
- data/lib/generators/cmdx/workflow_generator.rb +0 -84
- data/lib/locales/ar.yml +0 -35
- data/lib/locales/cs.yml +0 -35
- data/lib/locales/da.yml +0 -35
- data/lib/locales/de.yml +0 -35
- data/lib/locales/el.yml +0 -35
- data/lib/locales/es.yml +0 -35
- data/lib/locales/fi.yml +0 -35
- data/lib/locales/fr.yml +0 -35
- data/lib/locales/he.yml +0 -35
- data/lib/locales/hi.yml +0 -35
- data/lib/locales/it.yml +0 -35
- data/lib/locales/ja.yml +0 -35
- data/lib/locales/ko.yml +0 -35
- data/lib/locales/nl.yml +0 -35
- data/lib/locales/no.yml +0 -35
- data/lib/locales/pl.yml +0 -35
- data/lib/locales/pt.yml +0 -35
- data/lib/locales/ru.yml +0 -35
- data/lib/locales/sv.yml +0 -35
- data/lib/locales/th.yml +0 -35
- data/lib/locales/tr.yml +0 -35
- data/lib/locales/vi.yml +0 -35
- data/lib/locales/zh.yml +0 -35
data/docs/outcomes/states.md
CHANGED
@@ -7,39 +7,12 @@ decision making and monitoring.
|
|
7
7
|
|
8
8
|
## Table of Contents
|
9
9
|
|
10
|
-
- [
|
11
|
-
- [
|
12
|
-
- [
|
13
|
-
- [
|
14
|
-
- [State-Based Callbacks](#state-based-callbacks)
|
15
|
-
- [State vs Status Distinction](#state-vs-status-distinction)
|
16
|
-
- [State Persistence and Logging](#state-persistence-and-logging)
|
10
|
+
- [Definitions](#definitions)
|
11
|
+
- [Transitions](#transitions)
|
12
|
+
- [Predicates](#predicates)
|
13
|
+
- [Handlers](#handlers)
|
17
14
|
|
18
|
-
##
|
19
|
-
|
20
|
-
```ruby
|
21
|
-
# Check execution lifecycle
|
22
|
-
result.initialized? # → false (after execution)
|
23
|
-
result.executing? # → false (after execution)
|
24
|
-
result.complete? # → true (successful completion)
|
25
|
-
result.interrupted? # → false (no interruption)
|
26
|
-
result.executed? # → true (complete OR interrupted)
|
27
|
-
|
28
|
-
# State-based callbacks
|
29
|
-
result
|
30
|
-
.on_complete { |r| send_confirmation_email(r.context) }
|
31
|
-
.on_interrupted { |r| log_error_and_retry(r) }
|
32
|
-
.on_executed { |r| cleanup_resources(r) }
|
33
|
-
|
34
|
-
# States: WHERE in lifecycle, Status: HOW it ended
|
35
|
-
result.state #=> "complete" (finished executing)
|
36
|
-
result.status #=> "success" (executed successfully)
|
37
|
-
```
|
38
|
-
|
39
|
-
## State Definitions
|
40
|
-
|
41
|
-
> [!IMPORTANT]
|
42
|
-
> States are automatically managed during task execution and should **never** be modified manually. State transitions are handled internally by the CMDx framework.
|
15
|
+
## Definitions
|
43
16
|
|
44
17
|
| State | Description |
|
45
18
|
| ----- | ----------- |
|
@@ -48,221 +21,60 @@ result.status #=> "success" (executed successfully)
|
|
48
21
|
| `complete` | Task finished execution successfully without any interruption or halt. |
|
49
22
|
| `interrupted` | Task execution was stopped due to a fault, exception, or explicit halt. |
|
50
23
|
|
51
|
-
|
52
|
-
|
53
|
-
States follow a strict lifecycle with controlled transitions:
|
54
|
-
|
55
|
-
```ruby
|
56
|
-
# Valid state transition flow
|
57
|
-
initialized → executing → complete (successful execution)
|
58
|
-
initialized → executing → interrupted (failed/halted execution)
|
59
|
-
```
|
60
|
-
|
61
|
-
### Automatic State Management
|
62
|
-
|
63
|
-
```ruby
|
64
|
-
class ProcessPaymentTask < CMDx::Task
|
65
|
-
def call
|
66
|
-
# State automatically managed:
|
67
|
-
# 1. initialized → executing (when call begins)
|
68
|
-
# 2. executing → complete (successful completion)
|
69
|
-
# 3. executing → interrupted (on failure/halt)
|
70
|
-
|
71
|
-
charge_customer(amount)
|
72
|
-
send_receipt(email)
|
73
|
-
end
|
74
|
-
end
|
24
|
+
State-Status combinations:
|
75
25
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
26
|
+
| State | Status | Meaning |
|
27
|
+
| ----- | ------ | ------- |
|
28
|
+
| `initialized` | `success` | Task created, not yet executed |
|
29
|
+
| `executing` | `success` | Task currently running |
|
30
|
+
| `complete` | `success` | Task finished successfully |
|
31
|
+
| `complete` | `skipped` | Task finished by skipping execution |
|
32
|
+
| `interrupted` | `failed` | Task stopped due to failure |
|
33
|
+
| `interrupted` | `skipped` | Task stopped by skip condition |
|
82
34
|
|
83
|
-
|
35
|
+
## Transitions
|
84
36
|
|
85
|
-
> [!
|
86
|
-
>
|
37
|
+
> [!CAUTION]
|
38
|
+
> States are automatically managed during task execution and should **never** be modified manually. State transitions are handled internally by the CMDx framework.
|
87
39
|
|
88
40
|
```ruby
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
result.executing! # initialized → executing
|
93
|
-
result.complete! # executing → complete
|
94
|
-
result.interrupt! # executing → interrupted
|
95
|
-
result.executed! # executing → complete OR interrupted (based on status)
|
41
|
+
# Valid state transition flow
|
42
|
+
initialized → executing → complete (successful execution)
|
43
|
+
initialized → executing → interrupted (skipped/failed execution)
|
96
44
|
```
|
97
45
|
|
98
|
-
##
|
46
|
+
## Predicates
|
99
47
|
|
100
48
|
Use state predicates to check the current execution lifecycle:
|
101
49
|
|
102
50
|
```ruby
|
103
|
-
|
104
|
-
def call
|
105
|
-
process_order
|
106
|
-
ship_items
|
107
|
-
end
|
108
|
-
end
|
51
|
+
result = ProcessVideoUpload.execute
|
109
52
|
|
110
|
-
|
111
|
-
|
112
|
-
# Check current state
|
53
|
+
# Individual state checks
|
113
54
|
result.initialized? #=> false (after execution)
|
114
55
|
result.executing? #=> false (after execution)
|
115
56
|
result.complete? #=> true (successful completion)
|
116
57
|
result.interrupted? #=> false (no interruption)
|
117
58
|
|
118
|
-
#
|
59
|
+
# State categorization
|
119
60
|
result.executed? #=> true (complete OR interrupted)
|
120
61
|
```
|
121
62
|
|
122
|
-
|
123
|
-
|
124
|
-
```ruby
|
125
|
-
def handle_task_result(result)
|
126
|
-
if result.complete?
|
127
|
-
notify_success(result.context)
|
128
|
-
elsif result.interrupted?
|
129
|
-
handle_failure(result.metadata)
|
130
|
-
end
|
63
|
+
## Handlers
|
131
64
|
|
132
|
-
|
133
|
-
cleanup_resources if result.executed?
|
134
|
-
end
|
135
|
-
```
|
136
|
-
|
137
|
-
## State-Based Callbacks
|
138
|
-
|
139
|
-
> [!TIP]
|
140
|
-
> Use state-based callbacks for lifecycle event handling. The `on_executed` callback is particularly useful for cleanup operations that should run regardless of success or failure.
|
65
|
+
Use state-based handlers for lifecycle event handling. The `on_executed` handler is particularly useful for cleanup operations that should run regardless of success, skipped, or failure.
|
141
66
|
|
142
67
|
```ruby
|
143
|
-
|
144
|
-
def call
|
145
|
-
validate_inventory
|
146
|
-
charge_payment
|
147
|
-
update_stock
|
148
|
-
end
|
149
|
-
end
|
150
|
-
|
151
|
-
result = ProcessOrderTask.call
|
68
|
+
result = ProcessVideoUpload.execute
|
152
69
|
|
153
|
-
# Individual state
|
70
|
+
# Individual state handlers
|
154
71
|
result
|
155
|
-
.on_complete { |
|
156
|
-
.on_interrupted { |
|
157
|
-
.on_executed { |
|
158
|
-
```
|
159
|
-
|
160
|
-
### Advanced Callback Patterns
|
161
|
-
|
162
|
-
```ruby
|
163
|
-
ProcessOrderTask
|
164
|
-
.call(order_id: 123)
|
165
|
-
.on_complete { |result|
|
166
|
-
# Only runs if task completed successfully
|
167
|
-
OrderMailer.confirmation(result.context.order).deliver_now
|
168
|
-
Analytics.track("order_processed", order_id: result.context.order_id)
|
169
|
-
}
|
170
|
-
.on_interrupted { |result|
|
171
|
-
# Only runs if task was interrupted
|
172
|
-
ErrorLogger.log(result.metadata[:error])
|
173
|
-
|
174
|
-
if result.metadata[:retryable]
|
175
|
-
RetryWorker.perform_later(result.context.order_id)
|
176
|
-
end
|
177
|
-
}
|
178
|
-
.on_executed { |result|
|
179
|
-
# Always runs after execution (complete OR interrupted)
|
180
|
-
PerformanceTracker.record(result.runtime)
|
181
|
-
TempFileCleanup.perform(result.context.temp_files)
|
182
|
-
}
|
183
|
-
```
|
184
|
-
|
185
|
-
## State vs Status Distinction
|
186
|
-
|
187
|
-
> [!NOTE]
|
188
|
-
> State tracks the execution lifecycle (where the task is), while status tracks the outcome (how the task ended). Both provide valuable but different information about task execution.
|
189
|
-
|
190
|
-
Understanding the difference between states and statuses is crucial:
|
191
|
-
|
192
|
-
- **State**: Execution lifecycle position (`initialized` → `executing` → `complete`/`interrupted`)
|
193
|
-
- **Status**: Execution outcome (`success`, `skipped`, `failed`)
|
194
|
-
|
195
|
-
```ruby
|
196
|
-
class ProcessRefundTask < CMDx::Task
|
197
|
-
def call
|
198
|
-
return unless eligible_for_refund?
|
199
|
-
|
200
|
-
process_refund
|
201
|
-
notify_customer
|
202
|
-
end
|
203
|
-
end
|
204
|
-
|
205
|
-
# Successful execution
|
206
|
-
result = ProcessRefundTask.call
|
207
|
-
result.state #=> "complete" (finished executing)
|
208
|
-
result.status #=> "success" (executed successfully)
|
209
|
-
|
210
|
-
# Failed execution
|
211
|
-
failed_result = ProcessRefundTask.call(invalid_order_id: "xyz")
|
212
|
-
failed_result.state #=> "interrupted" (execution stopped)
|
213
|
-
failed_result.status #=> "failed" (outcome was failure)
|
214
|
-
```
|
215
|
-
|
216
|
-
### State-Status Combinations
|
217
|
-
|
218
|
-
| State | Status | Meaning |
|
219
|
-
| ----- | ------ | ------- |
|
220
|
-
| `initialized` | `success` | Task created, not yet executed |
|
221
|
-
| `executing` | `success` | Task currently running |
|
222
|
-
| `complete` | `success` | Task finished successfully |
|
223
|
-
| `complete` | `skipped` | Task finished by skipping execution |
|
224
|
-
| `interrupted` | `failed` | Task stopped due to failure |
|
225
|
-
| `interrupted` | `skipped` | Task stopped by skip condition |
|
226
|
-
|
227
|
-
## State Persistence and Logging
|
228
|
-
|
229
|
-
> [!IMPORTANT]
|
230
|
-
> States are automatically captured in result serialization and logging. All state information persists through the complete task execution lifecycle.
|
231
|
-
|
232
|
-
```ruby
|
233
|
-
result = ProcessOrderTask.call
|
234
|
-
|
235
|
-
# Hash representation includes state
|
236
|
-
result.to_h
|
237
|
-
#=> {
|
238
|
-
# class: "ProcessOrderTask",
|
239
|
-
# index: 0,
|
240
|
-
# state: "complete",
|
241
|
-
# status: "success",
|
242
|
-
# outcome: "success",
|
243
|
-
# runtime: 0.045,
|
244
|
-
# metadata: {},
|
245
|
-
# context: { order_id: 123 }
|
246
|
-
# }
|
247
|
-
|
248
|
-
# Human-readable inspection
|
249
|
-
result.to_s
|
250
|
-
#=> "ProcessOrderTask: type=Task index=0 state=complete status=success outcome=success runtime=0.045s"
|
251
|
-
|
252
|
-
# Chain-level state aggregation
|
253
|
-
result.chain.to_h
|
254
|
-
#=> {
|
255
|
-
# id: "chain-550e8400-e29b-41d4-a716-446655440000",
|
256
|
-
# state: "complete", # Derived from overall chain state
|
257
|
-
# status: "success", # Derived from overall chain status
|
258
|
-
# results: [
|
259
|
-
# { state: "complete", status: "success", ... },
|
260
|
-
# { state: "complete", status: "success", ... }
|
261
|
-
# ]
|
262
|
-
# }
|
72
|
+
.on_complete { |result| send_upload_notification(result) }
|
73
|
+
.on_interrupted { |result| cleanup_temp_files(result) }
|
74
|
+
.on_executed { |result| log_upload_metrics(result) }
|
263
75
|
```
|
264
76
|
|
265
77
|
---
|
266
78
|
|
267
|
-
- **Prev:** [Outcomes -
|
268
|
-
- **Next:** [
|
79
|
+
- **Prev:** [Outcomes - Result](result.md)
|
80
|
+
- **Next:** [Outcomes - Statuses](statuses.md)
|
data/docs/outcomes/statuses.md
CHANGED
@@ -4,43 +4,12 @@ Statuses represent the business outcome of task execution logic, indicating how
|
|
4
4
|
|
5
5
|
## Table of Contents
|
6
6
|
|
7
|
-
- [
|
8
|
-
- [
|
9
|
-
- [
|
10
|
-
- [
|
11
|
-
- [Status-Based Callbacks](#status-based-callbacks)
|
12
|
-
- [Status Metadata](#status-metadata)
|
13
|
-
- [Outcome-Based Logic](#outcome-based-logic)
|
14
|
-
- [Status vs State vs Outcome](#status-vs-state-vs-outcome)
|
15
|
-
- [Status Serialization and Inspection](#status-serialization-and-inspection)
|
7
|
+
- [Definitions](#definitions)
|
8
|
+
- [Transitions](#transitions)
|
9
|
+
- [Predicates](#predicates)
|
10
|
+
- [Handlers](#handlers)
|
16
11
|
|
17
|
-
##
|
18
|
-
|
19
|
-
```ruby
|
20
|
-
# Check business outcomes
|
21
|
-
result.success? # → true (default outcome)
|
22
|
-
result.skipped? # → false (via skip!)
|
23
|
-
result.failed? # → false (via fail!)
|
24
|
-
|
25
|
-
# Outcome-based logic
|
26
|
-
result.good? # → true (success OR skipped)
|
27
|
-
result.bad? # → false (skipped OR failed)
|
28
|
-
|
29
|
-
# Status-based callbacks
|
30
|
-
result
|
31
|
-
.on_success { |r| process_success(r) }
|
32
|
-
.on_skipped { |r| handle_skip_condition(r) }
|
33
|
-
.on_failed { |r| handle_business_failure(r) }
|
34
|
-
|
35
|
-
# Statuses: HOW it ended, States: WHERE in lifecycle
|
36
|
-
result.status #=> "success" (business outcome)
|
37
|
-
result.state #=> "complete" (execution lifecycle)
|
38
|
-
```
|
39
|
-
|
40
|
-
## Status Definitions
|
41
|
-
|
42
|
-
> [!IMPORTANT]
|
43
|
-
> Statuses represent business outcomes, not technical execution states. A task can be technically "complete" but have a "failed" status if business logic determined the operation could not succeed.
|
12
|
+
## Definitions
|
44
13
|
|
45
14
|
| Status | Description |
|
46
15
|
| ------ | ----------- |
|
@@ -48,13 +17,11 @@ result.state #=> "complete" (execution lifecycle)
|
|
48
17
|
| `skipped` | Task intentionally stopped execution because conditions weren't met or continuation was unnecessary. |
|
49
18
|
| `failed` | Task stopped execution due to business rule violations, validation errors, or exceptions. |
|
50
19
|
|
51
|
-
##
|
20
|
+
## Transitions
|
52
21
|
|
53
|
-
> [!
|
22
|
+
> [!IMPORTANT]
|
54
23
|
> Status transitions are unidirectional and final. Once a task is marked as skipped or failed, it cannot return to success status. Design your business logic accordingly.
|
55
24
|
|
56
|
-
Unlike states, statuses can only transition from success to skipped/failed:
|
57
|
-
|
58
25
|
```ruby
|
59
26
|
# Valid status transitions
|
60
27
|
success → skipped # via skip!
|
@@ -67,46 +34,12 @@ failed → success # ❌ Cannot transition
|
|
67
34
|
failed → skipped # ❌ Cannot transition
|
68
35
|
```
|
69
36
|
|
70
|
-
|
71
|
-
|
72
|
-
```ruby
|
73
|
-
class ProcessOrderTask < CMDx::Task
|
74
|
-
def call
|
75
|
-
# Task starts with success status
|
76
|
-
context.result.success? #=> true
|
77
|
-
|
78
|
-
# Conditional skip
|
79
|
-
if context.order.already_processed?
|
80
|
-
skip!(reason: "Order already processed")
|
81
|
-
# Status is now skipped, execution halts
|
82
|
-
end
|
83
|
-
|
84
|
-
# Conditional failure
|
85
|
-
unless context.user.authorized?
|
86
|
-
fail!(reason: "Insufficient permissions")
|
87
|
-
# Status is now failed, execution halts
|
88
|
-
end
|
89
|
-
|
90
|
-
# Continue with business logic
|
91
|
-
process_order
|
92
|
-
# Status remains success
|
93
|
-
end
|
94
|
-
end
|
95
|
-
```
|
96
|
-
|
97
|
-
## Status Predicates
|
37
|
+
## Predicates
|
98
38
|
|
99
39
|
Use status predicates to check execution outcomes:
|
100
40
|
|
101
41
|
```ruby
|
102
|
-
|
103
|
-
def call
|
104
|
-
charge_customer
|
105
|
-
send_receipt
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
result = PaymentProcessingTask.call
|
42
|
+
result = ProcessNotification.execute
|
110
43
|
|
111
44
|
# Individual status checks
|
112
45
|
result.success? #=> true/false
|
@@ -118,249 +51,26 @@ result.good? #=> true if success OR skipped
|
|
118
51
|
result.bad? #=> true if skipped OR failed (not success)
|
119
52
|
```
|
120
53
|
|
121
|
-
|
54
|
+
## Handlers
|
122
55
|
|
123
|
-
|
124
|
-
def handle_payment_result(result)
|
125
|
-
if result.success?
|
126
|
-
send_confirmation_email(result.context.customer)
|
127
|
-
elsif result.skipped?
|
128
|
-
log_skip_reason(result.metadata[:reason])
|
129
|
-
elsif result.failed?
|
130
|
-
handle_payment_failure(result.metadata)
|
131
|
-
end
|
132
|
-
end
|
133
|
-
```
|
134
|
-
|
135
|
-
## Status-Based Callbacks
|
136
|
-
|
137
|
-
> [!TIP]
|
138
|
-
> Use status-based callbacks for business logic branching. The `on_good` and `on_bad` callbacks are particularly useful for handling success/skip vs failed outcomes respectively.
|
56
|
+
Use status-based handlers for business logic branching. The `on_good` and `on_bad` handlers are particularly useful for handling success/skip vs failed outcomes respectively.
|
139
57
|
|
140
58
|
```ruby
|
141
|
-
|
142
|
-
def call
|
143
|
-
validate_inventory
|
144
|
-
process_payment
|
145
|
-
schedule_shipping
|
146
|
-
end
|
147
|
-
end
|
148
|
-
|
149
|
-
result = OrderFulfillmentTask.call
|
59
|
+
result = ProcessNotification.execute
|
150
60
|
|
151
|
-
# Individual status
|
61
|
+
# Individual status handlers
|
152
62
|
result
|
153
|
-
.on_success { |
|
154
|
-
.on_skipped { |
|
155
|
-
.on_failed { |
|
63
|
+
.on_success { |result| mark_notification_sent(result) }
|
64
|
+
.on_skipped { |result| log_notification_skipped(result) }
|
65
|
+
.on_failed { |result| queue_retry_notification(result) }
|
156
66
|
|
157
|
-
# Outcome-based
|
67
|
+
# Outcome-based handlers
|
158
68
|
result
|
159
|
-
.on_good { |
|
160
|
-
.on_bad { |
|
161
|
-
```
|
162
|
-
|
163
|
-
## Status Metadata
|
164
|
-
|
165
|
-
> [!NOTE]
|
166
|
-
> Always include rich metadata with skip and fail operations. This information is invaluable for debugging, user feedback, and automated error handling.
|
167
|
-
|
168
|
-
### Success Metadata
|
169
|
-
|
170
|
-
```ruby
|
171
|
-
class ProcessRefundTask < CMDx::Task
|
172
|
-
def call
|
173
|
-
refund = create_refund(context.payment_id)
|
174
|
-
context.refund_id = refund.id
|
175
|
-
context.processed_at = Time.now
|
176
|
-
end
|
177
|
-
end
|
178
|
-
|
179
|
-
result = ProcessRefundTask.call(payment_id: "pay_123")
|
180
|
-
result.success? #=> true
|
181
|
-
result.metadata #=> {} (typically empty for success)
|
182
|
-
```
|
183
|
-
|
184
|
-
### Skip Metadata
|
185
|
-
|
186
|
-
```ruby
|
187
|
-
class ProcessSubscriptionTask < CMDx::Task
|
188
|
-
def call
|
189
|
-
subscription = Subscription.find(context.subscription_id)
|
190
|
-
|
191
|
-
if subscription.cancelled?
|
192
|
-
skip!(
|
193
|
-
reason: "Subscription already cancelled",
|
194
|
-
cancelled_at: subscription.cancelled_at,
|
195
|
-
skip_code: "ALREADY_CANCELLED"
|
196
|
-
)
|
197
|
-
end
|
198
|
-
|
199
|
-
process_subscription(subscription)
|
200
|
-
end
|
201
|
-
end
|
202
|
-
|
203
|
-
result = ProcessSubscriptionTask.call(subscription_id: 123)
|
204
|
-
if result.skipped?
|
205
|
-
result.metadata[:reason] #=> "Subscription already cancelled"
|
206
|
-
result.metadata[:cancelled_at] #=> 2023-10-01 10:30:00 UTC
|
207
|
-
result.metadata[:skip_code] #=> "ALREADY_CANCELLED"
|
208
|
-
end
|
209
|
-
```
|
210
|
-
|
211
|
-
### Failure Metadata
|
212
|
-
|
213
|
-
```ruby
|
214
|
-
class ValidateUserDataTask < CMDx::Task
|
215
|
-
def call
|
216
|
-
user = User.find(context.user_id)
|
217
|
-
|
218
|
-
unless user.valid?
|
219
|
-
fail!(
|
220
|
-
reason: "User validation failed",
|
221
|
-
errors: user.errors.full_messages,
|
222
|
-
error_code: "VALIDATION_FAILED",
|
223
|
-
retryable: false
|
224
|
-
)
|
225
|
-
end
|
226
|
-
|
227
|
-
context.validated_user = user
|
228
|
-
end
|
229
|
-
end
|
230
|
-
|
231
|
-
result = ValidateUserDataTask.call(user_id: 123)
|
232
|
-
if result.failed?
|
233
|
-
result.metadata[:reason] #=> "User validation failed"
|
234
|
-
result.metadata[:errors] #=> ["Email is invalid", "Name can't be blank"]
|
235
|
-
result.metadata[:error_code] #=> "VALIDATION_FAILED"
|
236
|
-
result.metadata[:retryable] #=> false
|
237
|
-
end
|
238
|
-
```
|
239
|
-
|
240
|
-
## Outcome-Based Logic
|
241
|
-
|
242
|
-
Statuses enable sophisticated outcome-based decision making:
|
243
|
-
|
244
|
-
### Good vs Bad Outcomes
|
245
|
-
|
246
|
-
```ruby
|
247
|
-
class EmailDeliveryTask < CMDx::Task
|
248
|
-
def call
|
249
|
-
# Business logic here
|
250
|
-
send_email
|
251
|
-
end
|
252
|
-
end
|
253
|
-
|
254
|
-
result = EmailDeliveryTask.call
|
255
|
-
|
256
|
-
# Good outcomes (success OR skipped)
|
257
|
-
if result.good?
|
258
|
-
# Both success and skipped are "good" outcomes
|
259
|
-
update_user_interface(result)
|
260
|
-
track_completion_metrics(result)
|
261
|
-
end
|
262
|
-
|
263
|
-
# Bad outcomes (skipped OR failed, excluding success)
|
264
|
-
if result.bad?
|
265
|
-
# Handle any non-success outcome
|
266
|
-
show_error_message(result.metadata[:reason])
|
267
|
-
track_failure_metrics(result)
|
268
|
-
end
|
269
|
-
```
|
270
|
-
|
271
|
-
### Conditional Processing
|
272
|
-
|
273
|
-
```ruby
|
274
|
-
def process_batch_results(results)
|
275
|
-
successful_count = results.count(&:success?)
|
276
|
-
skipped_count = results.count(&:skipped?)
|
277
|
-
failed_count = results.count(&:failed?)
|
278
|
-
|
279
|
-
if results.all?(&:good?)
|
280
|
-
mark_batch_complete
|
281
|
-
elsif results.any?(&:failed?)
|
282
|
-
schedule_batch_retry(results.select(&:failed?))
|
283
|
-
end
|
284
|
-
end
|
285
|
-
```
|
286
|
-
|
287
|
-
## Status vs State vs Outcome
|
288
|
-
|
289
|
-
> [!NOTE]
|
290
|
-
> Status tracks the business outcome (how the task ended), while state tracks the execution lifecycle (where the task is). Both provide valuable but different information about task execution.
|
291
|
-
|
292
|
-
Understanding the relationship between these concepts:
|
293
|
-
|
294
|
-
- **Status**: Business execution outcome (`success`, `skipped`, `failed`)
|
295
|
-
- **State**: Technical execution lifecycle (`initialized`, `executing`, `complete`, `interrupted`)
|
296
|
-
- **Outcome**: Combined representation for unified logic
|
297
|
-
|
298
|
-
```ruby
|
299
|
-
class DataImportTask < CMDx::Task
|
300
|
-
def call
|
301
|
-
import_data
|
302
|
-
validate_data
|
303
|
-
end
|
304
|
-
end
|
305
|
-
|
306
|
-
result = DataImportTask.call
|
307
|
-
|
308
|
-
# Successful execution
|
309
|
-
result.state #=> "complete" (execution finished)
|
310
|
-
result.status #=> "success" (business outcome)
|
311
|
-
result.outcome #=> "success" (same as status when complete)
|
312
|
-
|
313
|
-
# Skipped execution
|
314
|
-
skipped_result = DataImportTask.call(skip_import: true)
|
315
|
-
skipped_result.state #=> "complete" (execution finished)
|
316
|
-
skipped_result.status #=> "skipped" (business outcome)
|
317
|
-
skipped_result.outcome #=> "skipped" (same as status)
|
318
|
-
|
319
|
-
# Failed execution
|
320
|
-
failed_result = DataImportTask.call(invalid_data: true)
|
321
|
-
failed_result.state #=> "interrupted" (execution stopped)
|
322
|
-
failed_result.status #=> "failed" (business outcome)
|
323
|
-
failed_result.outcome #=> "interrupted" (reflects state for interrupted tasks)
|
324
|
-
```
|
325
|
-
|
326
|
-
## Status Serialization and Inspection
|
327
|
-
|
328
|
-
> [!IMPORTANT]
|
329
|
-
> Statuses are automatically captured in result serialization and logging. All status information persists through the complete task execution lifecycle.
|
330
|
-
|
331
|
-
```ruby
|
332
|
-
result = ProcessOrderTask.call
|
333
|
-
|
334
|
-
# Hash representation includes status
|
335
|
-
result.to_h
|
336
|
-
#=> {
|
337
|
-
# class: "ProcessOrderTask",
|
338
|
-
# index: 0,
|
339
|
-
# state: "complete",
|
340
|
-
# status: "success",
|
341
|
-
# outcome: "success",
|
342
|
-
# runtime: 0.045,
|
343
|
-
# metadata: {},
|
344
|
-
# context: { order_id: 123 }
|
345
|
-
# }
|
346
|
-
|
347
|
-
# Human-readable inspection
|
348
|
-
result.to_s
|
349
|
-
#=> "ProcessOrderTask: type=Task index=0 state=complete status=success outcome=success runtime=0.045s"
|
350
|
-
|
351
|
-
# Chain-level status aggregation
|
352
|
-
result.chain.to_h
|
353
|
-
#=> {
|
354
|
-
# id: "chain-550e8400-e29b-41d4-a716-446655440000",
|
355
|
-
# state: "complete",
|
356
|
-
# status: "success",
|
357
|
-
# results: [
|
358
|
-
# { state: "complete", status: "success", ... }
|
359
|
-
# ]
|
360
|
-
# }
|
69
|
+
.on_good { |result| update_message_stats(result) }
|
70
|
+
.on_bad { |result| track_delivery_failure(result) }
|
361
71
|
```
|
362
72
|
|
363
73
|
---
|
364
74
|
|
365
|
-
- **Prev:** [Outcomes -
|
366
|
-
- **Next:** [
|
75
|
+
- **Prev:** [Outcomes - States](states.md)
|
76
|
+
- **Next:** [Attributes - Definitions](../attributes/definitions.md)
|