cmdx 1.1.0 → 1.1.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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/prompts/docs.md +9 -0
  3. data/.cursor/prompts/rspec.md +13 -12
  4. data/.cursor/prompts/yardoc.md +11 -6
  5. data/CHANGELOG.md +13 -2
  6. data/README.md +1 -0
  7. data/docs/ai_prompts.md +269 -195
  8. data/docs/basics/call.md +124 -58
  9. data/docs/basics/chain.md +190 -160
  10. data/docs/basics/context.md +242 -154
  11. data/docs/basics/setup.md +302 -32
  12. data/docs/callbacks.md +390 -94
  13. data/docs/configuration.md +181 -65
  14. data/docs/deprecation.md +245 -0
  15. data/docs/getting_started.md +161 -39
  16. data/docs/internationalization.md +590 -70
  17. data/docs/interruptions/exceptions.md +135 -118
  18. data/docs/interruptions/faults.md +150 -125
  19. data/docs/interruptions/halt.md +134 -80
  20. data/docs/logging.md +181 -118
  21. data/docs/middlewares.md +150 -377
  22. data/docs/outcomes/result.md +140 -112
  23. data/docs/outcomes/states.md +134 -99
  24. data/docs/outcomes/statuses.md +204 -146
  25. data/docs/parameters/coercions.md +232 -281
  26. data/docs/parameters/defaults.md +224 -169
  27. data/docs/parameters/definitions.md +289 -141
  28. data/docs/parameters/namespacing.md +250 -161
  29. data/docs/parameters/validations.md +260 -133
  30. data/docs/testing.md +191 -197
  31. data/docs/workflows.md +143 -98
  32. data/lib/cmdx/callback.rb +23 -19
  33. data/lib/cmdx/callback_registry.rb +1 -3
  34. data/lib/cmdx/chain_inspector.rb +23 -23
  35. data/lib/cmdx/chain_serializer.rb +38 -19
  36. data/lib/cmdx/coercion.rb +20 -12
  37. data/lib/cmdx/coercion_registry.rb +51 -32
  38. data/lib/cmdx/configuration.rb +84 -31
  39. data/lib/cmdx/context.rb +32 -21
  40. data/lib/cmdx/core_ext/hash.rb +13 -13
  41. data/lib/cmdx/core_ext/module.rb +1 -1
  42. data/lib/cmdx/core_ext/object.rb +12 -12
  43. data/lib/cmdx/correlator.rb +60 -39
  44. data/lib/cmdx/errors.rb +105 -131
  45. data/lib/cmdx/fault.rb +66 -45
  46. data/lib/cmdx/immutator.rb +20 -21
  47. data/lib/cmdx/lazy_struct.rb +78 -70
  48. data/lib/cmdx/log_formatters/json.rb +1 -1
  49. data/lib/cmdx/log_formatters/key_value.rb +1 -1
  50. data/lib/cmdx/log_formatters/line.rb +1 -1
  51. data/lib/cmdx/log_formatters/logstash.rb +1 -1
  52. data/lib/cmdx/log_formatters/pretty_json.rb +1 -1
  53. data/lib/cmdx/log_formatters/pretty_key_value.rb +1 -1
  54. data/lib/cmdx/log_formatters/pretty_line.rb +1 -1
  55. data/lib/cmdx/log_formatters/raw.rb +2 -2
  56. data/lib/cmdx/logger.rb +19 -14
  57. data/lib/cmdx/logger_ansi.rb +33 -17
  58. data/lib/cmdx/logger_serializer.rb +85 -24
  59. data/lib/cmdx/middleware.rb +39 -21
  60. data/lib/cmdx/middleware_registry.rb +4 -3
  61. data/lib/cmdx/parameter.rb +151 -89
  62. data/lib/cmdx/parameter_inspector.rb +34 -21
  63. data/lib/cmdx/parameter_registry.rb +36 -30
  64. data/lib/cmdx/parameter_serializer.rb +21 -14
  65. data/lib/cmdx/result.rb +136 -135
  66. data/lib/cmdx/result_ansi.rb +31 -17
  67. data/lib/cmdx/result_inspector.rb +32 -27
  68. data/lib/cmdx/result_logger.rb +23 -14
  69. data/lib/cmdx/result_serializer.rb +65 -27
  70. data/lib/cmdx/task.rb +234 -113
  71. data/lib/cmdx/task_deprecator.rb +22 -25
  72. data/lib/cmdx/task_processor.rb +89 -88
  73. data/lib/cmdx/task_serializer.rb +27 -14
  74. data/lib/cmdx/utils/monotonic_runtime.rb +2 -4
  75. data/lib/cmdx/validator.rb +25 -16
  76. data/lib/cmdx/validator_registry.rb +53 -31
  77. data/lib/cmdx/validators/exclusion.rb +1 -1
  78. data/lib/cmdx/validators/format.rb +2 -2
  79. data/lib/cmdx/validators/inclusion.rb +2 -2
  80. data/lib/cmdx/validators/length.rb +2 -2
  81. data/lib/cmdx/validators/numeric.rb +3 -3
  82. data/lib/cmdx/validators/presence.rb +2 -2
  83. data/lib/cmdx/version.rb +1 -1
  84. data/lib/cmdx/workflow.rb +54 -33
  85. data/lib/generators/cmdx/task_generator.rb +6 -6
  86. data/lib/generators/cmdx/workflow_generator.rb +6 -6
  87. metadata +3 -1
