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.
- checksums.yaml +4 -4
- data/.cursor/prompts/docs.md +9 -0
- data/.cursor/prompts/rspec.md +13 -12
- data/.cursor/prompts/yardoc.md +11 -6
- data/CHANGELOG.md +13 -2
- data/README.md +1 -0
- data/docs/ai_prompts.md +269 -195
- data/docs/basics/call.md +124 -58
- data/docs/basics/chain.md +190 -160
- data/docs/basics/context.md +242 -154
- data/docs/basics/setup.md +302 -32
- data/docs/callbacks.md +390 -94
- data/docs/configuration.md +181 -65
- data/docs/deprecation.md +245 -0
- data/docs/getting_started.md +161 -39
- data/docs/internationalization.md +590 -70
- data/docs/interruptions/exceptions.md +135 -118
- data/docs/interruptions/faults.md +150 -125
- data/docs/interruptions/halt.md +134 -80
- data/docs/logging.md +181 -118
- data/docs/middlewares.md +150 -377
- data/docs/outcomes/result.md +140 -112
- data/docs/outcomes/states.md +134 -99
- data/docs/outcomes/statuses.md +204 -146
- data/docs/parameters/coercions.md +232 -281
- data/docs/parameters/defaults.md +224 -169
- data/docs/parameters/definitions.md +289 -141
- data/docs/parameters/namespacing.md +250 -161
- data/docs/parameters/validations.md +260 -133
- data/docs/testing.md +191 -197
- data/docs/workflows.md +143 -98
- data/lib/cmdx/callback.rb +23 -19
- data/lib/cmdx/callback_registry.rb +1 -3
- data/lib/cmdx/chain_inspector.rb +23 -23
- data/lib/cmdx/chain_serializer.rb +38 -19
- data/lib/cmdx/coercion.rb +20 -12
- data/lib/cmdx/coercion_registry.rb +51 -32
- data/lib/cmdx/configuration.rb +84 -31
- data/lib/cmdx/context.rb +32 -21
- data/lib/cmdx/core_ext/hash.rb +13 -13
- data/lib/cmdx/core_ext/module.rb +1 -1
- data/lib/cmdx/core_ext/object.rb +12 -12
- data/lib/cmdx/correlator.rb +60 -39
- data/lib/cmdx/errors.rb +105 -131
- data/lib/cmdx/fault.rb +66 -45
- data/lib/cmdx/immutator.rb +20 -21
- data/lib/cmdx/lazy_struct.rb +78 -70
- data/lib/cmdx/log_formatters/json.rb +1 -1
- data/lib/cmdx/log_formatters/key_value.rb +1 -1
- data/lib/cmdx/log_formatters/line.rb +1 -1
- data/lib/cmdx/log_formatters/logstash.rb +1 -1
- data/lib/cmdx/log_formatters/pretty_json.rb +1 -1
- data/lib/cmdx/log_formatters/pretty_key_value.rb +1 -1
- data/lib/cmdx/log_formatters/pretty_line.rb +1 -1
- data/lib/cmdx/log_formatters/raw.rb +2 -2
- data/lib/cmdx/logger.rb +19 -14
- data/lib/cmdx/logger_ansi.rb +33 -17
- data/lib/cmdx/logger_serializer.rb +85 -24
- data/lib/cmdx/middleware.rb +39 -21
- data/lib/cmdx/middleware_registry.rb +4 -3
- data/lib/cmdx/parameter.rb +151 -89
- data/lib/cmdx/parameter_inspector.rb +34 -21
- data/lib/cmdx/parameter_registry.rb +36 -30
- data/lib/cmdx/parameter_serializer.rb +21 -14
- data/lib/cmdx/result.rb +136 -135
- data/lib/cmdx/result_ansi.rb +31 -17
- data/lib/cmdx/result_inspector.rb +32 -27
- data/lib/cmdx/result_logger.rb +23 -14
- data/lib/cmdx/result_serializer.rb +65 -27
- data/lib/cmdx/task.rb +234 -113
- data/lib/cmdx/task_deprecator.rb +22 -25
- data/lib/cmdx/task_processor.rb +89 -88
- data/lib/cmdx/task_serializer.rb +27 -14
- data/lib/cmdx/utils/monotonic_runtime.rb +2 -4
- data/lib/cmdx/validator.rb +25 -16
- data/lib/cmdx/validator_registry.rb +53 -31
- data/lib/cmdx/validators/exclusion.rb +1 -1
- data/lib/cmdx/validators/format.rb +2 -2
- data/lib/cmdx/validators/inclusion.rb +2 -2
- data/lib/cmdx/validators/length.rb +2 -2
- data/lib/cmdx/validators/numeric.rb +3 -3
- data/lib/cmdx/validators/presence.rb +2 -2
- data/lib/cmdx/version.rb +1 -1
- data/lib/cmdx/workflow.rb +54 -33
- data/lib/generators/cmdx/task_generator.rb +6 -6
- data/lib/generators/cmdx/workflow_generator.rb +6 -6
- metadata +3 -1
data/docs/interruptions/halt.md
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
# Interruptions - Halt
|
2
2
|
|
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.
|
3
|
+
Halting stops execution of a task with explicit intent signaling. Tasks provide two primary halt methods that control execution flow and result in different outcomes, each serving specific use cases in business logic.
|
6
4
|
|
7
5
|
## Table of Contents
|
8
6
|
|
@@ -12,147 +10,175 @@ outcomes, each serving specific use cases in business logic.
|
|
12
10
|
- [Metadata Enrichment](#metadata-enrichment)
|
13
11
|
- [State Transitions](#state-transitions)
|
14
12
|
- [Exception Behavior](#exception-behavior)
|
13
|
+
- [Error Handling](#error-handling)
|
15
14
|
- [The Reason Key](#the-reason-key)
|
16
15
|
|
17
16
|
## TLDR
|
18
17
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
18
|
+
```ruby
|
19
|
+
# Skip when task shouldn't execute (not an error)
|
20
|
+
skip!(reason: "Order already processed")
|
21
|
+
|
22
|
+
# Fail when task encounters error condition
|
23
|
+
fail!(reason: "Insufficient funds", error_code: "PAYMENT_DECLINED")
|
24
|
+
|
25
|
+
# With structured metadata
|
26
|
+
skip!(
|
27
|
+
reason: "User inactive",
|
28
|
+
user_id: 123,
|
29
|
+
last_active: "2023-01-01"
|
30
|
+
)
|
31
|
+
|
32
|
+
# Exception behavior with call vs call!
|
33
|
+
result = Task.call(params) # Returns result object
|
34
|
+
Task.call!(params) # Raises CMDx::Skipped/Failed on halt
|
35
|
+
```
|
24
36
|
|
25
37
|
## Skip (`skip!`)
|
26
38
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
39
|
+
> [!NOTE]
|
40
|
+
> 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.
|
41
|
+
|
42
|
+
The `skip!` method indicates that a task did not meet the criteria to continue execution. This represents a controlled, intentional interruption where the task determines that execution is not necessary or appropriate.
|
31
43
|
|
32
44
|
### Basic Usage
|
33
45
|
|
34
46
|
```ruby
|
35
|
-
class
|
47
|
+
class ProcessOrderTask < CMDx::Task
|
48
|
+
required :order_id, type: :integer
|
36
49
|
|
37
50
|
def call
|
38
|
-
context.order = Order.find(
|
51
|
+
context.order = Order.find(order_id)
|
39
52
|
|
40
|
-
# Skip if order
|
53
|
+
# Skip if order already processed
|
41
54
|
skip!(reason: "Order already processed") if context.order.processed?
|
42
55
|
|
43
|
-
# Skip if prerequisites
|
44
|
-
skip!(reason: "Payment method
|
56
|
+
# Skip if prerequisites not met
|
57
|
+
skip!(reason: "Payment method required") unless context.order.payment_method
|
45
58
|
|
46
59
|
# Continue with business logic
|
47
60
|
context.order.process!
|
48
61
|
end
|
49
|
-
|
50
62
|
end
|
51
63
|
```
|
52
64
|
|
53
|
-
|
54
|
-
|
65
|
+
### Common Skip Scenarios
|
66
|
+
|
67
|
+
| Scenario | Example |
|
68
|
+
|----------|---------|
|
69
|
+
| **Already processed** | `skip!(reason: "User already verified")` |
|
70
|
+
| **Prerequisites missing** | `skip!(reason: "Required documents not uploaded")` |
|
71
|
+
| **Business rules** | `skip!(reason: "Outside business hours")` |
|
72
|
+
| **State conditions** | `skip!(reason: "Account suspended")` |
|
55
73
|
|
56
74
|
## Fail (`fail!`)
|
57
75
|
|
58
|
-
|
59
|
-
prevents successful completion.
|
60
|
-
|
76
|
+
> [!IMPORTANT]
|
77
|
+
> Use `fail!` when a task encounters an error that prevents successful completion. Failed tasks represent error conditions that need to be handled or corrected.
|
78
|
+
|
79
|
+
The `fail!` method indicates that a task encountered an error condition that prevents successful completion. This represents controlled failure where the task explicitly determines that execution cannot continue.
|
61
80
|
|
62
81
|
### Basic Usage
|
63
82
|
|
64
83
|
```ruby
|
65
|
-
class
|
84
|
+
class ProcessPaymentTask < CMDx::Task
|
85
|
+
required :payment_id, type: :integer
|
66
86
|
|
67
87
|
def call
|
68
|
-
context.payment = Payment.find(
|
88
|
+
context.payment = Payment.find(payment_id)
|
69
89
|
|
70
90
|
# Fail on validation errors
|
71
91
|
fail!(reason: "Payment amount must be positive") unless context.payment.amount > 0
|
72
92
|
|
73
93
|
# Fail on business rule violations
|
74
|
-
fail!(reason: "Insufficient funds") unless sufficient_funds?
|
94
|
+
fail!(reason: "Insufficient funds", code: "INSUFFICIENT_FUNDS") unless sufficient_funds?
|
75
95
|
|
76
96
|
# Continue with processing
|
77
|
-
|
97
|
+
charge_payment
|
78
98
|
end
|
79
99
|
|
100
|
+
private
|
101
|
+
|
102
|
+
def sufficient_funds?
|
103
|
+
context.payment.account.balance >= context.payment.amount
|
104
|
+
end
|
80
105
|
end
|
81
106
|
```
|
82
107
|
|
83
|
-
|
84
|
-
|
108
|
+
### Common Fail Scenarios
|
109
|
+
|
110
|
+
| Scenario | Example |
|
111
|
+
|----------|---------|
|
112
|
+
| **Validation errors** | `fail!(reason: "Invalid email format")` |
|
113
|
+
| **Business rule violations** | `fail!(reason: "Credit limit exceeded")` |
|
114
|
+
| **External service errors** | `fail!(reason: "Payment gateway unavailable")` |
|
115
|
+
| **Data integrity issues** | `fail!(reason: "Duplicate transaction detected")` |
|
85
116
|
|
86
117
|
## Metadata Enrichment
|
87
118
|
|
88
|
-
Both halt methods accept metadata to provide context about the interruption.
|
89
|
-
Metadata is stored as a hash and becomes available through the result object.
|
119
|
+
Both halt methods accept metadata to provide context about the interruption. Metadata is stored as a hash and becomes available through the result object.
|
90
120
|
|
91
121
|
### Structured Metadata
|
92
122
|
|
93
123
|
```ruby
|
94
|
-
class
|
124
|
+
class ProcessSubscriptionTask < CMDx::Task
|
125
|
+
required :user_id, type: :integer
|
95
126
|
|
96
127
|
def call
|
97
|
-
context.
|
128
|
+
context.user = User.find(user_id)
|
98
129
|
|
99
|
-
if context.
|
130
|
+
if context.user.subscription_expired?
|
100
131
|
skip!(
|
101
|
-
reason: "
|
102
|
-
|
103
|
-
|
104
|
-
|
132
|
+
reason: "Subscription expired",
|
133
|
+
user_id: context.user.id,
|
134
|
+
expired_at: context.user.subscription_expires_at,
|
135
|
+
plan_type: context.user.subscription_plan,
|
136
|
+
grace_period_ends: context.user.subscription_expires_at + 7.days
|
105
137
|
)
|
106
138
|
end
|
107
139
|
|
108
|
-
unless
|
140
|
+
unless context.user.payment_method_valid?
|
109
141
|
fail!(
|
110
|
-
reason: "
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
142
|
+
reason: "Invalid payment method",
|
143
|
+
user_id: context.user.id,
|
144
|
+
payment_method_id: context.user.payment_method&.id,
|
145
|
+
error_code: "PAYMENT_METHOD_INVALID",
|
146
|
+
retry_after: Time.current + 1.hour
|
115
147
|
)
|
116
148
|
end
|
117
149
|
|
118
|
-
|
150
|
+
process_subscription
|
119
151
|
end
|
120
|
-
|
121
152
|
end
|
122
153
|
```
|
123
154
|
|
124
155
|
### Accessing Metadata
|
125
156
|
|
126
157
|
```ruby
|
127
|
-
result =
|
158
|
+
result = ProcessSubscriptionTask.call(user_id: 123)
|
128
159
|
|
129
160
|
# Check result status
|
130
|
-
result.skipped?
|
131
|
-
result.failed?
|
161
|
+
result.skipped? #=> true
|
162
|
+
result.failed? #=> false
|
132
163
|
|
133
164
|
# Access metadata
|
134
|
-
result.metadata[:reason]
|
135
|
-
result.metadata[:
|
136
|
-
result.metadata[:
|
137
|
-
result.metadata[:
|
165
|
+
result.metadata[:reason] #=> "Subscription expired"
|
166
|
+
result.metadata[:user_id] #=> 123
|
167
|
+
result.metadata[:expired_at] #=> 2023-01-01 10:00:00 UTC
|
168
|
+
result.metadata[:grace_period_ends] #=> 2023-01-08 10:00:00 UTC
|
138
169
|
```
|
139
170
|
|
140
171
|
## State Transitions
|
141
172
|
|
142
173
|
Halt methods trigger specific state and status transitions:
|
143
174
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
### Fail Transitions
|
150
|
-
- **State**: `initialized` → `executing` → `interrupted`
|
151
|
-
- **Status**: `success` → `failed`
|
152
|
-
- **Result**: `good? = false`, `bad? = true`
|
175
|
+
| Method | State Transition | Status | Outcome |
|
176
|
+
|--------|------------------|--------|---------|
|
177
|
+
| `skip!` | `executing` → `interrupted` | `skipped` | `good? = true`, `bad? = true` |
|
178
|
+
| `fail!` | `executing` → `interrupted` | `failed` | `good? = false`, `bad? = true` |
|
153
179
|
|
154
180
|
```ruby
|
155
|
-
result =
|
181
|
+
result = ProcessSubscriptionTask.call(user_id: 123)
|
156
182
|
|
157
183
|
# State information
|
158
184
|
result.state #=> "interrupted"
|
@@ -170,42 +196,63 @@ result.bad? #=> true for both skipped and failed
|
|
170
196
|
Halt methods behave differently depending on the call method used:
|
171
197
|
|
172
198
|
### With `call` (Non-bang)
|
199
|
+
|
173
200
|
Returns a result object without raising exceptions:
|
174
201
|
|
175
202
|
```ruby
|
176
|
-
result =
|
203
|
+
result = ProcessPaymentTask.call(payment_id: 123)
|
177
204
|
|
178
205
|
case result.status
|
179
206
|
when "success"
|
180
|
-
puts "
|
207
|
+
puts "Payment processed: $#{result.context.payment.amount}"
|
181
208
|
when "skipped"
|
182
|
-
puts "
|
209
|
+
puts "Payment skipped: #{result.metadata[:reason]}"
|
183
210
|
when "failed"
|
184
|
-
puts "
|
211
|
+
puts "Payment failed: #{result.metadata[:reason]}"
|
212
|
+
handle_payment_error(result.metadata[:code])
|
185
213
|
end
|
186
214
|
```
|
187
215
|
|
188
216
|
### With `call!` (Bang)
|
189
|
-
|
217
|
+
|
218
|
+
> [!WARNING]
|
219
|
+
> The `call!` method raises exceptions for halt conditions based on the `task_halt` configuration. Handle these exceptions appropriately in your application flow.
|
190
220
|
|
191
221
|
```ruby
|
192
222
|
begin
|
193
|
-
result =
|
194
|
-
puts "Success:
|
223
|
+
result = ProcessPaymentTask.call!(payment_id: 123)
|
224
|
+
puts "Success: Payment processed for $#{result.context.payment.amount}"
|
195
225
|
rescue CMDx::Skipped => e
|
196
226
|
puts "Skipped: #{e.message}"
|
197
|
-
|
227
|
+
log_skip_event(e.context.payment_id, e.result.metadata)
|
198
228
|
rescue CMDx::Failed => e
|
199
229
|
puts "Failed: #{e.message}"
|
200
|
-
|
230
|
+
handle_payment_failure(e.result.metadata[:code])
|
231
|
+
notify_payment_team(e.context.payment_id)
|
201
232
|
end
|
202
233
|
```
|
203
234
|
|
204
|
-
|
205
|
-
|
235
|
+
## Error Handling
|
236
|
+
|
237
|
+
### Invalid Metadata
|
238
|
+
|
239
|
+
```ruby
|
240
|
+
class ProcessOrderTask < CMDx::Task
|
241
|
+
def call
|
242
|
+
# This works - metadata accepts any hash
|
243
|
+
skip!(reason: "Valid skip", order_id: 123, custom_data: {nested: true})
|
244
|
+
|
245
|
+
# This also works - no metadata required
|
246
|
+
fail!
|
247
|
+
end
|
248
|
+
end
|
249
|
+
```
|
206
250
|
|
207
251
|
## The Reason Key
|
208
252
|
|
253
|
+
> [!TIP]
|
254
|
+
> Always include a `:reason` key in metadata when using halt methods. This provides clear context for debugging and creates meaningful exception messages.
|
255
|
+
|
209
256
|
The `:reason` key in metadata has special significance:
|
210
257
|
|
211
258
|
- Used as the exception message when faults are raised
|
@@ -213,19 +260,26 @@ The `:reason` key in metadata has special significance:
|
|
213
260
|
- Strongly recommended for all halt calls
|
214
261
|
|
215
262
|
```ruby
|
216
|
-
# Good:
|
217
|
-
skip!(reason: "User
|
218
|
-
fail!(reason: "Credit card
|
263
|
+
# Good: Clear, specific reason
|
264
|
+
skip!(reason: "User account suspended until manual review")
|
265
|
+
fail!(reason: "Credit card declined by issuer", code: "CARD_DECLINED")
|
219
266
|
|
220
267
|
# Acceptable: Other metadata without reason
|
221
|
-
skip!(status: "redundant",
|
268
|
+
skip!(status: "redundant", processed_at: Time.current)
|
222
269
|
|
223
270
|
# Fallback: Default message if no reason provided
|
224
271
|
skip! # Exception message: "no reason given"
|
272
|
+
fail! # Exception message: "no reason given"
|
225
273
|
```
|
226
274
|
|
227
|
-
|
228
|
-
|
275
|
+
### Reason Best Practices
|
276
|
+
|
277
|
+
| Practice | Example |
|
278
|
+
|----------|---------|
|
279
|
+
| **Be specific** | `"Credit card expired on 2023-12-31"` vs `"Payment error"` |
|
280
|
+
| **Include context** | `"Inventory insufficient: need 5, have 2"` |
|
281
|
+
| **Use actionable language** | `"Email verification required before login"` |
|
282
|
+
| **Avoid technical jargon** | `"Payment declined"` vs `"Gateway returned 402"` |
|
229
283
|
|
230
284
|
---
|
231
285
|
|