cmdx 0.4.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.
- checksums.yaml +4 -4
- data/.DS_Store +0 -0
- data/.cursor/rules/cursor-instructions.mdc +6 -0
- data/.rubocop.yml +16 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +42 -1
- data/README.md +72 -25
- data/docs/ai_prompts.md +309 -0
- data/docs/basics/call.md +225 -14
- data/docs/basics/chain.md +271 -0
- data/docs/basics/context.md +232 -33
- data/docs/basics/setup.md +76 -12
- data/docs/callbacks.md +273 -0
- data/docs/configuration.md +158 -28
- data/docs/getting_started.md +134 -22
- data/docs/interruptions/exceptions.md +189 -11
- data/docs/interruptions/faults.md +187 -44
- data/docs/interruptions/halt.md +179 -35
- data/docs/logging.md +194 -53
- data/docs/middlewares.md +735 -0
- data/docs/outcomes/result.md +296 -10
- data/docs/outcomes/states.md +212 -19
- data/docs/outcomes/statuses.md +284 -18
- data/docs/parameters/coercions.md +402 -29
- data/docs/parameters/defaults.md +249 -25
- data/docs/parameters/definitions.md +238 -72
- data/docs/parameters/namespacing.md +250 -27
- data/docs/parameters/validations.md +193 -168
- data/docs/testing.md +550 -0
- data/docs/tips_and_tricks.md +95 -43
- data/docs/workflows.md +319 -0
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/callback.rb +69 -0
- data/lib/cmdx/callback_registry.rb +106 -0
- data/lib/cmdx/chain.rb +190 -0
- data/lib/cmdx/chain_inspector.rb +149 -0
- data/lib/cmdx/chain_serializer.rb +175 -0
- data/lib/cmdx/coercions/array.rb +37 -0
- data/lib/cmdx/coercions/big_decimal.rb +33 -0
- data/lib/cmdx/coercions/boolean.rb +41 -1
- data/lib/cmdx/coercions/complex.rb +31 -0
- data/lib/cmdx/coercions/date.rb +39 -0
- data/lib/cmdx/coercions/date_time.rb +39 -0
- data/lib/cmdx/coercions/float.rb +31 -0
- data/lib/cmdx/coercions/hash.rb +42 -0
- data/lib/cmdx/coercions/integer.rb +32 -0
- data/lib/cmdx/coercions/rational.rb +31 -0
- data/lib/cmdx/coercions/string.rb +31 -0
- data/lib/cmdx/coercions/time.rb +39 -0
- data/lib/cmdx/coercions/virtual.rb +31 -0
- data/lib/cmdx/configuration.rb +217 -9
- data/lib/cmdx/context.rb +173 -2
- data/lib/cmdx/core_ext/hash.rb +72 -0
- data/lib/cmdx/core_ext/module.rb +94 -0
- data/lib/cmdx/core_ext/object.rb +105 -0
- data/lib/cmdx/correlator.rb +217 -0
- data/lib/cmdx/error.rb +210 -8
- data/lib/cmdx/errors.rb +256 -1
- data/lib/cmdx/fault.rb +177 -2
- data/lib/cmdx/faults.rb +158 -2
- data/lib/cmdx/immutator.rb +121 -2
- data/lib/cmdx/lazy_struct.rb +261 -18
- data/lib/cmdx/log_formatters/json.rb +46 -0
- data/lib/cmdx/log_formatters/key_value.rb +46 -0
- data/lib/cmdx/log_formatters/line.rb +54 -0
- data/lib/cmdx/log_formatters/logstash.rb +64 -0
- data/lib/cmdx/log_formatters/pretty_json.rb +57 -0
- data/lib/cmdx/log_formatters/pretty_key_value.rb +51 -0
- data/lib/cmdx/log_formatters/pretty_line.rb +60 -0
- data/lib/cmdx/log_formatters/raw.rb +54 -0
- data/lib/cmdx/logger.rb +85 -0
- data/lib/cmdx/logger_ansi.rb +93 -7
- data/lib/cmdx/logger_serializer.rb +116 -0
- data/lib/cmdx/middleware.rb +74 -0
- data/lib/cmdx/middleware_registry.rb +106 -0
- data/lib/cmdx/middlewares/correlate.rb +266 -0
- data/lib/cmdx/middlewares/timeout.rb +232 -0
- data/lib/cmdx/parameter.rb +228 -1
- data/lib/cmdx/parameter_inspector.rb +61 -0
- data/lib/cmdx/parameter_registry.rb +125 -0
- data/lib/cmdx/parameter_serializer.rb +83 -0
- data/lib/cmdx/parameter_validator.rb +62 -0
- data/lib/cmdx/parameter_value.rb +109 -1
- data/lib/cmdx/parameters_inspector.rb +59 -0
- data/lib/cmdx/parameters_serializer.rb +102 -0
- data/lib/cmdx/railtie.rb +123 -3
- data/lib/cmdx/result.rb +399 -20
- data/lib/cmdx/result_ansi.rb +105 -9
- data/lib/cmdx/result_inspector.rb +76 -0
- data/lib/cmdx/result_logger.rb +90 -3
- data/lib/cmdx/result_serializer.rb +137 -0
- data/lib/cmdx/rspec/result_matchers.rb +917 -0
- data/lib/cmdx/rspec/task_matchers.rb +570 -0
- data/lib/cmdx/task.rb +409 -34
- data/lib/cmdx/task_serializer.rb +74 -2
- data/lib/cmdx/utils/ansi_color.rb +95 -0
- data/lib/cmdx/utils/log_timestamp.rb +48 -0
- data/lib/cmdx/utils/monotonic_runtime.rb +71 -4
- data/lib/cmdx/utils/name_affix.rb +78 -0
- data/lib/cmdx/validators/custom.rb +82 -0
- data/lib/cmdx/validators/exclusion.rb +94 -0
- data/lib/cmdx/validators/format.rb +102 -8
- data/lib/cmdx/validators/inclusion.rb +104 -0
- data/lib/cmdx/validators/length.rb +128 -0
- data/lib/cmdx/validators/numeric.rb +128 -0
- data/lib/cmdx/validators/presence.rb +93 -7
- data/lib/cmdx/version.rb +7 -1
- data/lib/cmdx/workflow.rb +394 -0
- data/lib/cmdx.rb +25 -64
- data/lib/generators/cmdx/install_generator.rb +37 -1
- data/lib/generators/cmdx/task_generator.rb +69 -1
- data/lib/generators/cmdx/templates/install.rb +8 -12
- data/lib/generators/cmdx/workflow_generator.rb +109 -0
- metadata +54 -15
- data/docs/basics/run.md +0 -34
- data/docs/batch.md +0 -53
- data/docs/example.md +0 -82
- data/docs/hooks.md +0 -59
- data/lib/cmdx/batch.rb +0 -43
- data/lib/cmdx/parameters.rb +0 -34
- data/lib/cmdx/run.rb +0 -38
- data/lib/cmdx/run_inspector.rb +0 -26
- data/lib/cmdx/run_serializer.rb +0 -16
- data/lib/cmdx/task_hook.rb +0 -18
- data/lib/generators/cmdx/batch_generator.rb +0 -30
- /data/lib/generators/cmdx/templates/{batch.rb.tt → workflow.rb.tt} +0 -0
data/lib/cmdx/task.rb
CHANGED
@@ -1,119 +1,484 @@
|
|
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
|
-
|
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,
|
10
110
|
:after_execution,
|
111
|
+
:on_executed,
|
112
|
+
:on_good,
|
113
|
+
:on_bad,
|
11
114
|
*Result::STATUSES.map { |s| :"on_#{s}" },
|
12
115
|
*Result::STATES.map { |s| :"on_#{s}" }
|
13
116
|
].freeze
|
14
117
|
|
15
|
-
__cmdx_attr_setting :task_settings,
|
16
|
-
|
17
|
-
__cmdx_attr_setting :
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
23
158
|
alias ctx context
|
24
|
-
alias res result
|
25
159
|
|
26
|
-
|
160
|
+
# @return [Result] alias for result
|
161
|
+
alias res result
|
27
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
|
28
178
|
def initialize(context = {})
|
29
|
-
|
30
|
-
|
179
|
+
context = context.context if context.respond_to?(:context)
|
180
|
+
|
31
181
|
@context = Context.build(context)
|
32
|
-
@
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
@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)
|
37
186
|
end
|
38
187
|
|
39
188
|
class << self
|
40
189
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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)
|
45
213
|
end
|
46
214
|
end
|
47
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
|
48
223
|
def task_setting(key)
|
49
224
|
__cmdx_yield(task_settings[key])
|
50
225
|
end
|
51
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
|
52
232
|
def task_setting?(key)
|
53
233
|
task_settings.key?(key)
|
54
234
|
end
|
55
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)
|
56
243
|
def task_settings!(**options)
|
57
244
|
task_settings.merge!(options)
|
58
245
|
end
|
59
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
|
60
298
|
def optional(*attributes, **options, &)
|
61
299
|
parameters = Parameter.optional(*attributes, **options.merge(klass: self), &)
|
62
300
|
cmd_parameters.concat(parameters)
|
63
301
|
end
|
64
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
|
65
316
|
def required(*attributes, **options, &)
|
66
317
|
parameters = Parameter.required(*attributes, **options.merge(klass: self), &)
|
67
318
|
cmd_parameters.concat(parameters)
|
68
319
|
end
|
69
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
|
70
341
|
def call(...)
|
71
|
-
instance =
|
72
|
-
instance.
|
342
|
+
instance = new(...)
|
343
|
+
instance.perform
|
73
344
|
instance.result
|
74
345
|
end
|
75
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
|
76
372
|
def call!(...)
|
77
|
-
instance =
|
78
|
-
instance.
|
373
|
+
instance = new(...)
|
374
|
+
instance.perform!
|
79
375
|
instance.result
|
80
376
|
end
|
81
377
|
|
82
378
|
end
|
83
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
|
84
395
|
def call
|
85
396
|
raise UndefinedCallError, "call method not defined in #{self.class.name}"
|
86
397
|
end
|
87
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
|
+
|
88
422
|
private
|
89
423
|
|
424
|
+
##
|
425
|
+
# Returns the logger instance for this task.
|
426
|
+
#
|
427
|
+
# @return [Logger] configured logger instance
|
428
|
+
# @api private
|
90
429
|
def logger
|
91
430
|
Logger.call(self)
|
92
431
|
end
|
93
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
|
94
439
|
def before_call
|
95
|
-
|
440
|
+
cmd_callbacks.call(self, :before_execution)
|
96
441
|
|
97
442
|
result.executing!
|
98
|
-
|
443
|
+
cmd_callbacks.call(self, :on_executing)
|
99
444
|
|
100
|
-
|
445
|
+
cmd_callbacks.call(self, :before_validation)
|
101
446
|
ParameterValidator.call(self)
|
102
|
-
|
447
|
+
cmd_callbacks.call(self, :after_validation)
|
103
448
|
end
|
104
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
|
105
456
|
def after_call
|
106
|
-
|
107
|
-
|
457
|
+
cmd_callbacks.call(self, :"on_#{result.state}")
|
458
|
+
cmd_callbacks.call(self, :on_executed) if result.executed?
|
459
|
+
|
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?
|
108
463
|
|
109
|
-
|
464
|
+
cmd_callbacks.call(self, :after_execution)
|
110
465
|
end
|
111
466
|
|
467
|
+
##
|
468
|
+
# Finalizes task execution by freezing the task and logging results.
|
469
|
+
#
|
470
|
+
# @return [void]
|
471
|
+
# @api private
|
112
472
|
def terminate_call
|
113
473
|
Immutator.call(self)
|
114
474
|
ResultLogger.call(result)
|
115
475
|
end
|
116
476
|
|
477
|
+
##
|
478
|
+
# Executes the task directly without middleware for the non-bang call method.
|
479
|
+
#
|
480
|
+
# @return [void]
|
481
|
+
# @api private
|
117
482
|
def execute_call
|
118
483
|
result.runtime do
|
119
484
|
before_call
|
@@ -132,15 +497,25 @@ module CMDx
|
|
132
497
|
terminate_call
|
133
498
|
end
|
134
499
|
|
500
|
+
##
|
501
|
+
# Executes the task directly without middleware for the bang call method.
|
502
|
+
#
|
503
|
+
# @return [void]
|
504
|
+
# @api private
|
135
505
|
def execute_call!
|
136
506
|
result.runtime do
|
137
507
|
before_call
|
138
508
|
call
|
139
509
|
rescue UndefinedCallError => e
|
510
|
+
Chain.clear
|
140
511
|
raise(e)
|
141
512
|
rescue Fault => e
|
142
513
|
result.executed!
|
143
|
-
|
514
|
+
|
515
|
+
if Array(task_setting(:task_halt)).include?(e.result.status)
|
516
|
+
Chain.clear
|
517
|
+
raise(e)
|
518
|
+
end
|
144
519
|
|
145
520
|
after_call # HACK: treat as NO-OP
|
146
521
|
else
|
data/lib/cmdx/task_serializer.rb
CHANGED
@@ -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
|
-
|
12
|
-
type: task.is_a?(
|
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)
|