cmdx 1.20.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 +131 -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 -225
  23. data/lib/cmdx/context.rb +263 -242
  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 +252 -473
  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 -196
  64. data/lib/cmdx/signal.rb +165 -0
  65. data/lib/cmdx/task.rb +443 -336
  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 +74 -82
  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 +128 -52
  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 +9 -6
  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 -374
  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 -62
  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 -53
  112. data/lib/locales/ar.yml +0 -53
  113. data/lib/locales/az.yml +0 -53
  114. data/lib/locales/be.yml +0 -53
  115. data/lib/locales/bg.yml +0 -53
  116. data/lib/locales/bn.yml +0 -53
  117. data/lib/locales/bs.yml +0 -53
  118. data/lib/locales/ca.yml +0 -53
  119. data/lib/locales/cnr.yml +0 -53
  120. data/lib/locales/cs.yml +0 -53
  121. data/lib/locales/cy.yml +0 -53
  122. data/lib/locales/da.yml +0 -53
  123. data/lib/locales/de.yml +0 -53
  124. data/lib/locales/dz.yml +0 -53
  125. data/lib/locales/el.yml +0 -53
  126. data/lib/locales/eo.yml +0 -53
  127. data/lib/locales/es.yml +0 -53
  128. data/lib/locales/et.yml +0 -53
  129. data/lib/locales/eu.yml +0 -53
  130. data/lib/locales/fa.yml +0 -53
  131. data/lib/locales/fi.yml +0 -53
  132. data/lib/locales/fr.yml +0 -53
  133. data/lib/locales/fy.yml +0 -53
  134. data/lib/locales/gd.yml +0 -53
  135. data/lib/locales/gl.yml +0 -53
  136. data/lib/locales/he.yml +0 -53
  137. data/lib/locales/hi.yml +0 -53
  138. data/lib/locales/hr.yml +0 -53
  139. data/lib/locales/hu.yml +0 -53
  140. data/lib/locales/hy.yml +0 -53
  141. data/lib/locales/id.yml +0 -53
  142. data/lib/locales/is.yml +0 -53
  143. data/lib/locales/it.yml +0 -53
  144. data/lib/locales/ja.yml +0 -53
  145. data/lib/locales/ka.yml +0 -53
  146. data/lib/locales/kk.yml +0 -53
  147. data/lib/locales/km.yml +0 -53
  148. data/lib/locales/kn.yml +0 -53
  149. data/lib/locales/ko.yml +0 -53
  150. data/lib/locales/lb.yml +0 -53
  151. data/lib/locales/lo.yml +0 -53
  152. data/lib/locales/lt.yml +0 -53
  153. data/lib/locales/lv.yml +0 -53
  154. data/lib/locales/mg.yml +0 -53
  155. data/lib/locales/mk.yml +0 -53
  156. data/lib/locales/ml.yml +0 -53
  157. data/lib/locales/mn.yml +0 -53
  158. data/lib/locales/mr-IN.yml +0 -53
  159. data/lib/locales/ms.yml +0 -53
  160. data/lib/locales/nb.yml +0 -53
  161. data/lib/locales/ne.yml +0 -53
  162. data/lib/locales/nl.yml +0 -53
  163. data/lib/locales/nn.yml +0 -53
  164. data/lib/locales/oc.yml +0 -53
  165. data/lib/locales/or.yml +0 -53
  166. data/lib/locales/pa.yml +0 -53
  167. data/lib/locales/pl.yml +0 -53
  168. data/lib/locales/pt.yml +0 -53
  169. data/lib/locales/rm.yml +0 -53
  170. data/lib/locales/ro.yml +0 -53
  171. data/lib/locales/ru.yml +0 -53
  172. data/lib/locales/sc.yml +0 -53
  173. data/lib/locales/sk.yml +0 -53
  174. data/lib/locales/sl.yml +0 -53
  175. data/lib/locales/sq.yml +0 -53
  176. data/lib/locales/sr.yml +0 -53
  177. data/lib/locales/st.yml +0 -53
  178. data/lib/locales/sv.yml +0 -53
  179. data/lib/locales/sw.yml +0 -53
  180. data/lib/locales/ta.yml +0 -53
  181. data/lib/locales/te.yml +0 -53
  182. data/lib/locales/th.yml +0 -53
  183. data/lib/locales/tl.yml +0 -53
  184. data/lib/locales/tr.yml +0 -53
  185. data/lib/locales/tt.yml +0 -53
  186. data/lib/locales/ug.yml +0 -53
  187. data/lib/locales/uk.yml +0 -53
  188. data/lib/locales/ur.yml +0 -53
  189. data/lib/locales/uz.yml +0 -53
  190. data/lib/locales/vi.yml +0 -53
  191. data/lib/locales/wo.yml +0 -53
  192. data/lib/locales/zh-CN.yml +0 -53
  193. data/lib/locales/zh-HK.yml +0 -53
  194. data/lib/locales/zh-TW.yml +0 -53
  195. data/lib/locales/zh-YUE.yml +0 -53
