cmdx 1.21.0 → 2.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 (195) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +118 -1
  3. data/README.md +37 -24
  4. data/lib/cmdx/.DS_Store +0 -0
  5. data/lib/cmdx/callbacks.rb +179 -0
  6. data/lib/cmdx/chain.rb +78 -175
  7. data/lib/cmdx/coercions/array.rb +19 -33
  8. data/lib/cmdx/coercions/big_decimal.rb +12 -29
  9. data/lib/cmdx/coercions/boolean.rb +25 -45
  10. data/lib/cmdx/coercions/coerce.rb +32 -0
  11. data/lib/cmdx/coercions/complex.rb +12 -27
  12. data/lib/cmdx/coercions/date.rb +29 -33
  13. data/lib/cmdx/coercions/date_time.rb +29 -33
  14. data/lib/cmdx/coercions/float.rb +8 -29
  15. data/lib/cmdx/coercions/hash.rb +17 -43
  16. data/lib/cmdx/coercions/integer.rb +8 -32
  17. data/lib/cmdx/coercions/rational.rb +12 -33
  18. data/lib/cmdx/coercions/string.rb +6 -24
  19. data/lib/cmdx/coercions/symbol.rb +12 -26
  20. data/lib/cmdx/coercions/time.rb +31 -35
  21. data/lib/cmdx/coercions.rb +174 -0
  22. data/lib/cmdx/configuration.rb +45 -237
  23. data/lib/cmdx/context.rb +264 -243
  24. data/lib/cmdx/deprecation.rb +67 -0
  25. data/lib/cmdx/deprecators/error.rb +22 -0
  26. data/lib/cmdx/deprecators/log.rb +22 -0
  27. data/lib/cmdx/deprecators/warn.rb +21 -0
  28. data/lib/cmdx/deprecators.rb +101 -0
  29. data/lib/cmdx/errors.rb +145 -79
  30. data/lib/cmdx/executors/fiber.rb +42 -0
  31. data/lib/cmdx/executors/thread.rb +36 -0
  32. data/lib/cmdx/executors.rb +95 -0
  33. data/lib/cmdx/fault.rb +85 -78
  34. data/lib/cmdx/i18n_proxy.rb +104 -0
  35. data/lib/cmdx/input.rb +294 -0
  36. data/lib/cmdx/inputs.rb +218 -0
  37. data/lib/cmdx/log_formatters/json.rb +9 -20
  38. data/lib/cmdx/log_formatters/key_value.rb +10 -21
  39. data/lib/cmdx/log_formatters/line.rb +7 -19
  40. data/lib/cmdx/log_formatters/logstash.rb +8 -21
  41. data/lib/cmdx/log_formatters/raw.rb +8 -20
  42. data/lib/cmdx/logger_proxy.rb +30 -0
  43. data/lib/cmdx/mergers/deep_merge.rb +23 -0
  44. data/lib/cmdx/mergers/last_write_wins.rb +23 -0
  45. data/lib/cmdx/mergers/no_merge.rb +20 -0
  46. data/lib/cmdx/mergers.rb +95 -0
  47. data/lib/cmdx/middlewares.rb +128 -0
  48. data/lib/cmdx/output.rb +115 -0
  49. data/lib/cmdx/outputs.rb +66 -0
  50. data/lib/cmdx/pipeline.rb +144 -131
  51. data/lib/cmdx/railtie.rb +10 -36
  52. data/lib/cmdx/result.rb +247 -524
  53. data/lib/cmdx/retriers/bounded_random.rb +24 -0
  54. data/lib/cmdx/retriers/decorrelated_jitter.rb +28 -0
  55. data/lib/cmdx/retriers/exponential.rb +23 -0
  56. data/lib/cmdx/retriers/fibonacci.rb +39 -0
  57. data/lib/cmdx/retriers/full_random.rb +23 -0
  58. data/lib/cmdx/retriers/half_random.rb +24 -0
  59. data/lib/cmdx/retriers/linear.rb +23 -0
  60. data/lib/cmdx/retriers.rb +106 -0
  61. data/lib/cmdx/retry.rb +117 -138
  62. data/lib/cmdx/runtime.rb +251 -0
  63. data/lib/cmdx/settings.rb +68 -200
  64. data/lib/cmdx/signal.rb +165 -0
  65. data/lib/cmdx/task.rb +443 -343
  66. data/lib/cmdx/telemetry.rb +108 -0
  67. data/lib/cmdx/util.rb +73 -0
  68. data/lib/cmdx/validators/absence.rb +10 -39
  69. data/lib/cmdx/validators/exclusion.rb +33 -52
  70. data/lib/cmdx/validators/format.rb +19 -49
  71. data/lib/cmdx/validators/inclusion.rb +33 -54
  72. data/lib/cmdx/validators/length.rb +125 -127
  73. data/lib/cmdx/validators/numeric.rb +123 -123
  74. data/lib/cmdx/validators/presence.rb +10 -39
  75. data/lib/cmdx/validators/validate.rb +31 -0
  76. data/lib/cmdx/validators.rb +161 -0
  77. data/lib/cmdx/version.rb +2 -4
  78. data/lib/cmdx/workflow.rb +71 -96
  79. data/lib/cmdx.rb +111 -42
  80. data/lib/generators/cmdx/install_generator.rb +7 -17
  81. data/lib/generators/cmdx/task_generator.rb +12 -29
  82. data/lib/generators/cmdx/templates/install.rb +120 -48
  83. data/lib/generators/cmdx/templates/task.rb.tt +1 -1
  84. data/lib/generators/cmdx/templates/workflow.rb.tt +1 -2
  85. data/lib/generators/cmdx/workflow_generator.rb +12 -29
  86. data/lib/locales/en.yml +8 -7
  87. data/mkdocs.yml +25 -23
  88. metadata +39 -138
  89. data/lib/cmdx/attribute.rb +0 -440
  90. data/lib/cmdx/attribute_registry.rb +0 -185
  91. data/lib/cmdx/attribute_value.rb +0 -252
  92. data/lib/cmdx/callback_registry.rb +0 -169
  93. data/lib/cmdx/coercion_registry.rb +0 -138
  94. data/lib/cmdx/deprecator.rb +0 -77
  95. data/lib/cmdx/exception.rb +0 -46
  96. data/lib/cmdx/executor.rb +0 -378
  97. data/lib/cmdx/identifier.rb +0 -30
  98. data/lib/cmdx/locale.rb +0 -78
  99. data/lib/cmdx/middleware_registry.rb +0 -148
  100. data/lib/cmdx/middlewares/correlate.rb +0 -140
  101. data/lib/cmdx/middlewares/runtime.rb +0 -77
  102. data/lib/cmdx/middlewares/timeout.rb +0 -78
  103. data/lib/cmdx/parallelizer.rb +0 -100
  104. data/lib/cmdx/utils/call.rb +0 -53
  105. data/lib/cmdx/utils/condition.rb +0 -71
  106. data/lib/cmdx/utils/format.rb +0 -82
  107. data/lib/cmdx/utils/normalize.rb +0 -52
  108. data/lib/cmdx/utils/wrap.rb +0 -38
  109. data/lib/cmdx/validator_registry.rb +0 -143
  110. data/lib/generators/cmdx/locale_generator.rb +0 -39
  111. data/lib/locales/af.yml +0 -55
  112. data/lib/locales/ar.yml +0 -55
  113. data/lib/locales/az.yml +0 -55
  114. data/lib/locales/be.yml +0 -55
  115. data/lib/locales/bg.yml +0 -55
  116. data/lib/locales/bn.yml +0 -55
  117. data/lib/locales/bs.yml +0 -55
  118. data/lib/locales/ca.yml +0 -55
  119. data/lib/locales/cnr.yml +0 -55
  120. data/lib/locales/cs.yml +0 -55
  121. data/lib/locales/cy.yml +0 -55
  122. data/lib/locales/da.yml +0 -55
  123. data/lib/locales/de.yml +0 -55
  124. data/lib/locales/dz.yml +0 -55
  125. data/lib/locales/el.yml +0 -55
  126. data/lib/locales/eo.yml +0 -55
  127. data/lib/locales/es.yml +0 -55
  128. data/lib/locales/et.yml +0 -55
  129. data/lib/locales/eu.yml +0 -55
  130. data/lib/locales/fa.yml +0 -55
  131. data/lib/locales/fi.yml +0 -55
  132. data/lib/locales/fr.yml +0 -55
  133. data/lib/locales/fy.yml +0 -55
  134. data/lib/locales/gd.yml +0 -55
  135. data/lib/locales/gl.yml +0 -55
  136. data/lib/locales/he.yml +0 -55
  137. data/lib/locales/hi.yml +0 -55
  138. data/lib/locales/hr.yml +0 -55
  139. data/lib/locales/hu.yml +0 -55
  140. data/lib/locales/hy.yml +0 -55
  141. data/lib/locales/id.yml +0 -55
  142. data/lib/locales/is.yml +0 -55
  143. data/lib/locales/it.yml +0 -55
  144. data/lib/locales/ja.yml +0 -55
  145. data/lib/locales/ka.yml +0 -55
  146. data/lib/locales/kk.yml +0 -55
  147. data/lib/locales/km.yml +0 -55
  148. data/lib/locales/kn.yml +0 -55
  149. data/lib/locales/ko.yml +0 -55
  150. data/lib/locales/lb.yml +0 -55
  151. data/lib/locales/lo.yml +0 -55
  152. data/lib/locales/lt.yml +0 -55
  153. data/lib/locales/lv.yml +0 -55
  154. data/lib/locales/mg.yml +0 -55
  155. data/lib/locales/mk.yml +0 -55
  156. data/lib/locales/ml.yml +0 -55
  157. data/lib/locales/mn.yml +0 -55
  158. data/lib/locales/mr-IN.yml +0 -55
  159. data/lib/locales/ms.yml +0 -55
  160. data/lib/locales/nb.yml +0 -55
  161. data/lib/locales/ne.yml +0 -55
  162. data/lib/locales/nl.yml +0 -55
  163. data/lib/locales/nn.yml +0 -55
  164. data/lib/locales/oc.yml +0 -55
  165. data/lib/locales/or.yml +0 -55
  166. data/lib/locales/pa.yml +0 -55
  167. data/lib/locales/pl.yml +0 -55
  168. data/lib/locales/pt.yml +0 -55
  169. data/lib/locales/rm.yml +0 -55
  170. data/lib/locales/ro.yml +0 -55
  171. data/lib/locales/ru.yml +0 -55
  172. data/lib/locales/sc.yml +0 -55
  173. data/lib/locales/sk.yml +0 -55
  174. data/lib/locales/sl.yml +0 -55
  175. data/lib/locales/sq.yml +0 -55
  176. data/lib/locales/sr.yml +0 -55
  177. data/lib/locales/st.yml +0 -55
  178. data/lib/locales/sv.yml +0 -55
  179. data/lib/locales/sw.yml +0 -55
  180. data/lib/locales/ta.yml +0 -55
  181. data/lib/locales/te.yml +0 -55
  182. data/lib/locales/th.yml +0 -55
  183. data/lib/locales/tl.yml +0 -55
  184. data/lib/locales/tr.yml +0 -55
  185. data/lib/locales/tt.yml +0 -55
  186. data/lib/locales/ug.yml +0 -55
  187. data/lib/locales/uk.yml +0 -55
  188. data/lib/locales/ur.yml +0 -55
  189. data/lib/locales/uz.yml +0 -55
  190. data/lib/locales/vi.yml +0 -55
  191. data/lib/locales/wo.yml +0 -55
  192. data/lib/locales/zh-CN.yml +0 -55
  193. data/lib/locales/zh-HK.yml +0 -55
  194. data/lib/locales/zh-TW.yml +0 -55
  195. data/lib/locales/zh-YUE.yml +0 -55
