cmdx 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/prompts/rspec.md +20 -0
  3. data/.cursor/prompts/yardoc.md +8 -0
  4. data/.rubocop.yml +2 -0
  5. data/CHANGELOG.md +17 -2
  6. data/README.md +1 -1
  7. data/docs/basics/call.md +2 -2
  8. data/docs/basics/chain.md +1 -1
  9. data/docs/callbacks.md +3 -36
  10. data/docs/configuration.md +58 -12
  11. data/docs/interruptions/exceptions.md +1 -1
  12. data/docs/interruptions/faults.md +2 -2
  13. data/docs/logging.md +4 -4
  14. data/docs/middlewares.md +43 -43
  15. data/docs/parameters/coercions.md +49 -38
  16. data/docs/parameters/defaults.md +1 -1
  17. data/docs/parameters/validations.md +0 -39
  18. data/docs/testing.md +11 -12
  19. data/docs/workflows.md +4 -4
  20. data/lib/cmdx/.DS_Store +0 -0
  21. data/lib/cmdx/callback.rb +36 -56
  22. data/lib/cmdx/callback_registry.rb +82 -73
  23. data/lib/cmdx/chain.rb +65 -122
  24. data/lib/cmdx/chain_inspector.rb +22 -115
  25. data/lib/cmdx/chain_serializer.rb +17 -148
  26. data/lib/cmdx/coercion.rb +49 -0
  27. data/lib/cmdx/coercion_registry.rb +94 -0
  28. data/lib/cmdx/coercions/array.rb +18 -36
  29. data/lib/cmdx/coercions/big_decimal.rb +21 -33
  30. data/lib/cmdx/coercions/boolean.rb +21 -40
  31. data/lib/cmdx/coercions/complex.rb +18 -31
  32. data/lib/cmdx/coercions/date.rb +20 -39
  33. data/lib/cmdx/coercions/date_time.rb +22 -39
  34. data/lib/cmdx/coercions/float.rb +19 -32
  35. data/lib/cmdx/coercions/hash.rb +22 -41
  36. data/lib/cmdx/coercions/integer.rb +20 -33
  37. data/lib/cmdx/coercions/rational.rb +20 -32
  38. data/lib/cmdx/coercions/string.rb +23 -31
  39. data/lib/cmdx/coercions/time.rb +24 -40
  40. data/lib/cmdx/coercions/virtual.rb +14 -31
  41. data/lib/cmdx/configuration.rb +57 -171
  42. data/lib/cmdx/context.rb +22 -165
  43. data/lib/cmdx/core_ext/hash.rb +42 -67
  44. data/lib/cmdx/core_ext/module.rb +35 -79
  45. data/lib/cmdx/core_ext/object.rb +63 -98
  46. data/lib/cmdx/correlator.rb +40 -156
  47. data/lib/cmdx/error.rb +37 -202
  48. data/lib/cmdx/errors.rb +165 -202
  49. data/lib/cmdx/fault.rb +55 -158
  50. data/lib/cmdx/faults.rb +26 -137
  51. data/lib/cmdx/immutator.rb +22 -109
  52. data/lib/cmdx/lazy_struct.rb +103 -187
  53. data/lib/cmdx/log_formatters/json.rb +14 -40
  54. data/lib/cmdx/log_formatters/key_value.rb +14 -40
  55. data/lib/cmdx/log_formatters/line.rb +14 -48
  56. data/lib/cmdx/log_formatters/logstash.rb +14 -57
  57. data/lib/cmdx/log_formatters/pretty_json.rb +14 -50
  58. data/lib/cmdx/log_formatters/pretty_key_value.rb +13 -46
  59. data/lib/cmdx/log_formatters/pretty_line.rb +16 -54
  60. data/lib/cmdx/log_formatters/raw.rb +19 -49
  61. data/lib/cmdx/logger.rb +20 -82
  62. data/lib/cmdx/logger_ansi.rb +18 -75
  63. data/lib/cmdx/logger_serializer.rb +24 -114
  64. data/lib/cmdx/middleware.rb +38 -60
  65. data/lib/cmdx/middleware_registry.rb +81 -77
  66. data/lib/cmdx/middlewares/correlate.rb +41 -226
  67. data/lib/cmdx/middlewares/timeout.rb +46 -185
  68. data/lib/cmdx/parameter.rb +120 -198
  69. data/lib/cmdx/parameter_evaluator.rb +231 -0
  70. data/lib/cmdx/parameter_inspector.rb +25 -56
  71. data/lib/cmdx/parameter_registry.rb +59 -84
  72. data/lib/cmdx/parameter_serializer.rb +23 -74
  73. data/lib/cmdx/railtie.rb +24 -107
  74. data/lib/cmdx/result.rb +254 -260
  75. data/lib/cmdx/result_ansi.rb +19 -85
  76. data/lib/cmdx/result_inspector.rb +27 -68
  77. data/lib/cmdx/result_logger.rb +18 -81
  78. data/lib/cmdx/result_serializer.rb +28 -132
  79. data/lib/cmdx/rspec/matchers.rb +28 -0
  80. data/lib/cmdx/rspec/result_matchers/be_executed.rb +42 -0
  81. data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +94 -0
  82. data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +94 -0
  83. data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +59 -0
  84. data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +57 -0
  85. data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +87 -0
  86. data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +51 -0
  87. data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +58 -0
  88. data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +59 -0
  89. data/lib/cmdx/rspec/result_matchers/have_context.rb +86 -0
  90. data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +54 -0
  91. data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +52 -0
  92. data/lib/cmdx/rspec/result_matchers/have_metadata.rb +114 -0
  93. data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +66 -0
  94. data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +64 -0
  95. data/lib/cmdx/rspec/result_matchers/have_runtime.rb +78 -0
  96. data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +76 -0
  97. data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +62 -0
  98. data/lib/cmdx/rspec/task_matchers/have_callback.rb +85 -0
  99. data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +68 -0
  100. data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +92 -0
  101. data/lib/cmdx/rspec/task_matchers/have_middleware.rb +46 -0
  102. data/lib/cmdx/rspec/task_matchers/have_parameter.rb +181 -0
  103. data/lib/cmdx/task.rb +213 -425
  104. data/lib/cmdx/task_deprecator.rb +55 -0
  105. data/lib/cmdx/task_processor.rb +245 -0
  106. data/lib/cmdx/task_serializer.rb +22 -70
  107. data/lib/cmdx/utils/ansi_color.rb +13 -89
  108. data/lib/cmdx/utils/log_timestamp.rb +13 -42
  109. data/lib/cmdx/utils/monotonic_runtime.rb +13 -63
  110. data/lib/cmdx/utils/name_affix.rb +21 -71
  111. data/lib/cmdx/validator.rb +48 -0
  112. data/lib/cmdx/validator_registry.rb +86 -0
  113. data/lib/cmdx/validators/exclusion.rb +55 -94
  114. data/lib/cmdx/validators/format.rb +31 -85
  115. data/lib/cmdx/validators/inclusion.rb +65 -110
  116. data/lib/cmdx/validators/length.rb +117 -133
  117. data/lib/cmdx/validators/numeric.rb +123 -130
  118. data/lib/cmdx/validators/presence.rb +38 -79
  119. data/lib/cmdx/version.rb +1 -7
  120. data/lib/cmdx/workflow.rb +46 -339
  121. data/lib/cmdx.rb +1 -1
  122. data/lib/generators/cmdx/install_generator.rb +14 -31
  123. data/lib/generators/cmdx/task_generator.rb +39 -55
  124. data/lib/generators/cmdx/templates/install.rb +24 -6
  125. data/lib/generators/cmdx/workflow_generator.rb +41 -66
  126. data/lib/locales/ar.yml +0 -1
  127. data/lib/locales/cs.yml +0 -1
  128. data/lib/locales/da.yml +0 -1
  129. data/lib/locales/de.yml +0 -1
  130. data/lib/locales/el.yml +0 -1
  131. data/lib/locales/en.yml +0 -1
  132. data/lib/locales/es.yml +0 -1
  133. data/lib/locales/fi.yml +0 -1
  134. data/lib/locales/fr.yml +0 -1
  135. data/lib/locales/he.yml +0 -1
  136. data/lib/locales/hi.yml +0 -1
  137. data/lib/locales/it.yml +0 -1
  138. data/lib/locales/ja.yml +0 -1
  139. data/lib/locales/ko.yml +0 -1
  140. data/lib/locales/nl.yml +0 -1
  141. data/lib/locales/no.yml +0 -1
  142. data/lib/locales/pl.yml +0 -1
  143. data/lib/locales/pt.yml +0 -1
  144. data/lib/locales/ru.yml +0 -1
  145. data/lib/locales/sv.yml +0 -1
  146. data/lib/locales/th.yml +0 -1
  147. data/lib/locales/tr.yml +0 -1
  148. data/lib/locales/vi.yml +0 -1
  149. data/lib/locales/zh.yml +0 -1
  150. metadata +34 -8
  151. data/lib/cmdx/parameter_validator.rb +0 -81
  152. data/lib/cmdx/parameter_value.rb +0 -244
  153. data/lib/cmdx/parameters_inspector.rb +0 -72
  154. data/lib/cmdx/parameters_serializer.rb +0 -115
  155. data/lib/cmdx/rspec/result_matchers.rb +0 -917
  156. data/lib/cmdx/rspec/task_matchers.rb +0 -570
  157. data/lib/cmdx/validators/custom.rb +0 -102
