cmdx 0.5.0 → 1.0.1

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 (151) 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 +19 -1
  5. data/.ruby-version +1 -1
  6. data/CHANGELOG.md +95 -28
  7. data/README.md +73 -25
  8. data/docs/ai_prompts.md +319 -0
  9. data/docs/basics/call.md +234 -14
  10. data/docs/basics/chain.md +280 -0
  11. data/docs/basics/context.md +241 -33
  12. data/docs/basics/setup.md +85 -12
  13. data/docs/callbacks.md +283 -0
  14. data/docs/configuration.md +155 -30
  15. data/docs/getting_started.md +145 -22
  16. data/docs/internationalization.md +148 -0
  17. data/docs/interruptions/exceptions.md +198 -11
  18. data/docs/interruptions/faults.md +196 -44
  19. data/docs/interruptions/halt.md +188 -35
  20. data/docs/logging.md +204 -53
  21. data/docs/middlewares.md +745 -0
  22. data/docs/outcomes/result.md +305 -10
  23. data/docs/outcomes/states.md +212 -31
  24. data/docs/outcomes/statuses.md +284 -30
  25. data/docs/parameters/coercions.md +411 -29
  26. data/docs/parameters/defaults.md +258 -25
  27. data/docs/parameters/definitions.md +247 -72
  28. data/docs/parameters/namespacing.md +259 -27
  29. data/docs/parameters/validations.md +173 -168
  30. data/docs/testing.md +560 -0
  31. data/docs/tips_and_tricks.md +103 -42
  32. data/docs/workflows.md +329 -0
  33. data/lib/cmdx/.DS_Store +0 -0
  34. data/lib/cmdx/callback.rb +69 -0
  35. data/lib/cmdx/callback_registry.rb +106 -0
  36. data/lib/cmdx/chain.rb +190 -0
  37. data/lib/cmdx/chain_inspector.rb +149 -0
  38. data/lib/cmdx/chain_serializer.rb +175 -0
  39. data/lib/cmdx/coercions/array.rb +37 -0
  40. data/lib/cmdx/coercions/big_decimal.rb +33 -0
  41. data/lib/cmdx/coercions/boolean.rb +41 -1
  42. data/lib/cmdx/coercions/complex.rb +31 -0
  43. data/lib/cmdx/coercions/date.rb +39 -0
  44. data/lib/cmdx/coercions/date_time.rb +39 -0
  45. data/lib/cmdx/coercions/float.rb +31 -0
  46. data/lib/cmdx/coercions/hash.rb +42 -0
  47. data/lib/cmdx/coercions/integer.rb +32 -0
  48. data/lib/cmdx/coercions/rational.rb +31 -0
  49. data/lib/cmdx/coercions/string.rb +31 -0
  50. data/lib/cmdx/coercions/time.rb +39 -0
  51. data/lib/cmdx/coercions/virtual.rb +31 -0
  52. data/lib/cmdx/configuration.rb +217 -9
  53. data/lib/cmdx/context.rb +173 -2
  54. data/lib/cmdx/core_ext/hash.rb +72 -0
  55. data/lib/cmdx/core_ext/module.rb +94 -0
  56. data/lib/cmdx/core_ext/object.rb +105 -0
  57. data/lib/cmdx/correlator.rb +217 -0
  58. data/lib/cmdx/error.rb +210 -8
  59. data/lib/cmdx/errors.rb +256 -1
  60. data/lib/cmdx/fault.rb +177 -2
  61. data/lib/cmdx/faults.rb +158 -2
  62. data/lib/cmdx/immutator.rb +121 -2
  63. data/lib/cmdx/lazy_struct.rb +261 -18
  64. data/lib/cmdx/log_formatters/json.rb +46 -0
  65. data/lib/cmdx/log_formatters/key_value.rb +46 -0
  66. data/lib/cmdx/log_formatters/line.rb +54 -0
  67. data/lib/cmdx/log_formatters/logstash.rb +64 -0
  68. data/lib/cmdx/log_formatters/pretty_json.rb +57 -0
  69. data/lib/cmdx/log_formatters/pretty_key_value.rb +51 -0
  70. data/lib/cmdx/log_formatters/pretty_line.rb +60 -0
  71. data/lib/cmdx/log_formatters/raw.rb +54 -0
  72. data/lib/cmdx/logger.rb +85 -0
  73. data/lib/cmdx/logger_ansi.rb +93 -7
  74. data/lib/cmdx/logger_serializer.rb +116 -0
  75. data/lib/cmdx/middleware.rb +74 -0
  76. data/lib/cmdx/middleware_registry.rb +106 -0
  77. data/lib/cmdx/middlewares/correlate.rb +266 -0
  78. data/lib/cmdx/middlewares/timeout.rb +232 -0
  79. data/lib/cmdx/parameter.rb +228 -1
  80. data/lib/cmdx/parameter_inspector.rb +61 -0
  81. data/lib/cmdx/parameter_registry.rb +125 -0
  82. data/lib/cmdx/parameter_serializer.rb +83 -0
  83. data/lib/cmdx/parameter_validator.rb +62 -0
  84. data/lib/cmdx/parameter_value.rb +109 -1
  85. data/lib/cmdx/parameters_inspector.rb +59 -0
  86. data/lib/cmdx/parameters_serializer.rb +102 -0
  87. data/lib/cmdx/railtie.rb +123 -3
  88. data/lib/cmdx/result.rb +367 -25
  89. data/lib/cmdx/result_ansi.rb +105 -9
  90. data/lib/cmdx/result_inspector.rb +76 -0
  91. data/lib/cmdx/result_logger.rb +90 -3
  92. data/lib/cmdx/result_serializer.rb +137 -0
  93. data/lib/cmdx/rspec/result_matchers.rb +917 -0
  94. data/lib/cmdx/rspec/task_matchers.rb +570 -0
  95. data/lib/cmdx/task.rb +405 -37
  96. data/lib/cmdx/task_serializer.rb +74 -2
  97. data/lib/cmdx/utils/ansi_color.rb +95 -0
  98. data/lib/cmdx/utils/log_timestamp.rb +48 -0
  99. data/lib/cmdx/utils/monotonic_runtime.rb +71 -4
  100. data/lib/cmdx/utils/name_affix.rb +78 -0
  101. data/lib/cmdx/validators/custom.rb +82 -0
  102. data/lib/cmdx/validators/exclusion.rb +94 -0
  103. data/lib/cmdx/validators/format.rb +102 -8
  104. data/lib/cmdx/validators/inclusion.rb +104 -0
  105. data/lib/cmdx/validators/length.rb +128 -0
  106. data/lib/cmdx/validators/numeric.rb +128 -0
  107. data/lib/cmdx/validators/presence.rb +93 -7
  108. data/lib/cmdx/version.rb +7 -1
  109. data/lib/cmdx/workflow.rb +394 -0
  110. data/lib/cmdx.rb +25 -64
  111. data/lib/generators/cmdx/install_generator.rb +37 -1
  112. data/lib/generators/cmdx/task_generator.rb +69 -1
  113. data/lib/generators/cmdx/templates/install.rb +43 -15
  114. data/lib/generators/cmdx/workflow_generator.rb +109 -0
  115. data/lib/locales/ar.yml +36 -0
  116. data/lib/locales/cs.yml +36 -0
  117. data/lib/locales/da.yml +36 -0
  118. data/lib/locales/de.yml +36 -0
  119. data/lib/locales/el.yml +36 -0
  120. data/lib/locales/en.yml +20 -20
  121. data/lib/locales/es.yml +20 -20
  122. data/lib/locales/fi.yml +36 -0
  123. data/lib/locales/fr.yml +36 -0
  124. data/lib/locales/he.yml +36 -0
  125. data/lib/locales/hi.yml +36 -0
  126. data/lib/locales/it.yml +36 -0
  127. data/lib/locales/ja.yml +36 -0
  128. data/lib/locales/ko.yml +36 -0
  129. data/lib/locales/nl.yml +36 -0
  130. data/lib/locales/no.yml +36 -0
  131. data/lib/locales/pl.yml +36 -0
  132. data/lib/locales/pt.yml +36 -0
  133. data/lib/locales/ru.yml +36 -0
  134. data/lib/locales/sv.yml +36 -0
  135. data/lib/locales/th.yml +36 -0
  136. data/lib/locales/tr.yml +36 -0
  137. data/lib/locales/vi.yml +36 -0
  138. data/lib/locales/zh.yml +36 -0
  139. metadata +77 -15
  140. data/docs/basics/run.md +0 -34
  141. data/docs/batch.md +0 -53
  142. data/docs/example.md +0 -82
  143. data/docs/hooks.md +0 -62
  144. data/lib/cmdx/batch.rb +0 -43
  145. data/lib/cmdx/parameters.rb +0 -35
  146. data/lib/cmdx/run.rb +0 -39
  147. data/lib/cmdx/run_inspector.rb +0 -26
  148. data/lib/cmdx/run_serializer.rb +0 -20
  149. data/lib/cmdx/task_hook.rb +0 -18
  150. data/lib/generators/cmdx/batch_generator.rb +0 -30
  151. /data/lib/generators/cmdx/templates/{batch.rb.tt → workflow.rb.tt} +0 -0