data/lib/cmdx/result.rb CHANGED
@@ -1,635 +1,358 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CMDx
4
- # Represents the execution result of a CMDx task, tracking state transitions,
5
- # status changes, and providing methods for handling different outcomes.
4
+ # Frozen outcome of a task execution. Provides read-only access to the
5
+ # task's signal (state/status/reason/metadata/cause), the chain it belongs
6
+ # to, its context, and lifecycle metadata (retries, duration, rollback,
7
+ # deprecated). Constructed by Runtime at the end of `execute`.
6
8
  #
7
- # The Result class manages the lifecycle of task execution from initialization
8
- # through completion or interruption, offering a fluent interface for status
9
- # checking and conditional handling.
9
+ # @see Runtime#finalize_result
10
+ # @see Signal
10
11
  class Result
11
12
 
12
- extend Forwardable
13
-
14
- # @rbs STATES: Array[String]
15
- STATES = [
16
- INITIALIZED = "initialized", # Initial state before execution
17
- EXECUTING = "executing", # Currently executing task logic
18
- COMPLETE = "complete", # Successfully completed execution
19
- INTERRUPTED = "interrupted" # Execution was halted due to failure
20
- ].freeze
21
-
22
- # @rbs STATUSES: Array[String]
23
- STATUSES = [
24
- SUCCESS = "success", # Task completed successfully
25
- SKIPPED = "skipped", # Task was skipped intentionally
26
- FAILED = "failed" # Task failed due to error or validation
27
- ].freeze
28
-
29
- # @rbs STRIP_FAILURE: Proc
30
- STRIP_FAILURE = proc do |hash, result, key|
31
- unless result.public_send(:"#{key}?")
32
- # Strip caused/threw failures since its the same info as the log line
33
- hash[key] = result.public_send(key).to_h.except(:caused_failure, :threw_failure)
34
- end
35
- end.freeze
36
- private_constant :STRIP_FAILURE
37
-
38
- # @rbs FAILURE_KEY_REGEX: Regexp
39
- FAILURE_KEY_REGEX = /_failure\z/
40
- private_constant :FAILURE_KEY_REGEX
41
-
42
- # Returns the task instance associated with this result.
43
- #
44
- # @return [CMDx::Task] The task instance
45
- #
46
- # @example
47
- # result.task.id # => "users/create"
48
- #
49
- # @rbs @task: Task
50
- attr_reader :task
51
-
52
- # Returns the current execution state of the result.
53
- #
54
- # @return [String] One of: "initialized", "executing", "complete", "interrupted"
55
- #
56
- # @example
57
- # result.state # => "complete"
58
- #
59
- # @rbs @state: String
60
- attr_reader :state
13
+ EVENTS = Set[
14
+ *Signal::STATES,
15
+ *Signal::STATUSES,
16
+ :ok,
17
+ :ko
18
+ ].map!(&:to_sym).freeze
19
+ private_constant :EVENTS
20
+
21
+ attr_reader :chain
22
+
23
+ # @param chain [Chain] the chain this result belongs to
24
+ # @param task [Task] the executed task instance
25
+ # @param signal [Signal] the final signal from the task's lifecycle
26
+ # @param options [Hash{Symbol => Object}] frozen execution metadata
27
+ # @option options [String] :tid
28
+ # @option options [Boolean] :strict
29
+ # @option options [Boolean] :deprecated
30
+ # @option options [Boolean] :rolled_back
31
+ # @option options [Integer] :retries
32
+ # @option options [Float] :duration milliseconds
33
+ def initialize(chain, task, signal, **options)
34
+ @chain = chain
35
+ @task = task
36
+ @signal = signal
37
+ @options = options.freeze
38
+ end
61
39
 