data/lib/cmdx/task.rb CHANGED
@@ -1,157 +1,42 @@
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.
4
+ # Core task execution system for CMDx framework.
7
5
  #
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
6
+ # Task provides the foundational functionality for executing business logic
7
+ # with parameter validation, middleware support, callback execution, and
8
+ # result tracking. Tasks encapsulate reusable business operations with
9
+ # comprehensive error handling, logging, and execution state management.
99
10
  class Task
100
11
 
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 = [
107
- :before_validation,
108
- :after_validation,
109
- :before_execution,
110
- :after_execution,
111
- :on_executed,
112
- :on_good,
113
- :on_bad,
114
- *Result::STATUSES.map { |s| :"on_#{s}" },
115
- *Result::STATES.map { |s| :"on_#{s}" }
116
- ].freeze
117
-
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
12
+ cmdx_attr_setting :cmd_settings,
13
+ default: -> { CMDx.configuration.to_h.slice(:logger, :task_halt, :workflow_halt).merge(tags: []) }
14
+ cmdx_attr_setting :cmd_middlewares,
15
+ default: -> { MiddlewareRegistry.new(CMDx.configuration.middlewares) }
16
+ cmdx_attr_setting :cmd_callbacks,
17
+ default: -> { CallbackRegistry.new(CMDx.configuration.callbacks) }
18
+ cmdx_attr_setting :cmd_parameters,
19
+ default: -> { ParameterRegistry.new }
20
+
21
+ cmdx_attr_delegator :cmd_middlewares, :cmd_callbacks, :cmd_parameters,
22
+ :cmd_settings, :cmd_setting, :cmd_setting?,
23
+ to: :class
24
+ cmdx_attr_delegator :skip!, :fail!, :throw!,
25
+ to: :result
26
+
27
+ # @return [Context] parameter context for this task execution
135
28
  attr_reader :context
