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.
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 +42 -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 +212 -19
  23. data/docs/outcomes/statuses.md +284 -18
  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 +399 -20
  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 +409 -34
  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 -59
  119. data/lib/cmdx/batch.rb +0 -43
  120. data/lib/cmdx/parameters.rb +0 -34
  121. data/lib/cmdx/run.rb +0 -38
  122. data/lib/cmdx/run_inspector.rb +0 -26
  123. data/lib/cmdx/run_serializer.rb +0 -16
  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,89 +1,232 @@
1
1
  # Interruptions - Faults
2
2
 
3
- Faults are the mechanisms by which `CMDx` goes about halting execution of tasks
4
- via the `skip!` and `fail!` methods. When tasks are executed with bang `call!` method,
5
- a fault exception that matches the current task status will be raised.
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
- ## Rescue
8
+ ## Table of Contents
8
9
 
9
- Use the standard Ruby `rescue` method to handle any faults with custom logic.
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
- ProcessOrderTask.call!
14
- rescue CMDx::Skipped
15
- # Do work on any skipped tasks
16
- rescue CMDx::Failed
17
- # Do work on any failed tasks
18
- rescue CMDx::Fault
19
- # Do work on any skipped or failed tasks
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
- ## For
52
+ ## Fault Context Access
24
53
 
25
- Faults can be matched for the task that caused it.
54
+ Faults provide comprehensive access to task execution context:
26
55
 
27
56
  ```ruby
28
57
  begin
29
- ProcessOrderTask.call!
30
- rescue CMDx::Skipped.for?(ProcessOrderTask, DeliverOrderTask)
31
- # Do work on just skipped ProcessOrderTask or DeliverOrderTask tasks
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
- ## Matches
79
+ ## Advanced Fault Matching
80
+
81
+ ### Task-Specific Matching (`for?`)
36
82
 
37
- Faults allow advance rescue matching with access to the underlying task internals.
83
+ Match faults only from specific task classes using the `for?` method:
38
84
 
39
85
  ```ruby
40
86
  begin
41
- ProcessOrderTask.call!
42
- rescue CMDx::Fault.matches? { |f| f.result.metadata[:reason].includes?("out of stock") }
43
- # Do work on any skipped or failed tasks that have `:reason` metadata equals "out of stock"
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
- > [!IMPORTANT]
48
- > All fault exceptions have access to the `for?` and `matches?` methods.
99
+ ### Custom Matching Logic (`matches?`)
49
100
 
50
- ## Throw
101
+ Use the `matches?` method with blocks for sophisticated fault matching:
51
102
 
52
- Throw the result of subtasks to bubble up fault as its own. Throwing will use the
53
- subtask results' status and metadata to create a matching halt on the parent task.
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 ProcessOrderTask < CMDx::Task
129
+ class ProcessUserOrderTask < CMDx::Task
57
130
 
58
131
  def call
59
- throw!(SendConfirmationNotificationsTask.call)
132
+ # Execute subtask and propagate its failure
133
+ validation_result = ValidateUserOrderTask.call(context)
134
+ throw!(validation_result) if validation_result.failed?
60
135
 
61
- # Do other work...
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
- result = ProcessOrderTask.call
67
- result.state #=> "interrupted"
68
- result.status #=> "skipped"
69
- result.metadata #=> { reason: "Order confirmation could not be sent due to invalid email." }
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
- > [!NOTE]
73
- > `throw!` will bubble any skipped and failed results. To only throw skipped results, just add
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
- ## Results
172
+ ## Fault Chain Analysis
77
173
 
78
- The following represents a result output example of a thrown fault.
174
+ Results provide methods for analyzing fault propagation chains:
79
175
 
80
176
  ```ruby
81
- result = ProcessOrderTask.call
82
- result.threw_failure #=> <CMDx::Result[SendConfirmationNotificationsTask] ...>
83
- result.caused_failure #=> <CMDx::Result[DeliverEmailTask] ...>
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](https://github.com/drexed/cmdx/blob/main/docs/interruptions/halt.md)
89
- - **Next:** [Interruptions - Exceptions](https://github.com/drexed/cmdx/blob/main/docs/interruptions/exceptions.md)
231
+ - **Prev:** [Interruptions - Halt](halt.md)
232
+ - **Next:** [Interruptions - Exceptions](exceptions.md)
@@ -1,80 +1,224 @@
1
1
  # Interruptions - Halt
2
2
 
3
- Halting stops execution of a task. Halt methods signal the intent as to why a task
4
- is stopped executing.
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
- ## Skip
7
+ ## Table of Contents
7
8
 
8
- The `skip!` method indicates that a task did not meet the criteria to continue execution.
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 ProcessOrderTask < CMDx::Task
26
+ class ProcessUserOrderTask < CMDx::Task
12
27
 
13
28
  def call
14
- skip! if cart_abandoned?
29
+ context.order = Order.find(context.order_id)
15
30
 
16
- # Do work...
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
- ## Fail
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 met with incomplete, broken, or failed logic.
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 ProcessOrderTask < CMDx::Task
56
+ class ProcessOrderPaymentTask < CMDx::Task
28
57
 
29
58
  def call
30
- fail! if cart_items_out_of_stock?
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
- # Do work...
67
+ # Continue with processing
68
+ process_payment
33
69
  end
34
70
 
35
71
  end
36
72
  ```
37
73
 
38
- ## Metadata
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
- Pass metadata to enrich faults with additional contextual information. Metadata requires
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 ProcessOrderTask < CMDx::Task
85
+ class ProcessUserOrderTask < CMDx::Task
46
86
 
47
87
  def call
48
- if cart_abandoned?
49
- skip!(reason: "Cart was abandoned due to 30 days of inactivity")
50
- elsif cart_items_out_of_stock?
51
- fail!(reason: "Items in the cart are out of stock", item: [123, 987])
52
- else
53
- # Do work...
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
- result = ProcessOrderTask.call
60
- result.metadata #=> { reason: "Items in the cart are out of stock", item: [123, 987] }
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
- > [!Important]
64
- > The `:reason` key is used to define the fault exception message. While not
65
- > required, it is strongly recommended that it is used on every halt method.
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
- ## Results
200
+ The `:reason` key in metadata has special significance:
68
201
 
69
- The following represents a result output example of a halted task.
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
- result = ProcessOrderTask.call
73
- result.status #=> "failed"
74
- result.metadata #=> { reason: "Cart was abandoned due to 30 days of inactivity" }
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 - Run](https://github.com/drexed/cmdx/blob/main/docs/basics/run.md)
80
- - **Next:** [Interruptions - Faults](https://github.com/drexed/cmdx/blob/main/docs/interruptions/faults.md)
223
+ - **Prev:** [Basics - Chain](../basics/chain.md)
224
+ - **Next:** [Interruptions - Faults](faults.md)