62
- # Returns the execution status of the result.
63
- #
64
- # @return [String] One of: "success", "skipped", "failed"
65
- #
66
- # @example
67
- # result.status # => "success"
68
- #
69
- # @rbs @status: String
70
- attr_reader :status
40
+ # @return [String] uuid_v7 identifier for this execution
41
+ def tid
42
+ @options[:tid]
43
+ end
71
44
 
72
- # Returns additional metadata about the result.
73
- #
74
- # @return [Hash{Symbol => Object}] Metadata hash
75
- #
76
- # @example
77
- # result.metadata # => { duration: 1.5, code: 200, message: "Success" }
78
- #
79
- # @rbs @metadata: Hash[Symbol, untyped]
80
- attr_reader :metadata
45
+ # @return [Class<Task>] the task class that ran
46
+ def task
47
+ @task.class
48
+ end
81
49
 
82
- # Returns the reason for interruption (skip or failure).
83
- #
84
- # @return [String, nil] The reason message, or nil if not interrupted
85
- #
86
- # @example
87
- # result.reason # => "Validation failed"
88
- #
89
- # @rbs @reason: (String | nil)
90
- attr_reader :reason
50
+ # @return [String] `"Task"` or `"Workflow"`
51
+ def type
52
+ task.type
53
+ end
91
54
 