data/docs/basics/call.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Basics - Call
2
2
 
3
- Calling a task executes the business logic within it. Tasks provide two execution methods that handle success and failure scenarios differently. Understanding when to use each method is crucial for proper error handling and control flow.
3
+ Task execution in CMDx provides two distinct methods that handle success and failure scenarios differently. Understanding when to use each method is crucial for proper error handling and control flow in your application workflows.
4
4
 
5
5
  ## Table of Contents
6
6
 
@@ -13,18 +13,38 @@ Calling a task executes the business logic within it. Tasks provide two executio
13
13
  - [Result Propagation (`throw!`)](#result-propagation-throw)
14
14
  - [Result Callbacks](#result-callbacks)
15
15
  - [Task State Lifecycle](#task-state-lifecycle)
16
+ - [Error Handling](#error-handling)
16
17
  - [Return Value Details](#return-value-details)
17
18
 
18
19
  ## TLDR
19
20
 
20
- - **`call`** - Always returns `CMDx::Result`, never raises exceptions (preferred)
21
- - **`call!`** - Returns `CMDx::Result` on success, raises `CMDx::Fault` on failure/skip
22
- - **Results** - Check status with `result.success?`, `result.failed?`, `result.skipped?`
23
- - **Callbacks** - Chain results with `.on_success`, `.on_failed`, `.on_skipped`
24
- - **Propagation** - Use `throw!(other_result)` to bubble up failures from subtasks
21
+ ```ruby
22
+ # Standard execution (preferred)
23
+ result = ProcessOrderTask.call(order_id: 12345)
24
+ result.success? # true/false
25
+
26
+ # Exception-based execution
27
+ begin
28
+ result = ProcessOrderTask.call!(order_id: 12345)
29
+ # Handle success
30
+ rescue CMDx::Failed => e
31
+ # Handle failure
32
+ end
33
+
34
+ # Result callbacks
35
+ ProcessOrderTask.call(order_id: 12345)
36
+ .on_success { |result| notify_customer(result) }
37
+ .on_failed { |result| handle_error(result) }
38
+
39
+ # Propagate failures
40
+ throw!(validation_result) if validation_result.failed?
41
+ ```
25
42
 
26
43
  ## Execution Methods Overview
27
44
 
45
+ > [!NOTE]
46
+ > Tasks are single-use objects. Once executed, they are frozen and cannot be called again. Create a new instance for subsequent executions.
47
+
28
48
  | Method | Returns | Exceptions | Use Case |
29
49
  |--------|---------|------------|----------|
30
50
  | `call` | Always returns `CMDx::Result` | Never raises | Predictable result handling |
@@ -38,15 +58,15 @@ The `call` method always returns a `CMDx::Result` object regardless of execution
38
58
  result = ProcessOrderTask.call(order_id: 12345)
39
59
 
40
60
  # Check execution state
41
- result.success? #=> true/false
42
- result.failed? #=> true/false
43
- result.skipped? #=> true/false
61
+ result.success? # true/false
62
+ result.failed? # true/false
63
+ result.skipped? # true/false
44
64
 
45
65
  # Access result data
46
- result.context.order_id #=> 12345
47
- result.runtime #=> 0.05 (seconds)
48
- result.state #=> "complete"
49
- result.status #=> "success"
66
+ result.context.order_id # 12345
67
+ result.runtime # 0.05 (seconds)
68
+ result.state # "complete"
69
+ result.status # "success"
50
70
  ```
51
71
 
52
72
  ### Handling Different Outcomes
@@ -56,37 +76,38 @@ result = ProcessOrderTask.call(order_id: 12345)
56
76
 
57
77
  case result.status
58
78
  when "success"
59
- puts "Order processed: #{result.context.order_id}"
79
+ SendConfirmationTask.call(result.context)
60
80
  when "skipped"
61
- puts "Order skipped: #{result.metadata[:reason]}"
81
+ Rails.logger.info("Order skipped: #{result.metadata[:reason]}")
62
82
  when "failed"
63
- puts "Order failed: #{result.metadata[:reason]}"
83
+ RetryOrderJob.perform_later(result.context.order_id)
64
84
  end
65
85
  ```
66
86
 
67
87
  ## Bang Call (`call!`)
68
88
 
69
- The bang `call!` method raises a `CMDx::Fault` exception when tasks fail or are skipped, based on the `task_halt` configuration. It returns a `CMDx::Result` object only on success. This method is useful in scenarios where you want exception-based control flow.
89
+ The bang `call!` method raises a `CMDx::Fault` exception when tasks fail or are skipped. It returns a `CMDx::Result` object only on success.
90
+
91
+ > [!WARNING]
92
+ > `call!` behavior depends on the `task_halt` configuration. By default, it raises exceptions for both failures and skips.
70
93
 
71
94
  ```ruby
72
95
  begin
73
96
  result = ProcessOrderTask.call!(order_id: 12345)
74
- puts "Order processed: #{result.context.order_id}"
97
+ SendConfirmationTask.call(result.context)
75
98
  rescue CMDx::Failed => e
76
- # Handle failure
77
99
  RetryOrderJob.perform_later(e.result.context.order_id)
78
100
  rescue CMDx::Skipped => e
79
- # Handle skip
80
101
  Rails.logger.info("Order skipped: #{e.result.metadata[:reason]}")
81
102
  end
82
103
  ```
83
104
 
84
105
  ### Exception Types
85
106
 
86
- | Exception | Raised When | Purpose |
87
- |-----------|-------------|---------|
88
- | `CMDx::Failed` | Task execution fails | Handle failure scenarios |
89
- | `CMDx::Skipped` | Task execution is skipped | Handle skip scenarios |
107
+ | Exception | Raised When | Access Result |
108
+ |-----------|-------------|---------------|
109
+ | `CMDx::Failed` | Task execution fails | `exception.result` |
110
+ | `CMDx::Skipped` | Task execution is skipped | `exception.result` |
90
111
 
91
112
  ## Direct Instantiation
92
113
 
@@ -97,14 +118,14 @@ Tasks can be instantiated directly for advanced use cases, testing, and custom e
97
118
  task = ProcessOrderTask.new(order_id: 12345, notify_customer: true)
98
119
 
99
120
  # Access properties before execution
100
- task.id #=> "abc123..." (unique task ID)
101
- task.context.order_id #=> 12345
102
- task.context.notify_customer #=> true
103
- task.result.state #=> "initialized"
121
+ task.id # "abc123..." (unique task ID)
122
+ task.context.order_id # 12345
123
+ task.context.notify_customer # true
124
+ task.result.state # "initialized"
104
125
 
105
126
  # Manual execution
106
127
  task.process
107
- task.result.success? #=> true/false
128
+ task.result.success? # true/false
108
129
  ```
109
130
 
110
131
  ### Execution Approaches
@@ -115,9 +136,6 @@ task.result.success? #=> true/false
115
136
  | `TaskClass.call!(...)` | Exception-based flow | Automatic fault raising |
116
137
  | `TaskClass.new(...).process` | Advanced scenarios | Full control, testing flexibility |
117
138
 
118
- > [!NOTE]
119
- > Direct instantiation gives you access to the task instance before and after execution, but you must call the execution method manually.
120
-
121
139
  ## Parameter Passing
122
140
 
123
141
  All methods accept parameters that become available in the task context:
@@ -133,31 +151,36 @@ result = ProcessOrderTask.call(
133
151
  # From another task result
134
152
  validation_result = ValidateOrderTask.call(order_id: 12345)
135
153
 
136
- # -- Passing Result object
137
- result = ProcessOrderTask.call!(validation_result)
154
+ # Pass Result object directly
155
+ result = ProcessOrderTask.call(validation_result)
138
156
 
139
- # -- Passing context directly
140
- result = ProcessOrderTask.new(validation_result.context)
157
+ # Pass context from previous result
158
+ result = ProcessOrderTask.call(validation_result.context)
141
159
  ```
142
160
 
143
161
  ## Result Propagation (`throw!`)
144
162
 
145
163
  The `throw!` method enables result propagation, allowing tasks to bubble up failures from subtasks while preserving the original fault information:
146
164
 
165
+ > [!IMPORTANT]
166
+ > Use `throw!` to maintain failure context and prevent nested error handling in complex workflows.
167
+
147
168
  ```ruby
148
169
  class ProcessOrderTask < CMDx::Task
149
170
  def call
171
+ # Validate order
150
172
  validation_result = ValidateOrderTask.call(context)
151
173
  throw!(validation_result) if validation_result.failed?
152
174
 
175
+ # Process payment
153
176
  payment_result = ProcessPaymentTask.call(context)
154
- throw!(payment_result) if payment_result.skipped?
177
+ throw!(payment_result) if payment_result.failed?
155
178
 
179
+ # Schedule delivery
156
180
  delivery_result = ScheduleDeliveryTask.call(context)
157
- throw!(delivery_result) # failed or skipped
181
+ throw!(delivery_result) unless delivery_result.success?
158
182
 
159
183
  # Continue with main logic
160
- context.order = Order.find(context.order_id)
161
184
  finalize_order_processing
162
185
  end
163
186
  end
@@ -174,15 +197,18 @@ ProcessOrderTask
174
197
  SendOrderConfirmationTask.call(result.context)
175
198
  }
176
199
  .on_failed { |result|
177
- Honeybadger.notify(result.metadata[:error])
200
+ ErrorReportingService.notify(result.metadata[:error])
178
201
  }
179
202
  .on_executed { |result|
180
- StatsD.timing('order.processing_time', result.runtime)
203
+ MetricsService.timing('order.processing_time', result.runtime)
181
204
  }
182
205
  ```
183
206
 
184
207
  ### Available Callbacks
185
208
 
209
+ > [!TIP]
210
+ > Callbacks return the result object, enabling method chaining for complex conditional logic.
211
+
186
212
  ```ruby
187
213
  result = ProcessOrderTask.call(order_id: 12345)
188
214
 
@@ -200,8 +226,8 @@ result
200
226
 
201
227
  # Outcome-based callbacks
202
228
  result
203
- .on_good { |r| log_positive_outcome(r) }
204
- .on_bad { |r| log_negative_outcome(r) }
229
+ .on_good { |r| log_positive_outcome(r) } # success or skipped
230
+ .on_bad { |r| log_negative_outcome(r) } # failed only
205
231
  ```
206
232
 
207
233
  ## Task State Lifecycle
@@ -212,10 +238,52 @@ Tasks progress through defined states during execution:
212
238
  result = ProcessOrderTask.call(order_id: 12345)
213
239
 
214
240
  # Execution states
215
- result.state #=> "initialized" -> "executing" -> "complete"/"interrupted"
241
+ result.state # "initialized" "executing" "complete"/"interrupted"
216
242
 
217
243
  # Outcome statuses
218
- result.status #=> "success"/"failed"/"skipped"
244
+ result.status # "success"/"failed"/"skipped"
245
+ ```
246
+
247
+ ## Error Handling
248
+
249
+ ### Common Error Scenarios
250
+
251
+ ```ruby
252
+ # Parameter validation failure
253
+ result = ProcessOrderTask.call(order_id: nil)
254
+ result.failed? # → true
255
+ result.metadata[:reason] # → "order_id is required"
256
+
257
+ # Business logic failure
258
+ result = ProcessOrderTask.call(order_id: 99999)
259
+ result.failed? # → true
260
+ result.metadata[:error].class # → ActiveRecord::RecordNotFound
261
+
262
+ # Task skipped due to conditions
263
+ result = ProcessOrderTask.call(order_id: 12345, force: false)
264
+ result.skipped? # → true (if order already processed)
265
+ result.metadata[:reason] # → "Order already processed"
266
+ ```
267
+
268
+ ### Exception Handling with `call!`
269
+
270
+ ```ruby
271
+ begin
272
+ result = ProcessOrderTask.call!(order_id: 12345)
273
+ rescue CMDx::Failed => e
274
+ # Access original error details
275
+ error_type = e.result.metadata[:error].class
276
+ error_message = e.result.metadata[:reason]
277
+
278
+ case error_type
279
+ when ActiveRecord::RecordNotFound
280
+ render json: { error: "Order not found" }, status: 404
281
+ when PaymentError
282
+ render json: { error: "Payment failed" }, status: 402
283
+ else
284
+ render json: { error: "Processing failed" }, status: 500
285
+ end
286
+ end
219
287
  ```
220
288
 
221
289
  ## Return Value Details
@@ -226,25 +294,23 @@ The `Result` object provides comprehensive execution information:
226
294
  result = ProcessOrderTask.call(order_id: 12345)
227
295
 
228
296
  # Execution metadata
229
- result.id #=> "abc123..." (unique execution ID)
230
- result.runtime #=> 0.05 (execution time in seconds)
231
- result.task #=> ProcessOrderTask instance
232
- result.chain #=> Chain object for tracking executions
297
+ result.id # "abc123..." (unique execution ID)
298
+ result.runtime # 0.05 (execution time in seconds)
299
+ result.task # ProcessOrderTask instance
300
+ result.chain # Chain object for tracking executions
233
301
 
234
302
  # Context and metadata
235
- result.context #=> Context with all task data
236
- result.metadata #=> Hash with execution metadata
303
+ result.context # Context with all task data
304
+ result.metadata # Hash with execution metadata
237
305
 
238
306
  # State checking methods
239
- result.good? #=> true for success/skipped
240
- result.bad? #=> true for failed/skipped
241
- result.complete? #=> true when execution finished
242
- result.interrupted? #=> true for failed/skipped
307
+ result.good? # true for success/skipped
308
+ result.bad? # true for failed only
309
+ result.complete? # true when execution finished normally
310
+ result.interrupted? # true for failed/skipped
311
+ result.executed? # → true for any completed execution
243
312
  ```
244
313
 
245
- > [!IMPORTANT]
246
- > Tasks are single-use objects. Once executed, they are frozen and cannot be called again. Create a new task instance to execute the same task again.
247
-
248
314
  ---
249
315
 
250
316
  - **Prev:** [Basics - Setup](setup.md)