data/lib/cmdx/result.rb CHANGED
@@ -1,579 +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
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
51
39
 
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
40
+ # @return [String] uuid_v7 identifier for this execution
41
+ def tid
42
+ @options[:tid]
43
+ end
61
44
 
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
45
+ # @return [Class<Task>] the task class that ran
46
+ def task
47
+ @task.class
48
+ end
71
49
 
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
50
+ # @return [String] `"Task"` or `"Workflow"`
51
+ def type
52
+ task.type
53
+ end
81
54
 
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
55
+ # @return [String, nil] correlation id or the global configuration's correlation id
56
+ def xid
57
+ chain.xid
58
+ end
91
59
 
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
60
+ # @return [String] uuid_v7 identifier for the chain this result belongs to
61
+ def cid
62
+ chain.id
63
+ end
101
64
 
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
65
+ # @return [Integer, nil] this result's position in the chain
66
+ def index
67
+ @chain.index(self)
68
+ end
111
69
 
112
- # Returns whether the result has been rolled back.
113
- #
114
- # @return [Boolean] Whether the result has been rolled back
115
- #
116
- # @example
117
- # result.rolled_back? # => true
118
- #
119
- # @rbs @rolled_back: bool
120
- 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
121
74
 
122
- 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
123
79
  alias ctx context
124
80
 
125
- # @param task [CMDx::Task] The task instance this result represents
126
- #
127
- # @return [CMDx::Result] A new result instance for the task
128
- #
129
- # @raise [TypeError] When task is not a CMDx::Task instance
130
- #
131
- # @example
132
- # result = CMDx::Result.new(my_task)
133
- # result.state # => "initialized"
134
- #
135
- # @rbs (Task) -> void
136
- def initialize(task)
137
- raise TypeError, "must be a CMDx::Task" unless task.is_a?(CMDx::Task)
138
-
139
- @task = task
140
- @state = INITIALIZED
141
- @status = SUCCESS
142
- @metadata = {}
143
- @reason = nil
144
- @cause = nil
145
- @retries = 0
146
- @rolled_back = false
147
- end
148
-
149
- STATES.each do |s|
150
- # @return [Boolean] Whether the result is in the specified state
151
- #
152
- # @example
153
- # result.initialized? # => true
154
- # result.executing? # => false
155
- #
156
- # @rbs () -> bool
157
- define_method(:"#{s}?") { state == s }
158
- end
159
-
160
- # @return [self] Returns self for method chaining
161
- #
162
- # @example
163
- # result.executed! # Transitions to complete or interrupted
164
- #
165
- # @rbs () -> self
166
- def executed!
167
- success? ? complete! : interrupt!
81
+ # @return [Errors] frozen by Runtime teardown
82
+ def errors
83
+ @task.errors
168
84
  end
169
85
 