92
- # Returns the exception that caused the interruption.
93
- #
94
- # @return [Exception, nil] The causing exception, or nil if not interrupted
95
- #
96
- # @example
97
- # result.cause # => #<StandardError: Connection timeout>
98
- #
99
- # @rbs @cause: (Exception | nil)
100
- attr_reader :cause
55
+ # @return [String, nil] correlation id or the global configuration's correlation id
56
+ def xid
57
+ chain.xid
58
+ end
101
59
 
102
- # Returns the number of retries attempted.
103
- #
104
- # @return [Integer] The number of retries attempted
105
- #
106
- # @example
107
- # result.retries # => 2
108
- #
109
- # @rbs @retries: Integer
110
- attr_accessor :retries
60
+ # @return [String] uuid_v7 identifier for the chain this result belongs to
61
+ def cid
62
+ chain.id
63
+ end
111
64
 
112
- # Returns whether this result is strict.
113
- # When false, {CMDx::Executor#halt_execution?} returns false
114
- # regardless of the task's breakpoint settings.
115
- #
116
- # @return [Boolean] Whether the result is strict
117
- #
118
- # @example
119
- # result.strict? # => true
120
- #
121
- # @rbs @strict: bool
122
- attr_reader :strict
65
+ # @return [Integer, nil] this result's position in the chain
66
+ def index
67
+ @chain.index(self)
68
+ end
123
69
 
124
- # Returns whether the result has been rolled back.
125
- #
126
- # @return [Boolean] Whether the result has been rolled back
127
- #
128
- # @example
129
- # result.rolled_back? # => true
130
- #
131
- # @rbs @rolled_back: bool
132
- attr_accessor :rolled_back
70
+ # @return [Boolean] true when this result is the root of the chain
71
+ def root?
72
+ !!@options[:root]
73
+ end
133
74
 
134
- def_delegators :task, :context, :chain, :errors, :dry_run?
75
+ # @return [Context] frozen after the root task's teardown
76
+ def context
77
+ @task.context
78
+ end
135
79
  alias ctx context
136
80
 
137
- # @param task [CMDx::Task] The task instance this result represents
138
- #
139
- # @return [CMDx::Result] A new result instance for the task
140
- #
141
- # @raise [TypeError] When task is not a CMDx::Task instance
142
- #
143
- # @example
144
- # result = CMDx::Result.new(my_task)
145
- # result.state # => "initialized"
146
- #
147
- # @rbs (Task) -> void
148
- def initialize(task)
149
- raise TypeError, "must be a CMDx::Task" unless task.is_a?(CMDx::Task)
150
-
151
- @task = task
152
- @state = INITIALIZED
153
- @status = SUCCESS
154
- @metadata = {}
155
- @reason = nil
156
- @cause = nil
157
- @retries = 0
158
- @strict = true
159
- @rolled_back = false
160
- end
161
-
162
- STATES.each do |s|
163
- # @return [Boolean] Whether the result is in the specified state
164
- #
165
- # @example
166
- # result.initialized? # => true
167
- # result.executing? # => false
168
- #
169
- # @rbs () -> bool
170
- define_method(:"#{s}?") { state == s }
171
- end
172
-
173
- # @return [self] Returns self for method chaining
174
- #
175
- # @example
176
- # result.executed! # Transitions to complete or interrupted
177
- #
178
- # @rbs () -> self
179
- def executed!
180
- success? ? complete! : interrupt!
81
+ # @return [Errors] frozen by Runtime teardown
82
+ def errors
83
+ @task.errors
181
84
  end
182
85
 
183
- # @return [Boolean] Whether the task has been executed (complete or interrupted)
184
- #
185
- # @example
186
- # result.executed? # => true if complete? || interrupted?
187
- #
188
- # @rbs () -> bool
189
- def executed?
190
- complete? || interrupted?
86
+ # @return [String] one of {Signal::STATES}
87
+ def state
88
+ @signal.state
191
89
  end
192
90
 
193
- # @raise [RuntimeError] When attempting to transition from invalid state
194
- #
195
- # @example
196
- # result.executing! # Transitions from initialized to executing
197
- #
198
- # @rbs () -> void
199
- def executing!
200
- return if executing?
201
-
202
- raise "can only transition to #{EXECUTING} from #{INITIALIZED}" unless initialized?
203
-
204
- @state = EXECUTING
91
+ # @return [Boolean]
92
+ def complete?
93
+ @signal.complete?
205
94
  end
206
95
 