@@ -1,29 +1,216 @@
1
1
  # Interruptions - Exceptions
2
2
 
3
- ## Non-bang call
3
+ CMDx provides robust exception handling that differs between the `call` and `call!`
4
+ methods. Understanding how unhandled exceptions are processed is crucial for
5
+ building reliable task execution flows and implementing proper error handling strategies.
4
6
 
5
- Any unhandled exception will be caught and halted using `fail!`.
6
- The original exception will be passed as metadata of the result object.
7
+ ## Table of Contents
8
+
9
+ - [TLDR](#tldr)
10
+ - [Exception Handling Behavior](#exception-handling-behavior)
11
+ - [Bang Call (`call!`)](#bang-call-call)
12
+ - [Exception Classification](#exception-classification)
13
+
14
+ ## TLDR
15
+
16
+ - **`call`** - Captures ALL exceptions, converts to failed results with metadata
17
+ - **`call!`** - Lets exceptions propagate (except CMDx faults based on task_halt config)
18
+ - **Exception info** - Available in `result.metadata[:original_exception]` and `result.metadata[:reason]`
19
+ - **Guaranteed results** - `call` always returns a result object, never raises
20
+ - **Fault vs Exception** - CMDx faults have special handling, other exceptions propagate in `call!`
21
+
22
+ ## Exception Handling Behavior
23
+
24
+ ### Non-bang Call (`call`)
25
+
26
+ The `call` method captures **all** unhandled exceptions and converts them to
27
+ failed results, ensuring that no exceptions escape the task execution boundary.
28
+ This provides consistent, predictable behavior for result processing.
7
29
 
8
30
  ```ruby
9
- result = ProcessOrderTask.call
31
+ class ProcessUserOrderTask < CMDx::Task
32
+
33
+ def call
34
+ # This will raise a NoMethodError
35
+ undefined_method_call
36
+ end
37
+
38
+ end
39
+
40
+ result = ProcessUserOrderTask.call
10
41
  result.state #=> "interrupted"
11
42
  result.status #=> "failed"
43
+ result.failed? #=> true
12
44
  result.metadata #=> {
13
- #=> reason: "[RuntimeError] method xyz is not defined",
14
- #=> original_exception: <RuntimeError message="method xyz is not defined">
45
+ #=> reason: "[NoMethodError] undefined method `undefined_method_call`",
46
+ #=> original_exception: <NoMethodError>
15
47
  #=> }
16
48
  ```
17
49
 
18
- ## Bang call
50
+ > [!NOTE]
51
+ > The `call` method ensures no exceptions escape task execution, making it ideal
52
+ > for workflow processing and scenarios where you need guaranteed result objects.
53
+
54
+ ### Exception Metadata Structure
55
+
56
+ Captured exceptions populate result metadata with structured information:
57
+
58
+ ```ruby
59
+ class ConnectDatabaseTask < CMDx::Task
60
+
61
+ def call
62
+ # Simulate a database connection error
63
+ raise ActiveRecord::ConnectionNotEstablished, "Database unavailable"
64
+ end
65
+
66
+ end
67
+
68
+ result = ConnectDatabaseTask.call
69
+
70
+ # Exception information in metadata
71
+ result.metadata[:reason] #=> "[ActiveRecord::ConnectionNotEstablished] Database unavailable"
72
+ result.metadata[:original_exception] #=> <ActiveRecord::ConnectionNotEstablished>
73
+ result.metadata[:original_exception].class #=> ActiveRecord::ConnectionNotEstablished
74
+ result.metadata[:original_exception].message #=> "Database unavailable"
75
+ result.metadata[:original_exception].backtrace #=> ["..."]
76
+ ```
77
+
78
+ ### Accessing Original Exception Details
79
+
80
+ ```ruby
81
+ result = ProcessUserOrderTask.call
82
+
83
+ if result.failed? && result.metadata[:original_exception]
84
+ original = result.metadata[:original_exception]
85
+
86
+ puts "Exception type: #{original.class}"
87
+ puts "Exception message: #{original.message}"
88
+ puts "Exception backtrace:"
89
+ puts original.backtrace.first(5).join("\n")
90
+
91
+ # Check exception type for specific handling
92
+ case original
93
+ when ActiveRecord::RecordNotFound
94
+ handle_missing_record(original)
95
+ when Net::TimeoutError
96
+ handle_timeout_error(original)
97
+ when StandardError
98
+ handle_generic_error(original)
99
+ end
100
+ end
101
+ ```
102
+
103
+ ## Bang Call (`call!`)
104
+
105
+ The `call!` method allows unhandled exceptions to propagate **unless** they are
106
+ CMDx faults that match the `task_halt` configuration. This enables exception-based
107
+ control flow while still providing structured fault handling.
108
+
109
+ ```ruby
110
+ class ProcessUserOrderTask < CMDx::Task
111
+
112
+ def call
113
+ # This will raise a NoMethodError directly
114
+ undefined_method_call
115
+ end
116
+
117
+ end
118
+
119
+ begin
120
+ ProcessUserOrderTask.call!
121
+ rescue NoMethodError => e
122
+ puts "Caught original exception: #{e.message}"
123
+ # Handle the original exception directly
124
+ end
125
+ ```
126
+
127
+ ### Fault vs Exception Behavior
128
+
129
+ ```ruby
130
+ class ProcessOrderPaymentTask < CMDx::Task
131
+
132
+ def call
133
+ if context.simulate_fault
134
+ fail!(reason: "Controlled failure") # Becomes CMDx::Failed
135
+ else
136
+ raise StandardError, "Uncontrolled error" # Remains StandardError
137
+ end
138
+ end
139
+
140
+ end
141
+
142
+ # Fault behavior (controlled)
143
+ begin
144
+ ProcessOrderPaymentTask.call!(simulate_fault: true)
145
+ rescue CMDx::Failed => e
146
+ puts "Caught CMDx fault: #{e.message}"
147
+ end
148
+
149
+ # Exception behavior (uncontrolled)
150
+ begin
151
+ ProcessOrderPaymentTask.call!(simulate_fault: false)
152
+ rescue StandardError => e
153
+ puts "Caught standard exception: #{e.message}"
154
+ end
155
+ ```
19
156
 
20
- Any unhandled exception from a `call!` method will be raised as is.
157
+ ## Exception Classification
158
+
159
+ ### Protected Exceptions
160
+
161
+ Certain CMDx-specific exceptions are always allowed to propagate and are never
162
+ converted to failed results:
21
163
 
22
164
  ```ruby
23
- ProcessOrderTask.call! #=> raises NoMethodError, "method xyz is not defined"
165
+ class ProcessUndefinedOrderTask < CMDx::Task
166
+ # Intentionally not implementing call method
167
+ end
168
+
169
+ # These exceptions always propagate regardless of call method
170
+ begin
171
+ ProcessUndefinedOrderTask.call
172
+ rescue CMDx::UndefinedCallError => e
173
+ puts "This exception is never converted to a failed result"
174
+ end
175
+
176
+ begin
177
+ ProcessUndefinedOrderTask.call!
178
+ rescue CMDx::UndefinedCallError => e
179
+ puts "This exception propagates normally in call! too"
180
+ end
24
181
  ```
25
182
 
183
+ ### CMDx Fault Handling
184
+
185
+ CMDx faults have special handling in both call methods:
186
+
187
+ ```ruby
188
+ class ProcessOrderWithHaltTask < CMDx::Task
189
+ # Configure to halt on failures
190
+ task_settings!(task_halt: [CMDx::Result::FAILED])
191
+
192
+ def call
193
+ fail!(reason: "This is a controlled failure")
194
+ end
195
+ end
196
+
197
+ # With call - fault becomes failed result
198
+ result = ProcessOrderWithHaltTask.call
199
+ result.failed? #=> true
200
+
201
+ # With call! - fault becomes exception (due to task_halt configuration)
202
+ begin
203
+ ProcessOrderWithHaltTask.call!
204
+ rescue CMDx::Failed => e
205
+ puts "Fault converted to exception: #{e.message}"
206
+ end
207
+ ```
208
+
209
+ > [!IMPORTANT]
210
+ > Always preserve original exception information in metadata when handling
211
+ > exceptions manually. This maintains debugging capabilities and error traceability.
212
+
26
213
  ---
27
214
 
28
- - **Prev:** [Interruptions - Faults](https://github.com/drexed/cmdx/blob/main/docs/interruptions/faults.md)
29
- - **Next:** [Outcomes - Result](https://github.com/drexed/cmdx/blob/main/docs/outcomes/result.md)
215
+ - **Prev:** [Interruptions - Faults](faults.md)
216
+ - **Next:** [Outcomes - Result](../outcomes/result.md)
@@ -1,89 +1,241 @@
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
+ - [TLDR](#tldr)
11
+ - [Fault Types](#fault-types)
12
+ - [Basic Exception Handling](#basic-exception-handling)
13
+ - [Fault Context Access](#fault-context-access)
14
+ - [Advanced Fault Matching](#advanced-fault-matching)
15
+ - [Fault Propagation (`throw!`)](#fault-propagation-throw)
16
+ - [Fault Chain Analysis](#fault-chain-analysis)
17
+ - [Task Halt Configuration](#task-halt-configuration)
18
+
19
+ ## TLDR
20
+
21
+ - **Fault types** - `CMDx::Skipped` (from `skip!`) and `CMDx::Failed` (from `fail!`)
22
+ - **Exception handling** - Use `rescue CMDx::Fault` to catch both types
23
+ - **Full context** - Faults provide access to `result`, `task`, `context`, and `chain`
24
+ - **Advanced matching** - Use `for?(TaskClass)` and `matches? { |f| condition }` for specific fault handling
25
+ - **Propagation** - Use `throw!(result)` to bubble up failures while preserving fault context
26
+
27
+ ## Fault Types
28
+
29
+ CMDx provides two primary fault types that inherit from the base `CMDx::Fault` class:
30
+
31
+ - **`CMDx::Skipped`** - Raised when a task is skipped via `skip!`
32
+ - **`CMDx::Failed`** - Raised when a task fails via `fail!`
33
+
34
+ Both fault types provide full access to the task execution context, including
35
+ the result object, task instance, context data, and chain information.
36
+
37
+ > [!NOTE]
38
+ > All fault exceptions (`CMDx::Skipped` and `CMDx::Failed`) inherit from the base `CMDx::Fault` class and provide access to the complete task execution context.
39
+
40
+ ## Basic Exception Handling
41
+
42
+ Use standard Ruby `rescue` blocks to handle faults with custom logic:
10
43
 
11
44
  ```ruby
12
45
  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
46
+ ProcessUserOrderTask.call!(order_id: 123)
47
+ rescue CMDx::Skipped => e
48
+ # Handle skipped tasks
49
+ logger.info "Task skipped: #{e.message}"
50
+ e.result.metadata[:reason] #=> "Order already processed"
51
+ rescue CMDx::Failed => e
52
+ # Handle failed tasks
53
+ logger.error "Task failed: #{e.message}"
54
+ e.result.metadata[:error_code] #=> "PAYMENT_DECLINED"
55
+ rescue CMDx::Fault => e
56
+ # Handle any fault (skipped or failed)
57
+ logger.warn "Task interrupted: #{e.message}"
20
58
  end
21
59
  ```
22
60
 
23
- ## For
61
+ ## Fault Context Access
24
62
 
25
- Faults can be matched for the task that caused it.
63
+ Faults provide comprehensive access to task execution context:
26
64
 
27
65
  ```ruby
28
66
  begin
29
- ProcessOrderTask.call!
30
- rescue CMDx::Skipped.for?(ProcessOrderTask, DeliverOrderTask)
31
- # Do work on just skipped ProcessOrderTask or DeliverOrderTask tasks
67
+ ProcessUserOrderTask.call!(order_id: 123)
68
+ rescue CMDx::Fault => e
69
+ # Result information
70
+ e.result.status #=> "failed" or "skipped"
71
+ e.result.metadata[:reason] #=> "Insufficient inventory"
72
+ e.result.runtime #=> 0.05
73
+
74
+ # Task information
75
+ e.task.class.name #=> "ProcessUserOrderTask"
76
+ e.task.id #=> "abc123..."
77
+
78
+ # Context data
79
+ e.context.order_id #=> 123
80
+ e.context.customer_email #=> "user@example.com"
81
+
82
+ # Chain information
83
+ e.chain.id #=> "def456..."
84
+ e.chain.results.size #=> 3
32
85
  end
33
86
  ```
34
87
 
35
- ## Matches
88
+ ## Advanced Fault Matching
89
+
90
+ ### Task-Specific Matching (`for?`)
36
91
 
37
- Faults allow advance rescue matching with access to the underlying task internals.
92
+ Match faults only from specific task classes using the `for?` method:
38
93
 
39
94
  ```ruby
40
95
  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"
96
+ WorkflowProcessUserOrdersTask.call!(orders: orders)
97
+ rescue CMDx::Skipped.for?(ProcessUserOrderTask, ValidateUserOrderTask) => e
98
+ # Handle skips only from specific task types
99
+ logger.info "Order processing skipped: #{e.task.class.name}"
100
+ reschedule_order_processing(e.context.order_id)
101
+ rescue CMDx::Failed.for?(ProcessOrderPaymentTask, ProcessCardChargeTask) => e
102
+ # Handle failures only from payment-related tasks
103
+ logger.error "Payment processing failed: #{e.message}"
104
+ retry_with_backup_payment_method(e.context)
44
105
  end
45
106
  ```
46
107
 
47
- > [!IMPORTANT]
48
- > All fault exceptions have access to the `for?` and `matches?` methods.
108
+ ### Custom Matching Logic (`matches?`)
49
109
 
50
- ## Throw
110
+ Use the `matches?` method with blocks for sophisticated fault matching:
51
111
 
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.
112
+ ```ruby
113
+ begin
114
+ ProcessUserOrderTask.call!(order_id: 123)
115
+ rescue CMDx::Fault.matches? { |f| f.result.metadata[:error_code] == "PAYMENT_DECLINED" } => e
116
+ # Handle specific payment errors
117
+ retry_with_different_payment_method(e.context)
118
+ rescue CMDx::Fault.matches? { |f| f.context.order_value > 1000 } => e
119
+ # Handle high-value order failures differently
120
+ escalate_to_manager(e)
121
+ rescue CMDx::Failed.matches? { |f| f.result.metadata[:reason]&.include?("timeout") } => e
122
+ # Handle timeout-specific failures
123
+ retry_with_longer_timeout(e)
124
+ end
125
+ ```
126
+
127
+ > [!TIP]
128
+ > 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.
129
+
130
+ ## Fault Propagation (`throw!`)
131
+
132
+ The `throw!` method enables fault propagation, allowing parent tasks to bubble up
133
+ failures from subtasks while preserving the original fault information:
134
+
135
+ ### Basic Propagation
54
136
 
55
137
  ```ruby
56
- class ProcessOrderTask < CMDx::Task
138
+ class ProcessUserOrderTask < CMDx::Task
57
139
 
58
140
  def call
59
- throw!(SendConfirmationNotificationsTask.call)
141
+ # Execute subtask and propagate its failure
142
+ validation_result = ValidateUserOrderTask.call(context)
143
+ throw!(validation_result) if validation_result.failed?
144
+
145
+ payment_result = ProcessOrderPaymentTask.call(context)
146
+ throw!(payment_result) # failed or skipped
60
147
 
61
- # Do other work...
148
+ # Continue with main logic
149
+ finalize_order
62
150
  end
63
151
 
64
152
  end
153
+ ```
154
+
155
+ ### Propagation with Additional Context
156
+
157
+ ```ruby
158
+ class ProcessOrderWorkflowTask < CMDx::Task
159
+
160
+ def call
161
+ step1_result = ValidateOrderDataTask.call(context)
162
+
163
+ if step1_result.failed?
164
+ # Propagate with additional context
165
+ throw!(step1_result, {
166
+ workflow_stage: "initial_validation",
167
+ attempted_at: Time.now,
168
+ can_retry: true
169
+ })
170
+ end
171
+
172
+ continue_workflow
173
+ end
65
174
 
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." }
175
+ end
70
176
  ```
71
177
 
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.
178
+ > [!IMPORTANT]
179
+ > Use `throw!` to propagate failures while preserving the original fault context. This maintains the fault chain for debugging and provides better error traceability.
180
+
181
+ ## Fault Chain Analysis
182
+
183
+ Results provide methods for analyzing fault propagation chains:
184
+
185
+ ```ruby
186
+ result = ProcessOrderWorkflowTask.call(data: invalid_data)
187
+
188
+ if result.failed?
189
+ # Find the original cause of failure
190
+ original_failure = result.caused_failure
191
+ puts "Original failure: #{original_failure.task.class.name}"
192
+ puts "Reason: #{original_failure.metadata[:reason]}"
193
+
194
+ # Find what threw the failure to this result
195
+ throwing_task = result.threw_failure
196
+ puts "Failure thrown by: #{throwing_task.task.class.name}" if throwing_task
197
+
198
+ # Check if this result caused or threw the failure
199
+ if result.caused_failure?
200
+ puts "This task was the original cause"
201
+ elsif result.threw_failure?
202
+ puts "This task threw a failure from another task"
203
+ elsif result.thrown_failure?
204
+ puts "This task failed due to a thrown failure"
205
+ end
206
+ end
207
+ ```
75
208
 
76
- ## Results
209
+ ## Task Halt Configuration
77
210
 
78
- The following represents a result output example of a thrown fault.
211
+ Control which statuses raise exceptions using the `task_halt` setting:
79
212
 
80
213
  ```ruby
81
- result = ProcessOrderTask.call
82
- result.threw_failure #=> <CMDx::Result[SendConfirmationNotificationsTask] ...>
83
- result.caused_failure #=> <CMDx::Result[DeliverEmailTask] ...>
214
+ class ProcessUserOrderTask < CMDx::Task
215
+ # Only failed tasks raise exceptions on call!
216
+ task_settings!(task_halt: [CMDx::Result::FAILED])
217
+
218
+ def call
219
+ skip!(reason: "Order already processed") if already_processed?
220
+ # This will NOT raise an exception on call!
221
+ end
222
+ end
223
+
224
+ class ValidateUserDataTask < CMDx::Task
225
+ # Both failed and skipped tasks raise exceptions
226
+ task_settings!(task_halt: [CMDx::Result::FAILED, CMDx::Result::SKIPPED])
227
+
228
+ def call
229
+ skip!(reason: "Validation not required") if skip_validation?
230
+ # This WILL raise an exception on call!
231
+ end
232
+ end
84
233
  ```
85
234
 
235
+ > [!WARNING]
236
+ > 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.
237
+
86
238
  ---
87
239
 
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)
240
+ - **Prev:** [Interruptions - Halt](halt.md)
241
+ - **Next:** [Interruptions - Exceptions](exceptions.md)