136
29
 
137
- ##
138
- # @!attribute [r] errors
139
- # @return [Errors] collection of validation and execution errors
30
+ # @return [Errors] collection of validation and execution errors
140
31
  attr_reader :errors
141
32
 
142
- ##
143
- # @!attribute [r] id
144
- # @return [String] unique identifier for this task instance
33
+ # @return [String] unique identifier for this task instance
145
34
  attr_reader :id
146
35
 
147
- ##
148
- # @!attribute [r] result
149
- # @return [Result] execution result tracking state and status
36
+ # @return [Result] execution result tracking state and status
150
37
  attr_reader :result
151
38
 
152
- ##
153
- # @!attribute [r] chain
154
- # @return [Chain] execution chain containing this task and related executions
39
+ # @return [Chain] execution chain containing this task and related executions
155
40
  attr_reader :chain
156
41
 
157
42
  # @return [Context] alias for context
@@ -160,21 +45,20 @@ module CMDx
160
45
  # @return [Result] alias for result
161
46
  alias res result
162
47
 
163
- ##
164
- # Initializes a new task instance with the given context parameters.
48
+ # Creates a new task instance with the provided execution context.
49
+ #
50
+ # @param context [Hash, Context] execution context data or Context instance
165
51
  #
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.
52
+ # @return [Task] a new task instance ready for execution
169
53
  #
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)
54
+ # @example Create a task with hash context
55
+ # task = MyTask.new(user_id: 123, action: "process")
56
+ # task.context.user_id #=> 123
173
57
  #
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
58
+ # @example Create a task with existing context
59
+ # existing_context = CMDx::Context.build(name: "John")
60
+ # task = MyTask.new(existing_context)
61
+ # task.context.name #=> "John"
178
62
  def initialize(context = {})