207
- # @raise [RuntimeError] When attempting to transition from invalid state
208
- #
209
- # @example
210
- # result.complete! # Transitions from executing to complete
211
- #
212
- # @rbs () -> void
213
- def complete!
214
- return if complete?
215
-
216
- raise "can only transition to #{COMPLETE} from #{EXECUTING}" unless executing?
217
-
218
- @state = COMPLETE
96
+ # @return [Boolean]
97
+ def interrupted?
98
+ @signal.interrupted?
219
99
  end
220
100
 
221
- # @raise [RuntimeError] When attempting to transition from invalid state
222
- #
223
- # @example
224
- # result.interrupt! # Transitions from executing to interrupted
225
- #
226
- # @rbs () -> void
227
- def interrupt!
228
- return if interrupted?
229
-
230
- raise "cannot transition to #{INTERRUPTED} from #{COMPLETE}" if complete?
231
-
232
- @state = INTERRUPTED
101
+ # @return [String] one of {Signal::STATUSES}
102
+ def status
103
+ @signal.status
233
104
  end
234
105
 
235
- STATUSES.each do |s|
236
- # @return [Boolean] Whether the result has the specified status
237
- #
238
- # @example
239
- # result.success? # => true
240
- # result.failed? # => false
241
- #
242
- # @rbs () -> bool
243
- define_method(:"#{s}?") { status == s }
106
+ # @return [Boolean]
107
+ def success?
108
+ @signal.success?
244
109
  end
245
110
 
246
- # @return [Boolean] Whether the task execution was successful (not failed)
247
- #
248
- # @example
249
- # result.good? # => true if !failed?
250
- #
251
- # @rbs () -> bool
252
- def good?
253
- !failed?
111
+ # @return [Boolean]
112
+ def skipped?
113
+ @signal.skipped?
254
114
  end
255
- alias ok? good?
256
115
 
257
- # @return [Boolean] Whether the task execution was unsuccessful (not success)
258
- #
259
- # @example
260
- # result.bad? # => true if !success?
261
- #
262
- # @rbs () -> bool
263
- def bad?
264
- !success?
116
+ # @return [Boolean]
117
+ def failed?
118
+ @signal.failed?
265
119
  end
266
120
 
267
- # @yield [self] Executes the block if task status or state matches
268
- #
269
- # @return [self] Returns self for method chaining
270
- #
271
- # @raise [ArgumentError] When no block is provided
272
- #
273
- # @example
274
- # result.on(:bad) { |r| puts "Task had issues: #{r.reason}" }
275
- # result.on(:success, :complete) { |r| puts "Task completed successfully" }
276
- #
277
- # @rbs () { (Result) -> void } -> self
278
- def on(*states_or_statuses, &)
279
- raise ArgumentError, "block required" unless block_given?
280
-
281
- yield(self) if states_or_statuses.any? { |s| public_send(:"#{s}?") }
282
- self
121
+ # @return [Boolean]
122
+ def ok?
123
+ @signal.ok?
283
124
  end
284
125
 
285
- # Sets a reason and optional metadata on a successful result without
286
- # changing its state or status. Useful for annotating why a task succeeded.
287
- # When halt is true, uses throw/catch to exit the work method early.
288
- #
289
- # @param reason [String, nil] Reason or note for the success
290
- # @param halt [Boolean] Whether to halt execution after success
291
- # @param metadata [Hash] Additional metadata about the success
292
- # @option metadata [Object] :* Any key-value pairs for additional metadata
293
- #
294
- # @raise [RuntimeError] When status is not success
295
- #
296
- # @example
297
- # result.success!("Created 42 records")
298
- # result.success!("Imported", halt: false, rows: 100)
299
- #
300
- # @rbs (?String? reason, halt: bool, **untyped metadata) -> void
301
- def success!(reason = nil, halt: true, **metadata)
302
- raise "can only be used while #{SUCCESS}" unless success?
303
-
304
- @reason = reason
305
- @metadata = metadata
306
-
307
- throw(:cmdx_halt) if halt
126
+ # @return [Boolean]
127
+ def ko?
128
+ @signal.ko?
308
129
  end
309
130
 
310
- # @param reason [String, nil] Reason for skipping the task
311
- # @param halt [Boolean] Whether to halt execution after skipping
312
- # @param cause [Exception, nil] Exception that caused the skip
313
- # @param strict [Boolean] Whether this skip is strict (default: true).
314
- # When false, {CMDx::Executor#halt_execution?} returns false regardless of task settings.
315
- # @param metadata [Hash] Additional metadata about the skip
316
- # @option metadata [Object] :* Any key-value pairs for additional metadata
131
+ # Dispatches the block when any of `keys` matches a truthy predicate on
132
+ # this result. Returns `self` for chaining.
317
133
  #
318
- # @raise [RuntimeError] When attempting to skip from invalid status
134
+ # @param keys [Array<Symbol, String>] any of the predicate bases:
135
+ # `complete`, `interrupted`, `success`, `skipped`, `failed`, `ok`, `ko`
136
+ # @yieldparam result [Result] this result
137
+ # @return [Result] self for chaining
138
+ # @raise [ArgumentError] when no block is given or a key is unknown
319
139
  #
320
140
  # @example
