cmdx 0.4.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 +42 -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 +212 -19
- data/docs/outcomes/statuses.md +284 -18
- 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 +399 -20
- 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 +409 -34
- 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 -59
- data/lib/cmdx/batch.rb +0 -43
- data/lib/cmdx/parameters.rb +0 -34
- data/lib/cmdx/run.rb +0 -38
- data/lib/cmdx/run_inspector.rb +0 -26
- data/lib/cmdx/run_serializer.rb +0 -16
- 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
@@ -1,89 +1,232 @@
|
|
1
1
|
# Interruptions - Faults
|
2
2
|
|
3
|
-
Faults are the mechanisms by which
|
4
|
-
|
5
|
-
|
3
|
+
Faults are the exception mechanisms by which CMDx halts task execution via the
|
4
|
+
`skip!` and `fail!` methods. When tasks are executed with the bang `call!` method,
|
5
|
+
fault exceptions matching the task's interruption status are raised, enabling
|
6
|
+
sophisticated exception handling and control flow patterns.
|
6
7
|
|
7
|
-
##
|
8
|
+
## Table of Contents
|
8
9
|
|
9
|
-
|
10
|
+
- [Fault Types](#fault-types)
|
11
|
+
- [Basic Exception Handling](#basic-exception-handling)
|
12
|
+
- [Fault Context Access](#fault-context-access)
|
13
|
+
- [Advanced Fault Matching](#advanced-fault-matching)
|
14
|
+
- [Fault Propagation (`throw!`)](#fault-propagation-throw)
|
15
|
+
- [Fault Chain Analysis](#fault-chain-analysis)
|
16
|
+
- [Task Halt Configuration](#task-halt-configuration)
|
17
|
+
|
18
|
+
## Fault Types
|
19
|
+
|
20
|
+
CMDx provides two primary fault types that inherit from the base `CMDx::Fault` class:
|
21
|
+
|
22
|
+
- **`CMDx::Skipped`** - Raised when a task is skipped via `skip!`
|
23
|
+
- **`CMDx::Failed`** - Raised when a task fails via `fail!`
|
24
|
+
|
25
|
+
Both fault types provide full access to the task execution context, including
|
26
|
+
the result object, task instance, context data, and chain information.
|
27
|
+
|
28
|
+
> [!NOTE]
|
29
|
+
> All fault exceptions (`CMDx::Skipped` and `CMDx::Failed`) inherit from the base `CMDx::Fault` class and provide access to the complete task execution context.
|
30
|
+
|
31
|
+
## Basic Exception Handling
|
32
|
+
|
33
|
+
Use standard Ruby `rescue` blocks to handle faults with custom logic:
|
10
34
|
|
11
35
|
```ruby
|
12
36
|
begin
|
13
|
-
|
14
|
-
rescue CMDx::Skipped
|
15
|
-
#
|
16
|
-
|
17
|
-
|
18
|
-
rescue CMDx::
|
19
|
-
#
|
37
|
+
ProcessUserOrderTask.call!(order_id: 123)
|
38
|
+
rescue CMDx::Skipped => e
|
39
|
+
# Handle skipped tasks
|
40
|
+
logger.info "Task skipped: #{e.message}"
|
41
|
+
e.result.metadata[:reason] #=> "Order already processed"
|
42
|
+
rescue CMDx::Failed => e
|
43
|
+
# Handle failed tasks
|
44
|
+
logger.error "Task failed: #{e.message}"
|
45
|
+
e.result.metadata[:error_code] #=> "PAYMENT_DECLINED"
|
46
|
+
rescue CMDx::Fault => e
|
47
|
+
# Handle any fault (skipped or failed)
|
48
|
+
logger.warn "Task interrupted: #{e.message}"
|
20
49
|
end
|
21
50
|
```
|
22
51
|
|
23
|
-
##
|
52
|
+
## Fault Context Access
|
24
53
|
|
25
|
-
Faults
|
54
|
+
Faults provide comprehensive access to task execution context:
|
26
55
|
|
27
56
|
```ruby
|
28
57
|
begin
|
29
|
-
|
30
|
-
rescue CMDx::
|
31
|
-
#
|
58
|
+
ProcessUserOrderTask.call!(order_id: 123)
|
59
|
+
rescue CMDx::Fault => e
|
60
|
+
# Result information
|
61
|
+
e.result.status #=> "failed" or "skipped"
|
62
|
+
e.result.metadata[:reason] #=> "Insufficient inventory"
|
63
|
+
e.result.runtime #=> 0.05
|
64
|
+
|
65
|
+
# Task information
|
66
|
+
e.task.class.name #=> "ProcessUserOrderTask"
|
67
|
+
e.task.id #=> "abc123..."
|
68
|
+
|
69
|
+
# Context data
|
70
|
+
e.context.order_id #=> 123
|
71
|
+
e.context.customer_email #=> "user@example.com"
|
72
|
+
|
73
|
+
# Chain information
|
74
|
+
e.chain.id #=> "def456..."
|
75
|
+
e.chain.results.size #=> 3
|
32
76
|
end
|
33
77
|
```
|
34
78
|
|
35
|
-
##
|
79
|
+
## Advanced Fault Matching
|
80
|
+
|
81
|
+
### Task-Specific Matching (`for?`)
|
36
82
|
|
37
|
-
|
83
|
+
Match faults only from specific task classes using the `for?` method:
|
38
84
|
|
39
85
|
```ruby
|
40
86
|
begin
|
41
|
-
|
42
|
-
rescue CMDx::
|
43
|
-
#
|
87
|
+
WorkflowProcessUserOrdersTask.call!(orders: orders)
|
88
|
+
rescue CMDx::Skipped.for?(ProcessUserOrderTask, ValidateUserOrderTask) => e
|
89
|
+
# Handle skips only from specific task types
|
90
|
+
logger.info "Order processing skipped: #{e.task.class.name}"
|
91
|
+
reschedule_order_processing(e.context.order_id)
|
92
|
+
rescue CMDx::Failed.for?(ProcessOrderPaymentTask, ProcessCardChargeTask) => e
|
93
|
+
# Handle failures only from payment-related tasks
|
94
|
+
logger.error "Payment processing failed: #{e.message}"
|
95
|
+
retry_with_backup_payment_method(e.context)
|
44
96
|
end
|
45
97
|
```
|
46
98
|
|
47
|
-
|
48
|
-
> All fault exceptions have access to the `for?` and `matches?` methods.
|
99
|
+
### Custom Matching Logic (`matches?`)
|
49
100
|
|
50
|
-
|
101
|
+
Use the `matches?` method with blocks for sophisticated fault matching:
|
51
102
|
|
52
|
-
|
53
|
-
|
103
|
+
```ruby
|
104
|
+
begin
|
105
|
+
ProcessUserOrderTask.call!(order_id: 123)
|
106
|
+
rescue CMDx::Fault.matches? { |f| f.result.metadata[:error_code] == "PAYMENT_DECLINED" } => e
|
107
|
+
# Handle specific payment errors
|
108
|
+
retry_with_different_payment_method(e.context)
|
109
|
+
rescue CMDx::Fault.matches? { |f| f.context.order_value > 1000 } => e
|
110
|
+
# Handle high-value order failures differently
|
111
|
+
escalate_to_manager(e)
|
112
|
+
rescue CMDx::Failed.matches? { |f| f.result.metadata[:reason]&.include?("timeout") } => e
|
113
|
+
# Handle timeout-specific failures
|
114
|
+
retry_with_longer_timeout(e)
|
115
|
+
end
|
116
|
+
```
|
117
|
+
|
118
|
+
> [!TIP]
|
119
|
+
> Use `for?` and `matches?` methods for advanced exception matching. The `for?` method is ideal for task-specific handling, while `matches?` enables custom logic-based fault filtering.
|
120
|
+
|
121
|
+
## Fault Propagation (`throw!`)
|
122
|
+
|
123
|
+
The `throw!` method enables fault propagation, allowing parent tasks to bubble up
|
124
|
+
failures from subtasks while preserving the original fault information:
|
125
|
+
|
126
|
+
### Basic Propagation
|
54
127
|
|
55
128
|
```ruby
|
56
|
-
class
|
129
|
+
class ProcessUserOrderTask < CMDx::Task
|
57
130
|
|
58
131
|
def call
|
59
|
-
|
132
|
+
# Execute subtask and propagate its failure
|
133
|
+
validation_result = ValidateUserOrderTask.call(context)
|
134
|
+
throw!(validation_result) if validation_result.failed?
|
60
135
|
|
61
|
-
|
136
|
+
payment_result = ProcessOrderPaymentTask.call(context)
|
137
|
+
throw!(payment_result) # failed or skipped
|
138
|
+
|
139
|
+
# Continue with main logic
|
140
|
+
finalize_order
|
62
141
|
end
|
63
142
|
|
64
143
|
end
|
144
|
+
```
|
145
|
+
|
146
|
+
### Propagation with Additional Context
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
class ProcessOrderWorkflowTask < CMDx::Task
|
65
150
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
151
|
+
def call
|
152
|
+
step1_result = ValidateOrderDataTask.call(context)
|
153
|
+
|
154
|
+
if step1_result.failed?
|
155
|
+
# Propagate with additional context
|
156
|
+
throw!(step1_result, {
|
157
|
+
workflow_stage: "initial_validation",
|
158
|
+
attempted_at: Time.now,
|
159
|
+
can_retry: true
|
160
|
+
})
|
161
|
+
end
|
162
|
+
|
163
|
+
continue_workflow
|
164
|
+
end
|
165
|
+
|
166
|
+
end
|
70
167
|
```
|
71
168
|
|
72
|
-
> [!
|
73
|
-
> `throw!`
|
74
|
-
> a conditional for the specific status.
|
169
|
+
> [!IMPORTANT]
|
170
|
+
> Use `throw!` to propagate failures while preserving the original fault context. This maintains the fault chain for debugging and provides better error traceability.
|
75
171
|
|
76
|
-
##
|
172
|
+
## Fault Chain Analysis
|
77
173
|
|
78
|
-
|
174
|
+
Results provide methods for analyzing fault propagation chains:
|
79
175
|
|
80
176
|
```ruby
|
81
|
-
result =
|
82
|
-
|
83
|
-
result.
|
177
|
+
result = ProcessOrderWorkflowTask.call(data: invalid_data)
|
178
|
+
|
179
|
+
if result.failed?
|
180
|
+
# Find the original cause of failure
|
181
|
+
original_failure = result.caused_failure
|
182
|
+
puts "Original failure: #{original_failure.task.class.name}"
|
183
|
+
puts "Reason: #{original_failure.metadata[:reason]}"
|
184
|
+
|
185
|
+
# Find what threw the failure to this result
|
186
|
+
throwing_task = result.threw_failure
|
187
|
+
puts "Failure thrown by: #{throwing_task.task.class.name}" if throwing_task
|
188
|
+
|
189
|
+
# Check if this result caused or threw the failure
|
190
|
+
if result.caused_failure?
|
191
|
+
puts "This task was the original cause"
|
192
|
+
elsif result.threw_failure?
|
193
|
+
puts "This task threw a failure from another task"
|
194
|
+
elsif result.thrown_failure?
|
195
|
+
puts "This task failed due to a thrown failure"
|
196
|
+
end
|
197
|
+
end
|
84
198
|
```
|
85
199
|
|
200
|
+
## Task Halt Configuration
|
201
|
+
|
202
|
+
Control which statuses raise exceptions using the `task_halt` setting:
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
class ProcessUserOrderTask < CMDx::Task
|
206
|
+
# Only failed tasks raise exceptions on call!
|
207
|
+
task_settings!(task_halt: [CMDx::Result::FAILED])
|
208
|
+
|
209
|
+
def call
|
210
|
+
skip!(reason: "Order already processed") if already_processed?
|
211
|
+
# This will NOT raise an exception on call!
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
class ValidateUserDataTask < CMDx::Task
|
216
|
+
# Both failed and skipped tasks raise exceptions
|
217
|
+
task_settings!(task_halt: [CMDx::Result::FAILED, CMDx::Result::SKIPPED])
|
218
|
+
|
219
|
+
def call
|
220
|
+
skip!(reason: "Validation not required") if skip_validation?
|
221
|
+
# This WILL raise an exception on call!
|
222
|
+
end
|
223
|
+
end
|
224
|
+
```
|
225
|
+
|
226
|
+
> [!WARNING]
|
227
|
+
> Task halt configuration only affects the `call!` method. The `call` method always captures all exceptions and converts them to result objects regardless of halt settings.
|
228
|
+
|
86
229
|
---
|
87
230
|
|
88
|
-
- **Prev:** [Interruptions - Halt](
|
89
|
-
- **Next:** [Interruptions - Exceptions](
|
231
|
+
- **Prev:** [Interruptions - Halt](halt.md)
|
232
|
+
- **Next:** [Interruptions - Exceptions](exceptions.md)
|
data/docs/interruptions/halt.md
CHANGED
@@ -1,80 +1,224 @@
|
|
1
1
|
# Interruptions - Halt
|
2
2
|
|
3
|
-
Halting stops execution of a task
|
4
|
-
|
3
|
+
Halting stops execution of a task with explicit intent signaling. Tasks provide
|
4
|
+
two primary halt methods that control execution flow and result in different
|
5
|
+
outcomes, each serving specific use cases in business logic.
|
5
6
|
|
6
|
-
##
|
7
|
+
## Table of Contents
|
7
8
|
|
8
|
-
|
9
|
+
- [Skip (`skip!`)](#skip-skip)
|
10
|
+
- [Fail (`fail!`)](#fail-fail)
|
11
|
+
- [Metadata Enrichment](#metadata-enrichment)
|
12
|
+
- [State Transitions](#state-transitions)
|
13
|
+
- [Exception Behavior](#exception-behavior)
|
14
|
+
- [The Reason Key](#the-reason-key)
|
15
|
+
|
16
|
+
## Skip (`skip!`)
|
17
|
+
|
18
|
+
The `skip!` method indicates that a task did not meet the criteria to continue
|
19
|
+
execution. This represents a controlled, intentional interruption where the
|
20
|
+
task determines that execution is not necessary or appropriate under current
|
21
|
+
conditions.
|
22
|
+
|
23
|
+
### Basic Usage
|
9
24
|
|
10
25
|
```ruby
|
11
|
-
class
|
26
|
+
class ProcessUserOrderTask < CMDx::Task
|
12
27
|
|
13
28
|
def call
|
14
|
-
|
29
|
+
context.order = Order.find(context.order_id)
|
15
30
|
|
16
|
-
#
|
31
|
+
# Skip if order is already processed
|
32
|
+
skip!(reason: "Order already processed") if context.order.processed?
|
33
|
+
|
34
|
+
# Skip if prerequisites aren't met
|
35
|
+
skip!(reason: "Payment method not configured") unless context.order.payment_method
|
36
|
+
|
37
|
+
# Continue with business logic
|
38
|
+
context.order.process!
|
17
39
|
end
|
18
40
|
|
19
41
|
end
|
20
42
|
```
|
21
43
|
|
22
|
-
|
44
|
+
> [!NOTE]
|
45
|
+
> Use `skip!` when a task cannot or should not execute under current conditions, but this is not an error. Skipped tasks are considered successful outcomes.
|
46
|
+
|
47
|
+
## Fail (`fail!`)
|
23
48
|
|
24
|
-
The `fail!` method indicates that a task
|
49
|
+
The `fail!` method indicates that a task encountered an error condition that
|
50
|
+
prevents successful completion. This represents controlled failure where the
|
51
|
+
task explicitly determines that execution cannot continue successfully.
|
52
|
+
|
53
|
+
### Basic Usage
|
25
54
|
|
26
55
|
```ruby
|
27
|
-
class
|
56
|
+
class ProcessOrderPaymentTask < CMDx::Task
|
28
57
|
|
29
58
|
def call
|
30
|
-
|
59
|
+
context.payment = Payment.find(context.payment_id)
|
60
|
+
|
61
|
+
# Fail on validation errors
|
62
|
+
fail!(reason: "Payment amount must be positive") unless context.payment.amount > 0
|
63
|
+
|
64
|
+
# Fail on business rule violations
|
65
|
+
fail!(reason: "Insufficient funds") unless sufficient_funds?
|
31
66
|
|
32
|
-
#
|
67
|
+
# Continue with processing
|
68
|
+
process_payment
|
33
69
|
end
|
34
70
|
|
35
71
|
end
|
36
72
|
```
|
37
73
|
|
38
|
-
|
74
|
+
> [!IMPORTANT]
|
75
|
+
> Use `fail!` when a task encounters an error that prevents successful completion. Failed tasks represent error conditions that need to be handled or corrected.
|
76
|
+
|
77
|
+
## Metadata Enrichment
|
78
|
+
|
79
|
+
Both halt methods accept metadata to provide context about the interruption.
|
80
|
+
Metadata is stored as a hash and becomes available through the result object.
|
39
81
|
|
40
|
-
|
41
|
-
that it be passed as a hash object. Internal failures will hydrate metadata into its result,
|
42
|
-
eg: failed validations and unrescued exceptions.
|
82
|
+
### Structured Metadata
|
43
83
|
|
44
84
|
```ruby
|
45
|
-
class
|
85
|
+
class ProcessUserOrderTask < CMDx::Task
|
46
86
|
|
47
87
|
def call
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
88
|
+
context.order = Order.find(context.order_id)
|
89
|
+
|
90
|
+
if context.order.status == "cancelled"
|
91
|
+
skip!(
|
92
|
+
reason: "Order was cancelled",
|
93
|
+
order_id: context.order.id,
|
94
|
+
cancelled_at: context.order.cancelled_at,
|
95
|
+
reason_code: context.order.cancellation_reason
|
96
|
+
)
|
54
97
|
end
|
98
|
+
|
99
|
+
unless inventory_available?
|
100
|
+
fail!(
|
101
|
+
reason: "Insufficient inventory",
|
102
|
+
required_quantity: context.order.quantity,
|
103
|
+
available_quantity: current_inventory,
|
104
|
+
restock_date: estimated_restock_date,
|
105
|
+
error_code: "INVENTORY_DEPLETED"
|
106
|
+
)
|
107
|
+
end
|
108
|
+
|
109
|
+
process_order
|
55
110
|
end
|
56
111
|
|
57
112
|
end
|
113
|
+
```
|
114
|
+
|
115
|
+
### Accessing Metadata
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
result = ProcessUserOrderTask.call(order_id: 123)
|
119
|
+
|
120
|
+
# Check result status
|
121
|
+
result.skipped? #=> true
|
122
|
+
result.failed? #=> false
|
123
|
+
|
124
|
+
# Access metadata
|
125
|
+
result.metadata[:reason] #=> "Order was cancelled"
|
126
|
+
result.metadata[:order_id] #=> 123
|
127
|
+
result.metadata[:cancelled_at] #=> 2023-01-01 10:00:00 UTC
|
128
|
+
result.metadata[:reason_code] #=> "customer_request"
|
129
|
+
```
|
130
|
+
|
131
|
+
## State Transitions
|
132
|
+
|
133
|
+
Halt methods trigger specific state and status transitions:
|
134
|
+
|
135
|
+
### Skip Transitions
|
136
|
+
- **State**: `initialized` → `executing` → `interrupted`
|
137
|
+
- **Status**: `success` → `skipped`
|
138
|
+
- **Result**: `good? = true`, `bad? = true`
|
139
|
+
|
140
|
+
### Fail Transitions
|
141
|
+
- **State**: `initialized` → `executing` → `interrupted`
|
142
|
+
- **Status**: `success` → `failed`
|
143
|
+
- **Result**: `good? = false`, `bad? = true`
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
result = ProcessUserOrderTask.call(order_id: 123)
|
147
|
+
|
148
|
+
# State information
|
149
|
+
result.state #=> "interrupted"
|
150
|
+
result.status #=> "skipped" or "failed"
|
151
|
+
result.interrupted? #=> true
|
152
|
+
result.complete? #=> false
|
153
|
+
|
154
|
+
# Outcome categorization
|
155
|
+
result.good? #=> true for skipped, false for failed
|
156
|
+
result.bad? #=> true for both skipped and failed
|
157
|
+
```
|
158
|
+
|
159
|
+
## Exception Behavior
|
160
|
+
|
161
|
+
Halt methods behave differently depending on the call method used:
|
162
|
+
|
163
|
+
### With `call` (Non-bang)
|
164
|
+
Returns a result object without raising exceptions:
|
165
|
+
|
166
|
+
```ruby
|
167
|
+
result = ProcessUserOrderTask.call(order_id: 123)
|
168
|
+
|
169
|
+
case result.status
|
170
|
+
when "success"
|
171
|
+
puts "Order processed successfully"
|
172
|
+
when "skipped"
|
173
|
+
puts "Order skipped: #{result.metadata[:reason]}"
|
174
|
+
when "failed"
|
175
|
+
puts "Order failed: #{result.metadata[:reason]}"
|
176
|
+
end
|
177
|
+
```
|
178
|
+
|
179
|
+
### With `call!` (Bang)
|
180
|
+
Raises fault exceptions based on `task_halt` configuration:
|
58
181
|
|
59
|
-
|
60
|
-
|
182
|
+
```ruby
|
183
|
+
begin
|
184
|
+
result = ProcessUserOrderTask.call!(order_id: 123)
|
185
|
+
puts "Success: #{result.context.order.id}"
|
186
|
+
rescue CMDx::Skipped => e
|
187
|
+
puts "Skipped: #{e.message}"
|
188
|
+
puts "Order ID: #{e.context.order_id}"
|
189
|
+
rescue CMDx::Failed => e
|
190
|
+
puts "Failed: #{e.message}"
|
191
|
+
puts "Error code: #{e.result.metadata[:error_code]}"
|
192
|
+
end
|
61
193
|
```
|
62
194
|
|
63
|
-
> [!
|
64
|
-
> The
|
65
|
-
|
195
|
+
> [!WARNING]
|
196
|
+
> The `call!` method raises exceptions for halt conditions based on the `task_halt` configuration. The `call` method always returns result objects without raising exceptions.
|
197
|
+
|
198
|
+
## The Reason Key
|
66
199
|
|
67
|
-
|
200
|
+
The `:reason` key in metadata has special significance:
|
68
201
|
|
69
|
-
|
202
|
+
- Used as the exception message when faults are raised
|
203
|
+
- Provides human-readable explanation of the halt
|
204
|
+
- Strongly recommended for all halt calls
|
70
205
|
|
71
206
|
```ruby
|
72
|
-
|
73
|
-
|
74
|
-
|
207
|
+
# Good: Provides clear reason
|
208
|
+
skip!(reason: "User already has an active session")
|
209
|
+
fail!(reason: "Credit card expired", code: "EXPIRED_CARD")
|
210
|
+
|
211
|
+
# Acceptable: Other metadata without reason
|
212
|
+
skip!(status: "redundant", timestamp: Time.now)
|
213
|
+
|
214
|
+
# Fallback: Default message if no reason provided
|
215
|
+
skip! # Exception message: "no reason given"
|
75
216
|
```
|
76
217
|
|
218
|
+
> [!TIP]
|
219
|
+
> Always try to include a `:reason` key in metadata when using halt methods. This provides clear context for debugging and creates meaningful exception messages when using `call!`.
|
220
|
+
|
77
221
|
---
|
78
222
|
|
79
|
-
- **Prev:** [Basics -
|
80
|
-
- **Next:** [Interruptions - Faults](
|
223
|
+
- **Prev:** [Basics - Chain](../basics/chain.md)
|
224
|
+
- **Next:** [Interruptions - Faults](faults.md)
|