cmdx 1.0.1 → 1.1.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/.cursor/prompts/rspec.md +20 -0
- data/.cursor/prompts/yardoc.md +8 -0
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +17 -2
- data/README.md +1 -1
- data/docs/basics/call.md +2 -2
- data/docs/basics/chain.md +1 -1
- data/docs/callbacks.md +3 -36
- data/docs/configuration.md +58 -12
- data/docs/interruptions/exceptions.md +1 -1
- data/docs/interruptions/faults.md +2 -2
- data/docs/logging.md +4 -4
- data/docs/middlewares.md +43 -43
- data/docs/parameters/coercions.md +49 -38
- data/docs/parameters/defaults.md +1 -1
- data/docs/parameters/validations.md +0 -39
- data/docs/testing.md +11 -12
- data/docs/workflows.md +4 -4
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/callback.rb +36 -56
- data/lib/cmdx/callback_registry.rb +82 -73
- data/lib/cmdx/chain.rb +65 -122
- data/lib/cmdx/chain_inspector.rb +22 -115
- data/lib/cmdx/chain_serializer.rb +17 -148
- data/lib/cmdx/coercion.rb +49 -0
- data/lib/cmdx/coercion_registry.rb +94 -0
- data/lib/cmdx/coercions/array.rb +18 -36
- data/lib/cmdx/coercions/big_decimal.rb +21 -33
- data/lib/cmdx/coercions/boolean.rb +21 -40
- data/lib/cmdx/coercions/complex.rb +18 -31
- data/lib/cmdx/coercions/date.rb +20 -39
- data/lib/cmdx/coercions/date_time.rb +22 -39
- data/lib/cmdx/coercions/float.rb +19 -32
- data/lib/cmdx/coercions/hash.rb +22 -41
- data/lib/cmdx/coercions/integer.rb +20 -33
- data/lib/cmdx/coercions/rational.rb +20 -32
- data/lib/cmdx/coercions/string.rb +23 -31
- data/lib/cmdx/coercions/time.rb +24 -40
- data/lib/cmdx/coercions/virtual.rb +14 -31
- data/lib/cmdx/configuration.rb +57 -171
- data/lib/cmdx/context.rb +22 -165
- data/lib/cmdx/core_ext/hash.rb +42 -67
- data/lib/cmdx/core_ext/module.rb +35 -79
- data/lib/cmdx/core_ext/object.rb +63 -98
- data/lib/cmdx/correlator.rb +40 -156
- data/lib/cmdx/error.rb +37 -202
- data/lib/cmdx/errors.rb +165 -202
- data/lib/cmdx/fault.rb +55 -158
- data/lib/cmdx/faults.rb +26 -137
- data/lib/cmdx/immutator.rb +22 -109
- data/lib/cmdx/lazy_struct.rb +103 -187
- data/lib/cmdx/log_formatters/json.rb +14 -40
- data/lib/cmdx/log_formatters/key_value.rb +14 -40
- data/lib/cmdx/log_formatters/line.rb +14 -48
- data/lib/cmdx/log_formatters/logstash.rb +14 -57
- data/lib/cmdx/log_formatters/pretty_json.rb +14 -50
- data/lib/cmdx/log_formatters/pretty_key_value.rb +13 -46
- data/lib/cmdx/log_formatters/pretty_line.rb +16 -54
- data/lib/cmdx/log_formatters/raw.rb +19 -49
- data/lib/cmdx/logger.rb +20 -82
- data/lib/cmdx/logger_ansi.rb +18 -75
- data/lib/cmdx/logger_serializer.rb +24 -114
- data/lib/cmdx/middleware.rb +38 -60
- data/lib/cmdx/middleware_registry.rb +81 -77
- data/lib/cmdx/middlewares/correlate.rb +41 -226
- data/lib/cmdx/middlewares/timeout.rb +46 -185
- data/lib/cmdx/parameter.rb +120 -198
- data/lib/cmdx/parameter_evaluator.rb +231 -0
- data/lib/cmdx/parameter_inspector.rb +25 -56
- data/lib/cmdx/parameter_registry.rb +59 -84
- data/lib/cmdx/parameter_serializer.rb +23 -74
- data/lib/cmdx/railtie.rb +24 -107
- data/lib/cmdx/result.rb +254 -260
- data/lib/cmdx/result_ansi.rb +19 -85
- data/lib/cmdx/result_inspector.rb +27 -68
- data/lib/cmdx/result_logger.rb +18 -81
- data/lib/cmdx/result_serializer.rb +28 -132
- data/lib/cmdx/rspec/matchers.rb +28 -0
- data/lib/cmdx/rspec/result_matchers/be_executed.rb +42 -0
- data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +94 -0
- data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +94 -0
- data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +59 -0
- data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +57 -0
- data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +87 -0
- data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +51 -0
- data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +58 -0
- data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +59 -0
- data/lib/cmdx/rspec/result_matchers/have_context.rb +86 -0
- data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +54 -0
- data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +52 -0
- data/lib/cmdx/rspec/result_matchers/have_metadata.rb +114 -0
- data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +66 -0
- data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +64 -0
- data/lib/cmdx/rspec/result_matchers/have_runtime.rb +78 -0
- data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +76 -0
- data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +62 -0
- data/lib/cmdx/rspec/task_matchers/have_callback.rb +85 -0
- data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +68 -0
- data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +92 -0
- data/lib/cmdx/rspec/task_matchers/have_middleware.rb +46 -0
- data/lib/cmdx/rspec/task_matchers/have_parameter.rb +181 -0
- data/lib/cmdx/task.rb +213 -425
- data/lib/cmdx/task_deprecator.rb +55 -0
- data/lib/cmdx/task_processor.rb +245 -0
- data/lib/cmdx/task_serializer.rb +22 -70
- data/lib/cmdx/utils/ansi_color.rb +13 -89
- data/lib/cmdx/utils/log_timestamp.rb +13 -42
- data/lib/cmdx/utils/monotonic_runtime.rb +13 -63
- data/lib/cmdx/utils/name_affix.rb +21 -71
- data/lib/cmdx/validator.rb +48 -0
- data/lib/cmdx/validator_registry.rb +86 -0
- data/lib/cmdx/validators/exclusion.rb +55 -94
- data/lib/cmdx/validators/format.rb +31 -85
- data/lib/cmdx/validators/inclusion.rb +65 -110
- data/lib/cmdx/validators/length.rb +117 -133
- data/lib/cmdx/validators/numeric.rb +123 -130
- data/lib/cmdx/validators/presence.rb +38 -79
- data/lib/cmdx/version.rb +1 -7
- data/lib/cmdx/workflow.rb +46 -339
- data/lib/cmdx.rb +1 -1
- data/lib/generators/cmdx/install_generator.rb +14 -31
- data/lib/generators/cmdx/task_generator.rb +39 -55
- data/lib/generators/cmdx/templates/install.rb +24 -6
- data/lib/generators/cmdx/workflow_generator.rb +41 -66
- data/lib/locales/ar.yml +0 -1
- data/lib/locales/cs.yml +0 -1
- data/lib/locales/da.yml +0 -1
- data/lib/locales/de.yml +0 -1
- data/lib/locales/el.yml +0 -1
- data/lib/locales/en.yml +0 -1
- data/lib/locales/es.yml +0 -1
- data/lib/locales/fi.yml +0 -1
- data/lib/locales/fr.yml +0 -1
- data/lib/locales/he.yml +0 -1
- data/lib/locales/hi.yml +0 -1
- data/lib/locales/it.yml +0 -1
- data/lib/locales/ja.yml +0 -1
- data/lib/locales/ko.yml +0 -1
- data/lib/locales/nl.yml +0 -1
- data/lib/locales/no.yml +0 -1
- data/lib/locales/pl.yml +0 -1
- data/lib/locales/pt.yml +0 -1
- data/lib/locales/ru.yml +0 -1
- data/lib/locales/sv.yml +0 -1
- data/lib/locales/th.yml +0 -1
- data/lib/locales/tr.yml +0 -1
- data/lib/locales/vi.yml +0 -1
- data/lib/locales/zh.yml +0 -1
- metadata +34 -8
- data/lib/cmdx/parameter_validator.rb +0 -81
- data/lib/cmdx/parameter_value.rb +0 -244
- data/lib/cmdx/parameters_inspector.rb +0 -72
- data/lib/cmdx/parameters_serializer.rb +0 -115
- data/lib/cmdx/rspec/result_matchers.rb +0 -917
- data/lib/cmdx/rspec/task_matchers.rb +0 -570
- data/lib/cmdx/validators/custom.rb +0 -102
data/lib/cmdx/result.rb
CHANGED
@@ -1,62 +1,54 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module CMDx
|
4
|
-
#
|
4
|
+
# Represents the execution result of a task, tracking state, status, and metadata.
|
5
5
|
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
# and
|
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
|
6
|
+
# Result objects encapsulate the outcome of task execution, providing detailed
|
7
|
+
# information about execution state (initialized, executing, complete, interrupted),
|
8
|
+
# status (success, skipped, failed), and associated metadata. They support
|
9
|
+
# state transitions, status changes, and provide introspection capabilities
|
10
|
+
# for debugging and monitoring task execution.
|
36
11
|
class Result
|
37
12
|
|
38
|
-
|
39
|
-
|
13
|
+
STATES = [
|
14
|
+
INITIALIZED = "initialized", # Initial state before execution
|
15
|
+
EXECUTING = "executing", # Currently executing task logic
|
16
|
+
COMPLETE = "complete", # Successfully completed execution
|
17
|
+
INTERRUPTED = "interrupted" # Execution was halted due to failure
|
18
|
+
].freeze
|
19
|
+
STATUSES = [
|
20
|
+
SUCCESS = "success", # Task completed successfully
|
21
|
+
SKIPPED = "skipped", # Task was skipped intentionally
|
22
|
+
FAILED = "failed" # Task failed due to error or validation
|
23
|
+
].freeze
|
24
|
+
|
25
|
+
cmdx_attr_delegator :context, :chain,
|
26
|
+
to: :task
|
40
27
|
|
41
28
|
# @return [CMDx::Task] The task instance that generated this result
|
29
|
+
attr_reader :task
|
30
|
+
|
42
31
|
# @return [String] The current execution state (initialized, executing, complete, interrupted)
|
32
|
+
attr_reader :state
|
33
|
+
|
43
34
|
# @return [String] The current execution status (success, skipped, failed)
|
35
|
+
attr_reader :status
|
36
|
+
|
44
37
|
# @return [Hash] Additional metadata associated with the result
|
45
|
-
attr_reader :
|
38
|
+
attr_reader :metadata
|
46
39
|
|
47
|
-
#
|
40
|
+
# Creates a new Result instance for the specified task.
|
41
|
+
#
|
42
|
+
# @param task [CMDx::Task] the task instance that will generate this result
|
48
43
|
#
|
49
|
-
#
|
50
|
-
# Results start in initialized state with success status.
|
44
|
+
# @return [Result] a new Result instance
|
51
45
|
#
|
52
|
-
# @
|
53
|
-
# @raise [TypeError] If task is not a Task or Workflow instance
|
46
|
+
# @raise [TypeError] if task is not a Task or Workflow instance
|
54
47
|
#
|
55
|
-
# @example
|
56
|
-
# task =
|
48
|
+
# @example Create a result for a task
|
49
|
+
# task = MyTask.new
|
57
50
|
# result = Result.new(task)
|
58
|
-
# result.
|
59
|
-
# result.success? # => true
|
51
|
+
# result.state # => "initialized"
|
60
52
|
def initialize(task)
|
61
53
|
raise TypeError, "must be a Task or Workflow" unless task.is_a?(Task)
|
62
54
|
|
@@ -66,27 +58,43 @@ module CMDx
|
|
66
58
|
@metadata = {}
|
67
59
|
end
|
68
60
|
|
69
|
-
# Available execution states for task results.
|
70
|
-
#
|
71
|
-
# States represent the execution lifecycle of a task from initialization
|
72
|
-
# through completion or interruption.
|
73
|
-
STATES = [
|
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
|
78
|
-
].freeze
|
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`)
|
85
61
|
STATES.each do |s|
|
86
|
-
#
|
62
|
+
# Checks if the result is in the specified state.
|
63
|
+
#
|
64
|
+
# @return [Boolean] true if the result matches the state
|
65
|
+
#
|
66
|
+
# @example Check if result is initialized
|
67
|
+
# result.initialized? # => true
|
68
|
+
#
|
69
|
+
# @example Check if result is executing
|
70
|
+
# result.executing? # => false
|
71
|
+
#
|
72
|
+
# @example Check if result is complete
|
73
|
+
# result.complete? # => false
|
74
|
+
#
|
75
|
+
# @example Check if result is interrupted
|
76
|
+
# result.interrupted? # => false
|
87
77
|
define_method(:"#{s}?") { state == s }
|
88
78
|
|
89
|
-
#
|
79
|
+
# Executes the provided block if the result is in the specified state.
|
80
|
+
#
|
81
|
+
# @param block [Proc] the block to execute if result matches the state
|
82
|
+
#
|
83
|
+
# @return [Result] returns self for method chaining
|
84
|
+
#
|
85
|
+
# @raise [ArgumentError] if no block is provided
|
86
|
+
#
|
87
|
+
# @example Handle initialized state
|
88
|
+
# result.on_initialized { |r| puts "Task is ready to start" }
|
89
|
+
#
|
90
|
+
# @example Handle executing state
|
91
|
+
# result.on_executing { |r| puts "Task is currently running" }
|
92
|
+
#
|
93
|
+
# @example Handle complete state
|
94
|
+
# result.on_complete { |r| puts "Task finished successfully" }
|
95
|
+
#
|
96
|
+
# @example Handle interrupted state
|
97
|
+
# result.on_interrupted { |r| puts "Task was interrupted" }
|
90
98
|
define_method(:"on_#{s}") do |&block|
|
91
99
|
raise ArgumentError, "block required" unless block
|
92
100
|
|
@@ -95,43 +103,45 @@ module CMDx
|
|
95
103
|
end
|
96
104
|
end
|
97
105
|
|
98
|
-
#
|
106
|
+
# Transitions the result to its final executed state based on current status.
|
99
107
|
#
|
100
|
-
#
|
101
|
-
# if the task has failed or been skipped.
|
108
|
+
# @return [Result] returns self for method chaining
|
102
109
|
#
|
103
|
-
# @
|
110
|
+
# @example Complete successful execution
|
111
|
+
# result.success? # => true
|
112
|
+
# result.executed! # transitions to complete
|
113
|
+
# result.complete? # => true
|
104
114
|
#
|
105
|
-
# @example
|
106
|
-
# result.
|
107
|
-
# result.
|
108
|
-
#
|
109
|
-
# @example Failed execution
|
110
|
-
# result.fail!(reason: "Something went wrong")
|
111
|
-
# result.executed!
|
112
|
-
# result.interrupted? # => true
|
115
|
+
# @example Handle failed execution
|
116
|
+
# result.fail!
|
117
|
+
# result.executed! # transitions to interrupted
|
118
|
+
# result.interrupted? # => true
|
113
119
|
def executed!
|
114
120
|
success? ? complete! : interrupt!
|
115
121
|
end
|
116
122
|
|
117
|
-
# Checks if the result has
|
123
|
+
# Checks if the result has finished executing (either complete or interrupted).
|
118
124
|
#
|
119
|
-
# @return [Boolean] true if result is
|
125
|
+
# @return [Boolean] true if the result is in a final execution state
|
120
126
|
#
|
121
|
-
# @example
|
122
|
-
# result.executed?
|
127
|
+
# @example Check execution completion
|
128
|
+
# result.executed? # => false
|
129
|
+
# result.complete!
|
130
|
+
# result.executed? # => true
|
123
131
|
def executed?
|
124
132
|
complete? || interrupted?
|
125
133
|
end
|
126
134
|
|
127
|
-
# Executes
|
135
|
+
# Executes the provided block if the result has finished executing.
|
128
136
|
#
|
129
|
-
# @
|
130
|
-
# @return [Result] Self for method chaining
|
131
|
-
# @raise [ArgumentError] If no block is provided
|
137
|
+
# @param block [Proc] the block to execute if result is executed
|
132
138
|
#
|
133
|
-
# @
|
134
|
-
#
|
139
|
+
# @return [Result] returns self for method chaining
|
140
|
+
#
|
141
|
+
# @raise [ArgumentError] if no block is provided
|
142
|
+
#
|
143
|
+
# @example Handle executed result
|
144
|
+
# result.on_executed { |r| puts "Task completed with #{r.status}" }
|
135
145
|
def on_executed(&)
|
136
146
|
raise ArgumentError, "block required" unless block_given?
|
137
147
|
|
@@ -141,12 +151,14 @@ module CMDx
|
|
141
151
|
|
142
152
|
# Transitions the result to executing state.
|
143
153
|
#
|
144
|
-
# @return [
|
145
|
-
#
|
154
|
+
# @return [Result] returns self for method chaining
|
155
|
+
#
|
156
|
+
# @raise [RuntimeError] if not transitioning from initialized state
|
146
157
|
#
|
147
|
-
# @example
|
158
|
+
# @example Start task execution
|
159
|
+
# result.initialized? # => true
|
148
160
|
# result.executing!
|
149
|
-
# result.executing?
|
161
|
+
# result.executing? # => true
|
150
162
|
def executing!
|
151
163
|
return if executing?
|
152
164
|
|
@@ -157,12 +169,14 @@ module CMDx
|
|
157
169
|
|
158
170
|
# Transitions the result to complete state.
|
159
171
|
#
|
160
|
-
# @return [
|
161
|
-
# @raise [RuntimeError] If not transitioning from executing state
|
172
|
+
# @return [Result] returns self for method chaining
|
162
173
|
#
|
163
|
-
# @
|
174
|
+
# @raise [RuntimeError] if not transitioning from executing state
|
175
|
+
#
|
176
|
+
# @example Complete task execution
|
177
|
+
# result.executing!
|
164
178
|
# result.complete!
|
165
|
-
# result.complete?
|
179
|
+
# result.complete? # => true
|
166
180
|
def complete!
|
167
181
|
return if complete?
|
168
182
|
|
@@ -171,14 +185,17 @@ module CMDx
|
|
171
185
|
@state = COMPLETE
|
172
186
|
end
|
173
187
|
|
174
|
-
# Transitions the result to interrupted state.
|
188
|
+
# Transitions the result to interrupted state due to failure.
|
175
189
|
#
|
176
|
-
# @return [
|
177
|
-
# @raise [RuntimeError] If trying to interrupt from complete state
|
190
|
+
# @return [Result] returns self for method chaining
|
178
191
|
#
|
179
|
-
# @
|
192
|
+
# @raise [RuntimeError] if trying to transition from complete state
|
193
|
+
#
|
194
|
+
# @example Interrupt execution on failure
|
195
|
+
# result.executing!
|
196
|
+
# result.fail!
|
180
197
|
# result.interrupt!
|
181
|
-
# result.interrupted?
|
198
|
+
# result.interrupted? # => true
|
182
199
|
def interrupt!
|
183
200
|
return if interrupted?
|
184
201
|
|
@@ -187,25 +204,37 @@ module CMDx
|
|
187
204
|
@state = INTERRUPTED
|
188
205
|
end
|
189
206
|
|
190
|
-
# Available execution statuses for task results.
|
191
|
-
#
|
192
|
-
# Statuses represent the outcome of task logic execution.
|
193
|
-
STATUSES = [
|
194
|
-
SUCCESS = "success", # Task completed successfully
|
195
|
-
SKIPPED = "skipped", # Task was skipped intentionally
|
196
|
-
FAILED = "failed" # Task failed due to error or validation
|
197
|
-
].freeze
|
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`)
|
204
207
|
STATUSES.each do |s|
|
205
|
-
#
|
208
|
+
# Checks if the result has the specified status.
|
209
|
+
#
|
210
|
+
# @return [Boolean] true if the result matches the status
|
211
|
+
#
|
212
|
+
# @example Check if result is successful
|
213
|
+
# result.success? # => true
|
214
|
+
#
|
215
|
+
# @example Check if result is skipped
|
216
|
+
# result.skipped? # => false
|
217
|
+
#
|
218
|
+
# @example Check if result is failed
|
219
|
+
# result.failed? # => false
|
206
220
|
define_method(:"#{s}?") { status == s }
|
207
221
|
|
208
|
-
#
|
222
|
+
# Executes the provided block if the result has the specified status.
|
223
|
+
#
|
224
|
+
# @param block [Proc] the block to execute if result matches the status
|
225
|
+
#
|
226
|
+
# @return [Result] returns self for method chaining
|
227
|
+
#
|
228
|
+
# @raise [ArgumentError] if no block is provided
|
229
|
+
#
|
230
|
+
# @example Handle successful status
|
231
|
+
# result.on_success { |r| puts "Task completed successfully" }
|
232
|
+
#
|
233
|
+
# @example Handle skipped status
|
234
|
+
# result.on_skipped { |r| puts "Task was skipped: #{r.metadata[:reason]}" }
|
235
|
+
#
|
236
|
+
# @example Handle failed status
|
237
|
+
# result.on_failed { |r| puts "Task failed: #{r.metadata[:error]}" }
|
209
238
|
define_method(:"on_#{s}") do |&block|
|
210
239
|
raise ArgumentError, "block required" unless block
|
211
240
|
|
@@ -214,24 +243,28 @@ module CMDx
|
|
214
243
|
end
|
215
244
|
end
|
216
245
|
|
217
|
-
# Checks if the result
|
246
|
+
# Checks if the result has a positive outcome (not failed).
|
218
247
|
#
|
219
|
-
# @return [Boolean] true if
|
248
|
+
# @return [Boolean] true if the result is successful or skipped
|
220
249
|
#
|
221
|
-
# @example
|
222
|
-
# result.good?
|
250
|
+
# @example Check for good outcome
|
251
|
+
# result.good? # => true (success or skipped)
|
252
|
+
# result.fail!
|
253
|
+
# result.good? # => false
|
223
254
|
def good?
|
224
255
|
!failed?
|
225
256
|
end
|
226
257
|
|
227
|
-
# Executes
|
258
|
+
# Executes the provided block if the result has a good outcome.
|
259
|
+
#
|
260
|
+
# @param block [Proc] the block to execute if result is good
|
261
|
+
#
|
262
|
+
# @return [Result] returns self for method chaining
|
228
263
|
#
|
229
|
-
# @
|
230
|
-
# @return [Result] Self for method chaining
|
231
|
-
# @raise [ArgumentError] If no block is provided
|
264
|
+
# @raise [ArgumentError] if no block is provided
|
232
265
|
#
|
233
|
-
# @example
|
234
|
-
# result.on_good { |r|
|
266
|
+
# @example Handle good outcome
|
267
|
+
# result.on_good { |r| puts "Task succeeded with #{r.status}" }
|
235
268
|
def on_good(&)
|
236
269
|
raise ArgumentError, "block required" unless block_given?
|
237
270
|
|
@@ -239,24 +272,28 @@ module CMDx
|
|
239
272
|
self
|
240
273
|
end
|
241
274
|
|
242
|
-
# Checks if the result
|
275
|
+
# Checks if the result has a negative outcome (not successful).
|
243
276
|
#
|
244
|
-
# @return [Boolean] true if
|
277
|
+
# @return [Boolean] true if the result is skipped or failed
|
245
278
|
#
|
246
|
-
# @example
|
247
|
-
# result.bad?
|
279
|
+
# @example Check for bad outcome
|
280
|
+
# result.bad? # => false (initially successful)
|
281
|
+
# result.skip!
|
282
|
+
# result.bad? # => true
|
248
283
|
def bad?
|
249
284
|
!success?
|
250
285
|
end
|
251
286
|
|
252
|
-
# Executes
|
287
|
+
# Executes the provided block if the result has a bad outcome.
|
253
288
|
#
|
254
|
-
# @
|
255
|
-
# @return [Result] Self for method chaining
|
256
|
-
# @raise [ArgumentError] If no block is provided
|
289
|
+
# @param block [Proc] the block to execute if result is bad
|
257
290
|
#
|
258
|
-
# @
|
259
|
-
#
|
291
|
+
# @return [Result] returns self for method chaining
|
292
|
+
#
|
293
|
+
# @raise [ArgumentError] if no block is provided
|
294
|
+
#
|
295
|
+
# @example Handle bad outcome
|
296
|
+
# result.on_bad { |r| puts "Task had issues: #{r.status}" }
|
260
297
|
def on_bad(&)
|
261
298
|
raise ArgumentError, "block required" unless block_given?
|
262
299
|
|
@@ -264,22 +301,18 @@ module CMDx
|
|
264
301
|
self
|
265
302
|
end
|
266
303
|
|
267
|
-
#
|
304
|
+
# Transitions the result to skipped status with optional metadata.
|
268
305
|
#
|
269
|
-
#
|
270
|
-
# unless the skip was caused by an original exception.
|
306
|
+
# @param metadata [Hash] additional metadata to store with the skip
|
271
307
|
#
|
272
|
-
# @
|
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)
|
308
|
+
# @return [Result] returns self for method chaining
|
276
309
|
#
|
277
|
-
# @
|
278
|
-
# result.skip!(reason: "Order already processed")
|
279
|
-
# result.skipped? # => true
|
310
|
+
# @raise [RuntimeError] if not transitioning from success status
|
280
311
|
#
|
281
|
-
# @example Skip with
|
282
|
-
# result.skip!(
|
312
|
+
# @example Skip a task with reason
|
313
|
+
# result.skip!(reason: "condition not met")
|
314
|
+
# result.skipped? # => true
|
315
|
+
# result.metadata[:reason] # => "condition not met"
|
283
316
|
def skip!(**metadata)
|
284
317
|
return if skipped?
|
285
318
|
|
@@ -291,22 +324,18 @@ module CMDx
|
|
291
324
|
halt! unless metadata[:original_exception]
|
292
325
|
end
|
293
326
|
|
294
|
-
#
|
327
|
+
# Transitions the result to failed status with optional metadata.
|
295
328
|
#
|
296
|
-
#
|
297
|
-
# unless the failure was caused by an original exception.
|
329
|
+
# @param metadata [Hash] additional metadata to store with the failure
|
298
330
|
#
|
299
|
-
# @
|
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)
|
331
|
+
# @return [Result] returns self for method chaining
|
303
332
|
#
|
304
|
-
# @
|
305
|
-
# result.fail!(reason: "Invalid order data", code: 422)
|
306
|
-
# result.failed? # => true
|
333
|
+
# @raise [RuntimeError] if not transitioning from success status
|
307
334
|
#
|
308
|
-
# @example
|
309
|
-
# result.fail!(
|
335
|
+
# @example Fail a task with error details
|
336
|
+
# result.fail!(error: "validation failed", code: 422)
|
337
|
+
# result.failed? # => true
|
338
|
+
# result.metadata[:error] # => "validation failed"
|
310
339
|
def fail!(**metadata)
|
311
340
|
return if failed?
|
312
341
|
|
@@ -318,37 +347,35 @@ module CMDx
|
|
318
347
|
halt! unless metadata[:original_exception]
|
319
348
|
end
|
320
349
|
|
321
|
-
#
|
350
|
+
# Raises a Fault exception to halt execution chain if result is not successful.
|
322
351
|
#
|
323
|
-
# @return [
|
324
|
-
# @raise [CMDx::Fault] If result status is not success
|
352
|
+
# @return [nil] never returns normally
|
325
353
|
#
|
326
|
-
# @
|
327
|
-
#
|
328
|
-
#
|
354
|
+
# @raise [Fault] if the result is not successful
|
355
|
+
#
|
356
|
+
# @example Halt execution on failure
|
357
|
+
# result.fail!
|
358
|
+
# result.halt! # raises Fault
|
329
359
|
def halt!
|
330
360
|
return if success?
|
331
361
|
|
332
362
|
raise Fault.build(self)
|
333
363
|
end
|
334
364
|
|
335
|
-
# Propagates another result
|
365
|
+
# Propagates status and metadata from another result to this result.
|
336
366
|
#
|
337
|
-
#
|
338
|
-
#
|
367
|
+
# @param result [Result] the result to throw/propagate from
|
368
|
+
# @param local_metadata [Hash] additional metadata to merge
|
339
369
|
#
|
340
|
-
# @
|
341
|
-
# @param local_metadata [Hash] Additional metadata to merge
|
342
|
-
# @return [void]
|
343
|
-
# @raise [TypeError] If result parameter is not a Result instance
|
370
|
+
# @return [Result] returns self for method chaining
|
344
371
|
#
|
345
|
-
# @
|
346
|
-
# first_result = FirstTask.call
|
347
|
-
# second_result = SecondTask.call
|
348
|
-
# second_result.throw!(first_result) if first_result.failed?
|
372
|
+
# @raise [TypeError] if result is not a Result instance
|
349
373
|
#
|
350
|
-
# @example
|
351
|
-
#
|
374
|
+
# @example Throw failure from another result
|
375
|
+
# failed_result = Result.new(task)
|
376
|
+
# failed_result.fail!(error: "network timeout")
|
377
|
+
# current_result.throw!(failed_result)
|
378
|
+
# current_result.failed? # => true
|
352
379
|
def throw!(result, local_metadata = {})
|
353
380
|
raise TypeError, "must be a Result" unless result.is_a?(Result)
|
354
381
|
|
@@ -358,38 +385,38 @@ module CMDx
|
|
358
385
|
fail!(**md) if result.failed?
|
359
386
|
end
|
360
387
|
|
361
|
-
# Finds the result that
|
388
|
+
# Finds the original result that caused a failure in the execution chain.
|
362
389
|
#
|
363
|
-
# @return [
|
390
|
+
# @return [Result, nil] the result that originally caused the failure, or nil if not failed
|
364
391
|
#
|
365
|
-
# @example
|
366
|
-
#
|
367
|
-
#
|
392
|
+
# @example Find original failure cause
|
393
|
+
# chain.results.last.caused_failure
|
394
|
+
# # => #<Result task=OriginalTask status=failed>
|
368
395
|
def caused_failure
|
369
396
|
return unless failed?
|
370
397
|
|
371
398
|
chain.results.reverse.find(&:failed?)
|
372
399
|
end
|
373
400
|
|
374
|
-
# Checks if this result
|
401
|
+
# Checks if this result is the original cause of failure in the chain.
|
375
402
|
#
|
376
403
|
# @return [Boolean] true if this result caused the failure chain
|
377
404
|
#
|
378
|
-
# @example
|
379
|
-
# result.caused_failure?
|
405
|
+
# @example Check if this result caused failure
|
406
|
+
# result.caused_failure? # => true if this is the original failure
|
380
407
|
def caused_failure?
|
381
408
|
return false unless failed?
|
382
409
|
|
383
410
|
caused_failure == self
|
384
411
|
end
|
385
412
|
|
386
|
-
# Finds the result that threw/propagated
|
413
|
+
# Finds the result that threw/propagated this failure.
|
387
414
|
#
|
388
|
-
# @return [
|
415
|
+
# @return [Result, nil] the result that threw this failure, or nil if not applicable
|
389
416
|
#
|
390
|
-
# @example
|
391
|
-
#
|
392
|
-
#
|
417
|
+
# @example Find failure propagator
|
418
|
+
# result.threw_failure
|
419
|
+
# # => #<Result task=PropagatorTask status=failed>
|
393
420
|
def threw_failure
|
394
421
|
return unless failed?
|
395
422
|
|
@@ -399,66 +426,58 @@ module CMDx
|
|
399
426
|
|
400
427
|
# Checks if this result threw/propagated a failure.
|
401
428
|
#
|
402
|
-
# @return [Boolean] true if this result threw a failure
|
429
|
+
# @return [Boolean] true if this result threw a failure
|
403
430
|
#
|
404
|
-
# @example
|
405
|
-
# result.threw_failure?
|
431
|
+
# @example Check if this result threw failure
|
432
|
+
# result.threw_failure? # => true if this result propagated failure
|
406
433
|
def threw_failure?
|
407
434
|
return false unless failed?
|
408
435
|
|
409
436
|
threw_failure == self
|
410
437
|
end
|
411
438
|
|
412
|
-
# Checks if this result
|
439
|
+
# Checks if this result represents a propagated failure (not the original cause).
|
413
440
|
#
|
414
|
-
# @return [Boolean] true if
|
441
|
+
# @return [Boolean] true if this is a propagated failure
|
415
442
|
#
|
416
|
-
# @example
|
417
|
-
# result.thrown_failure?
|
443
|
+
# @example Check if failure was propagated
|
444
|
+
# result.thrown_failure? # => true if failure came from earlier in chain
|
418
445
|
def thrown_failure?
|
419
446
|
failed? && !caused_failure?
|
420
447
|
end
|
421
448
|
|
422
|
-
# Gets the index of this result
|
449
|
+
# Gets the index position of this result in the execution chain.
|
423
450
|
#
|
424
|
-
# @return [Integer]
|
451
|
+
# @return [Integer] the zero-based index position in the chain
|
425
452
|
#
|
426
|
-
# @example
|
427
|
-
# result.index
|
453
|
+
# @example Get result position
|
454
|
+
# result.index # => 2 (third result in chain)
|
428
455
|
def index
|
429
456
|
chain.index(self)
|
430
457
|
end
|
431
458
|
|
432
|
-
#
|
433
|
-
#
|
434
|
-
# Returns state for initialized results or thrown failures,
|
435
|
-
# otherwise returns the status.
|
459
|
+
# Determines the overall outcome of this result.
|
436
460
|
#
|
437
|
-
# @return [String]
|
461
|
+
# @return [String] the outcome - state for certain conditions, status otherwise
|
438
462
|
#
|
439
|
-
# @example
|
440
|
-
# result.outcome
|
463
|
+
# @example Get result outcome
|
464
|
+
# result.outcome # => "success" or "failed" or "initialized"
|
441
465
|
def outcome
|
442
466
|
initialized? || thrown_failure? ? state : status
|
443
467
|
end
|
444
468
|
|
445
|
-
#
|
469
|
+
# Gets or measures the runtime duration of the result's execution.
|
446
470
|
#
|
447
|
-
#
|
448
|
-
# If called with a block, executes and measures the execution
|
449
|
-
# time using monotonic clock.
|
471
|
+
# @param block [Proc] optional block to measure execution time
|
450
472
|
#
|
451
|
-
# @
|
452
|
-
# @return [Float] Runtime in seconds
|
473
|
+
# @return [Float, nil] the runtime in seconds, or nil if not measured
|
453
474
|
#
|
454
|
-
# @example
|
455
|
-
# result.runtime
|
475
|
+
# @example Get existing runtime
|
476
|
+
# result.runtime # => 0.523 (seconds)
|
456
477
|
#
|
457
|
-
# @example
|
458
|
-
# result.runtime
|
459
|
-
#
|
460
|
-
# perform_work
|
461
|
-
# end # => 0.5 (and stores the runtime)
|
478
|
+
# @example Measure execution time
|
479
|
+
# result.runtime { sleep 1; do_work }
|
480
|
+
# result.runtime # => 1.001
|
462
481
|
def runtime(&)
|
463
482
|
return @runtime unless block_given?
|
464
483
|
|
@@ -467,75 +486,50 @@ module CMDx
|
|
467
486
|
|
468
487
|
# Converts the result to a hash representation.
|
469
488
|
#
|
470
|
-
# @return [Hash]
|
489
|
+
# @return [Hash] hash representation of the result
|
471
490
|
#
|
472
|
-
# @example
|
491
|
+
# @example Convert to hash
|
473
492
|
# 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
|
-
# # }
|
493
|
+
# # => {state: "complete", status: "success", metadata: {}}
|
485
494
|
def to_h
|
486
495
|
ResultSerializer.call(self)
|
487
496
|
end
|
488
497
|
|
489
|
-
#
|
498
|
+
# Returns a string representation of the result.
|
490
499
|
#
|
491
|
-
# @return [String]
|
500
|
+
# @return [String] formatted string representation
|
492
501
|
#
|
493
|
-
# @example
|
502
|
+
# @example Get string representation
|
494
503
|
# result.to_s
|
495
|
-
# # => "
|
504
|
+
# # => "MyTask [complete/success]"
|
496
505
|
def to_s
|
497
506
|
ResultInspector.call(to_h)
|
498
507
|
end
|
499
508
|
|
500
|
-
# Deconstructs the result for
|
509
|
+
# Deconstructs the result for pattern matching.
|
501
510
|
#
|
502
|
-
#
|
503
|
-
# state and status in order.
|
511
|
+
# @return [Array] array containing state and status
|
504
512
|
#
|
505
|
-
# @
|
506
|
-
#
|
507
|
-
# @example Array pattern matching
|
508
|
-
# result = ProcessOrderTask.call(order_id: 123)
|
513
|
+
# @example Pattern match on result
|
509
514
|
# case result
|
510
515
|
# in ["complete", "success"]
|
511
516
|
# puts "Task completed successfully"
|
512
|
-
# in ["interrupted", "failed"]
|
513
|
-
# puts "Task failed"
|
514
517
|
# end
|
515
518
|
def deconstruct
|
516
519
|
[state, status]
|
517
520
|
end
|
518
521
|
|
519
|
-
# Deconstructs the result
|
522
|
+
# Deconstructs the result to a hash for pattern matching with keys.
|
520
523
|
#
|
521
|
-
#
|
522
|
-
# specific result attributes.
|
524
|
+
# @param keys [Array<Symbol>, nil] specific keys to extract, or nil for all
|
523
525
|
#
|
524
|
-
# @
|
525
|
-
# @return [Hash] Hash containing result attributes
|
526
|
+
# @return [Hash] hash containing requested attributes
|
526
527
|
#
|
527
|
-
# @example
|
528
|
-
# result = ProcessOrderTask.call(order_id: 123)
|
528
|
+
# @example Pattern match with keys
|
529
529
|
# case result
|
530
|
-
# in {
|
531
|
-
# puts "
|
532
|
-
# in { state: "interrupted", status: "failed", metadata: { reason: String => reason } }
|
533
|
-
# puts "Failed: #{reason}"
|
530
|
+
# in {state: "complete", good: true}
|
531
|
+
# puts "Task completed successfully"
|
534
532
|
# end
|
535
|
-
#
|
536
|
-
# @example Specific key extraction
|
537
|
-
# result.deconstruct_keys([:state, :status])
|
538
|
-
# # => { state: "complete", status: "success" }
|
539
533
|
def deconstruct_keys(keys)
|
540
534
|
attributes = {
|
541
535
|
state: state,
|