321
- # result.skip!("Dependencies not met", cause: dependency_error)
322
- # result.skip!("Already processed", halt: false)
323
- # result.skip!("Optional step", strict: false)
324
- #
325
- # @rbs (?String? reason, halt: bool, cause: Exception?, strict: bool, **untyped metadata) -> void
326
- def skip!(reason = nil, halt: true, cause: nil, strict: true, **metadata)
327
- return if skipped?
141
+ # result
142
+ # .on(:success) { |r| deliver(r.context) }
143
+ # .on(:failed) { |r| alert(r.reason) }
144
+ def on(*keys)
145
+ raise ArgumentError, "block required" unless block_given?
328
146
 
329
- raise "can only transition to #{SKIPPED} from #{SUCCESS}" unless success?
147
+ yield(self) if keys.any? do |k|
148
+ unless EVENTS.include?(k.to_sym)
149
+ raise ArgumentError,
150
+ "unknown event #{k.inspect}, must be one of #{EVENTS.join(', ')}"
151
+ end
330
152
 
331
- @state = INTERRUPTED
332
- @status = SKIPPED
333
- @reason = reason || Locale.t("cmdx.reasons.unspecified")
334
- @cause = cause
335
- @strict = strict
336
- @metadata = metadata
153
+ public_send(:"#{k}?")
154
+ end
337
155
 
338
- halt! if halt
156
+ self
339
157
  end
340
158
 
341
- # @param reason [String, nil] Reason for task failure
342
- # @param halt [Boolean] Whether to halt execution after failure
343
- # @param cause [Exception, nil] Exception that caused the failure
344
- # @param strict [Boolean] Whether this failure is strict (default: true).
345
- # When false, {CMDx::Executor#halt_execution?} returns false regardless of task settings.
346
- # @param metadata [Hash] Additional metadata about the failure
347
- # @option metadata [Object] :* Any key-value pairs for additional metadata
348
- #
349
- # @raise [RuntimeError] When attempting to fail from invalid status
350
- #
351
- # @example
352
- # result.fail!("Validation failed", cause: validation_error)
353
- # result.fail!("Network timeout", halt: false, timeout: 30)
354
- # result.fail!("Soft failure", strict: false)
355
- #
356
- # @rbs (?String? reason, halt: bool, cause: Exception?, strict: bool, **untyped metadata) -> void
357
- def fail!(reason = nil, halt: true, cause: nil, strict: true, **metadata)
358
- return if failed?
359
-
360
- raise "can only transition to #{FAILED} from #{SUCCESS}" unless success?
361
-
362
- @state = INTERRUPTED
363
- @status = FAILED
364
- @reason = reason || Locale.t("cmdx.reasons.unspecified")
365
- @cause = cause
366
- @strict = strict
367
- @metadata = metadata
368
-
369
- halt! if halt
159
+ # @return [String, nil]
160
+ def reason
161
+ @signal.reason
370
162
  end
371
163
 
372
- # @raise [SkipFault] When task was skipped
373
- # @raise [FailFault] When task failed
374
- #
375
- # @example
376
- # result.halt! # Raises appropriate fault based on status
377
- #
378
- # @rbs () -> void
379
- def halt!
380
- return if success?
381
-
382
- klass = skipped? ? SkipFault : FailFault
383
- fault = klass.new(self)
384
-
385
- # Strip the first two frames (this method and the delegator)
386
- frames = caller_locations(3..-1)
387
-
388
- unless frames.empty?
389
- frames = frames.map(&:to_s)
390
-
391
- if (cleaner = task.class.settings.backtrace_cleaner)
392
- cleaner.call(frames)
393
- end
394
-
395
- fault.set_backtrace(frames)
396
- end
397
-
398
- raise(fault)
164
+ # @return [Hash{Symbol => Object}] frozen empty hash when none provided
165
+ def metadata
166
+ @signal.metadata
399
167
  end
400
168
 
401
- # @param result [CMDx::Result] Result to throw from current result
402
- # @param halt [Boolean] Whether to halt execution after throwing
403
- # @param cause [Exception, nil] Exception that caused the throw
404
- # @param metadata [Hash] Additional metadata to merge
405
- # @option metadata [Object] :* Any key-value pairs for additional metadata
406
- #
407
- # @raise [TypeError] When result is not a CMDx::Result instance
169
+ # The upstream failed result this one was echoed from (via `Task#throw!`
170
+ # or a rescued {Fault} inside `work`). `nil` when this is a locally
171
+ # originated failure or the result didn't fail.
408
172
  #
409
- # @example
410
- # other_result = OtherTask.execute
411
- # result.throw!(other_result, cause: upstream_error)
412
- #
413
- # @rbs (Result result, halt: bool, cause: Exception?, **untyped metadata) -> void
414
- def throw!(result, halt: true, cause: nil, **metadata)
415
- raise TypeError, "must be a CMDx::Result" unless result.is_a?(Result)
416
-
417
- @state = result.state
418
- @status = result.status
419
- @reason = result.reason
420
- @cause = cause || result.cause
421
- @metadata = result.metadata.merge(metadata)
173
+ # @return [Result, nil]
174
+ def origin
175
+ @signal.origin
176
+ end
422
177
 
423
- halt! if halt
178
+ # @return [Exception, nil]
179
+ def cause
180
+ @signal.cause
424
181
  end
425
182
 
426
- # @return [CMDx::Result, nil] The result that caused this failure, or nil
427
- #
428
- # @example
429
- # cause = result.caused_failure
430
- # puts "Caused by: #{cause.task.id}" if cause
183
+ # The originating failed result at the bottom of the propagation chain.
184
+ # Walks `origin` recursively. `self` when this result is the originator;
185
+ # `nil` when not failed.
431
186
  #
