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.
- 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 +31 -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 +203 -31
- data/docs/outcomes/statuses.md +275 -30
- 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 +367 -25
- 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 +405 -37
- 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 -62
- data/lib/cmdx/batch.rb +0 -43
- data/lib/cmdx/parameters.rb +0 -35
- data/lib/cmdx/run.rb +0 -39
- data/lib/cmdx/run_inspector.rb +0 -26
- data/lib/cmdx/run_serializer.rb +0 -20
- 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
@@ -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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
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/
|
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 "
|
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
|