179
63
  context = context.context if context.respond_to?(:context)
180
64
 
@@ -183,347 +67,251 @@ module CMDx
183
67
  @id = CMDx::Correlator.generate
184
68
  @result = Result.new(self)
185
69
  @chain = Chain.build(@result)
70
+
71
+ TaskDeprecator.call(self)
186
72
  end
187
73
 
188
74
  class << self
189
75
 
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|
76
+ # Registers callbacks for task execution lifecycle events.
77
+ #
78
+ # These methods are dynamically defined for each callback type and provide
79
+ # a clean DSL for registering callbacks that will be executed at specific
80
+ # points during task execution.
81
+ #
82
+ # @param callables [Array<Object>] callback objects to register (symbols, procs, classes)
83
+ # @param options [Hash] conditional execution options
84
+ # @param block [Proc] optional block to register as a callback
85
+ #
86
+ # @return [void]
87
+ #
88
+ # @example Register before_execution callback with symbol
89
+ # MyTask.before_execution :setup_database
90
+ #
91
+ # @example Register before_execution callback with proc
92
+ # MyTask.before_execution -> { puts "Starting task execution" }
93
+ #
94
+ # @example Register before_execution callback with class
95
+ # MyTask.before_execution SetupCallback
96
+ #
97
+ # @example Register before_execution callback with block
98
+ # MyTask.before_execution { |task| task.context.started_at = Time.now }
99
+ #
100
+ # @example Register on_success callback with conditional options
101
+ # MyTask.on_success :send_notification, if: -> { Rails.env.production? }
102
+ #
103
+ # @example Register on_success callback with multiple callables
104
+ # MyTask.on_success :log_success, :send_email, :update_metrics
105
+ CallbackRegistry::TYPES.each do |callback|
211
106
  define_method(callback) do |*callables, **options, &block|
212
107
  cmd_callbacks.register(callback, *callables, **options, &block)
213
108
  end
214
109
  end
215
110
 
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
223
- def task_setting(key)
224
- __cmdx_yield(task_settings[key])
225
- end
226
-
227
- ##
228
- # Checks if a task setting exists.
111
+ # Retrieves the value of a task setting.
112
+ #
113
+ # @param key [Symbol] the setting key to retrieve
229
114
  #
230
- # @param key [Symbol, String] setting key to check
231
- # @return [Boolean] true if setting exists
232
- def task_setting?(key)
233
- task_settings.key?(key)
115
+ # @return [Object] the setting value, processed through cmdx_yield
116
+ #
117
+ # @example Get logger setting
118
+ # MyTask.cmd_setting(:logger) #=> #<Logger:...>
119
+ #
120
+ # @example Get halt setting
121
+ # MyTask.cmd_setting(:task_halt) #=> "failed"
122
+ def cmd_setting(key)
123
+ cmdx_yield(cmd_settings[key])
234
124
  end
235
125
 
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)
243
- def task_settings!(**options)
244
- task_settings.merge!(options)
126
+ # Checks if a task setting key exists.
127
+ #
128
+ # @param key [Symbol] the setting key to check
129
+ #
130
+ # @return [Boolean] true if the setting exists, false otherwise
131
+ #
132
+ # @example Check if setting exists
133
+ # MyTask.cmd_setting?(:logger) #=> true
134
+ # MyTask.cmd_setting?(:invalid) #=> false
135
+ def cmd_setting?(key)
136
+ cmd_settings.key?(key)
245
137
  end
246
138
 
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, ...)
139
+ # Updates task settings with new values.
140
+ #
141
+ # @param options [Hash] hash of setting keys and values to merge
142
+ #
143
+ # @return [Hash] the updated task settings hash
144
+ #
145
+ # @example Update task settings
146
+ # MyTask.cmd_settings!(task_halt: ["failed", "error"])
147
+ # MyTask.cmd_setting(:task_halt) #=> ["failed", "error"]
148
+ def cmd_settings!(**options)
149
+ cmd_settings.merge!(options)
263
150
  end
