cmdx 0.5.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 +31 -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 +203 -31
- data/docs/outcomes/statuses.md +275 -30
- 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 +367 -25
- 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 +405 -37
- 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 -62
- data/lib/cmdx/batch.rb +0 -43
- data/lib/cmdx/parameters.rb +0 -35
- data/lib/cmdx/run.rb +0 -39
- data/lib/cmdx/run_inspector.rb +0 -26
- data/lib/cmdx/run_serializer.rb +0 -20
- 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/lib/cmdx/result.rb
CHANGED
@@ -1,14 +1,64 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module CMDx
|
4
|
+
# Result object representing the outcome of task execution.
|
5
|
+
#
|
6
|
+
# The Result class encapsulates all information about a task's execution,
|
7
|
+
# including its state, status, metadata, and runtime information. It provides
|
8
|
+
# a comprehensive interface for tracking task lifecycle, handling failures,
|
9
|
+
# and chaining execution outcomes.
|
10
|
+
#
|
11
|
+
# @example Basic result usage
|
12
|
+
# result = ProcessOrderTask.call(order_id: 123)
|
13
|
+
# result.success? # => true
|
14
|
+
# result.complete? # => true
|
15
|
+
# result.runtime # => 0.5
|
16
|
+
#
|
17
|
+
# @example Result with failure handling
|
18
|
+
# result = ProcessOrderTask.call(invalid_params)
|
19
|
+
# result.failed? # => true
|
20
|
+
# result.bad? # => true
|
21
|
+
# result.metadata # => { reason: "Invalid parameters" }
|
22
|
+
#
|
23
|
+
# @example Result state callbacks
|
24
|
+
# ProcessOrderTask.call(order_id: 123)
|
25
|
+
# .on_success { |result| logger.info "Order processed successfully" }
|
26
|
+
# .on_failed { |result| logger.error "Order processing failed: #{result.metadata[:reason]}" }
|
27
|
+
#
|
28
|
+
# @example Result chaining and failure propagation
|
29
|
+
# result1 = FirstTask.call
|
30
|
+
# result2 = SecondTask.call
|
31
|
+
# result2.throw!(result1) if result1.failed? # Propagate failure
|
32
|
+
#
|
33
|
+
# @see CMDx::Task Task execution and result creation
|
34
|
+
# @see CMDx::Chain Chain execution context and result tracking
|
35
|
+
# @see CMDx::Fault Fault handling for result failures
|
4
36
|
class Result
|
5
37
|
|
6
|
-
__cmdx_attr_delegator :context, :
|
38
|
+
__cmdx_attr_delegator :context, :chain,
|
39
|
+
to: :task
|
7
40
|
|
41
|
+
# @return [CMDx::Task] The task instance that generated this result
|
42
|
+
# @return [String] The current execution state (initialized, executing, complete, interrupted)
|
43
|
+
# @return [String] The current execution status (success, skipped, failed)
|
44
|
+
# @return [Hash] Additional metadata associated with the result
|
8
45
|
attr_reader :task, :state, :status, :metadata
|
9
46
|
|
47
|
+
# Initializes a new Result instance.
|
48
|
+
#
|
49
|
+
# Creates a result object for tracking task execution outcomes.
|
50
|
+
# Results start in initialized state with success status.
|
51
|
+
#
|
52
|
+
# @param task [CMDx::Task] The task instance this result belongs to
|
53
|
+
# @raise [TypeError] If task is not a Task or Workflow instance
|
54
|
+
#
|
55
|
+
# @example Creating a result
|
56
|
+
# task = ProcessOrderTask.new
|
57
|
+
# result = Result.new(task)
|
58
|
+
# result.initialized? # => true
|
59
|
+
# result.success? # => true
|
10
60
|
def initialize(task)
|
11
|
-
raise
|
61
|
+
raise TypeError, "must be a Task or Workflow" unless task.is_a?(Task)
|
12
62
|
|
13
63
|
@task = task
|
14
64
|
@state = INITIALIZED
|
@@ -16,41 +66,87 @@ module CMDx
|
|
16
66
|
@metadata = {}
|
17
67
|
end
|
18
68
|
|
69
|
+
# Available execution states for task results.
|
70
|
+
#
|
71
|
+
# States represent the execution lifecycle of a task from initialization
|
72
|
+
# through completion or interruption.
|
19
73
|
STATES = [
|
20
|
-
INITIALIZED = "initialized",
|
21
|
-
EXECUTING = "executing",
|
22
|
-
COMPLETE = "complete",
|
23
|
-
INTERRUPTED = "interrupted"
|
74
|
+
INITIALIZED = "initialized", # Initial state before execution
|
75
|
+
EXECUTING = "executing", # Currently executing task logic
|
76
|
+
COMPLETE = "complete", # Successfully completed execution
|
77
|
+
INTERRUPTED = "interrupted" # Execution was halted due to failure
|
24
78
|
].freeze
|
25
79
|
|
80
|
+
# Dynamically defines state predicate and callback methods.
|
81
|
+
#
|
82
|
+
# For each state, creates:
|
83
|
+
# - Predicate method (e.g., `executing?`)
|
84
|
+
# - Callback method (e.g., `on_executing`)
|
26
85
|
STATES.each do |s|
|
27
86
|
# eg: executing?
|
28
87
|
define_method(:"#{s}?") { state == s }
|
29
88
|
|
30
89
|
# eg: on_interrupted { ... }
|
31
90
|
define_method(:"on_#{s}") do |&block|
|
32
|
-
raise ArgumentError, "
|
91
|
+
raise ArgumentError, "block required" unless block
|
33
92
|
|
34
93
|
block.call(self) if send(:"#{s}?")
|
35
94
|
self
|
36
95
|
end
|
37
96
|
end
|
38
97
|
|
98
|
+
# Marks the result as executed based on current status.
|
99
|
+
#
|
100
|
+
# Transitions to complete state if successful, or interrupted state
|
101
|
+
# if the task has failed or been skipped.
|
102
|
+
#
|
103
|
+
# @return [void]
|
104
|
+
#
|
105
|
+
# @example Successful execution
|
106
|
+
# result.executed!
|
107
|
+
# result.complete? # => true (if status was success)
|
108
|
+
#
|
109
|
+
# @example Failed execution
|
110
|
+
# result.fail!(reason: "Something went wrong")
|
111
|
+
# result.executed!
|
112
|
+
# result.interrupted? # => true
|
39
113
|
def executed!
|
40
114
|
success? ? complete! : interrupt!
|
41
115
|
end
|
42
116
|
|
117
|
+
# Checks if the result has been executed (completed or interrupted).
|
118
|
+
#
|
119
|
+
# @return [Boolean] true if result is complete or interrupted
|
120
|
+
#
|
121
|
+
# @example
|
122
|
+
# result.executed? # => true if complete? || interrupted?
|
43
123
|
def executed?
|
44
124
|
complete? || interrupted?
|
45
125
|
end
|
46
126
|
|
127
|
+
# Executes a callback if the result has been executed.
|
128
|
+
#
|
129
|
+
# @yield [Result] The result instance
|
130
|
+
# @return [Result] Self for method chaining
|
131
|
+
# @raise [ArgumentError] If no block is provided
|
132
|
+
#
|
133
|
+
# @example
|
134
|
+
# result.on_executed { |r| logger.info "Task finished: #{r.status}" }
|
47
135
|
def on_executed(&)
|
48
|
-
raise ArgumentError, "
|
136
|
+
raise ArgumentError, "block required" unless block_given?
|
49
137
|
|
50
138
|
yield(self) if executed?
|
51
139
|
self
|
52
140
|
end
|
53
141
|
|
142
|
+
# Transitions the result to executing state.
|
143
|
+
#
|
144
|
+
# @return [void]
|
145
|
+
# @raise [RuntimeError] If not transitioning from initialized state
|
146
|
+
#
|
147
|
+
# @example
|
148
|
+
# result.executing!
|
149
|
+
# result.executing? # => true
|
54
150
|
def executing!
|
55
151
|
return if executing?
|
56
152
|
|
@@ -59,6 +155,14 @@ module CMDx
|
|
59
155
|
@state = EXECUTING
|
60
156
|
end
|
61
157
|
|
158
|
+
# Transitions the result to complete state.
|
159
|
+
#
|
160
|
+
# @return [void]
|
161
|
+
# @raise [RuntimeError] If not transitioning from executing state
|
162
|
+
#
|
163
|
+
# @example
|
164
|
+
# result.complete!
|
165
|
+
# result.complete? # => true
|
62
166
|
def complete!
|
63
167
|
return if complete?
|
64
168
|
|
@@ -67,6 +171,14 @@ module CMDx
|
|
67
171
|
@state = COMPLETE
|
68
172
|
end
|
69
173
|
|
174
|
+
# Transitions the result to interrupted state.
|
175
|
+
#
|
176
|
+
# @return [void]
|
177
|
+
# @raise [RuntimeError] If trying to interrupt from complete state
|
178
|
+
#
|
179
|
+
# @example
|
180
|
+
# result.interrupt!
|
181
|
+
# result.interrupted? # => true
|
70
182
|
def interrupt!
|
71
183
|
return if interrupted?
|
72
184
|
|
@@ -75,47 +187,99 @@ module CMDx
|
|
75
187
|
@state = INTERRUPTED
|
76
188
|
end
|
77
189
|
|
190
|
+
# Available execution statuses for task results.
|
191
|
+
#
|
192
|
+
# Statuses represent the outcome of task logic execution.
|
78
193
|
STATUSES = [
|
79
|
-
SUCCESS = "success",
|
80
|
-
SKIPPED = "skipped",
|
81
|
-
FAILED = "failed"
|
194
|
+
SUCCESS = "success", # Task completed successfully
|
195
|
+
SKIPPED = "skipped", # Task was skipped intentionally
|
196
|
+
FAILED = "failed" # Task failed due to error or validation
|
82
197
|
].freeze
|
83
198
|
|
199
|
+
# Dynamically defines status predicate and callback methods.
|
200
|
+
#
|
201
|
+
# For each status, creates:
|
202
|
+
# - Predicate method (e.g., `success?`)
|
203
|
+
# - Callback method (e.g., `on_success`)
|
84
204
|
STATUSES.each do |s|
|
85
205
|
# eg: skipped?
|
86
206
|
define_method(:"#{s}?") { status == s }
|
87
207
|
|
88
208
|
# eg: on_failed { ... }
|
89
209
|
define_method(:"on_#{s}") do |&block|
|
90
|
-
raise ArgumentError, "
|
210
|
+
raise ArgumentError, "block required" unless block
|
91
211
|
|
92
212
|
block.call(self) if send(:"#{s}?")
|
93
213
|
self
|
94
214
|
end
|
95
215
|
end
|
96
216
|
|
217
|
+
# Checks if the result represents a good outcome (success or skipped).
|
218
|
+
#
|
219
|
+
# @return [Boolean] true if not failed
|
220
|
+
#
|
221
|
+
# @example
|
222
|
+
# result.good? # => true if success? || skipped?
|
97
223
|
def good?
|
98
224
|
!failed?
|
99
225
|
end
|
100
226
|
|
227
|
+
# Executes a callback if the result has a good outcome.
|
228
|
+
#
|
229
|
+
# @yield [Result] The result instance
|
230
|
+
# @return [Result] Self for method chaining
|
231
|
+
# @raise [ArgumentError] If no block is provided
|
232
|
+
#
|
233
|
+
# @example
|
234
|
+
# result.on_good { |r| logger.info "Task completed successfully" }
|
101
235
|
def on_good(&)
|
102
|
-
raise ArgumentError, "
|
236
|
+
raise ArgumentError, "block required" unless block_given?
|
103
237
|
|
104
238
|
yield(self) if good?
|
105
239
|
self
|
106
240
|
end
|
107
241
|
|
242
|
+
# Checks if the result represents a bad outcome (skipped or failed).
|
243
|
+
#
|
244
|
+
# @return [Boolean] true if not successful
|
245
|
+
#
|
246
|
+
# @example
|
247
|
+
# result.bad? # => true if skipped? || failed?
|
108
248
|
def bad?
|
109
249
|
!success?
|
110
250
|
end
|
111
251
|
|
252
|
+
# Executes a callback if the result has a bad outcome.
|
253
|
+
#
|
254
|
+
# @yield [Result] The result instance
|
255
|
+
# @return [Result] Self for method chaining
|
256
|
+
# @raise [ArgumentError] If no block is provided
|
257
|
+
#
|
258
|
+
# @example
|
259
|
+
# result.on_bad { |r| logger.error "Task had issues: #{r.status}" }
|
112
260
|
def on_bad(&)
|
113
|
-
raise ArgumentError, "
|
261
|
+
raise ArgumentError, "block required" unless block_given?
|
114
262
|
|
115
263
|
yield(self) if bad?
|
116
264
|
self
|
117
265
|
end
|
118
266
|
|
267
|
+
# Marks the result as skipped with optional metadata.
|
268
|
+
#
|
269
|
+
# Transitions from success to skipped status and halts execution
|
270
|
+
# unless the skip was caused by an original exception.
|
271
|
+
#
|
272
|
+
# @param metadata [Hash] Additional metadata about the skip
|
273
|
+
# @return [void]
|
274
|
+
# @raise [RuntimeError] If not transitioning from success status
|
275
|
+
# @raise [CMDx::Fault] If halting due to skip (unless original_exception present)
|
276
|
+
#
|
277
|
+
# @example Basic skip
|
278
|
+
# result.skip!(reason: "Order already processed")
|
279
|
+
# result.skipped? # => true
|
280
|
+
#
|
281
|
+
# @example Skip with exception context
|
282
|
+
# result.skip!(original_exception: StandardError.new("DB unavailable"))
|
119
283
|
def skip!(**metadata)
|
120
284
|
return if skipped?
|
121
285
|
|
@@ -127,6 +291,22 @@ module CMDx
|
|
127
291
|
halt! unless metadata[:original_exception]
|
128
292
|
end
|
129
293
|
|
294
|
+
# Marks the result as failed with optional metadata.
|
295
|
+
#
|
296
|
+
# Transitions from success to failed status and halts execution
|
297
|
+
# unless the failure was caused by an original exception.
|
298
|
+
#
|
299
|
+
# @param metadata [Hash] Additional metadata about the failure
|
300
|
+
# @return [void]
|
301
|
+
# @raise [RuntimeError] If not transitioning from success status
|
302
|
+
# @raise [CMDx::Fault] If halting due to failure (unless original_exception present)
|
303
|
+
#
|
304
|
+
# @example Basic failure
|
305
|
+
# result.fail!(reason: "Invalid order data", code: 422)
|
306
|
+
# result.failed? # => true
|
307
|
+
#
|
308
|
+
# @example Failure with exception context
|
309
|
+
# result.fail!(original_exception: StandardError.new("Validation failed"))
|
130
310
|
def fail!(**metadata)
|
131
311
|
return if failed?
|
132
312
|
|
@@ -138,14 +318,39 @@ module CMDx
|
|
138
318
|
halt! unless metadata[:original_exception]
|
139
319
|
end
|
140
320
|
|
321
|
+
# Halts execution by raising a fault if the result is not successful.
|
322
|
+
#
|
323
|
+
# @return [void]
|
324
|
+
# @raise [CMDx::Fault] If result status is not success
|
325
|
+
#
|
326
|
+
# @example
|
327
|
+
# result.fail!(reason: "Something went wrong")
|
328
|
+
# result.halt! # Raises CMDx::Fault
|
141
329
|
def halt!
|
142
330
|
return if success?
|
143
331
|
|
144
332
|
raise Fault.build(self)
|
145
333
|
end
|
146
334
|
|
335
|
+
# Propagates another result's failure status to this result.
|
336
|
+
#
|
337
|
+
# Copies the failure or skip status from another result, merging
|
338
|
+
# metadata and preserving failure chain information.
|
339
|
+
#
|
340
|
+
# @param result [CMDx::Result] The result to propagate from
|
341
|
+
# @param local_metadata [Hash] Additional metadata to merge
|
342
|
+
# @return [void]
|
343
|
+
# @raise [TypeError] If result parameter is not a Result instance
|
344
|
+
#
|
345
|
+
# @example Propagating failure
|
346
|
+
# first_result = FirstTask.call
|
347
|
+
# second_result = SecondTask.call
|
348
|
+
# second_result.throw!(first_result) if first_result.failed?
|
349
|
+
#
|
350
|
+
# @example Propagating with additional context
|
351
|
+
# result.throw!(other_result, context: "During order processing")
|
147
352
|
def throw!(result, local_metadata = {})
|
148
|
-
raise
|
353
|
+
raise TypeError, "must be a Result" unless result.is_a?(Result)
|
149
354
|
|
150
355
|
md = result.metadata.merge(local_metadata)
|
151
356
|
|
@@ -153,61 +358,198 @@ module CMDx
|
|
153
358
|
fail!(**md) if result.failed?
|
154
359
|
end
|
155
360
|
|
361
|
+
# Finds the result that originally caused a failure in the execution chain.
|
362
|
+
#
|
363
|
+
# @return [CMDx::Result, nil] The result that first failed, or nil if not failed
|
364
|
+
#
|
365
|
+
# @example
|
366
|
+
# failed_result = result.caused_failure
|
367
|
+
# puts "Original failure: #{failed_result.metadata[:reason]}" if failed_result
|
156
368
|
def caused_failure
|
157
369
|
return unless failed?
|
158
370
|
|
159
|
-
|
371
|
+
chain.results.reverse.find(&:failed?)
|
160
372
|
end
|
161
373
|
|
374
|
+
# Checks if this result was the original cause of failure.
|
375
|
+
#
|
376
|
+
# @return [Boolean] true if this result caused the failure chain
|
377
|
+
#
|
378
|
+
# @example
|
379
|
+
# result.caused_failure? # => true if this result started the failure chain
|
162
380
|
def caused_failure?
|
163
381
|
return false unless failed?
|
164
382
|
|
165
383
|
caused_failure == self
|
166
384
|
end
|
167
385
|
|
386
|
+
# Finds the result that threw/propagated the failure to this result.
|
387
|
+
#
|
388
|
+
# @return [CMDx::Result, nil] The result that threw the failure, or nil if not failed
|
389
|
+
#
|
390
|
+
# @example
|
391
|
+
# throwing_result = result.threw_failure
|
392
|
+
# puts "Failure thrown by: #{throwing_result.task.class}" if throwing_result
|
168
393
|
def threw_failure
|
169
394
|
return unless failed?
|
170
395
|
|
171
|
-
results =
|
396
|
+
results = chain.results.select(&:failed?)
|
172
397
|
results.find { |r| r.index > index } || results.last
|
173
398
|
end
|
174
399
|
|
400
|
+
# Checks if this result threw/propagated a failure.
|
401
|
+
#
|
402
|
+
# @return [Boolean] true if this result threw a failure to another result
|
403
|
+
#
|
404
|
+
# @example
|
405
|
+
# result.threw_failure? # => true if this result propagated failure
|
175
406
|
def threw_failure?
|
176
407
|
return false unless failed?
|
177
408
|
|
178
409
|
threw_failure == self
|
179
410
|
end
|
180
411
|
|
412
|
+
# Checks if this result received a thrown failure (not the original cause).
|
413
|
+
#
|
414
|
+
# @return [Boolean] true if failed but not the original cause
|
415
|
+
#
|
416
|
+
# @example
|
417
|
+
# result.thrown_failure? # => true if failed due to propagated failure
|
181
418
|
def thrown_failure?
|
182
419
|
failed? && !caused_failure?
|
183
420
|
end
|
184
421
|
|
422
|
+
# Gets the index of this result within the execution chain.
|
423
|
+
#
|
424
|
+
# @return [Integer] The zero-based index of this result in the chain
|
425
|
+
#
|
426
|
+
# @example
|
427
|
+
# result.index # => 0 for first result, 1 for second, etc.
|
185
428
|
def index
|
186
|
-
|
429
|
+
chain.index(self)
|
187
430
|
end
|
188
431
|
|
432
|
+
# Gets the outcome of the result based on state and status.
|
433
|
+
#
|
434
|
+
# Returns state for initialized results or thrown failures,
|
435
|
+
# otherwise returns the status.
|
436
|
+
#
|
437
|
+
# @return [String] The result outcome (state or status)
|
438
|
+
#
|
439
|
+
# @example
|
440
|
+
# result.outcome # => "success", "failed", "interrupted", etc.
|
189
441
|
def outcome
|
190
442
|
initialized? || thrown_failure? ? state : status
|
191
443
|
end
|
192
444
|
|
193
|
-
|
445
|
+
# Measures and returns the runtime of a block execution.
|
446
|
+
#
|
447
|
+
# If called without a block, returns the stored runtime value.
|
448
|
+
# If called with a block, executes and measures the execution
|
449
|
+
# time using monotonic clock.
|
450
|
+
#
|
451
|
+
# @yield Block to execute and measure
|
452
|
+
# @return [Float] Runtime in seconds
|
453
|
+
#
|
454
|
+
# @example Getting stored runtime
|
455
|
+
# result.runtime # => 0.5
|
456
|
+
#
|
457
|
+
# @example Measuring block execution
|
458
|
+
# result.runtime do
|
459
|
+
# # Task execution logic
|
460
|
+
# perform_work
|
461
|
+
# end # => 0.5 (and stores the runtime)
|
462
|
+
def runtime(&)
|
194
463
|
return @runtime unless block_given?
|
195
464
|
|
196
|
-
|
197
|
-
timeout_secs = task.task_setting(timeout_type)
|
198
|
-
|
199
|
-
Timeout.timeout(timeout_secs, TimeoutError, "execution exceeded #{timeout_secs} seconds") do
|
200
|
-
@runtime = Utils::MonotonicRuntime.call(&block)
|
201
|
-
end
|
465
|
+
@runtime = Utils::MonotonicRuntime.call(&)
|
202
466
|
end
|
203
467
|
|
468
|
+
# Converts the result to a hash representation.
|
469
|
+
#
|
470
|
+
# @return [Hash] Serialized result data including task info, state, status, and metadata
|
471
|
+
#
|
472
|
+
# @example
|
473
|
+
# result.to_h
|
474
|
+
# # => {
|
475
|
+
# # class: "ProcessOrderTask",
|
476
|
+
# # type: "Task",
|
477
|
+
# # index: 0,
|
478
|
+
# # id: "018c2b95-b764-7615-a924-cc5b910ed1e5",
|
479
|
+
# # state: "complete",
|
480
|
+
# # status: "success",
|
481
|
+
# # outcome: "success",
|
482
|
+
# # metadata: {},
|
483
|
+
# # runtime: 0.5
|
484
|
+
# # }
|
204
485
|
def to_h
|
205
486
|
ResultSerializer.call(self)
|
206
487
|
end
|
207
488
|
|
489
|
+
# Converts the result to a string representation for inspection.
|
490
|
+
#
|
491
|
+
# @return [String] Human-readable result description
|
492
|
+
#
|
493
|
+
# @example
|
494
|
+
# result.to_s
|
495
|
+
# # => "ProcessOrderTask: type=Task index=0 id=018c2b95... state=complete status=success outcome=success runtime=0.5"
|
208
496
|
def to_s
|
209
497
|
ResultInspector.call(to_h)
|
210
498
|
end
|
211
499
|
|
500
|
+
# Deconstructs the result for array pattern matching.
|
501
|
+
#
|
502
|
+
# Enables pattern matching with array syntax to match against
|
503
|
+
# state and status in order.
|
504
|
+
#
|
505
|
+
# @return [Array<String>] Array containing [state, status]
|
506
|
+
#
|
507
|
+
# @example Array pattern matching
|
508
|
+
# result = ProcessOrderTask.call(order_id: 123)
|
509
|
+
# case result
|
510
|
+
# in ["complete", "success"]
|
511
|
+
# puts "Task completed successfully"
|
512
|
+
# in ["interrupted", "failed"]
|
513
|
+
# puts "Task failed"
|
514
|
+
# end
|
515
|
+
def deconstruct
|
516
|
+
[state, status]
|
517
|
+
end
|
518
|
+
|
519
|
+
# Deconstructs the result for hash pattern matching.
|
520
|
+
#
|
521
|
+
# Enables pattern matching with hash syntax to match against
|
522
|
+
# specific result attributes.
|
523
|
+
#
|
524
|
+
# @param keys [Array<Symbol>] Specific keys to extract (optional)
|
525
|
+
# @return [Hash] Hash containing result attributes
|
526
|
+
#
|
527
|
+
# @example Hash pattern matching
|
528
|
+
# result = ProcessOrderTask.call(order_id: 123)
|
529
|
+
# case result
|
530
|
+
# in { state: "complete", status: "success" }
|
531
|
+
# puts "Success!"
|
532
|
+
# in { state: "interrupted", status: "failed", metadata: { reason: String => reason } }
|
533
|
+
# puts "Failed: #{reason}"
|
534
|
+
# end
|
535
|
+
#
|
536
|
+
# @example Specific key extraction
|
537
|
+
# result.deconstruct_keys([:state, :status])
|
538
|
+
# # => { state: "complete", status: "success" }
|
539
|
+
def deconstruct_keys(keys)
|
540
|
+
attributes = {
|
541
|
+
state: state,
|
542
|
+
status: status,
|
543
|
+
metadata: metadata,
|
544
|
+
executed: executed?,
|
545
|
+
good: good?,
|
546
|
+
bad: bad?
|
547
|
+
}
|
548
|
+
|
549
|
+
return attributes if keys.nil?
|
550
|
+
|
551
|
+
attributes.slice(*keys)
|
552
|
+
end
|
553
|
+
|
212
554
|
end
|
213
555
|
end
|
data/lib/cmdx/result_ansi.rb
CHANGED
@@ -1,26 +1,122 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module CMDx
|
4
|
+
# ANSI color formatting module for result states and statuses.
|
5
|
+
#
|
6
|
+
# The ResultAnsi module provides ANSI color formatting for result state and
|
7
|
+
# status values to enhance readability in terminal output. It maps different
|
8
|
+
# result states and statuses to appropriate colors for visual distinction.
|
9
|
+
#
|
10
|
+
# @example Basic result colorization
|
11
|
+
# ResultAnsi.call("complete") # => Green colored text
|
12
|
+
# ResultAnsi.call("success") # => Green colored text
|
13
|
+
# ResultAnsi.call("failed") # => Red colored text
|
14
|
+
# ResultAnsi.call("interrupted") # => Red colored text
|
15
|
+
#
|
16
|
+
# @example Usage in log formatters
|
17
|
+
# result_data = { state: "complete", status: "success" }
|
18
|
+
# colored_state = ResultAnsi.call(result_data[:state])
|
19
|
+
# colored_status = ResultAnsi.call(result_data[:status])
|
20
|
+
#
|
21
|
+
# @example Integration with pretty formatters
|
22
|
+
# # Used internally by PrettyLine, PrettyJson, PrettyKeyValue formatters
|
23
|
+
# formatted_status = ResultAnsi.call("failed") # => Red "failed"
|
24
|
+
#
|
25
|
+
# @see CMDx::Result Result states and statuses
|
26
|
+
# @see CMDx::Utils::AnsiColor ANSI color utility functions
|
27
|
+
# @see CMDx::LogFormatters::PrettyLine Pretty line formatter with colors
|
4
28
|
module ResultAnsi
|
5
29
|
|
30
|
+
# Mapping of result states to ANSI colors.
|
31
|
+
#
|
32
|
+
# Maps Result state constants to their corresponding color codes
|
33
|
+
# for consistent visual representation of execution states.
|
6
34
|
STATE_COLORS = {
|
7
|
-
Result::INITIALIZED => :blue,
|
8
|
-
Result::EXECUTING => :yellow,
|
9
|
-
Result::COMPLETE => :green,
|
10
|
-
Result::INTERRUPTED => :red
|
35
|
+
Result::INITIALIZED => :blue, # Initial state - blue
|
36
|
+
Result::EXECUTING => :yellow, # Currently executing - yellow
|
37
|
+
Result::COMPLETE => :green, # Successfully completed - green
|
38
|
+
Result::INTERRUPTED => :red # Execution interrupted - red
|
11
39
|
}.freeze
|
40
|
+
|
41
|
+
# Mapping of result statuses to ANSI colors.
|
42
|
+
#
|
43
|
+
# Maps Result status constants to their corresponding color codes
|
44
|
+
# for consistent visual representation of execution outcomes.
|
12
45
|
STATUS_COLORS = {
|
13
|
-
Result::SUCCESS => :green,
|
14
|
-
Result::SKIPPED => :yellow,
|
15
|
-
Result::FAILED => :red
|
46
|
+
Result::SUCCESS => :green, # Successful completion - green
|
47
|
+
Result::SKIPPED => :yellow, # Intentionally skipped - yellow
|
48
|
+
Result::FAILED => :red # Failed execution - red
|
16
49
|
}.freeze
|
17
50
|
|
18
51
|
module_function
|
19
52
|
|
53
|
+
# Applies ANSI color formatting to a result state or status string.
|
54
|
+
#
|
55
|
+
# Formats the input string with appropriate ANSI color codes based on
|
56
|
+
# whether it matches a known result state or status value. Falls back
|
57
|
+
# to default color for unknown values.
|
58
|
+
#
|
59
|
+
# @param s [String] The state or status string to colorize
|
60
|
+
# @return [String] The string with ANSI color codes applied
|
61
|
+
#
|
62
|
+
# @example Colorizing result states
|
63
|
+
# ResultAnsi.call("initialized") # => "\e[34minitialized\e[0m" (blue)
|
64
|
+
# ResultAnsi.call("executing") # => "\e[33mexecuting\e[0m" (yellow)
|
65
|
+
# ResultAnsi.call("complete") # => "\e[32mcomplete\e[0m" (green)
|
66
|
+
# ResultAnsi.call("interrupted") # => "\e[31minterrupted\e[0m" (red)
|
67
|
+
#
|
68
|
+
# @example Colorizing result statuses
|
69
|
+
# ResultAnsi.call("success") # => "\e[32msuccess\e[0m" (green)
|
70
|
+
# ResultAnsi.call("skipped") # => "\e[33mskipped\e[0m" (yellow)
|
71
|
+
# ResultAnsi.call("failed") # => "\e[31mfailed\e[0m" (red)
|
72
|
+
#
|
73
|
+
# @example Unknown value
|
74
|
+
# ResultAnsi.call("unknown") # => "\e[39munknown\e[0m" (default color)
|
75
|
+
#
|
76
|
+
# @example Usage in result formatting
|
77
|
+
# result = ProcessOrderTask.call
|
78
|
+
# colored_state = ResultAnsi.call(result.state)
|
79
|
+
# colored_status = ResultAnsi.call(result.status)
|
80
|
+
# puts "Task #{colored_state} with #{colored_status}"
|
81
|
+
# # => "Task complete with success" (with appropriate colors)
|
20
82
|
def call(s)
|
21
|
-
|
83
|
+
Utils::AnsiColor.call(s, color: color(s))
|
84
|
+
end
|
22
85
|
|
23
|
-
|
86
|
+
# Determines the appropriate color for a result state or status string.
|
87
|
+
#
|
88
|
+
# Looks up the input string in both the STATE_COLORS and STATUS_COLORS
|
89
|
+
# mapping hashes to find the corresponding color symbol. First checks
|
90
|
+
# STATE_COLORS, then STATUS_COLORS, and falls back to the default color
|
91
|
+
# if no mapping is found in either hash.
|
92
|
+
#
|
93
|
+
# @param s [String] The state or status string to determine color for
|
94
|
+
# @return [Symbol] The color symbol for the state or status
|
95
|
+
#
|
96
|
+
# @example Result state color mapping
|
97
|
+
# color("initialized") # => :blue
|
98
|
+
# color("executing") # => :yellow
|
99
|
+
# color("complete") # => :green
|
100
|
+
# color("interrupted") # => :red
|
101
|
+
#
|
102
|
+
# @example Result status color mapping
|
103
|
+
# color("success") # => :green
|
104
|
+
# color("skipped") # => :yellow
|
105
|
+
# color("failed") # => :red
|
106
|
+
#
|
107
|
+
# @example Unknown state or status
|
108
|
+
# color("unknown") # => :default
|
109
|
+
# color("pending") # => :default
|
110
|
+
#
|
111
|
+
# @example Precedence behavior
|
112
|
+
# # If a string exists in both hashes, STATE_COLORS takes precedence
|
113
|
+
# color("some_value") # => STATE_COLORS["some_value"] || STATUS_COLORS["some_value"] || :default
|
114
|
+
#
|
115
|
+
# @note STATE_COLORS mapping is checked before STATUS_COLORS mapping
|
116
|
+
# @see STATE_COLORS The mapping hash for result states
|
117
|
+
# @see STATUS_COLORS The mapping hash for result statuses
|
118
|
+
def color(s)
|
119
|
+
STATE_COLORS[s] || STATUS_COLORS[s] || :default
|
24
120
|
end
|
25
121
|
|
26
122
|
end
|