432
- # @rbs () -> Result?
187
+ # @return [Result, nil]
433
188
  def caused_failure
434
189
  return unless failed?
435
190
 
436
- chain.results.reverse_each.find(&:failed?)
191
+ @caused_failure ||= origin ? origin.caused_failure : self
437
192
  end
438
193
 
439
- # @return [Boolean] Whether this result caused the failure
440
- #
441
- # @example
442
- # if result.caused_failure?
443
- # puts "This task caused the failure"
444
- # end
445
- #
446
- # @rbs () -> bool
194
+ # @return [Boolean] true when this result originated the failure chain
447
195
  def caused_failure?
448
- return false unless failed?
449
-
450
- caused_failure == self
196
+ failed? && origin.nil?
451
197
  end
452
198
 
453
- # @return [CMDx::Result, nil] The result that threw this failure, or nil
454
- #
455
- # @example
456
- # thrown = result.threw_failure
457
- # puts "Thrown by: #{thrown.task.id}" if thrown
199
+ # The nearest upstream failed result. `self` when this result is the
200
+ # originator; `nil` when not failed.
458
201
  #
459
- # @rbs () -> Result?
202
+ # @return [Result, nil]
460
203
  def threw_failure
461
204
  return unless failed?
462
205
 
463
- current = index
464
- last_failed = nil
465
-
466
- chain.results.each do |r|
467
- next unless r.failed?
468
-
469
- return r if r.index > current
470
-
471
- last_failed = r
472
- end
206
+ origin || self
207
+ end
473
208
 
474
- last_failed
209
+ # @return [Boolean] true when this result re-threw an upstream failure
210
+ def thrown_failure?
211
+ failed? && !origin.nil?
475
212
  end
476
213
 
477
- # @return [Boolean] Whether this result threw the failure
478
- #
479
- # @example
480
- # if result.threw_failure?
481
- # puts "This task threw the failure"
482
- # end
214
+ # The backtrace captured by `fail!` / `throw!` for Fault propagation.
215
+ # `nil` when this result is not a failure or the failure didn't capture
216
+ # a backtrace.
483
217
  #
484
- # @rbs () -> bool
485
- def threw_failure?
486
- return false unless failed?
487
-
488
- threw_failure == self
218
+ # @return [Array<String>, nil]
219
+ def backtrace
220
+ @signal.backtrace
489
221
  end
490
222
 
491
- # @return [Boolean] Whether this result is a thrown failure
492
- #
493
- # @example
494
- # if result.thrown_failure?
495
- # puts "This failure was thrown from another task"
496
- # end
497
- #
498
- # @rbs () -> bool
499
- def thrown_failure?
500
- failed? && !caused_failure?
223
+ # @return [Integer]
224
+ def retries
225
+ @options[:retries] || 0
501
226
  end
502
227
 
503
- # @return [Boolean] Whether the result has been retried
504
- #
505
- # @example
506
- # result.retried? # => true
507
- #
508
- # @rbs () -> bool
228
+ # @return [Boolean]
509
229
  def retried?
510
230
  retries.positive?
511
231
  end
512
232
 
513
- # @return [Boolean] Whether the result is strict
514
- #
515
- # @example
516
- # result.strict? # => true
517
- #
518
- # @rbs () -> bool
233
+ # @return [Boolean] true when produced via `execute!`
519
234
  def strict?
520
- !!@strict
235
+ !!@options[:strict]
521
236
  end
522
237
 
523
- # @return [Boolean] Whether the result has been rolled back
524
- #
525
- # @example
526
- # result.rolled_back? # => true
527
- #
528
- # @rbs () -> bool
238
+ # @return [Boolean] true when the task class is marked deprecated
239
+ def deprecated?
240
+ !!@options[:deprecated]
241
+ end
242
+
243
+ # @return [Boolean] true when a failing task's `rollback` ran
529
244
  def rolled_back?
530
- !!@rolled_back
245
+ !!@options[:rolled_back]
531
246
  end
532
247
 
533
- # @return [Integer] Index of this result in the chain
534
- #
535
- # @example
536
- # position = result.index
537
- # puts "Task #{position + 1} of #{chain.results.count}"
538
- #
539
- # @rbs () -> Integer
540
- def index
541
- @chain_index || chain.index(self)
248
+ # @return [Float, nil] lifecycle duration in milliseconds
249
+ def duration
250
+ @options[:duration]
542
251
  end
543
252
 
544
- # @return [String] The outcome of the task execution
545
- #
546
- # @example
547
- # result.outcome # => "success" or "interrupted"
548
- #
549
- # @rbs () -> String
550
- def outcome
551
- initialized? || thrown_failure? ? state : status
253
+ # @return [Array<Symbol, String>]
254
+ def tags
255
+ task.settings.tags
552
256
  end
553
257
 
554
- # @return [Hash] Hash representation of the result
555
- #
556
- # @example
557
- # result.to_h
558
- # # => {state: "complete", status: "success", outcome: "success", reason: "Unspecified", metadata: {}}
559
- #
560
- # @rbs () -> Hash[Symbol, untyped]
258
+ # @return [Hash{Symbol => Object}] memoized serialization. Includes
259
+ # `:cause`, `:origin`, `:threw_failure`, `:caused_failure`, `:rolled_back`
260
+ # on failure.
561
261
  def to_h
