cmdx 0.5.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/.DS_Store +0 -0
  3. data/.cursor/rules/cursor-instructions.mdc +6 -0
  4. data/.rubocop.yml +16 -1
  5. data/.ruby-version +1 -1
  6. data/CHANGELOG.md +31 -1
  7. data/README.md +72 -25
  8. data/docs/ai_prompts.md +309 -0
  9. data/docs/basics/call.md +225 -14
  10. data/docs/basics/chain.md +271 -0
  11. data/docs/basics/context.md +232 -33
  12. data/docs/basics/setup.md +76 -12
  13. data/docs/callbacks.md +273 -0
  14. data/docs/configuration.md +158 -28
  15. data/docs/getting_started.md +134 -22
  16. data/docs/interruptions/exceptions.md +189 -11
  17. data/docs/interruptions/faults.md +187 -44
  18. data/docs/interruptions/halt.md +179 -35
  19. data/docs/logging.md +194 -53
  20. data/docs/middlewares.md +735 -0
  21. data/docs/outcomes/result.md +296 -10
  22. data/docs/outcomes/states.md +203 -31
  23. data/docs/outcomes/statuses.md +275 -30
  24. data/docs/parameters/coercions.md +402 -29
  25. data/docs/parameters/defaults.md +249 -25
  26. data/docs/parameters/definitions.md +238 -72
  27. data/docs/parameters/namespacing.md +250 -27
  28. data/docs/parameters/validations.md +193 -168
  29. data/docs/testing.md +550 -0
  30. data/docs/tips_and_tricks.md +95 -43
  31. data/docs/workflows.md +319 -0
  32. data/lib/cmdx/.DS_Store +0 -0
  33. data/lib/cmdx/callback.rb +69 -0
  34. data/lib/cmdx/callback_registry.rb +106 -0
  35. data/lib/cmdx/chain.rb +190 -0
  36. data/lib/cmdx/chain_inspector.rb +149 -0
  37. data/lib/cmdx/chain_serializer.rb +175 -0
  38. data/lib/cmdx/coercions/array.rb +37 -0
  39. data/lib/cmdx/coercions/big_decimal.rb +33 -0
  40. data/lib/cmdx/coercions/boolean.rb +41 -1
  41. data/lib/cmdx/coercions/complex.rb +31 -0
  42. data/lib/cmdx/coercions/date.rb +39 -0
  43. data/lib/cmdx/coercions/date_time.rb +39 -0
  44. data/lib/cmdx/coercions/float.rb +31 -0
  45. data/lib/cmdx/coercions/hash.rb +42 -0
  46. data/lib/cmdx/coercions/integer.rb +32 -0
  47. data/lib/cmdx/coercions/rational.rb +31 -0
  48. data/lib/cmdx/coercions/string.rb +31 -0
  49. data/lib/cmdx/coercions/time.rb +39 -0
  50. data/lib/cmdx/coercions/virtual.rb +31 -0
  51. data/lib/cmdx/configuration.rb +217 -9
  52. data/lib/cmdx/context.rb +173 -2
  53. data/lib/cmdx/core_ext/hash.rb +72 -0
  54. data/lib/cmdx/core_ext/module.rb +94 -0
  55. data/lib/cmdx/core_ext/object.rb +105 -0
  56. data/lib/cmdx/correlator.rb +217 -0
  57. data/lib/cmdx/error.rb +210 -8
  58. data/lib/cmdx/errors.rb +256 -1
  59. data/lib/cmdx/fault.rb +177 -2
  60. data/lib/cmdx/faults.rb +158 -2
  61. data/lib/cmdx/immutator.rb +121 -2
  62. data/lib/cmdx/lazy_struct.rb +261 -18
  63. data/lib/cmdx/log_formatters/json.rb +46 -0
  64. data/lib/cmdx/log_formatters/key_value.rb +46 -0
  65. data/lib/cmdx/log_formatters/line.rb +54 -0
  66. data/lib/cmdx/log_formatters/logstash.rb +64 -0
  67. data/lib/cmdx/log_formatters/pretty_json.rb +57 -0
  68. data/lib/cmdx/log_formatters/pretty_key_value.rb +51 -0
  69. data/lib/cmdx/log_formatters/pretty_line.rb +60 -0
  70. data/lib/cmdx/log_formatters/raw.rb +54 -0
  71. data/lib/cmdx/logger.rb +85 -0
  72. data/lib/cmdx/logger_ansi.rb +93 -7
  73. data/lib/cmdx/logger_serializer.rb +116 -0
  74. data/lib/cmdx/middleware.rb +74 -0
  75. data/lib/cmdx/middleware_registry.rb +106 -0
  76. data/lib/cmdx/middlewares/correlate.rb +266 -0
  77. data/lib/cmdx/middlewares/timeout.rb +232 -0
  78. data/lib/cmdx/parameter.rb +228 -1
  79. data/lib/cmdx/parameter_inspector.rb +61 -0
  80. data/lib/cmdx/parameter_registry.rb +125 -0
  81. data/lib/cmdx/parameter_serializer.rb +83 -0
  82. data/lib/cmdx/parameter_validator.rb +62 -0
  83. data/lib/cmdx/parameter_value.rb +109 -1
  84. data/lib/cmdx/parameters_inspector.rb +59 -0
  85. data/lib/cmdx/parameters_serializer.rb +102 -0
  86. data/lib/cmdx/railtie.rb +123 -3
  87. data/lib/cmdx/result.rb +367 -25
  88. data/lib/cmdx/result_ansi.rb +105 -9
  89. data/lib/cmdx/result_inspector.rb +76 -0
  90. data/lib/cmdx/result_logger.rb +90 -3
  91. data/lib/cmdx/result_serializer.rb +137 -0
  92. data/lib/cmdx/rspec/result_matchers.rb +917 -0
  93. data/lib/cmdx/rspec/task_matchers.rb +570 -0
  94. data/lib/cmdx/task.rb +405 -37
  95. data/lib/cmdx/task_serializer.rb +74 -2
  96. data/lib/cmdx/utils/ansi_color.rb +95 -0
  97. data/lib/cmdx/utils/log_timestamp.rb +48 -0
  98. data/lib/cmdx/utils/monotonic_runtime.rb +71 -4
  99. data/lib/cmdx/utils/name_affix.rb +78 -0
  100. data/lib/cmdx/validators/custom.rb +82 -0
  101. data/lib/cmdx/validators/exclusion.rb +94 -0
  102. data/lib/cmdx/validators/format.rb +102 -8
  103. data/lib/cmdx/validators/inclusion.rb +104 -0
  104. data/lib/cmdx/validators/length.rb +128 -0
  105. data/lib/cmdx/validators/numeric.rb +128 -0
  106. data/lib/cmdx/validators/presence.rb +93 -7
  107. data/lib/cmdx/version.rb +7 -1
  108. data/lib/cmdx/workflow.rb +394 -0
  109. data/lib/cmdx.rb +25 -64
  110. data/lib/generators/cmdx/install_generator.rb +37 -1
  111. data/lib/generators/cmdx/task_generator.rb +69 -1
  112. data/lib/generators/cmdx/templates/install.rb +8 -12
  113. data/lib/generators/cmdx/workflow_generator.rb +109 -0
  114. metadata +54 -15
  115. data/docs/basics/run.md +0 -34
  116. data/docs/batch.md +0 -53
  117. data/docs/example.md +0 -82
  118. data/docs/hooks.md +0 -62
  119. data/lib/cmdx/batch.rb +0 -43
  120. data/lib/cmdx/parameters.rb +0 -35
  121. data/lib/cmdx/run.rb +0 -39
  122. data/lib/cmdx/run_inspector.rb +0 -26
  123. data/lib/cmdx/run_serializer.rb +0 -20
  124. data/lib/cmdx/task_hook.rb +0 -18
  125. data/lib/generators/cmdx/batch_generator.rb +0 -30
  126. /data/lib/generators/cmdx/templates/{batch.rb.tt → workflow.rb.tt} +0 -0
