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
@@ -2,18 +2,104 @@
2
2
 
3
3
  module CMDx
4
4
  module Validators
5
+ # Presence validator for parameter validation ensuring values are not empty.
6
+ #
7
+ # The Presence validator checks that parameter values are not nil, not empty
8
+ # strings (including whitespace-only strings), and not empty collections.
9
+ # It provides intelligent presence checking for different value types with
10
+ # appropriate logic for strings, arrays, hashes, and other objects.
11
+ #
12
+ # @example Basic presence validation
13
+ # class ProcessUserTask < CMDx::Task
14
+ # required :name, presence: true
15
+ # required :email, presence: true
16
+ # optional :bio, presence: true # Only validated if provided
17
+ # end
18
+ #
19
+ # @example Custom presence message
20
+ # class ProcessUserTask < CMDx::Task
21
+ # required :name, presence: { message: "is required for processing" }
22
+ # required :email, presence: { message: "must be provided" }
23
+ # end
24
+ #
25
+ # @example Boolean field presence validation
26
+ # class ProcessUserTask < CMDx::Task
27
+ # # For boolean fields, use inclusion instead of presence
28
+ # required :active, inclusion: { in: [true, false] }
29
+ # # presence: true would fail for false values
30
+ # end
31
+ #
32
+ # @example Presence validation behavior
33
+ # # String presence checking
34
+ # Presence.call("hello", presence: true) # passes
35
+ # Presence.call("", presence: true) # raises ValidationError
36
+ # Presence.call(" ", presence: true) # raises ValidationError (whitespace only)
37
+ # Presence.call("\n\t", presence: true) # raises ValidationError (whitespace only)
38
+ #
39
+ # # Collection presence checking
40
+ # Presence.call([1, 2], presence: true) # passes
41
+ # Presence.call([], presence: true) # raises ValidationError
42
+ # Presence.call({a: 1}, presence: true) # passes
43
+ # Presence.call({}, presence: true) # raises ValidationError
44
+ #
45
+ # # General object presence checking
46
+ # Presence.call(42, presence: true) # passes
47
+ # Presence.call(0, presence: true) # passes (zero is present)
48
+ # Presence.call(false, presence: true) # passes (false is present)
49
+ # Presence.call(nil, presence: true) # raises ValidationError
50
+ #
51
+ # @see CMDx::Validators::Inclusion For validating boolean fields
52
+ # @see CMDx::Parameter Parameter validation integration
53
+ # @see CMDx::ValidationError Raised when validation fails
5
54
  module Presence
6
55
 
7
56
  module_function
8
57
 
58
+ # Validates that a parameter value is present (not empty or nil).
59
+ #
60
+ # Performs intelligent presence checking based on the value type:
61
+ # - Strings: Must contain non-whitespace characters
62
+ # - Collections: Must not be empty (arrays, hashes, etc.)
63
+ # - Other objects: Must not be nil
64
+ #
65
+ # @param value [Object] The parameter value to validate
66
+ # @param options [Hash] Validation configuration options
67
+ # @option options [Boolean, Hash] :presence Presence validation configuration
68
+ # @option options [String] :presence.message Custom error message
69
+ #
70
+ # @return [void]
71
+ # @raise [ValidationError] If value is not present according to type-specific rules
72
+ #
73
+ # @example String presence validation
74
+ # Presence.call("hello", presence: true) # passes
75
+ # Presence.call("", presence: true) # raises ValidationError
76
+ # Presence.call(" ", presence: true) # raises ValidationError
77
+ #
78
+ # @example Collection presence validation
79
+ # Presence.call([1, 2, 3], presence: true) # passes
80
+ # Presence.call([], presence: true) # raises ValidationError
81
+ # Presence.call({key: "value"}, presence: true) # passes
82
+ # Presence.call({}, presence: true) # raises ValidationError
83
+ #
84
+ # @example Object presence validation
85
+ # Presence.call(42, presence: true) # passes
86
+ # Presence.call(false, presence: true) # passes (false is present)
87
+ # Presence.call(nil, presence: true) # raises ValidationError
88
+ #
89
+ # @example Custom error message
90
+ # Presence.call("", presence: { message: "is required" })
91
+ # # => raises ValidationError: "is required"
9
92
  def call(value, options = {})