170
- # @return [Boolean] Whether the task has been executed (complete or interrupted)
171
- #
172
- # @example
173
- # result.executed? # => true if complete? || interrupted?
174
- #
175
- # @rbs () -> bool
176
- def executed?
177
- complete? || interrupted?
86
+ # @return [String] one of {Signal::STATES}
87
+ def state
88
+ @signal.state
178
89
  end
179
90
 
180
- # @raise [RuntimeError] When attempting to transition from invalid state
181
- #
182
- # @example
183
- # result.executing! # Transitions from initialized to executing
184
- #
185
- # @rbs () -> void
186
- def executing!
187
- return if executing?
188
-
189
- raise "can only transition to #{EXECUTING} from #{INITIALIZED}" unless initialized?
190
-
191
- @state = EXECUTING
91
+ # @return [Boolean]
92
+ def complete?
93
+ @signal.complete?
192
94
  end
193
95
 
194
- # @raise [RuntimeError] When attempting to transition from invalid state
195
- #
196
- # @example
197
- # result.complete! # Transitions from executing to complete
198
- #
199
- # @rbs () -> void
200
- def complete!
201
- return if complete?
202
-
203
- raise "can only transition to #{COMPLETE} from #{EXECUTING}" unless executing?
204
-
205
- @state = COMPLETE
96
+ # @return [Boolean]
97
+ def interrupted?
98
+ @signal.interrupted?
206
99
  end
207
100
 
208
- # @raise [RuntimeError] When attempting to transition from invalid state
209
- #
210
- # @example
211
- # result.interrupt! # Transitions from executing to interrupted
212
- #
213
- # @rbs () -> void
214
- def interrupt!
215
- return if interrupted?
216
-
217
- raise "cannot transition to #{INTERRUPTED} from #{COMPLETE}" if complete?
218
-
219
- @state = INTERRUPTED
101
+ # @return [String] one of {Signal::STATUSES}
102
+ def status
103
+ @signal.status
220
104
  end
221
105
 
222
- STATUSES.each do |s|
223
- # @return [Boolean] Whether the result has the specified status
224
- #
225
- # @example
226
- # result.success? # => true
227
- # result.failed? # => false
228
- #
229
- # @rbs () -> bool
230
- define_method(:"#{s}?") { status == s }
106
+ # @return [Boolean]
107
+ def success?
108
+ @signal.success?
231
109
  end
232
110
 
233
- # @return [Boolean] Whether the task execution was successful (not failed)
234
- #
235
- # @example
236
- # result.good? # => true if !failed?
237
- #
238
- # @rbs () -> bool
239
- def good?
240
- !failed?
111
+ # @return [Boolean]
112
+ def skipped?
113
+ @signal.skipped?
241
114
  end
242
- alias ok? good?
243
115
 
244
- # @return [Boolean] Whether the task execution was unsuccessful (not success)
245
- #
246
- # @example
247
- # result.bad? # => true if !success?
248
- #
249
- # @rbs () -> bool
250
- def bad?
251
- !success?
116
+ # @return [Boolean]
117
+ def failed?
118
+ @signal.failed?
252
119
  end
253
120
 
254
- # @yield [self] Executes the block if task status or state matches
255
- #
256
- # @return [self] Returns self for method chaining
257
- #
258
- # @raise [ArgumentError] When no block is provided
259
- #
260
- # @example
261
- # result.on(:bad) { |r| puts "Task had issues: #{r.reason}" }
262
- # result.on(:success, :complete) { |r| puts "Task completed successfully" }
263
- #
264
- # @rbs () { (Result) -> void } -> self
265
- def on(*states_or_statuses, &)
266
- raise ArgumentError, "block required" unless block_given?
267
-
268
- yield(self) if states_or_statuses.any? { |s| public_send(:"#{s}?") }
269
- self
121
+ # @return [Boolean]
122
+ def ok?
123
+ @signal.ok?
270
124
  end
271
125
 
