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
data/docs/outcomes/statuses.md
CHANGED
@@ -1,33 +1,299 @@
|
|
1
|
-
# Outcomes -
|
1
|
+
# Outcomes - Statuses
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
Statuses represent the outcome of task execution logic, indicating how the task's business logic concluded. Statuses differ from execution states by focusing on the business outcome rather than the technical execution lifecycle. Understanding statuses is crucial for implementing proper business logic branching and error handling.
|
4
|
+
|
5
|
+
## Table of Contents
|
6
|
+
|
7
|
+
- [Status Definitions](#status-definitions)
|
8
|
+
- [Status Characteristics](#status-characteristics)
|
9
|
+
- [Status Predicates](#status-predicates)
|
10
|
+
- [Status Transitions](#status-transitions)
|
11
|
+
- [Status-Based Callbacks](#status-based-callbacks)
|
12
|
+
- [Status Metadata](#status-metadata)
|
13
|
+
- [Outcome-Based Logic](#outcome-based-logic)
|
14
|
+
- [Status Serialization and Inspection](#status-serialization-and-inspection)
|
15
|
+
- [Status vs State vs Outcome](#status-vs-state-vs-outcome)
|
16
|
+
|
17
|
+
## Status Definitions
|
5
18
|
|
6
19
|
| Status | Description |
|
7
20
|
| --------- | ----------- |
|
8
|
-
| `success` |
|
9
|
-
| `skipped` | Task stopped
|
10
|
-
| `failed` | Task stopped
|
21
|
+
| `success` | Task execution completed successfully with expected business outcome |
|
22
|
+
| `skipped` | Task intentionally stopped execution because conditions weren't met or continuation was unnecessary |
|
23
|
+
| `failed` | Task stopped execution due to business rule violations, validation errors, or exceptions |
|
11
24
|
|
12
25
|
> [!NOTE]
|
13
|
-
> Statuses
|
26
|
+
> Statuses focus on business outcomes, not technical execution states. A task can be technically "complete" but have a "failed" status if business logic determined the operation could not succeed.
|
27
|
+
|
28
|
+
## Status Characteristics
|
29
|
+
|
30
|
+
### Success
|
31
|
+
- **Default status** for all newly created tasks
|
32
|
+
- Indicates business logic completed as expected
|
33
|
+
- Remains even if no actual execution occurred (e.g., cached results)
|
34
|
+
- Compatible with both `complete` and `interrupted` states
|
35
|
+
|
36
|
+
### Skipped
|
37
|
+
- Indicates intentional early termination
|
38
|
+
- Business logic determined execution was unnecessary
|
39
|
+
- Often used for conditional workflows and guard clauses
|
40
|
+
- Triggered by `skip!` method with contextual metadata
|
41
|
+
|
42
|
+
### Failed
|
43
|
+
- Indicates business logic could not complete successfully
|
44
|
+
- Can result from explicit failures or caught exceptions
|
45
|
+
- Contains detailed error information in metadata
|
46
|
+
- Triggered by `fail!` method or automatic exception handling
|
47
|
+
|
48
|
+
## Status Predicates
|
49
|
+
|
50
|
+
Use status predicates to check execution outcomes:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
result = ProcessUserOrderTask.call
|
54
|
+
|
55
|
+
# Individual status checks
|
56
|
+
result.success? #=> true/false
|
57
|
+
result.skipped? #=> true/false
|
58
|
+
result.failed? #=> true/false
|
59
|
+
|
60
|
+
# Outcome categorization
|
61
|
+
result.good? #=> true if success OR skipped
|
62
|
+
result.bad? #=> true if skipped OR failed (not success)
|
63
|
+
```
|
64
|
+
|
65
|
+
## Status Transitions
|
66
|
+
|
67
|
+
Unlike states, statuses can only transition from success to skipped/failed:
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
# Valid status transitions
|
71
|
+
success -> skipped # (via skip!)
|
72
|
+
success -> failed # (via fail! or exception)
|
73
|
+
|
74
|
+
# Invalid transitions (will raise errors)
|
75
|
+
skipped -> success # ❌ Cannot transition
|
76
|
+
skipped -> failed # ❌ Cannot transition
|
77
|
+
failed -> success # ❌ Cannot transition
|
78
|
+
failed -> skipped # ❌ Cannot transition
|
79
|
+
```
|
80
|
+
|
81
|
+
> [!IMPORTANT]
|
82
|
+
> Status transitions are unidirectional and final. Once a task is marked as skipped or failed, it cannot return to success status. Design your business logic accordingly.
|
83
|
+
|
84
|
+
### Status Transition Examples
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
class ProcessUserOrderTask < CMDx::Task
|
88
|
+
def call
|
89
|
+
# Task starts with success status
|
90
|
+
context.result.success? #=> true
|
91
|
+
|
92
|
+
# Conditional skip
|
93
|
+
if context.order.already_processed?
|
94
|
+
skip!(reason: "Order already processed")
|
95
|
+
# Status is now skipped, execution halts
|
96
|
+
end
|
97
|
+
|
98
|
+
# Conditional failure
|
99
|
+
unless context.user.has_permission?
|
100
|
+
fail!(reason: "Insufficient permissions")
|
101
|
+
# Status is now failed, execution halts
|
102
|
+
end
|
103
|
+
|
104
|
+
# Continue with business logic
|
105
|
+
process_order
|
106
|
+
# Status remains success
|
107
|
+
end
|
108
|
+
end
|
109
|
+
```
|
110
|
+
|
111
|
+
## Status-Based Callbacks
|
112
|
+
|
113
|
+
Results provide comprehensive callback methods for status-based logic:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
result = ProcessUserOrderTask.call
|
117
|
+
|
118
|
+
# Individual status callbacks
|
119
|
+
result
|
120
|
+
.on_success { |r| handle_success(r) }
|
121
|
+
.on_skipped { |r| handle_skip(r) }
|
122
|
+
.on_failed { |r| handle_failure(r) }
|
123
|
+
|
124
|
+
# Outcome-based callbacks
|
125
|
+
result
|
126
|
+
.on_good { |r| log_positive_outcome(r) }
|
127
|
+
.on_bad { |r| log_negative_outcome(r) }
|
128
|
+
```
|
129
|
+
|
130
|
+
> [!TIP]
|
131
|
+
> Use status-based callbacks for business logic branching. The `on_good` and `on_bad` callbacks are particularly useful for handling success/skip vs failed outcomes respectively.
|
132
|
+
|
133
|
+
## Status Metadata
|
134
|
+
|
135
|
+
Statuses carry rich metadata providing context about execution outcomes:
|
136
|
+
|
137
|
+
### Success Metadata
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
class ProcessUserOrderTask < CMDx::Task
|
141
|
+
def call
|
142
|
+
# Success metadata can include business context
|
143
|
+
context.order = Order.find(context.order_id)
|
144
|
+
context.order.process!
|
145
|
+
|
146
|
+
# Success status typically has empty metadata
|
147
|
+
# but can include business-relevant information
|
148
|
+
context.processing_time = Time.now - context.start_time
|
149
|
+
context.confirmation_number = generate_confirmation
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
result = ProcessUserOrderTask.call(order_id: 123)
|
154
|
+
result.success? #=> true
|
155
|
+
result.metadata #=> {} (usually empty for success)
|
156
|
+
```
|
157
|
+
|
158
|
+
### Skip Metadata
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
class ProcessUserOrderTask < CMDx::Task
|
162
|
+
def call
|
163
|
+
order = Order.find(context.order_id)
|
164
|
+
|
165
|
+
if order.already_processed?
|
166
|
+
skip!(
|
167
|
+
reason: "Order already processed",
|
168
|
+
processed_at: order.processed_at,
|
169
|
+
original_processor: order.processor_id,
|
170
|
+
skip_code: "DUPLICATE_PROCESSING"
|
171
|
+
)
|
172
|
+
end
|
173
|
+
|
174
|
+
# Continue processing...
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
result = ProcessUserOrderTask.call(order_id: 123)
|
179
|
+
if result.skipped?
|
180
|
+
result.metadata[:reason] #=> "Order already processed"
|
181
|
+
result.metadata[:processed_at] #=> 2023-10-01 10:30:00 UTC
|
182
|
+
result.metadata[:original_processor] #=> "user-456"
|
183
|
+
result.metadata[:skip_code] #=> "DUPLICATE_PROCESSING"
|
184
|
+
end
|
185
|
+
```
|
186
|
+
|
187
|
+
### Failure Metadata
|
188
|
+
|
189
|
+
```ruby
|
190
|
+
class ValidateOrderDataTask < CMDx::Task
|
191
|
+
def call
|
192
|
+
unless context.order.valid?
|
193
|
+
fail!(
|
194
|
+
reason: "Order validation failed",
|
195
|
+
errors: context.order.errors.full_messages,
|
196
|
+
error_code: "VALIDATION_FAILED",
|
197
|
+
retryable: false,
|
198
|
+
failed_at: Time.now
|
199
|
+
)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
result = ValidateOrderDataTask.call(order_id: 123)
|
205
|
+
if result.failed?
|
206
|
+
result.metadata[:reason] #=> "Order validation failed"
|
207
|
+
result.metadata[:errors] #=> ["Name can't be blank", "Email is invalid"]
|
208
|
+
result.metadata[:error_code] #=> "VALIDATION_FAILED"
|
209
|
+
result.metadata[:retryable] #=> false
|
210
|
+
result.metadata[:failed_at] #=> 2023-10-01 10:30:00 UTC
|
211
|
+
end
|
212
|
+
```
|
213
|
+
|
214
|
+
> [!TIP]
|
215
|
+
> Always try to include rich metadata with skip and fail operations. This information is invaluable for debugging, user feedback, and automated error handling.
|
216
|
+
|
217
|
+
## Outcome-Based Logic
|
218
|
+
|
219
|
+
Statuses enable sophisticated outcome-based decision making:
|
220
|
+
|
221
|
+
### Good vs Bad Outcomes
|
222
|
+
|
223
|
+
```ruby
|
224
|
+
# Good outcomes (success OR skipped)
|
225
|
+
result.good? #=> true if success? || skipped?
|
226
|
+
result.bad? #=> true if !success? (skipped OR failed)
|
227
|
+
|
228
|
+
# Usage patterns
|
229
|
+
if result.good?
|
230
|
+
# Both success and skipped are "good" outcomes
|
231
|
+
update_user_interface(result)
|
232
|
+
log_completed_action(result)
|
233
|
+
end
|
234
|
+
|
235
|
+
if result.bad?
|
236
|
+
# Handle any non-success outcome (skipped or failed)
|
237
|
+
show_error_message(result.metadata[:reason])
|
238
|
+
track_negative_outcome(result)
|
239
|
+
end
|
240
|
+
```
|
241
|
+
|
242
|
+
## Status Serialization and Inspection
|
243
|
+
|
244
|
+
Statuses are fully captured in result serialization:
|
245
|
+
|
246
|
+
```ruby
|
247
|
+
result = ProcessUserOrderTask.call
|
248
|
+
|
249
|
+
# Hash representation
|
250
|
+
result.to_h[:status] #=> "success"
|
251
|
+
|
252
|
+
# Full serialization includes status
|
253
|
+
result.to_h
|
254
|
+
#=> {
|
255
|
+
# class: "ProcessUserOrderTask",
|
256
|
+
# index: 0,
|
257
|
+
# state: "complete",
|
258
|
+
# status: "success",
|
259
|
+
# outcome: "success",
|
260
|
+
# metadata: {},
|
261
|
+
# # ... other attributes
|
262
|
+
# }
|
263
|
+
|
264
|
+
# Human-readable inspection
|
265
|
+
result.to_s
|
266
|
+
#=> "ProcessUserOrderTask: type=Task index=0 state=complete status=success outcome=success..."
|
267
|
+
```
|
268
|
+
|
269
|
+
## Status vs State vs Outcome
|
270
|
+
|
271
|
+
Understanding the relationship between these concepts:
|
272
|
+
|
273
|
+
- **Status**: Business execution outcome (`success`, `skipped`, `failed`)
|
274
|
+
- **State**: Technical execution lifecycle (`initialized`, `executing`, `complete`, `interrupted`)
|
275
|
+
- **Outcome**: Combined representation for unified logic
|
14
276
|
|
15
277
|
```ruby
|
16
|
-
result =
|
17
|
-
result.status #=> "skipped"
|
278
|
+
result = ProcessUserOrderTask.call
|
18
279
|
|
19
|
-
|
20
|
-
result.
|
21
|
-
result.
|
280
|
+
# Different scenarios
|
281
|
+
result.state #=> "complete"
|
282
|
+
result.status #=> "success"
|
283
|
+
result.outcome #=> "success" (same as status when complete)
|
22
284
|
|
23
|
-
#
|
24
|
-
|
285
|
+
# Skipped task
|
286
|
+
skipped_result.state #=> "complete" (execution finished)
|
287
|
+
skipped_result.status #=> "skipped" (business outcome)
|
288
|
+
skipped_result.outcome #=> "skipped" (same as status)
|
25
289
|
|
26
|
-
#
|
27
|
-
|
290
|
+
# Failed task
|
291
|
+
failed_result.state #=> "interrupted" (execution stopped)
|
292
|
+
failed_result.status #=> "failed" (business outcome)
|
293
|
+
failed_result.outcome #=> "interrupted" (reflects state for interrupted tasks)
|
28
294
|
```
|
29
295
|
|
30
296
|
---
|
31
297
|
|
32
|
-
- **Prev:** [Outcomes - Result](
|
33
|
-
- **Next:** [Outcomes - States](
|
298
|
+
- **Prev:** [Outcomes - Result](result.md)
|
299
|
+
- **Next:** [Outcomes - States](states.md)
|