10
- return if if value.is_a?(String)
11
- /\S/.match?(value)
12
- elsif value.respond_to?(:empty?)
13
- !value.empty?
14
- else
15
- !value.nil?
16
- end
93
+ present =
94
+ if value.is_a?(String)
95
+ /\S/.match?(value)
96
+ elsif value.respond_to?(:empty?)
97
+ !value.empty?
98
+ else
99
+ !value.nil?
100
+ end
101
+
102
+ return if present
17
103
 
18
104
  message = options.dig(:presence, :message) if options[:presence].is_a?(Hash)
19
105
  raise ValidationError, message || I18n.t(
data/lib/cmdx/version.rb CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  module CMDx
4
4
 
5
- VERSION = "0.5.0"
5
+ # Current version of the CMDx gem.
6
+ #
7
+ # This constant contains the version string following semantic versioning
8
+ # conventions (major.minor.patch).
9
+ #
10
+ # @return [String] the current version
11
+ VERSION = "1.0.0"
6
12
 
7
13
  end
@@ -0,0 +1,394 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ ##
5
+ # Orchestrates sequential execution of multiple tasks in a linear pipeline.
6
+ # Workflow provides a declarative DSL for composing complex business workflows
7
+ # from individual task components, with support for conditional execution,
8
+ # context passing, and configurable halt behavior.
9
+ #
10
+ # Workflows inherit from Task, gaining all task capabilities including callbacks,
11
+ # parameter validation, result tracking, and configuration. The key difference
12
+ # is that workflows coordinate other tasks rather than implementing business logic directly.
13
+ #
14
+ #
15
+ # ## Execution Flow
16
+ #
17
+ # 1. **Group Evaluation**: Check if group conditions (`:if`/`:unless`) are met
18
+ # 2. **Task Execution**: Run each task in the group sequentially
19
+ # 3. **Result Checking**: Evaluate task result against halt conditions
20
+ # 4. **Halt Decision**: Stop execution if halt conditions are met, otherwise continue
21
+ # 5. **Context Propagation**: Pass updated context to next task/group
22
+ #
23
+ # ## Halt Behavior
24
+ #
25
+ # By default, workflows halt on `FAILED` status but continue on `SKIPPED`.
26
+ # This reflects the philosophy that skipped tasks are bypass mechanisms,
27
+ # not execution blockers. Halt behavior can be customized at class or group level.
28
+ #
29
+ # @example Basic workflow definition
30
+ # class ProcessOrderWorkflow < CMDx::Workflow
31
+ # process ValidateOrderTask
32
+ # process CalculateTaxTask
33
+ # process ChargePaymentTask
34
+ # process FulfillOrderTask
35
+ # end
36
+ #
37
+ # @example Multiple task declarations
38
+ # class NotificationWorkflow < CMDx::Workflow
39
+ # # Single task
40
+ # process PrepareNotificationTask
41
+ #
42
+ # # Multiple tasks in one declaration
43
+ # process SendEmailTask, SendSmsTask, SendPushTask
44
+ # end
45
+ #
46
+ # @example Conditional execution
47
+ # class ConditionalWorkflow < CMDx::Workflow
48
+ # process AlwaysRunTask
49
+ #
50
+ # # Conditional execution with proc
51
+ # process PremiumFeatureTask, if: proc { context.user.premium? }
52
+ #
53
+ # # Conditional execution with lambda
54
+ # process InternationalTask, unless: -> { context.order.domestic? }
55
+ #
56
+ # # Conditional execution with method
57
+ # process DebugTask, if: :debug_mode?
58
+ #
59
+ # private
60
+ #
61
+ # def debug_mode?
62
+ # Rails.env.development?
63
+ # end
64
+ # end
65
+ #
66
+ # @example Custom halt behavior
67
+ # class StrictWorkflow < CMDx::Workflow
68
+ # # Class-level halt configuration
69
+ # task_settings!(workflow_halt: [CMDx::Result::FAILED, CMDx::Result::SKIPPED])
70
+ #
71
+ # process CriticalTask
72
+ # process AnotherCriticalTask
73
+ # end
74
+ #
75
+ # @example Group-level halt behavior
76
+ # class FlexibleWorkflow < CMDx::Workflow
77
+ # # Critical tasks - halt on any failure
78
+ # process CoreTask1, CoreTask2, workflow_halt: [CMDx::Result::FAILED, CMDx::Result::SKIPPED]
79
+ #
80
+ # # Optional tasks - continue even if they fail
81
+ # process OptionalTask1, OptionalTask2, workflow_halt: []
82
+ #
83
+ # # Notification tasks - halt only on failures, allow skips
84
+ # process NotifyTask1, NotifyTask2 # Uses default halt behavior
85
+ # end
86
+ #
87
+ # @example Complex workflow
88
+ # class EcommerceCheckoutWorkflow < CMDx::Workflow
89
+ # # Pre-processing
90
+ # process ValidateCartTask
91
+ # process CalculateShippingTask
92
+ #
93
+ # # Payment processing (critical)
94
+ # process AuthorizePaymentTask, CapturePaymentTask,
95
+ # workflow_halt: [CMDx::Result::FAILED, CMDx::Result::SKIPPED]
96
+ #
97
+ # # Fulfillment (conditional)
98
+ # process CreateShipmentTask, unless: :digital_only?
99
+ # process SendDigitalDeliveryTask, if: :has_digital_items?
100
+ #
101
+ # # Post-processing notifications
102
+ # process SendConfirmationEmailTask
103
+ # process SendConfirmationSmsTask, if: proc { context.user.sms_enabled? }
104
+ #
105
+ # private
106
+ #
107
+ # def digital_only?
108
+ # context.order.items.all?(&:digital?)
109
+ # end
110
+ #
111
+ # def has_digital_items?
112
+ # context.order.items.any?(&:digital?)
113
+ # end
114
+ # end
115
+ #
116
+ # @example Workflow execution and result handling
117
+ # # Execute workflow
118
+ # result = ProcessOrderWorkflow.call(order: order, user: current_user)
119
+ #
120
+ # # Check results
121
+ # if result.success?
122
+ # redirect_to success_path
123
+ # elsif result.failed?
124
+ # # Handle failure - context contains data from all executed tasks
125
+ # flash[:error] = "Order processing failed: #{result.context.error_message}"
126
+ # redirect_to cart_path
127
+ # end
128
+ #
129
+ # @example Nested workflows
130
+ # class MasterWorkflow < CMDx::Workflow
131
+ # process PreProcessingWorkflow
132
+ # process CoreProcessingWorkflow
133
+ # process PostProcessingWorkflow
134
+ # end
135
+ #
136
+ # @see Task Base class providing callbacks, parameters, and result tracking
137
+ # @see Context Shared data object passed between tasks
138
+ # @see Result Task execution results and status tracking
139
+ # @since 1.0.0
140
+ class Workflow < Task
141
+
142
+ ##
143
+ # Represents a logical group of tasks with shared execution options.
144
+ # Groups allow organizing related tasks and applying common configuration
145
+ # such as conditional execution and halt behavior.
146
+ #
147
+ # @!attribute [r] tasks
148
+ # @return [Array<Class>] array of task classes to execute
149
+ # @!attribute [r] options
150
+ # @return [Hash] execution options including conditions and halt behavior
151
+ #
152
+ # @example Group creation
153
+ # group = CMDx::Workflow::Group.new(
154
+ # [TaskA, TaskB, TaskC],
155
+ # { if: proc { condition }, workflow_halt: ["failed"] }
156
+ # )
157
+ Group = Struct.new(:tasks, :options)
158
+
159
+ class << self
160
+
161
+ ##
162
+ # Returns the collection of task groups defined for this workflow.
163
+ # Groups are created through `process` declarations and store
164
+ # both the tasks to execute and their execution options.
165
+ #
166
+ # @return [Array<Group>] array of task groups in declaration order
167
+ #
168
+ # @example Accessing workflow groups
169
+ # class MyWorkflow < CMDx::Workflow
170
+ # process TaskA, TaskB
171
+ # process TaskC, if: proc { condition }
172
+ # end
173
+ #
174
+ # MyWorkflow.workflow_groups.size #=> 2
175
+ # MyWorkflow.workflow_groups.first.tasks #=> [TaskA, TaskB]
176
+ # MyWorkflow.workflow_groups.last.options #=> { if: proc { condition } }
177
+ #
178
+ # @example Inspecting group configuration
179
+ # workflow_class.workflow_groups.each_with_index do |group, index|
180
+ # puts "Group #{index}: #{group.tasks.map(&:name).join(', ')}"
181
+ # puts "Options: #{group.options}" if group.options.any?
182
+ # end
183
+ def workflow_groups
184
+ @workflow_groups ||= []
185
+ end
186
+
187
+ ##
188
+ # Declares tasks to be executed as part of this workflow.
189
+ # Tasks are organized into groups with shared execution options.
190
+ # Multiple calls to `process` create separate groups that can have
191
+ # different conditional logic and halt behavior.
192
+ #
193
+ # ## Supported Options
194
+ #
195
+ # - **`:if`** - Callable that must return truthy for group to execute
196
+ # - **`:unless`** - Callable that must return falsy for group to execute
197
+ # - **`:workflow_halt`** - Array of result statuses that stop execution
198
+ #
199
+ # ## Conditional Callables
200
+ #
201
+ # Conditions can be:
202
+ # - **Proc/Lambda**: Executed in workflow instance context
203
+ # - **Symbol**: Method name called on workflow instance
204
+ # - **String**: Method name called on workflow instance
205
+ #
206
+ # @param tasks [Array<Class>] task classes that inherit from Task or Workflow
207
+ # @param options [Hash] execution options for this group
208
+ #
209
+ # @option options [Proc, Symbol, String] :if condition that must be truthy
210
+ # @option options [Proc, Symbol, String] :unless condition that must be falsy
211
+ # @option options [Array<Symbol>] :workflow_halt result statuses that halt execution
212
+ #
213
+ # @raise [TypeError] if any task doesn't inherit from Task
214
+ #
215
+ # @example Basic task declaration
216
+ # class SimpleWorkflow < CMDx::Workflow
217
+ # process TaskA
218
+ # process TaskB, TaskC
219
+ # end
220
+ #
221
+ # @example Conditional execution
222
+ # class ConditionalWorkflow < CMDx::Workflow
223
+ # process AlwaysTask
224
+ #
225
+ # # Proc condition
226
+ # process PremiumTask, if: proc { context.user.premium? }
227
+ #
228
+ # # Lambda condition
229
+ # process InternationalTask, unless: -> { context.domestic_only? }
230
+ #
231
+ # # Method condition
232
+ # process DebugTask, if: :debug_enabled?
233
+ #
234
+ # private
235
+ #
236
+ # def debug_enabled?
237
+ # Rails.env.development?
238
+ # end
239
+ # end
240
+ #
241
+ # @example Custom halt behavior
242
+ # class HaltBehaviorWorkflow < CMDx::Workflow
243
+ # # Critical tasks - halt on any non-success
244
+ # process CriticalTaskA, CriticalTaskB,
245
+ # workflow_halt: [CMDx::Result::FAILED, CMDx::Result::SKIPPED]
246
+ #
247
+ # # Optional tasks - never halt
248
+ # process OptionalTaskA, OptionalTaskB, workflow_halt: []
249
+ #
250
+ # # Default behavior tasks
251
+ # process NormalTaskA, NormalTaskB # Halts on FAILED only
252
+ # end
253
+ #
254
+ # @example Complex conditions
255
+ # class ComplexWorkflow < CMDx::Workflow
256
+ # process BaseTask
257
+ #
258
+ # # Multiple conditions can be combined in proc
259
+ # process ConditionalTask, if: proc {
260
+ # context.user.active? &&
261
+ # context.feature_enabled?(:new_feature) &&
262
+ # Time.now.hour.between?(9, 17)
263
+ # }
264
+ #
265
+ # # Conditional with custom halt behavior
266
+ # process RiskyTask,
267
+ # unless: :safe_mode?,
268
+ # workflow_halt: [CMDx::Result::FAILED, CMDx::Result::SKIPPED]
269
+ # end
270
+ #
271
+ # @example Nested workflow processing
272
+ # class MasterWorkflow < CMDx::Workflow
273
+ # process PreProcessingWorkflow
274
+ # process CoreWorkflow, if: proc { context.pre_processing_successful? }
275
+ # process PostProcessingWorkflow, unless: proc { context.skip_post_processing? }
276
+ # end
277
+ def process(*tasks, **options)
278
+ workflow_groups << Group.new(
279
+ tasks.flatten.map do |task|
280
+ next task if task <= Task
281
+
282
+ raise TypeError, "must be a Task or Workflow"
283
+ end,
284
+ options
285
+ )
286
+ end
287
+
288
+ end
289
+
290
+ ##
291
+ # Executes all defined task groups in sequential order.
292
+ # This method is automatically defined and should not be overridden.
293
+ # The execution flow handles conditional evaluation, task execution,
294
+ # and halt behavior according to the workflow configuration.
295
+ #
296
+ # ## Execution Algorithm
297
+ #
298
+ # 1. **Group Iteration**: Process each group in declaration order
299
+ # 2. **Condition Evaluation**: Check `:if`/`:unless` conditions
300
+ # 3. **Task Execution**: Run each task in the group sequentially
301
+ # 4. **Result Evaluation**: Check task result against halt conditions
302
+ # 5. **Halt Decision**: Stop execution or continue to next task
303
+ # 6. **Context Propagation**: Pass updated context through pipeline
304
+ #
305
+ # ## Context Behavior
306
+ #
307
+ # The context object is shared across all tasks in the workflow:
308
+ # - Tasks can read data added by previous tasks
309
+ # - Tasks can modify context for subsequent tasks
310
+ # - Context persists throughout the entire workflow execution
311
+ # - Final context is available in the workflow result
312
+ #
313
+ # ## Error Handling
314
+ #
315
+ # Workflow execution follows the same error handling as individual tasks:
316
+ # - Exceptions become failed results
317
+ # - Faults are propagated through the result chain
318
+ # - Halt behavior determines whether execution continues
319
+ #
320
+ # @return [Result] workflow execution result with aggregated context
321
+ #
322
+ # @example Basic execution flow
323
+ # # Given this workflow:
324
+ # class ProcessOrderWorkflow < CMDx::Workflow
325
+ # process ValidateOrderTask # Sets context.validation_result
326
+ # process CalculateTaxTask # Uses context.order, sets context.tax_amount
327
+ # process ChargePaymentTask # Uses context.tax_amount, sets context.payment_id
328
+ # process FulfillOrderTask # Uses context.payment_id, sets context.tracking_number
329
+ # end
330
+ #
331
+ # # Execution creates a pipeline:
332
+ # result = ProcessOrderWorkflow.call(order: order)
333
+ # result.context.validation_result # From ValidateOrderTask
334
+ # result.context.tax_amount # From CalculateTaxTask
335
+ # result.context.payment_id # From ChargePaymentTask
336
+ # result.context.tracking_number # From FulfillOrderTask
337
+ #
338
+ # @example Conditional execution
339
+ # # Given this workflow:
340
+ # class ConditionalWorkflow < CMDx::Workflow
341
+ # process TaskA # Always runs
342
+ # process TaskB, if: proc { context.run_b? } # Conditional
343
+ # process TaskC, unless: proc { context.skip_c? } # Conditional
344
+ # end
345
+ #
346
+ # # Execution evaluates conditions:
347
+ # # 1. TaskA runs (always)
348
+ # # 2. TaskB runs only if context.run_b? is truthy
349
+ # # 3. TaskC runs only if context.skip_c? is falsy
350
+ #
351
+ # @example Halt behavior
352
+ # # Given this workflow with custom halt:
353
+ # class HaltWorkflow < CMDx::Workflow
354
+ # process TaskA # Default halt (FAILED)
355
+ # process TaskB, TaskC, workflow_halt: [] # Never halt
356
+ # process TaskD # Default halt (FAILED)
357
+ # end
358
+ #
359
+ # # If TaskB fails:
360
+ # # - TaskB execution completes with failed status
361
+ # # - TaskC still executes (workflow_halt: [] means no halt)
362
+ # # - TaskD still executes
363
+ # # - Workflow continues to completion
364
+ #
365
+ # # If TaskA fails:
366
+ # # - TaskA execution completes with failed status
367
+ # # - Workflow halts (default behavior)
368
+ # # - TaskB, TaskC, TaskD never execute
369
+ # # - Workflow result shows failed status
370
+ #
371
+ # @note Do not override this method. Workflow execution logic is automatically
372
+ # provided and handles all the complexity of group processing, conditional
373
+ # evaluation, and halt behavior.
374
+ #
375
+ # @see Task#call Base task execution method
376
+ # @see Context Shared data object
377
+ # @see Result Task execution results
378
+ def call
379
+ self.class.workflow_groups.each do |group|
380
+ next unless __cmdx_eval(group.options)
381
+
382
+ workflow_halt = group.options[:workflow_halt] || task_setting(:workflow_halt)
383
+
384
+ group.tasks.each do |task|
385
+ task_result = task.call(context)
386
+ next unless Array(workflow_halt).include?(task_result.status)
387
+
388
+ throw!(task_result)
389
+ end
390
+ end
391
+ end
392
+
393
+ end
394
+ end
data/lib/cmdx.rb CHANGED
@@ -9,79 +9,40 @@ require "pp"
9
9
  require "securerandom"
10
10
  require "time"
11
11
  require "timeout"
12
+ require "zeitwerk"
12
13
 
13
- require_relative "cmdx/version"
14
+ module CMDx; end
15
+
16
+ # Set up Zeitwerk loader for the CMDx gem
17
+ loader = Zeitwerk::Loader.for_gem
18
+ loader.inflector.inflect("cmdx" => "CMDx")
19
+ loader.ignore("#{__dir__}/cmdx/core_ext")
20
+ loader.ignore("#{__dir__}/cmdx/configuration")
21
+ loader.ignore("#{__dir__}/cmdx/faults")
22
+ loader.ignore("#{__dir__}/cmdx/railtie")
23
+ loader.ignore("#{__dir__}/cmdx/rspec")
24
+ loader.ignore("#{__dir__}/generators")
25
+ loader.ignore("#{__dir__}/locales")
26
+ loader.setup
27
+
28
+ # Pre-load core extensions to avoid circular dependencies
14
29
  require_relative "cmdx/core_ext/object"
15
30
  require_relative "cmdx/core_ext/hash"
16
31
  require_relative "cmdx/core_ext/module"
17
- require_relative "cmdx/log_formatters/json"
18
- require_relative "cmdx/log_formatters/key_value"
19
- require_relative "cmdx/log_formatters/line"
20
- require_relative "cmdx/log_formatters/logstash"
21
- require_relative "cmdx/log_formatters/raw"
22
- require_relative "cmdx/log_formatters/pretty_json"
23
- require_relative "cmdx/log_formatters/pretty_key_value"
24
- require_relative "cmdx/log_formatters/pretty_line"
25
- require_relative "cmdx/coercions/array"
26
- require_relative "cmdx/coercions/big_decimal"
27
- require_relative "cmdx/coercions/boolean"
28
- require_relative "cmdx/coercions/complex"
29
- require_relative "cmdx/coercions/date"
30
- require_relative "cmdx/coercions/date_time"
31
- require_relative "cmdx/coercions/float"
32
- require_relative "cmdx/coercions/hash"
33
- require_relative "cmdx/coercions/integer"
34
- require_relative "cmdx/coercions/rational"
35
- require_relative "cmdx/coercions/string"
36
- require_relative "cmdx/coercions/time"
37
- require_relative "cmdx/coercions/virtual"
38
- require_relative "cmdx/validators/custom"
39
- require_relative "cmdx/validators/exclusion"
40
- require_relative "cmdx/validators/format"
41
- require_relative "cmdx/validators/inclusion"
42
- require_relative "cmdx/validators/length"
43
- require_relative "cmdx/validators/numeric"
44
- require_relative "cmdx/validators/presence"
45
- require_relative "cmdx/utils/ansi_color"
46
- require_relative "cmdx/utils/log_timestamp"
47
- require_relative "cmdx/utils/name_affix"
48
- require_relative "cmdx/utils/monotonic_runtime"
49
- require_relative "cmdx/error"
50
- require_relative "cmdx/errors"
51
- require_relative "cmdx/fault"
52
- require_relative "cmdx/faults"
53
- require_relative "cmdx/logger_serializer"
54
- require_relative "cmdx/logger_ansi"
55
- require_relative "cmdx/logger"
56
- require_relative "cmdx/lazy_struct"
32
+
33
+ # Pre-load configuration to make module methods available
34
+ # This is acceptable since configuration is fundamental to the framework
57
35
  require_relative "cmdx/configuration"
58
- require_relative "cmdx/context"
59
- require_relative "cmdx/run"
60
- require_relative "cmdx/run_serializer"
61
- require_relative "cmdx/run_inspector"
62
- require_relative "cmdx/parameter"
63
- require_relative "cmdx/parameter_value"
64
- require_relative "cmdx/parameter_validator"
65
- require_relative "cmdx/parameter_serializer"
66
- require_relative "cmdx/parameter_inspector"
67
- require_relative "cmdx/parameters"
68
- require_relative "cmdx/parameters_serializer"
69
- require_relative "cmdx/parameters_inspector"
70
- require_relative "cmdx/result"
71
- require_relative "cmdx/result_serializer"
72
- require_relative "cmdx/result_inspector"
73
- require_relative "cmdx/result_ansi"
74
- require_relative "cmdx/result_logger"
75
- require_relative "cmdx/task"
76
- require_relative "cmdx/task_hook"
77
- require_relative "cmdx/task_serializer"
78
- require_relative "cmdx/batch"
79
- require_relative "cmdx/immutator"
80
36
 
37
+ # Pre-load fault classes to make them available at the top level
38
+ # This ensures CMDx::Failed and CMDx::Skipped are always available
39
+ require_relative "cmdx/faults"
40
+
41
+ # Conditionally load Rails components if Rails is available
81
42
  if defined?(Rails::Generators)
82
43
  require_relative "generators/cmdx/install_generator"
83
44
  require_relative "generators/cmdx/task_generator"
84
- require_relative "generators/cmdx/batch_generator"
45
+ require_relative "generators/cmdx/workflow_generator"
85
46
  end
86
47
 
87
48
  # Load the Railtie last after everything else is required so we don't
@@ -1,12 +1,48 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cmdx
4
+ ##
5
+ # Rails generator for creating CMDx initializer configuration.
6
+ #
7
+ # This generator creates a configuration initializer file that sets up
8
+ # global CMDx settings for task execution, workflow processing, logging,
9
+ # and error handling behaviors.
10
+ #
11
+ # The generated initializer provides sensible defaults that can be
12
+ # customized for specific application requirements.
13
+ #
14
+ # @example Generate CMDx initializer
15
+ # rails generate cmdx:install
16
+ #
17
+ # @example Generated file location
18
+ # config/initializers/cmdx.rb
19
+ #
20
+ # @since 1.0.0
4
21
  class InstallGenerator < Rails::Generators::Base
5
22
 
6
23
  source_root File.expand_path("templates", __dir__)
7
24
 
8
- desc "Generates a CMDx configurations files for global settings."
25
+ desc "Creates CMDx initializer with global configuration settings"
9
26
 
27
+ ##
28
+ # Copies the CMDx configuration template to the Rails initializers directory.
29
+ #
30
+ # Creates a new initializer file at `config/initializers/cmdx.rb` with
31
+ # default configuration settings for:
32
+ # - Task halt behaviors
33
+ # - Timeout settings
34
+ # - Workflow execution controls
35
+ # - Logger configuration
36
+ #
37
+ # @return [void]
38
+ # @raise [Thor::Error] if the destination file cannot be created
39
+ #
40
+ # @example Generated initializer content
41
+ # CMDx.configure do |config|
42
+ # config.task_halt = CMDx::Result::FAILED
43
+
44
+ # # ... additional settings
45
+ # end
10
46
  def copy_initializer_file
11
47
  copy_file("install.rb", "config/initializers/cmdx.rb")
12
48
  end