264
151
 
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, ...)
152
+ # Registers middleware, callbacks, validators, or coercions with the task.
153
+ #
154
+ # @param type [Symbol] the type of registration (:middleware, :callback, :validator, :coercion)
155
+ # @param object [Object] the object to register
156
+ # @param args [Array] additional arguments passed to the registration method
157
+ #
158
+ # @return [void]
159
+ #
160
+ # @example Register middleware
161
+ # MyTask.use(:middleware, TimeoutMiddleware, timeout: 30)
162
+ #
163
+ # @example Register callback
164
+ # MyTask.use(:callback, :before_execution, MyCallback)
165
+ def use(type, object, ...)
166
+ case type
167
+ when :middleware
168
+ cmd_middlewares.register(object, ...)
169
+ when :callback
170
+ cmd_callbacks.register(type, object, ...)
171
+ when :validator
172
+ cmd_validators.register(type, object, ...)
173
+ when :coercion
174
+ cmd_coercions.register(type, object, ...)
175
+ end
284
176
  end
285
177
 
286
- ##
287
178
  # Defines optional parameters for the task.
288
179
  #
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
180
+ # @param attributes [Array<Symbol>] parameter names to define as optional
181
+ # @param options [Hash] parameter configuration options
182
+ # @param block [Proc] optional block for defining nested parameters
183
+ #
184
+ # @return [void]
185
+ #
186
+ # @example Define optional parameters
187
+ # MyTask.optional :name, :email, type: :string
188
+ #
189
+ # @example Define optional parameter with validation
190
+ # MyTask.optional :age, type: :integer, validate: { numeric: { greater_than: 0 } }
298
191
  def optional(*attributes, **options, &)
299
192
  parameters = Parameter.optional(*attributes, **options.merge(klass: self), &)
300
- cmd_parameters.concat(parameters)
193
+ cmd_parameters.registry.concat(parameters)
301
194
  end
302
195
 
303
- ##
304
196
  # Defines required parameters for the task.
305
197
  #
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
198
+ # @param attributes [Array<Symbol>] parameter names to define as required
199
+ # @param options [Hash] parameter configuration options
200
+ # @param block [Proc] optional block for defining nested parameters
201
+ #
202
+ # @return [void]
203
+ #
204
+ # @example Define required parameters
205
+ # MyTask.required :user_id, :action, type: :string
206
+ #
207
+ # @example Define required parameter with nested structure
208
+ # MyTask.required :user, type: :hash do
313
209
  # required :name, type: :string
314
- # optional :age, type: :integer
210
+ # optional :email, type: :string
315
211
  # end
316
212
  def required(*attributes, **options, &)
317
213
  parameters = Parameter.required(*attributes, **options.merge(klass: self), &)
318
- cmd_parameters.concat(parameters)
214
+ cmd_parameters.registry.concat(parameters)
319
215
  end
320
216
 
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
217
+ # Executes the task with fault tolerance and returns the result.
218
+ #
219
+ # @param args [Array] arguments passed to the task constructor
220
+ #
221
+ # @return [Result] the task execution result
222
+ #
223
+ # @example Execute task with fault tolerance
224
+ # result = MyTask.call(user_id: 123)
225
+ # result.success? #=> true
226
+ # result.context.user_id #=> 123
341
227
  def call(...)
342
228
  instance = new(...)
343
- instance.perform
229
+ instance.process
344
230
  instance.result
345
231
  end
346
232
 
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
233
+ # Executes the task with strict fault handling and returns the result.
234
+ #
235
+ # @param args [Array] arguments passed to the task constructor
236
+ #
237
+ # @return [Result] the task execution result
238
+ #
239
+ # @raise [Fault] if the task fails and task_halt setting includes the failure status
364
240
  #
365
- # @example With Result object (task chaining)
241
+ # @example Execute task with strict fault handling
242
+ # result = MyTask.call!(user_id: 123)
243
+ # result.success? #=> true
244
+ #
245
+ # @example Handling fault on failure
366
246
  # 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
247
+ # MyTask.call!(invalid_data: true)
248
+ # rescue CMDx::Fault => e
249
+ # puts "Task failed: #{e.message}"
371
250
  # end
372
251
  def call!(...)
373
252
  instance = new(...)
374
- instance.perform!
253
+ instance.process!
375
254
  instance.result
376
255
  end
377
256
 
378
257
  end
379
258
 
380
- ##
381
- # The main execution method that must be implemented by subclasses.
382
- # This method contains the core business logic of the task.
259
+ # Abstract method that must be implemented by task subclasses.
260
+ #
261
+ # This method contains the core business logic to be executed by the task.
262
+ # Subclasses must override this method to provide their specific implementation.
383
263
  #
384
- # @abstract Subclasses must implement this method
385
264
  # @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