272
- # @param reason [String, nil] Reason for skipping the task
273
- # @param halt [Boolean] Whether to halt execution after skipping
274
- # @param cause [Exception, nil] Exception that caused the skip
275
- # @param metadata [Hash] Additional metadata about the skip
276
- # @option metadata [Object] :* Any key-value pairs for additional metadata
277
- #
278
- # @raise [RuntimeError] When attempting to skip from invalid status
279
- #
280
- # @example
281
- # result.skip!("Dependencies not met", cause: dependency_error)
282
- # result.skip!("Already processed", halt: false)
283
- #
284
- # @rbs (?String? reason, halt: bool, cause: Exception?, **untyped metadata) -> void
285
- def skip!(reason = nil, halt: true, cause: nil, **metadata)
286
- return if skipped?
287
-
288
- raise "can only transition to #{SKIPPED} from #{SUCCESS}" unless success?
289
-
290
- @state = INTERRUPTED
291
- @status = SKIPPED
292
- @reason = reason || Locale.t("cmdx.faults.unspecified")
293
- @cause = cause
294
- @metadata = metadata
295
-
296
- halt! if halt
126
+ # @return [Boolean]
127
+ def ko?
128
+ @signal.ko?
297
129
  end
298
130
 
299
- # @param reason [String, nil] Reason for task failure
300
- # @param halt [Boolean] Whether to halt execution after failure
301
- # @param cause [Exception, nil] Exception that caused the failure
302
- # @param metadata [Hash] Additional metadata about the failure
303
- # @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.
304
133
  #
305
- # @raise [RuntimeError] When attempting to fail 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
306
139
  #
307
140
  # @example
308
- # result.fail!("Validation failed", cause: validation_error)
309
- # result.fail!("Network timeout", halt: false, timeout: 30)
310
- #
311
- # @rbs (?String? reason, halt: bool, cause: Exception?, **untyped metadata) -> void
312
- def fail!(reason = nil, halt: true, cause: nil, **metadata)
313
- return if failed?
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?
314
146
 
315
- raise "can only transition to #{FAILED} 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
316
152
 
317
- @state = INTERRUPTED
318
- @status = FAILED
319
- @reason = reason || Locale.t("cmdx.faults.unspecified")
320
- @cause = cause
321
- @metadata = metadata
153
+ public_send(:"#{k}?")
154
+ end
322
155
 
323
- halt! if halt
156
+ self
324
157
  end
325
158
 
326
- # @raise [SkipFault] When task was skipped
327
- # @raise [FailFault] When task failed
328
- #
329
- # @example
330
- # result.halt! # Raises appropriate fault based on status
331
- #
332
- # @rbs () -> void
333
- def halt!
334
- return if success?
335
-
336
- klass = skipped? ? SkipFault : FailFault
337
- fault = klass.new(self)
338
-
339
- # Strip the first two frames (this method and the delegator)
340
- frames = caller_locations(3..-1)
341
-
342
- unless frames.empty?
343
- frames = frames.map(&:to_s)
344
-
345
- if (cleaner = task.class.settings.backtrace_cleaner)
346
- cleaner.call(frames)
347
- end
348
-
349
- fault.set_backtrace(frames)
350
- end
159
+ # @return [String, nil]
160
+ def reason
161
+ @signal.reason
162
+ end
351
163
 
352
- raise(fault)
164
+ # @return [Hash{Symbol => Object}] frozen empty hash when none provided
165
+ def metadata
166
+ @signal.metadata
353
167
  end
354
168
 
355
- # @param result [CMDx::Result] Result to throw from current result
356
- # @param halt [Boolean] Whether to halt execution after throwing
357
- # @param cause [Exception, nil] Exception that caused the throw
358
- # @param metadata [Hash] Additional metadata to merge
359
- # @option metadata [Object] :* Any key-value pairs for additional metadata
360
- #
361
- # @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.
362
172
  #
363
- # @example
364
- # other_result = OtherTask.execute
365
- # result.throw!(other_result, cause: upstream_error)
366
- #
367
- # @rbs (Result result, halt: bool, cause: Exception?, **untyped metadata) -> void
368
- def throw!(result, halt: true, cause: nil, **metadata)
369
- raise TypeError, "must be a CMDx::Result" unless result.is_a?(Result)
370
-
371
- @state = result.state
372
- @status = result.status
373
- @reason = result.reason
374
- @cause = cause || result.cause
375
- @metadata = result.metadata.merge(metadata)
173
+ # @return [Result, nil]
174
+ def origin
175
+ @signal.origin
176
+ end
376
177
 
