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.
Files changed (126) 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 +16 -1
  5. data/.ruby-version +1 -1
  6. data/CHANGELOG.md +31 -1
  7. data/README.md +72 -25
  8. data/docs/ai_prompts.md +309 -0
  9. data/docs/basics/call.md +225 -14
  10. data/docs/basics/chain.md +271 -0
  11. data/docs/basics/context.md +232 -33
  12. data/docs/basics/setup.md +76 -12
  13. data/docs/callbacks.md +273 -0
  14. data/docs/configuration.md +158 -28
  15. data/docs/getting_started.md +134 -22
  16. data/docs/interruptions/exceptions.md +189 -11
  17. data/docs/interruptions/faults.md +187 -44
  18. data/docs/interruptions/halt.md +179 -35
  19. data/docs/logging.md +194 -53
  20. data/docs/middlewares.md +735 -0
  21. data/docs/outcomes/result.md +296 -10
  22. data/docs/outcomes/states.md +203 -31
  23. data/docs/outcomes/statuses.md +275 -30
  24. data/docs/parameters/coercions.md +402 -29
  25. data/docs/parameters/defaults.md +249 -25
  26. data/docs/parameters/definitions.md +238 -72
  27. data/docs/parameters/namespacing.md +250 -27
  28. data/docs/parameters/validations.md +193 -168
  29. data/docs/testing.md +550 -0
  30. data/docs/tips_and_tricks.md +95 -43
  31. data/docs/workflows.md +319 -0
  32. data/lib/cmdx/.DS_Store +0 -0
  33. data/lib/cmdx/callback.rb +69 -0
  34. data/lib/cmdx/callback_registry.rb +106 -0
  35. data/lib/cmdx/chain.rb +190 -0
  36. data/lib/cmdx/chain_inspector.rb +149 -0
  37. data/lib/cmdx/chain_serializer.rb +175 -0
  38. data/lib/cmdx/coercions/array.rb +37 -0
  39. data/lib/cmdx/coercions/big_decimal.rb +33 -0
  40. data/lib/cmdx/coercions/boolean.rb +41 -1
  41. data/lib/cmdx/coercions/complex.rb +31 -0
  42. data/lib/cmdx/coercions/date.rb +39 -0
  43. data/lib/cmdx/coercions/date_time.rb +39 -0
  44. data/lib/cmdx/coercions/float.rb +31 -0
  45. data/lib/cmdx/coercions/hash.rb +42 -0
  46. data/lib/cmdx/coercions/integer.rb +32 -0
  47. data/lib/cmdx/coercions/rational.rb +31 -0
  48. data/lib/cmdx/coercions/string.rb +31 -0
  49. data/lib/cmdx/coercions/time.rb +39 -0
  50. data/lib/cmdx/coercions/virtual.rb +31 -0
  51. data/lib/cmdx/configuration.rb +217 -9
  52. data/lib/cmdx/context.rb +173 -2
  53. data/lib/cmdx/core_ext/hash.rb +72 -0
  54. data/lib/cmdx/core_ext/module.rb +94 -0
  55. data/lib/cmdx/core_ext/object.rb +105 -0
  56. data/lib/cmdx/correlator.rb +217 -0
  57. data/lib/cmdx/error.rb +210 -8
  58. data/lib/cmdx/errors.rb +256 -1
  59. data/lib/cmdx/fault.rb +177 -2
  60. data/lib/cmdx/faults.rb +158 -2
  61. data/lib/cmdx/immutator.rb +121 -2
  62. data/lib/cmdx/lazy_struct.rb +261 -18
  63. data/lib/cmdx/log_formatters/json.rb +46 -0
  64. data/lib/cmdx/log_formatters/key_value.rb +46 -0
  65. data/lib/cmdx/log_formatters/line.rb +54 -0
  66. data/lib/cmdx/log_formatters/logstash.rb +64 -0
  67. data/lib/cmdx/log_formatters/pretty_json.rb +57 -0
  68. data/lib/cmdx/log_formatters/pretty_key_value.rb +51 -0
  69. data/lib/cmdx/log_formatters/pretty_line.rb +60 -0
  70. data/lib/cmdx/log_formatters/raw.rb +54 -0
  71. data/lib/cmdx/logger.rb +85 -0
  72. data/lib/cmdx/logger_ansi.rb +93 -7
  73. data/lib/cmdx/logger_serializer.rb +116 -0
  74. data/lib/cmdx/middleware.rb +74 -0
  75. data/lib/cmdx/middleware_registry.rb +106 -0
  76. data/lib/cmdx/middlewares/correlate.rb +266 -0
  77. data/lib/cmdx/middlewares/timeout.rb +232 -0
  78. data/lib/cmdx/parameter.rb +228 -1
  79. data/lib/cmdx/parameter_inspector.rb +61 -0
  80. data/lib/cmdx/parameter_registry.rb +125 -0
  81. data/lib/cmdx/parameter_serializer.rb +83 -0
  82. data/lib/cmdx/parameter_validator.rb +62 -0
  83. data/lib/cmdx/parameter_value.rb +109 -1
  84. data/lib/cmdx/parameters_inspector.rb +59 -0
  85. data/lib/cmdx/parameters_serializer.rb +102 -0
  86. data/lib/cmdx/railtie.rb +123 -3
  87. data/lib/cmdx/result.rb +367 -25
  88. data/lib/cmdx/result_ansi.rb +105 -9
  89. data/lib/cmdx/result_inspector.rb +76 -0
  90. data/lib/cmdx/result_logger.rb +90 -3
  91. data/lib/cmdx/result_serializer.rb +137 -0
  92. data/lib/cmdx/rspec/result_matchers.rb +917 -0
  93. data/lib/cmdx/rspec/task_matchers.rb +570 -0
  94. data/lib/cmdx/task.rb +405 -37
  95. data/lib/cmdx/task_serializer.rb +74 -2
  96. data/lib/cmdx/utils/ansi_color.rb +95 -0
  97. data/lib/cmdx/utils/log_timestamp.rb +48 -0
  98. data/lib/cmdx/utils/monotonic_runtime.rb +71 -4
  99. data/lib/cmdx/utils/name_affix.rb +78 -0
  100. data/lib/cmdx/validators/custom.rb +82 -0
  101. data/lib/cmdx/validators/exclusion.rb +94 -0
  102. data/lib/cmdx/validators/format.rb +102 -8
  103. data/lib/cmdx/validators/inclusion.rb +104 -0
  104. data/lib/cmdx/validators/length.rb +128 -0
  105. data/lib/cmdx/validators/numeric.rb +128 -0
  106. data/lib/cmdx/validators/presence.rb +93 -7
  107. data/lib/cmdx/version.rb +7 -1
  108. data/lib/cmdx/workflow.rb +394 -0
  109. data/lib/cmdx.rb +25 -64
  110. data/lib/generators/cmdx/install_generator.rb +37 -1
  111. data/lib/generators/cmdx/task_generator.rb +69 -1
  112. data/lib/generators/cmdx/templates/install.rb +8 -12
  113. data/lib/generators/cmdx/workflow_generator.rb +109 -0
  114. metadata +54 -15
  115. data/docs/basics/run.md +0 -34
  116. data/docs/batch.md +0 -53
  117. data/docs/example.md +0 -82
  118. data/docs/hooks.md +0 -62
  119. data/lib/cmdx/batch.rb +0 -43
  120. data/lib/cmdx/parameters.rb +0 -35
  121. data/lib/cmdx/run.rb +0 -39
  122. data/lib/cmdx/run_inspector.rb +0 -26
  123. data/lib/cmdx/run_serializer.rb +0 -20
  124. data/lib/cmdx/task_hook.rb +0 -18
  125. data/lib/generators/cmdx/batch_generator.rb +0 -30
  126. /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, :run, to: :task
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 ArgumentError, "must be a Task or Batch" unless task.is_a?(Task)
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, "a block is required" unless block
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, "a block is required" unless block_given?
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, "a block is required" unless block
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, "a block is required" unless block_given?
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, "a block is required" unless block_given?
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 ArgumentError, "must be a Result" unless result.is_a?(Result)
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
- run.results.reverse.find(&:failed?)
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 = run.results.select(&:failed?)
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
- run.index(self)
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
- def runtime(&block)
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
- timeout_type = is_a?(Batch) ? :batch_timeout : :task_timeout
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
@@ -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
- color = STATE_COLORS[s] || STATUS_COLORS[s] || :default
83
+ Utils::AnsiColor.call(s, color: color(s))
84
+ end
22
85
 
23
- Utils::AnsiColor.call(s, color:)
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