data/lib/cmdx/task.rb CHANGED
@@ -1,9 +1,109 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CMDx
4
+ ##
5
+ # Task is the base class for all command objects in CMDx, providing a framework
6
+ # for encapsulating business logic with parameter validation, callbacks, and result tracking.
7
+ #
8
+ # Tasks follow a single-use pattern where each instance can only be executed once,
9
+ # after which it becomes frozen and immutable. This ensures predictable execution
10
+ # and prevents side effects from multiple calls.
11
+ #
12
+ # @example Basic task definition
13
+ # class ProcessOrderTask < CMDx::Task
14
+ # required :order_id, type: :integer
15
+ # optional :notify_user, type: :boolean, default: true
16
+ #
17
+ # def call
18
+ # # Business logic here
19
+ # context.order = Order.find(order_id)
20
+ # skip!(reason: "Order already processed") if context.order.processed?
21
+ #
22
+ # context.order.process!
23
+ # NotificationService.call(order_id: order_id) if notify_user
24
+ # end
25
+ # end
26
+ #
27
+ # @example Task execution
28
+ # result = ProcessOrderTask.call(order_id: 123, notify_user: false)
29
+ # result.success? #=> true
30
+ # result.context.order #=> <Order id: 123>
31
+ #
32
+ # @example Task chaining with Result objects
33
+ # # First task extracts data
34
+ # class ExtractDataTask < CMDx::Task
35
+ # required :source_id, type: :integer
36
+ #
37
+ # def call
38
+ # context.extracted_data = DataSource.extract(source_id)
39
+ # context.extraction_time = Time.now
40
+ # end
41
+ # end
42
+ #
43
+ # # Second task processes the extracted data
44
+ # class ProcessDataTask < CMDx::Task
45
+ # def call
46
+ # # Access data from previous task's context
47
+ # fail!(reason: "No data to process") unless context.extracted_data
48
+ #
49
+ # context.processed_data = DataProcessor.process(context.extracted_data)
50
+ # context.processing_time = Time.now
51
+ # end
52
+ # end
53
+ #
54
+ # # Chain tasks by passing Result objects
55
+ # extraction_result = ExtractDataTask.call(source_id: 123)
56
+ # processing_result = ProcessDataTask.call(extraction_result)
57
+ #
58
+ # # Result object context is automatically extracted
59
+ # processing_result.context.extracted_data #=> data from first task
60
+ # processing_result.context.processed_data #=> data from second task
61
+ #
62
+ # @example Using callbacks
63
+ # class ProcessOrderTask < CMDx::Task
64
+ # before_validation :log_start
65
+ # after_execution :cleanup_resources
66
+ # on_success :send_confirmation
67
+ # on_failure :alert_support, if: :critical_order?
68
+ #
69
+ # def call
70
+ # # Implementation
71
+ # end
72
+ #
73
+ # private
74
+ #
75
+ # def critical_order?
76
+ # context.order.value > 10_000
77
+ # end
78
+ # end
79
+ #
80
+ # == Task Chaining and Data Flow
81
+ #
82
+ # Tasks can be seamlessly chained by passing Result objects between them.
83
+ # This enables powerful workflows where the output of one task becomes the
84
+ # input for the next, maintaining data consistency and enabling complex
85
+ # business logic composition.
86
+ #
87
+ # Benefits of Result object chaining:
88
+ # - Automatic context extraction and data flow
89
+ # - Preserves all context data including custom attributes
90
+ # - Maintains execution chain relationships
91
+ # - Enables conditional task execution based on previous results
92
+ # - Simplifies error handling and rollback scenarios
93
+ #
94
+ # @see Result Result object for execution outcomes
95
+ # @see Context Context object for parameter access
96
+ # @see ParameterRegistry Parameter definition and validation
97
+ # @see Workflow Workflow for executing multiple tasks
98
+ # @since 1.0.0
4
99
  class Task