377
- halt! if halt
178
+ # @return [Exception, nil]
179
+ def cause
180
+ @signal.cause
378
181
  end
379
182
 
380
- # @return [CMDx::Result, nil] The result that caused this failure, or nil
381
- #
382
- # @example
383
- # cause = result.caused_failure
384
- # 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.
385
186
  #
386
- # @rbs () -> Result?
187
+ # @return [Result, nil]
387
188
  def caused_failure
388
189
  return unless failed?
389
190
 
390
- chain.results.reverse_each.find(&:failed?)
191
+ @caused_failure ||= origin ? origin.caused_failure : self
391
192
  end
392
193
 
393
- # @return [Boolean] Whether this result caused the failure
394
- #
395
- # @example
396
- # if result.caused_failure?
397
- # puts "This task caused the failure"
398
- # end
399
- #
400
- # @rbs () -> bool
194
+ # @return [Boolean] true when this result originated the failure chain
401
195
  def caused_failure?
402
- return false unless failed?
403
-
404
- caused_failure == self
196
+ failed? && origin.nil?
405
197
  end
406
198
 
407
- # @return [CMDx::Result, nil] The result that threw this failure, or nil
408
- #
409
- # @example
410
- # thrown = result.threw_failure
411
- # 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.
412
201
  #
413
- # @rbs () -> Result?
202
+ # @return [Result, nil]
414
203
  def threw_failure
415
204
  return unless failed?
416
205
 
417
- current = index
418
- last_failed = nil
419
-
420
- chain.results.each do |r|
421
- next unless r.failed?
422
-
423
- return r if r.index > current
424
-
425
- last_failed = r
426
- end
206
+ origin || self
207
+ end
427
208
 
428
- last_failed
209
+ # @return [Boolean] true when this result re-threw an upstream failure
210
+ def thrown_failure?
211
+ failed? && !origin.nil?
429
212
  end
430
213
 
431
- # @return [Boolean] Whether this result threw the failure
432
- #
433
- # @example
434
- # if result.threw_failure?
435
- # puts "This task threw the failure"
436
- # 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.
437
217
  #
438
- # @rbs () -> bool
439
- def threw_failure?
440
- return false unless failed?
441
-
442
- threw_failure == self
218
+ # @return [Array<String>, nil]
219
+ def backtrace
220
+ @signal.backtrace
443
221
  end
444
222
 
445
- # @return [Boolean] Whether this result is a thrown failure
446
- #
447
- # @example
448
- # if result.thrown_failure?
449
- # puts "This failure was thrown from another task"
450
- # end
451
- #
452
- # @rbs () -> bool
453
- def thrown_failure?
454
- failed? && !caused_failure?
223
+ # @return [Integer]
224
+ def retries
225
+ @options[:retries] || 0
455
226
  end
456
227
 
457
- # @return [Boolean] Whether the result has been retried
458
- #
459
- # @example
460
- # result.retried? # => true
461
- #
462
- # @rbs () -> bool
228
+ # @return [Boolean]
463
229
  def retried?
464
230
  retries.positive?
465
231
  end
466
232
 
467
- # @return [Boolean] Whether the result has been rolled back
468
- #
469
- # @example
470
- # result.rolled_back? # => true
471
- #
472
- # @rbs () -> bool
233
+ # @return [Boolean] true when produced via `execute!`
234
+ def strict?
235
+ !!@options[:strict]
236
+ end
237
+
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
473
244
  def rolled_back?
474
- !!@rolled_back
245
+ !!@options[:rolled_back]
475
246
  end
476
247
 