265
  #
392
- # context.user.activate!
393
- # context.activation_date = Time.now
266
+ # @raise [UndefinedCallError] if not implemented by subclass
267
+ #
268
+ # @example Implement in a subclass
269
+ # class ProcessUserTask < CMDx::Task
270
+ # def call
271
+ # # Business logic here
272
+ # context.processed = true
273
+ # end
394
274
  # end
395
275
  def call
396
276
  raise UndefinedCallError, "call method not defined in #{self.class.name}"
397
277
  end
398
278
 
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.
279
+ # Performs task execution with middleware support and fault tolerance.
413
280
  #
414
281
  # @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
-
422
- private
423
-
424
- ##
425
- # Returns the logger instance for this task.
426
282
  #
427
- # @return [Logger] configured logger instance
428
- # @api private
429
- def logger
430
- Logger.call(self)
283
+ # @example Task execution with middleware
284
+ # task = MyTask.new(user_id: 123)
285
+ # task.process
286
+ # task.result.success? #=> true
287
+ def process
288
+ cmd_middlewares.call(self) { |task| TaskProcessor.call(task) }
431
289
  end
432
290
 
433
- ##
434
- # Executes before-call callbacks and validations.
435
- # Sets up the execution context and validates parameters.
291
+ # Performs task execution with middleware support and strict fault handling.
436
292
  #
437
293
  # @return [void]
438
- # @api private
439
- def before_call
440
- cmd_callbacks.call(self, :before_execution)
441
-
442
- result.executing!
443
- cmd_callbacks.call(self, :on_executing)
444
-
445
- cmd_callbacks.call(self, :before_validation)
446
- ParameterValidator.call(self)
447
- cmd_callbacks.call(self, :after_validation)
448
- end
449
-
450
- ##
451
- # Executes after-call callbacks based on execution results.
452
- # Handles state and status transitions with appropriate callbacks.
453
294
  #
454
- # @return [void]
455
- # @api private
456
- def after_call
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?
463
-
464
- cmd_callbacks.call(self, :after_execution)
465
- end
466
-
467
- ##
468
- # Finalizes task execution by freezing the task and logging results.
295
+ # @raise [Fault] if task fails and task_halt setting includes the failure status
469
296
  #
470
- # @return [void]
471
- # @api private
472
- def terminate_call
473
- Immutator.call(self)
474
- ResultLogger.call(result)
297
+ # @example Task execution with strict fault handling
298
+ # task = MyTask.new(user_id: 123)
299
+ # task.process!
300
+ # task.result.success? #=> true
301
+ def process!
302
+ cmd_middlewares.call(self) { |task| TaskProcessor.call!(task) }
475
303
  end
476
304
 
477
- ##
478
- # Executes the task directly without middleware for the non-bang call method.
305
+ # Creates a logger instance for this task.
479
306
  #
480
- # @return [void]
481
- # @api private
482
- def execute_call
483
- result.runtime do
484
- before_call
485
- call
486
- rescue UndefinedCallError => e
487
- raise(e)
488
- rescue Fault => e
489
- throw!(e.result, original_exception: e) if Array(task_setting(:task_halt)).include?(e.result.status)
490
- rescue StandardError => e
491
- fail!(reason: "[#{e.class}] #{e.message}", original_exception: e)
492
- ensure
493
- result.executed!
494
- after_call
495
- end
496
-
497
- terminate_call
498
- end
499
-
500
- ##
501
- # Executes the task directly without middleware for the bang call method.
307
+ # @return [Logger] logger instance configured for this task
502
308
  #
503
- # @return [void]
504
- # @api private
505
- def execute_call!
506
- result.runtime do
507
- before_call
508
- call
509
- rescue UndefinedCallError => e
510
- Chain.clear
511
- raise(e)
512
- rescue Fault => e
513
- result.executed!
514
-
515
- if Array(task_setting(:task_halt)).include?(e.result.status)
516
- Chain.clear
517
- raise(e)
518
- end
519
-
520
- after_call # HACK: treat as NO-OP
521
- else
522
- result.executed!
523
- after_call # ELSE: treat as success
524
- end
525
-
526
- terminate_call
309
+ # @example Getting task logger
310
+ # task = MyTask.new
311
+ # logger = task.send(:logger)
312
+ # logger.info("Task started")
313
+ def logger
314
+ Logger.call(self)
527
315
  end
528
316
 
529
317
  end