5
100
 
6
- HOOKS = [
101
+ ##
102
+ # Available callback types for task lifecycle events.
103
+ # Callbacks are executed in a specific order during task execution.
104
+ #
105
+ # @return [Array<Symbol>] frozen array of available callback names
106
+ CALLBACKS = [
7
107
  :before_validation,
8
108
  :after_validation,
9
109
  :before_execution,
@@ -15,112 +115,370 @@ module CMDx
15
115
  *Result::STATES.map { |s| :"on_#{s}" }
16
116
  ].freeze
17
117
 
18
- __cmdx_attr_setting :task_settings, default: -> { CMDx.configuration.to_h.merge(tags: []) }
19
- __cmdx_attr_setting :cmd_parameters, default: -> { Parameters.new }
20
- __cmdx_attr_setting :cmd_hooks, default: {}
21
-
22
- __cmdx_attr_delegator :task_setting, :task_setting?, to: :class
23
- __cmdx_attr_delegator :skip!, :fail!, :throw!, to: :result
24
-
25
- attr_reader :id, :errors, :context, :result, :run
118
+ __cmdx_attr_setting :task_settings,
119
+ default: -> { CMDx.configuration.to_h.merge(tags: []) }
120
+ __cmdx_attr_setting :cmd_middlewares,
121
+ default: -> { MiddlewareRegistry.new(CMDx.configuration.middlewares) }
122
+ __cmdx_attr_setting :cmd_callbacks,
123
+ default: -> { CallbackRegistry.new(CMDx.configuration.callbacks) }
124
+ __cmdx_attr_setting :cmd_parameters,
125
+ default: -> { ParameterRegistry.new }
126
+
127
+ __cmdx_attr_delegator :cmd_middlewares, :cmd_callbacks, :cmd_parameters, :task_setting, :task_setting?,
128
+ to: :class
129
+ __cmdx_attr_delegator :skip!, :fail!, :throw!,
130
+ to: :result
131
+
132
+ ##
133
+ # @!attribute [r] context
134
+ # @return [Context] parameter context for this task execution
135
+ attr_reader :context
136
+
137
+ ##
138
+ # @!attribute [r] errors
139
+ # @return [Errors] collection of validation and execution errors
140
+ attr_reader :errors
141
+
142
+ ##
143
+ # @!attribute [r] id
144
+ # @return [String] unique identifier for this task instance
145
+ attr_reader :id
146
+
147
+ ##
148
+ # @!attribute [r] result
149
+ # @return [Result] execution result tracking state and status
150
+ attr_reader :result
151
+
152
+ ##
153
+ # @!attribute [r] chain
154
+ # @return [Chain] execution chain containing this task and related executions
155
+ attr_reader :chain
156
+
157
+ # @return [Context] alias for context
26
158
  alias ctx context
27
- alias res result
28
159
 
29
- private_class_method :new
160
+ # @return [Result] alias for result
161
+ alias res result
30
162
 
163
+ ##
164
+ # Initializes a new task instance with the given context parameters.
165
+ #
166
+ # The context can be provided as a Hash, Context object, or Result object.
167
+ # When a Result object is passed, its context is automatically extracted,
168
+ # enabling seamless task chaining and data flow between tasks.
169
+ #
170
+ # @param context [Hash, Context, Result] parameters and configuration for task execution
171
+ # @example With hash parameters
172
+ # task = ProcessOrderTask.new(order_id: 123, notify_user: true)
173
+ #
174
+ # @example With Result object (task chaining)
175
+ # extraction_result = ExtractDataTask.call(source_id: 456)
176
+ # processing_task = ProcessDataTask.new(extraction_result)
177
+ # # Context from extraction_result is automatically extracted
31
178
  def initialize(context = {})
32
- @id = SecureRandom.uuid
33
- @errors = Errors.new
179
+ context = context.context if context.respond_to?(:context)
180
+
34
181
  @context = Context.build(context)
35
- @run = @context.run || begin
36
- run = Run.new(@context.delete!(:run).to_h)
37
- @context.instance_variable_set(:@run, run)
38
- end
39
- @run.results << @result = Result.new(self)
182
+ @errors = Errors.new
183
+ @id = CMDx::Correlator.generate
184
+ @result = Result.new(self)
185
+ @chain = Chain.build(@result)
40
186
  end
41
187
 
42
188
  class << self
43
189
 
44
- HOOKS.each do |hook|
45
- define_method(hook) do |*callables, **options, &block|
46
- callables << block if block_given?
47
- (cmd_hooks[hook] ||= []).push([callables, options]).uniq!
190
+ ##
191
+ # Dynamically defines callback methods for each available callback type.
192
+ # Each callback method accepts callables and options for conditional execution.
193
+ #
194
+ # @example Callback with method name
195
+ # before_validation :validate_permissions
196
+ #
197
+ # @example Callback with proc
198
+ # on_success -> { logger.info "Task completed successfully" }
199
+ #
200
+ # @example Callback with conditions
201
+ # on_failure :alert_support, if: :critical_error?
202
+ # after_execution :cleanup, unless: :skip_cleanup?
203
+ #
204
+ # @param callables [Array<Symbol, Proc, #call>] methods or callables to execute
205
+ # @param options [Hash] conditions for callback execution
206
+ # @option options [Symbol, Proc, #call] :if condition that must be truthy
207
+ # @option options [Symbol, Proc, #call] :unless condition that must be falsy
208
+ # @param block [Proc] block to execute as part of the callback
209
+ # @return [Array] updated callbacks array
210
+ CALLBACKS.each do |callback|
211
+ define_method(callback) do |*callables, **options, &block|
212
+ cmd_callbacks.register(callback, *callables, **options, &block)
48
213
  end
49
214
  end
50
215
 
216
+ ##
217
+ # Retrieves a task setting value, evaluating it if it's a callable.
218
+ #
219
+ # @param key [Symbol, String] setting key to retrieve
220
+ # @return [Object] the setting value
221
+ # @example
222
+ # task_setting(:timeout) #=> 30
51
223
  def task_setting(key)
52
224
  __cmdx_yield(task_settings[key])
53
225
  end
54
226
 
227
+ ##
228
+ # Checks if a task setting exists.
229
+ #
230
+ # @param key [Symbol, String] setting key to check
231
+ # @return [Boolean] true if setting exists
55
232
  def task_setting?(key)
56
233
  task_settings.key?(key)
57
234
  end
58
235
 
236
+ ##
237
+ # Updates task settings with new options.
238
+ #
239
+ # @param options [Hash] settings to merge
240
+ # @return [Hash] updated settings
241
+ # @example
242
+ # task_settings!(timeout: 60, retries: 3)
59
243
  def task_settings!(**options)
60
244
  task_settings.merge!(options)
61
245
  end
62
246
 
247
+ ##
248
+ # Adds middleware to the task execution stack.
249
+ #
250
+ # Middleware can wrap task execution to provide cross-cutting concerns
251
+ # like logging, authentication, caching, or error handling.
252
+ #
253
+ # @param middleware [Class, Object, Proc] middleware to add
254
+ # @param args [Array] arguments for middleware instantiation
255
+ # @param block [Proc] block for middleware instantiation
256
+ # @return [MiddlewareRegistry] updated middleware registry
257
+ # @example
258
+ # use LoggingMiddleware
259
+ # use AuthenticationMiddleware, "admin"
260
+ # use CachingMiddleware.new(ttl: 300)
261
+ def use(middleware, ...)
262
+ cmd_middlewares.use(middleware, ...)
263
+ end
264
+
265
+ ##
266
+ # Registers callbacks for the task execution lifecycle.
267
+ #
268
+ # Callbacks can observe or modify task execution at specific lifecycle
269
+ # points like before validation, on success, after execution, etc.
270
+ #
271
+ # @param callback [Symbol] The callback type to register for
272
+ # @param callables [Array<Symbol, Proc, Callback, #call>] Methods, callables, or Callback instances to execute
273
+ # @param options [Hash] Conditions for callback execution
274
+ # @option options [Symbol, Proc, #call] :if condition that must be truthy
275
+ # @option options [Symbol, Proc, #call] :unless condition that must be falsy
276
+ # @param block [Proc] Block to execute as part of the callback
277
+ # @return [CallbackRegistry] updated callback registry
278
+ # @example
279
+ # register :before_execution, LoggingCallback.new(:debug)
280
+ # register :on_success, NotificationCallback.new([:email, :slack])
281
+ # register :on_failure, :alert_admin, if: :critical?
282
+ def register(callback, ...)
283
+ cmd_callbacks.register(callback, ...)
284
+ end
285
+
286
+ ##
287
+ # Defines optional parameters for the task.
288
+ #
289
+ # @param attributes [Array<Symbol>] parameter names
290
+ # @param options [Hash] parameter options (type, default, validations, etc.)
291
+ # @param block [Proc] block for nested parameter definitions
292
+ # @return [ParameterRegistry] updated parameters collection
293
+ # @example
294
+ # optional :timeout, type: :integer, default: 30
295
+ # optional :options, type: :hash do
296
+ # required :api_key, type: :string
297
+ # end
63
298
  def optional(*attributes, **options, &)
64
299
  parameters = Parameter.optional(*attributes, **options.merge(klass: self), &)
65
300
  cmd_parameters.concat(parameters)
66
301
  end
67
302
 
303
+ ##
304
+ # Defines required parameters for the task.
305
+ #
306
+ # @param attributes [Array<Symbol>] parameter names
307
+ # @param options [Hash] parameter options (type, validations, etc.)
308
+ # @param block [Proc] block for nested parameter definitions
309
+ # @return [ParameterRegistry] updated parameters collection
310
+ # @example
311
+ # required :user_id, type: :integer
312
+ # required :profile, type: :hash do
313
+ # required :name, type: :string
314
+ # optional :age, type: :integer
315
+ # end
68
316
  def required(*attributes, **options, &)
69
317
  parameters = Parameter.required(*attributes, **options.merge(klass: self), &)
70
318
  cmd_parameters.concat(parameters)
71
319
  end
72
320
 
321
+ ##
322
+ # Executes the task with the given parameters, returning a result object.
323
+ # This method handles all exceptions and ensures the task completes properly.
324
+ #
325
+ # Parameters can be provided as a Hash, Context object, or Result object.
326
+ # When a Result object is passed, its context is automatically extracted,
327
+ # enabling seamless task chaining.
328
+ #
329
+ # @param args [Array] arguments passed to task initialization
330
+ # @return [Result] execution result with state and status information
331
+ # @example With hash parameters
332
+ # result = ProcessOrderTask.call(order_id: 123)
333
+ # result.success? #=> true or false
334
+ # result.context.order #=> processed order
335
+ #
336
+ # @example With Result object (task chaining)
337
+ # extraction_result = ExtractDataTask.call(source_id: 456)
338
+ # processing_result = ProcessDataTask.call(extraction_result)
339
+ # # Context from extraction_result is automatically used
340
+ # processing_result.context.source_id #=> 456
73
341
  def call(...)
74
- instance = send(:new, ...)
75
- instance.send(:execute_call)
342
+ instance = new(...)
343
+ instance.perform
76
344
  instance.result
77
345
  end
78
346
 
347
+ ##
348
+ # Executes the task with the given parameters, raising exceptions for failures.
349
+ # This method is useful in background jobs where retries are handled via exceptions.
350
+ #
351
+ # Parameters can be provided as a Hash, Context object, or Result object.
352
+ # When a Result object is passed, its context is automatically extracted,
353
+ # enabling seamless task chaining with exception propagation.
354
+ #
355
+ # @param args [Array] arguments passed to task initialization
356
+ # @return [Result] execution result if successful
357
+ # @raise [Fault] if task fails and task_halt includes the failure status
358
+ # @example With hash parameters
359
+ # begin
360
+ # result = ProcessOrderTask.call!(order_id: 123)
361
+ # rescue CMDx::Failed => e
362
+ # # Handle failure
363
+ # end
364
+ #
365
+ # @example With Result object (task chaining)
366
+ # begin
367
+ # extraction_result = ExtractDataTask.call!(source_id: 456)
368
+ # processing_result = ProcessDataTask.call!(extraction_result)
369
+ # rescue CMDx::Failed => e
370
+ # # Handle failure from either task
371
+ # end
79
372
  def call!(...)
80
- instance = send(:new, ...)
81
- instance.send(:execute_call!)
373
+ instance = new(...)
374
+ instance.perform!
82
375
  instance.result
83
376
  end
84
377
 
85
378
  end
86
379
 
380
+ ##
381
+ # The main execution method that must be implemented by subclasses.
382
+ # This method contains the core business logic of the task.
383
+ #
384
+ # @abstract Subclasses must implement this method
385
+ # @return [void]
386
+ # @raise [UndefinedCallError] if not implemented in subclass
387
+ # @example
388
+ # def call
389
+ # context.user = User.find(user_id)
390
+ # fail!(reason: "User not found") unless context.user
391
+ #
392
+ # context.user.activate!
393
+ # context.activation_date = Time.now
394
+ # end
87
395
  def call
88
396
  raise UndefinedCallError, "call method not defined in #{self.class.name}"
89
397
  end
90
398
 
399
+ ##
400
+ # Executes the task with full exception handling for the non-bang call method.
401
+ # Captures all exceptions and converts them to appropriate result states.
402
+ #
403
+ # @return [void]
404
+ def perform
405
+ return execute_call if cmd_middlewares.empty?
406
+
407
+ cmd_middlewares.call(self) { |task| task.send(:execute_call) }
408
+ end
409
+
410
+ ##
411
+ # Executes the task with exception propagation for the bang call method.
412
+ # Allows exceptions to bubble up for external handling.
413
+ #
414
+ # @return [void]
415
+ # @raise [Fault] if task fails and task_halt includes the failure status
416
+ def perform!
417
+ return execute_call! if cmd_middlewares.empty?
418
+
419
+ cmd_middlewares.call(self) { |task| task.send(:execute_call!) }
420
+ end
421
+
91
422
  private
92
423
 
424
+ ##
425
+ # Returns the logger instance for this task.
426
+ #
427
+ # @return [Logger] configured logger instance
428
+ # @api private
93
429
  def logger
94
430
  Logger.call(self)
95
431
  end
96
432
 
433
+ ##
434
+ # Executes before-call callbacks and validations.
435
+ # Sets up the execution context and validates parameters.
436
+ #
437
+ # @return [void]
438
+ # @api private
97
439
  def before_call
98
- TaskHook.call(self, :before_execution)
440
+ cmd_callbacks.call(self, :before_execution)
99
441
 
100
442
  result.executing!
101
- TaskHook.call(self, :on_executing)
443
+ cmd_callbacks.call(self, :on_executing)
102
444
 
103
- TaskHook.call(self, :before_validation)
445
+ cmd_callbacks.call(self, :before_validation)
104
446
  ParameterValidator.call(self)
105
- TaskHook.call(self, :after_validation)
447
+ cmd_callbacks.call(self, :after_validation)
106
448
  end
107
449
 
450
+ ##
451
+ # Executes after-call callbacks based on execution results.
452
+ # Handles state and status transitions with appropriate callbacks.
453
+ #
454
+ # @return [void]
455
+ # @api private
108
456
  def after_call
109
- TaskHook.call(self, :"on_#{result.state}")
110
- TaskHook.call(self, :on_executed) if result.executed?
457
+ cmd_callbacks.call(self, :"on_#{result.state}")
458
+ cmd_callbacks.call(self, :on_executed) if result.executed?
111
459
 
112
- TaskHook.call(self, :"on_#{result.status}")
113
- TaskHook.call(self, :on_good) if result.good?
114
- TaskHook.call(self, :on_bad) if result.bad?
460
+ cmd_callbacks.call(self, :"on_#{result.status}")
461
+ cmd_callbacks.call(self, :on_good) if result.good?
462
+ cmd_callbacks.call(self, :on_bad) if result.bad?
115
463
 
116
- TaskHook.call(self, :after_execution)
464
+ cmd_callbacks.call(self, :after_execution)
117
465
  end
118
466
 
467
+ ##
468
+ # Finalizes task execution by freezing the task and logging results.
469
+ #
470
+ # @return [void]
471
+ # @api private
119
472
  def terminate_call
120
473
  Immutator.call(self)
121
474
  ResultLogger.call(result)
122
475
  end
123
476
 
477
+ ##
478
+ # Executes the task directly without middleware for the non-bang call method.
479
+ #
480
+ # @return [void]
481
+ # @api private
124
482
  def execute_call
125
483
  result.runtime do
126
484
  before_call
@@ -139,15 +497,25 @@ module CMDx
139
497
  terminate_call
140
498
  end
141
499
 
500
+ ##
501
+ # Executes the task directly without middleware for the bang call method.
502
+ #
503
+ # @return [void]
504
+ # @api private
142
505
  def execute_call!
143
506
  result.runtime do
144
507
  before_call
145
508
  call
146
509
  rescue UndefinedCallError => e
510
+ Chain.clear
147
511
  raise(e)
148
512
  rescue Fault => e
149
513
  result.executed!
150
- raise(e) if Array(task_setting(:task_halt)).include?(e.result.status)
514
+
515
+ if Array(task_setting(:task_halt)).include?(e.result.status)
516
+ Chain.clear
517
+ raise(e)
518
+ end
151
519
 
152
520
  after_call # HACK: treat as NO-OP
153
521
  else
@@ -1,15 +1,87 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CMDx
4
+ ##
5
+ # TaskSerializer converts task instances into hash representations for
6
+ # logging, debugging, and serialization purposes. It extracts key metadata
7
+ # about the task execution context and identification.
8
+ #
9
+ # The serialized format includes:
10
+ # - Execution index within the chain
11
+ # - Chain identifier for grouping related tasks
12
+ # - Task type (Task vs Workflow)
13
+ # - Class name for identification
14
+ # - Unique task instance ID
15
+ # - Associated tags for categorization
16
+ #
17
+ # @example Basic serialization
18
+ # class ProcessOrderTask < CMDx::Task
19
+ # task_settings!(tags: [:order, :payment])
20
+ # end
21
+ #
22
+ # task = ProcessOrderTask.call(order_id: 123)
23
+ # TaskSerializer.call(task)
24
+ # #=> {
25
+ # # index: 1,
26
+ # # chain_id: "abc123...",
27
+ # # type: "Task",
28
+ # # class: "ProcessOrderTask",
29
+ # # id: "def456...",
30
+ # # tags: [:order, :payment]
31
+ # # }
32
+ #
33
+ # @example Workflow serialization
34
+ # class OrderProcessingWorkflow < CMDx::Workflow
35
+ # task_settings!(tags: [:workflow, :orders])
36
+ # end
37
+ #
38
+ # workflow = OrderProcessingWorkflow.call(orders: [...])
39
+ # TaskSerializer.call(workflow)
40
+ # #=> {
41
+ # # index: 1,
42
+ # # chain_id: "abc123...",
43
+ # # type: "Workflow",
44
+ # # class: "OrderProcessingWorkflow",
45
+ # # id: "ghi789...",
46
+ # # tags: [:workflow, :orders]
47
+ # # }
48
+ #
49
+ # @see Task Task class for execution context
50
+ # @see Workflow Workflow class for multi-task execution
51
+ # @see Chain Chain class for execution grouping
52
+ # @since 1.0.0
4
53
  module TaskSerializer
5
54
 
6
55
  module_function
7
56
 
57
+ ##
58
+ # Serializes a task instance into a hash representation containing
59
+ # essential metadata for identification and tracking.
60
+ #
61
+ # @param task [Task, Workflow] the task instance to serialize
62
+ # @return [Hash] serialized task data with the following keys:
63
+ # - :index [Integer] position within the execution chain
64
+ # - :chain_id [String] identifier of the containing chain
65
+ # - :type [String] "Task" or "Workflow" based on instance type
66
+ # - :class [String] class name of the task
67
+ # - :id [String] unique identifier for this task instance
68
+ # - :tags [Array] array of tags associated with the task
69
+ #
70
+ # @example Serializing a task
71
+ # task = MyTask.call(param: "value")
72
+ # data = TaskSerializer.call(task)
73
+ # data[:class] #=> "MyTask"
74
+ # data[:type] #=> "Task"
75
+ # data[:id] #=> "550e8400-e29b-41d4-a716-446655440000"
76
+ #
77
+ # @example Using serialized data for logging
78
+ # task_data = TaskSerializer.call(task)
79
+ # logger.info("Task executed", task_data)
8
80
  def call(task)
9
81
  {
10
82
  index: task.result.index,
11
- run_id: task.run.id,
12
- type: task.is_a?(Batch) ? "Batch" : "Task",
83
+ chain_id: task.chain.id,
84
+ type: task.is_a?(Workflow) ? "Workflow" : "Task",
13
85
  class: task.class.name,
14
86
  id: task.id,
15
87
  tags: task.task_setting(:tags)