562
- task.to_h.merge!(
262
+ @to_h ||= {
263
+ xid:,
264
+ cid:,
265
+ index:,
266
+ root: root?,
267
+ type:,
268
+ task:,
269
+ tid:,
270
+ context:,
563
271
  state:,
564
272
  status:,
565
- outcome:,
566
273
  reason:,
567
- metadata:
568
- ).tap do |hash|
569
- if interrupted?
274
+ metadata:,
275
+ strict: strict?,
276
+ deprecated: deprecated?,
277
+ retried: retried?,
278
+ retries:,
279
+ duration:,
280
+ tags:
281
+ }.tap do |hash|
282
+ if failed?
570
283
  hash[:cause] = cause
284
+ hash[:origin] = hash_for_failure(:origin)
285
+ hash[:threw_failure] = hash_for_failure(:threw_failure)
286
+ hash[:caused_failure] = hash_for_failure(:caused_failure)
571
287
  hash[:rolled_back] = rolled_back?
572
288
  end
573
-
574
- if failed?
575
- STRIP_FAILURE.call(hash, self, :threw_failure)
576
- STRIP_FAILURE.call(hash, self, :caused_failure)
577
- end
578
289
  end
579
290
  end
580
291
 
581
- # @return [String] String representation of the result
292
+ # JSON-friendly hash view. Aliases the memoized {#to_h} for conventional
293
+ # `as_json` callers (e.g. Rails).
582
294
  #
583
- # @example
584
- # result.to_s # => "task_id=my_task state=complete status=success"
585
- # @example With failure
586
- # result.to_s # => "task_id=my_task state=complete status=failed threw_failure=<[1] MyTask: my_task>"
295
+ # @return [Hash{Symbol => Object}]
296
+ def as_json(*)
297
+ to_h
298
+ end
299
+
300
+ # Serializes the result to a JSON string. Non-primitive entries (the
301
+ # `:task` Class, `:cause` Exception) emit via their stdlib `to_json`
302
+ # defaults; `:context` delegates to {Context#to_json}.
587
303
  #
588
- # @rbs () -> String
304
+ # @param args [Array] forwarded to `Hash#to_json`
305
+ # @return [String]
306
+ def to_json(*args)
307
+ to_h.to_json(*args)
308
+ end
309
+
310
+ # @return [String] space-separated `key=value.inspect` pairs; failure
311
+ # references render as `<TaskClass uuid>`.
589
312
  def to_s
590
- Utils::Format.to_str(to_h) do |key, value|
591
- if FAILURE_KEY_REGEX.match?(key)
592
- "#{key}=<[#{value[:index]}] #{value[:class]}: #{value[:id]}>"
593
- else
594
- "#{key}=#{value.inspect}"
313
+ @to_s ||= begin
314
+ buf = String.new(capacity: 256)
315
+
316
+ to_h.each_with_object(buf) do |(k, v), buf|
317
+ buf << " " unless buf.empty?
318
+
319
+ ks = k.name
320
+
321
+ if v.nil?
322
+ buf << ks << "=nil"
323
+ elsif ks == "origin" || ks.end_with?("_failure")
324
+ buf << ks << "=<" << v[:task].to_s << " " << v[:tid] << ">"
325
+ else
326
+ buf << ks << "=" << v.inspect
327
+ end
595
328
  end
596
329
  end
597
330
  end
598
331
 
599
- # @return [Array] Array containing state, status, reason, cause, and metadata
332
+ # Pattern-matching support for `case result in {...}`.
600
333
  #
601
- # @example
602
- # state, status = result.deconstruct
603
- # puts "State: #{state}, Status: #{status}"
604
- #
605
- # @rbs (*untyped) -> Array[untyped]
606
- def deconstruct(*)
607
- [state, status, reason, cause, metadata]
334
+ # @param keys [Array<Symbol>, nil] restrict the returned hash to these keys
335
+ # @return [Hash{Symbol => Object}]
336
+ def deconstruct_keys(keys)
337
+ keys.nil? ? to_h : to_h.slice(*keys)
608
338
  end
609
339
 
610
- # @return [Hash] Hash with key-value pairs for pattern matching
340
+ # Pattern-matching support for `case result in [...]`.
611
341
  #
612
- # @example
613
- # case result.deconstruct_keys
614
- # in {state: "complete", good: true}
615
- # puts "Task completed successfully"
616
- # in {bad: true}
617
- # puts "Task had issues"
618
- # end
619
- #
620
- # @rbs (*untyped) -> Hash[Symbol, untyped]
621
- def deconstruct_keys(*)
622
- {
623
- state: state,
624
- status: status,
625
- reason: reason,
626
- cause: cause,
627
- metadata: metadata,
628
- outcome: outcome,
629
- executed: executed?,
630
- good: good?,
631
- bad: bad?
632
- }
342
+ # @return [Array<Array(Symbol, Object)>]
343
+ def deconstruct
344
+ to_h.to_a
345
+ end
346
+
347
+ private
348
+
349
+ # @param key [Symbol] reader name such as `:caused_failure` or `:threw_failure`
350
+ # @return [Hash{Symbol => Object}, nil] compact `{task:, tid:}` map for graph hints
351
+ def hash_for_failure(key)
352
+ r = public_send(key)
353
+ return if r.nil?
354
+
355
+ { task: r.task, tid: r.tid }
633
356
  end
634
357
 
635
358
  end