477
- # @return [Integer] Index of this result in the chain
478
- #
479
- # @example
480
- # position = result.index
481
- # puts "Task #{position + 1} of #{chain.results.count}"
482
- #
483
- # @rbs () -> Integer
484
- def index
485
- @chain_index || chain.index(self)
248
+ # @return [Float, nil] lifecycle duration in milliseconds
249
+ def duration
250
+ @options[:duration]
486
251
  end
487
252
 
488
- # @return [String] The outcome of the task execution
489
- #
490
- # @example
491
- # result.outcome # => "success" or "interrupted"
492
- #
493
- # @rbs () -> String
494
- def outcome
495
- initialized? || thrown_failure? ? state : status
253
+ # @return [Array<Symbol, String>]
254
+ def tags
255
+ task.settings.tags
496
256
  end
497
257
 
498
- # @return [Hash] Hash representation of the result
499
- #
500
- # @example
501
- # result.to_h
502
- # # => {state: "complete", status: "success", outcome: "success", metadata: {}}
503
- #
504
- # @rbs () -> Hash[Symbol, untyped]
258
+ # @return [Hash{Symbol => Object}] memoized serialization. Includes
259
+ # `:cause`, `:origin`, `:threw_failure`, `:caused_failure`, `:rolled_back`
260
+ # on failure.
505
261
  def to_h
506
- task.to_h.merge!(
262
+ @to_h ||= {
263
+ xid:,
264
+ cid:,
265
+ index:,
266
+ root: root?,
267
+ type:,
268
+ task:,
269
+ tid:,
270
+ context:,
507
271
  state:,
508
272
  status:,
509
- outcome:,
510
- metadata:
511
- ).tap do |hash|
512
- if interrupted?
513
- hash[:reason] = reason
273
+ reason:,
274
+ metadata:,
275
+ strict: strict?,
276
+ deprecated: deprecated?,
277
+ retried: retried?,
278
+ retries:,
279
+ duration:,
280
+ tags:
281
+ }.tap do |hash|
282
+ if failed?
514
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)
515
287
  hash[:rolled_back] = rolled_back?
516
288
  end
517
-
518
- if failed?
519
- STRIP_FAILURE.call(hash, self, :threw_failure)
520
- STRIP_FAILURE.call(hash, self, :caused_failure)
521
- end
522
289
  end
523
290
  end
524
291
 
525
- # @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).
526
294
  #
527
- # @example
528
- # result.to_s # => "task_id=my_task state=complete status=success"
529
- # @example With failure
530
- # 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}.
531
303
  #
532
- # @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>`.
533
312
  def to_s
534
- Utils::Format.to_str(to_h) do |key, value|
535
- if FAILURE_KEY_REGEX.match?(key)
536
- "#{key}=<[#{value[:index]}] #{value[:class]}: #{value[:id]}>"
537
- else
538
- "#{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
539
328
  end
540
329
  end
541
330
  end
542
331
 
543
- # @return [Array] Array containing state, status, reason, cause, and metadata
332
+ # Pattern-matching support for `case result in {...}`.
544
333
  #
545
- # @example
546
- # state, status = result.deconstruct
547
- # puts "State: #{state}, Status: #{status}"
548
- #
549
- # @rbs (*untyped) -> Array[untyped]
550
- def deconstruct(*)
551
- [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)
552
338
  end
553
339
 
554
- # @return [Hash] Hash with key-value pairs for pattern matching
555
- #
556
- # @example
557
- # case result.deconstruct_keys
558
- # in {state: "complete", good: true}
559
- # puts "Task completed successfully"
560
- # in {bad: true}
561
- # puts "Task had issues"
562
- # end
340
+ # Pattern-matching support for `case result in [...]`.
563
341
  #
564
- # @rbs (*untyped) -> Hash[Symbol, untyped]
565
- def deconstruct_keys(*)
566
- {
567
- state: state,
568
- status: status,
569
- reason: reason,
570
- cause: cause,
571
- metadata: metadata,
572
- outcome: outcome,
573
- executed: executed?,
574
- good: good?,
575
- bad: bad?
576
- }
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 }
577
356
  end
578
357
 
579
358
  end