cmdx 1.0.1 → 1.1.1
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/.cursor/prompts/docs.md +9 -0
- data/.cursor/prompts/rspec.md +21 -0
- data/.cursor/prompts/yardoc.md +13 -0
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +29 -3
- data/README.md +2 -1
- data/docs/ai_prompts.md +269 -195
- data/docs/basics/call.md +126 -60
- data/docs/basics/chain.md +190 -160
- data/docs/basics/context.md +242 -154
- data/docs/basics/setup.md +302 -32
- data/docs/callbacks.md +382 -119
- data/docs/configuration.md +211 -49
- data/docs/deprecation.md +245 -0
- data/docs/getting_started.md +161 -39
- data/docs/internationalization.md +590 -70
- data/docs/interruptions/exceptions.md +135 -118
- data/docs/interruptions/faults.md +152 -127
- data/docs/interruptions/halt.md +134 -80
- data/docs/logging.md +183 -120
- data/docs/middlewares.md +165 -392
- data/docs/outcomes/result.md +140 -112
- data/docs/outcomes/states.md +134 -99
- data/docs/outcomes/statuses.md +204 -146
- data/docs/parameters/coercions.md +251 -289
- data/docs/parameters/defaults.md +224 -169
- data/docs/parameters/definitions.md +289 -141
- data/docs/parameters/namespacing.md +250 -161
- data/docs/parameters/validations.md +247 -159
- data/docs/testing.md +196 -203
- data/docs/workflows.md +146 -101
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/callback.rb +39 -55
- data/lib/cmdx/callback_registry.rb +80 -73
- data/lib/cmdx/chain.rb +65 -122
- data/lib/cmdx/chain_inspector.rb +23 -116
- data/lib/cmdx/chain_serializer.rb +34 -146
- data/lib/cmdx/coercion.rb +57 -0
- data/lib/cmdx/coercion_registry.rb +113 -0
- data/lib/cmdx/coercions/array.rb +18 -36
- data/lib/cmdx/coercions/big_decimal.rb +21 -33
- data/lib/cmdx/coercions/boolean.rb +21 -40
- data/lib/cmdx/coercions/complex.rb +18 -31
- data/lib/cmdx/coercions/date.rb +20 -39
- data/lib/cmdx/coercions/date_time.rb +22 -39
- data/lib/cmdx/coercions/float.rb +19 -32
- data/lib/cmdx/coercions/hash.rb +22 -41
- data/lib/cmdx/coercions/integer.rb +20 -33
- data/lib/cmdx/coercions/rational.rb +20 -32
- data/lib/cmdx/coercions/string.rb +23 -31
- data/lib/cmdx/coercions/time.rb +24 -40
- data/lib/cmdx/coercions/virtual.rb +14 -31
- data/lib/cmdx/configuration.rb +101 -162
- data/lib/cmdx/context.rb +34 -166
- data/lib/cmdx/core_ext/hash.rb +42 -67
- data/lib/cmdx/core_ext/module.rb +35 -79
- data/lib/cmdx/core_ext/object.rb +63 -98
- data/lib/cmdx/correlator.rb +59 -154
- data/lib/cmdx/error.rb +37 -202
- data/lib/cmdx/errors.rb +153 -216
- data/lib/cmdx/fault.rb +68 -150
- data/lib/cmdx/faults.rb +26 -137
- data/lib/cmdx/immutator.rb +22 -110
- data/lib/cmdx/lazy_struct.rb +110 -186
- data/lib/cmdx/log_formatters/json.rb +14 -40
- data/lib/cmdx/log_formatters/key_value.rb +14 -40
- data/lib/cmdx/log_formatters/line.rb +14 -48
- data/lib/cmdx/log_formatters/logstash.rb +14 -57
- data/lib/cmdx/log_formatters/pretty_json.rb +14 -50
- data/lib/cmdx/log_formatters/pretty_key_value.rb +13 -46
- data/lib/cmdx/log_formatters/pretty_line.rb +16 -54
- data/lib/cmdx/log_formatters/raw.rb +19 -49
- data/lib/cmdx/logger.rb +22 -79
- data/lib/cmdx/logger_ansi.rb +31 -72
- data/lib/cmdx/logger_serializer.rb +74 -103
- data/lib/cmdx/middleware.rb +56 -60
- data/lib/cmdx/middleware_registry.rb +82 -77
- data/lib/cmdx/middlewares/correlate.rb +41 -226
- data/lib/cmdx/middlewares/timeout.rb +46 -185
- data/lib/cmdx/parameter.rb +167 -183
- data/lib/cmdx/parameter_evaluator.rb +231 -0
- data/lib/cmdx/parameter_inspector.rb +37 -55
- data/lib/cmdx/parameter_registry.rb +65 -84
- data/lib/cmdx/parameter_serializer.rb +32 -76
- data/lib/cmdx/railtie.rb +24 -107
- data/lib/cmdx/result.rb +254 -259
- data/lib/cmdx/result_ansi.rb +28 -80
- data/lib/cmdx/result_inspector.rb +34 -70
- data/lib/cmdx/result_logger.rb +23 -77
- data/lib/cmdx/result_serializer.rb +59 -125
- data/lib/cmdx/rspec/matchers.rb +28 -0
- data/lib/cmdx/rspec/result_matchers/be_executed.rb +42 -0
- data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +94 -0
- data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +94 -0
- data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +59 -0
- data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +57 -0
- data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +87 -0
- data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +51 -0
- data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +58 -0
- data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +59 -0
- data/lib/cmdx/rspec/result_matchers/have_context.rb +86 -0
- data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +54 -0
- data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +52 -0
- data/lib/cmdx/rspec/result_matchers/have_metadata.rb +114 -0
- data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +66 -0
- data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +64 -0
- data/lib/cmdx/rspec/result_matchers/have_runtime.rb +78 -0
- data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +76 -0
- data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +62 -0
- data/lib/cmdx/rspec/task_matchers/have_callback.rb +85 -0
- data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +68 -0
- data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +92 -0
- data/lib/cmdx/rspec/task_matchers/have_middleware.rb +46 -0
- data/lib/cmdx/rspec/task_matchers/have_parameter.rb +181 -0
- data/lib/cmdx/task.rb +336 -427
- data/lib/cmdx/task_deprecator.rb +52 -0
- data/lib/cmdx/task_processor.rb +246 -0
- data/lib/cmdx/task_serializer.rb +34 -69
- data/lib/cmdx/utils/ansi_color.rb +13 -89
- data/lib/cmdx/utils/log_timestamp.rb +13 -42
- data/lib/cmdx/utils/monotonic_runtime.rb +11 -63
- data/lib/cmdx/utils/name_affix.rb +21 -71
- data/lib/cmdx/validator.rb +57 -0
- data/lib/cmdx/validator_registry.rb +108 -0
- data/lib/cmdx/validators/exclusion.rb +55 -94
- data/lib/cmdx/validators/format.rb +31 -85
- data/lib/cmdx/validators/inclusion.rb +65 -110
- data/lib/cmdx/validators/length.rb +117 -133
- data/lib/cmdx/validators/numeric.rb +123 -130
- data/lib/cmdx/validators/presence.rb +38 -79
- data/lib/cmdx/version.rb +1 -7
- data/lib/cmdx/workflow.rb +58 -330
- data/lib/cmdx.rb +1 -1
- data/lib/generators/cmdx/install_generator.rb +14 -31
- data/lib/generators/cmdx/task_generator.rb +39 -55
- data/lib/generators/cmdx/templates/install.rb +24 -6
- data/lib/generators/cmdx/workflow_generator.rb +41 -66
- data/lib/locales/ar.yml +0 -1
- data/lib/locales/cs.yml +0 -1
- data/lib/locales/da.yml +0 -1
- data/lib/locales/de.yml +0 -1
- data/lib/locales/el.yml +0 -1
- data/lib/locales/en.yml +0 -1
- data/lib/locales/es.yml +0 -1
- data/lib/locales/fi.yml +0 -1
- data/lib/locales/fr.yml +0 -1
- data/lib/locales/he.yml +0 -1
- data/lib/locales/hi.yml +0 -1
- data/lib/locales/it.yml +0 -1
- data/lib/locales/ja.yml +0 -1
- data/lib/locales/ko.yml +0 -1
- data/lib/locales/nl.yml +0 -1
- data/lib/locales/no.yml +0 -1
- data/lib/locales/pl.yml +0 -1
- data/lib/locales/pt.yml +0 -1
- data/lib/locales/ru.yml +0 -1
- data/lib/locales/sv.yml +0 -1
- data/lib/locales/th.yml +0 -1
- data/lib/locales/tr.yml +0 -1
- data/lib/locales/vi.yml +0 -1
- data/lib/locales/zh.yml +0 -1
- metadata +36 -8
- data/lib/cmdx/parameter_validator.rb +0 -81
- data/lib/cmdx/parameter_value.rb +0 -244
- data/lib/cmdx/parameters_inspector.rb +0 -72
- data/lib/cmdx/parameters_serializer.rb +0 -115
- data/lib/cmdx/rspec/result_matchers.rb +0 -917
- data/lib/cmdx/rspec/task_matchers.rb +0 -570
- data/lib/cmdx/validators/custom.rb +0 -102
@@ -2,93 +2,52 @@
|
|
2
2
|
|
3
3
|
module CMDx
|
4
4
|
module Validators
|
5
|
-
#
|
5
|
+
# Validator class for ensuring values are present (not empty or nil).
|
6
6
|
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
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
|
54
|
-
module Presence
|
55
|
-
|
56
|
-
module_function
|
7
|
+
# This validator checks that a value is not empty, blank, or nil. For strings,
|
8
|
+
# it validates that there are non-whitespace characters. For objects that respond
|
9
|
+
# to empty?, it ensures they are not empty. For all other objects, it validates
|
10
|
+
# they are not nil.
|
11
|
+
class Presence < Validator
|
57
12
|
|
58
|
-
# Validates that
|
13
|
+
# Validates that the given value is present (not empty or nil).
|
14
|
+
#
|
15
|
+
# @param value [Object] the value to validate
|
16
|
+
# @param options [Hash] validation options containing presence configuration
|
17
|
+
# @option options [Hash] :presence presence validation configuration
|
18
|
+
# @option options [String] :presence.message custom error message
|
19
|
+
#
|
20
|
+
# @return [void] returns nothing when validation passes
|
21
|
+
#
|
22
|
+
# @raise [ValidationError] if the value is empty, blank, or nil
|
59
23
|
#
|
60
|
-
#
|
61
|
-
#
|
62
|
-
#
|
63
|
-
# - Other objects: Must not be nil
|
24
|
+
# @example Validating a non-empty string
|
25
|
+
# Validators::Presence.call("hello", presence: {})
|
26
|
+
# #=> nil (no error raised)
|
64
27
|
#
|
65
|
-
# @
|
66
|
-
#
|
67
|
-
#
|
68
|
-
# @option options [String] :presence.message Custom error message
|
28
|
+
# @example Validating an empty string
|
29
|
+
# Validators::Presence.call("", presence: {})
|
30
|
+
# # raises ValidationError: "cannot be empty"
|
69
31
|
#
|
70
|
-
# @
|
71
|
-
#
|
32
|
+
# @example Validating a whitespace-only string
|
33
|
+
# Validators::Presence.call(" ", presence: {})
|
34
|
+
# # raises ValidationError: "cannot be empty"
|
72
35
|
#
|
73
|
-
# @example
|
74
|
-
# Presence.call(
|
75
|
-
#
|
76
|
-
# Presence.call(" ", presence: true) # raises ValidationError
|
36
|
+
# @example Validating a non-empty array
|
37
|
+
# Validators::Presence.call([1, 2, 3], presence: {})
|
38
|
+
# #=> nil (no error raised)
|
77
39
|
#
|
78
|
-
# @example
|
79
|
-
# Presence.call([
|
80
|
-
#
|
81
|
-
# Presence.call({key: "value"}, presence: true) # passes
|
82
|
-
# Presence.call({}, presence: true) # raises ValidationError
|
40
|
+
# @example Validating an empty array
|
41
|
+
# Validators::Presence.call([], presence: {})
|
42
|
+
# # raises ValidationError: "cannot be empty"
|
83
43
|
#
|
84
|
-
# @example
|
85
|
-
# Presence.call(
|
86
|
-
#
|
87
|
-
# Presence.call(nil, presence: true) # raises ValidationError
|
44
|
+
# @example Validating a nil value
|
45
|
+
# Validators::Presence.call(nil, presence: {})
|
46
|
+
# # raises ValidationError: "cannot be empty"
|
88
47
|
#
|
89
|
-
# @example
|
90
|
-
# Presence.call("", presence: { message: "is required" })
|
91
|
-
# #
|
48
|
+
# @example Using a custom message
|
49
|
+
# Validators::Presence.call("", presence: { message: "This field is required" })
|
50
|
+
# # raises ValidationError: "This field is required"
|
92
51
|
def call(value, options = {})
|
93
52
|
present =
|
94
53
|
if value.is_a?(String)
|
@@ -101,7 +60,7 @@ module CMDx
|
|
101
60
|
|
102
61
|
return if present
|
103
62
|
|
104
|
-
message = options
|
63
|
+
message = options[:message] if options.is_a?(Hash)
|
105
64
|
raise ValidationError, message || I18n.t(
|
106
65
|
"cmdx.validators.presence",
|
107
66
|
default: "cannot be empty"
|
data/lib/cmdx/version.rb
CHANGED
@@ -2,12 +2,6 @@
|
|
2
2
|
|
3
3
|
module CMDx
|
4
4
|
|
5
|
-
|
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.1"
|
5
|
+
VERSION = "1.1.1"
|
12
6
|
|
13
7
|
end
|
data/lib/cmdx/workflow.rb
CHANGED
@@ -1,283 +1,84 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
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.
|
4
|
+
# Sequential task execution orchestration system for CMDx framework.
|
9
5
|
#
|
6
|
+
# Workflow provides declarative composition of multiple tasks into linear pipelines
|
7
|
+
# with conditional execution, context propagation, and configurable halt behavior.
|
10
8
|
# Workflows inherit from Task, gaining all task capabilities including callbacks,
|
11
|
-
# parameter validation, result tracking, and configuration
|
12
|
-
#
|
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
|
9
|
+
# parameter validation, result tracking, and configuration while coordinating
|
10
|
+
# other tasks rather than implementing business logic directly.
|
140
11
|
class Workflow < Task
|
141
12
|
|
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.
|
13
|
+
# Data structure containing a group of tasks and their execution options.
|
146
14
|
#
|
147
15
|
# @!attribute [r] tasks
|
148
|
-
# @return [Array<Class>] array of
|
16
|
+
# @return [Array<Class>] array of Task or Workflow classes to execute
|
149
17
|
# @!attribute [r] options
|
150
|
-
# @return [Hash] execution options including
|
151
|
-
#
|
152
|
-
# @example Group creation
|
153
|
-
# group = CMDx::Workflow::Group.new(
|
154
|
-
# [TaskA, TaskB, TaskC],
|
155
|
-
# { if: proc { condition }, workflow_halt: ["failed"] }
|
156
|
-
# )
|
18
|
+
# @return [Hash] execution options including conditional and halt configuration
|
157
19
|
Group = Struct.new(:tasks, :options)
|
158
20
|
|
159
21
|
class << self
|
160
22
|
|
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.
|
23
|
+
# Returns the array of workflow groups defined for this workflow class.
|
165
24
|
#
|
166
|
-
#
|
25
|
+
# Each group contains tasks and their execution options. Groups are processed
|
26
|
+
# sequentially during workflow execution, with each group's tasks executing
|
27
|
+
# in order unless halted by a result status.
|
167
28
|
#
|
168
|
-
# @
|
29
|
+
# @return [Array<Group>] array of workflow groups containing tasks and options
|
30
|
+
#
|
31
|
+
# @example Access workflow groups
|
169
32
|
# class MyWorkflow < CMDx::Workflow
|
170
33
|
# process TaskA, TaskB
|
171
|
-
# process TaskC, if:
|
34
|
+
# process TaskC, if: :condition_met?
|
172
35
|
# end
|
173
36
|
#
|
174
|
-
# MyWorkflow.workflow_groups.size
|
175
|
-
# MyWorkflow.workflow_groups.first.tasks
|
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
|
37
|
+
# MyWorkflow.workflow_groups.size #=> 2
|
38
|
+
# MyWorkflow.workflow_groups.first.tasks #=> [TaskA, TaskB]
|
183
39
|
def workflow_groups
|
184
40
|
@workflow_groups ||= []
|
185
41
|
end
|
186
42
|
|
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
|
43
|
+
# Declares a group of tasks to execute sequentially with optional conditions.
|
208
44
|
#
|
209
|
-
#
|
210
|
-
#
|
211
|
-
#
|
45
|
+
# Tasks are executed in the order specified, with shared context propagated
|
46
|
+
# between executions. Groups support conditional execution and configurable
|
47
|
+
# halt behavior to control workflow flow based on task results.
|
212
48
|
#
|
213
|
-
# @
|
49
|
+
# @param tasks [Array<Class>] Task or Workflow classes to execute in sequence
|
50
|
+
# @param options [Hash] execution configuration options
|
214
51
|
#
|
215
|
-
# @
|
216
|
-
#
|
217
|
-
#
|
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? }
|
52
|
+
# @option options [Proc, Symbol, String] :if condition that must be truthy for group execution
|
53
|
+
# @option options [Proc, Symbol, String] :unless condition that must be falsy for group execution
|
54
|
+
# @option options [String, Array<String>] :workflow_halt result statuses that halt workflow execution
|
230
55
|
#
|
231
|
-
#
|
232
|
-
# process DebugTask, if: :debug_enabled?
|
56
|
+
# @return [void]
|
233
57
|
#
|
234
|
-
#
|
58
|
+
# @raise [TypeError] when tasks contain objects that are not Task or Workflow classes
|
235
59
|
#
|
236
|
-
#
|
237
|
-
#
|
238
|
-
#
|
60
|
+
# @example Declare sequential tasks
|
61
|
+
# class UserRegistrationWorkflow < CMDx::Workflow
|
62
|
+
# process CreateUserTask, SendWelcomeEmailTask
|
239
63
|
# end
|
240
64
|
#
|
241
|
-
# @example
|
242
|
-
# class
|
243
|
-
#
|
244
|
-
# process
|
245
|
-
#
|
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
|
65
|
+
# @example Declare conditional task group
|
66
|
+
# class OrderProcessingWorkflow < CMDx::Workflow
|
67
|
+
# process ValidateOrderTask
|
68
|
+
# process ChargePaymentTask, if: ->(workflow) { workflow.context.payment_required? }
|
69
|
+
# process ShipOrderTask, unless: :digital_product?
|
70
|
+
# process NotifyAdminTask, if: proc { context.admin.active? }
|
252
71
|
# end
|
253
72
|
#
|
254
|
-
# @example
|
255
|
-
# class
|
256
|
-
# process
|
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? }
|
73
|
+
# @example Configure halt behavior per group
|
74
|
+
# class DataProcessingWorkflow < CMDx::Workflow
|
75
|
+
# process LoadDataTask, ValidateDataTask, workflow_halt: %w[failed skipped]
|
76
|
+
# process OptionalCleanupTask, workflow_halt: []
|
276
77
|
# end
|
277
78
|
def process(*tasks, **options)
|
278
79
|
workflow_groups << Group.new(
|
279
80
|
tasks.flatten.map do |task|
|
280
|
-
next task if task <= Task
|
81
|
+
next task if task.is_a?(Class) && (task <= Task)
|
281
82
|
|
282
83
|
raise TypeError, "must be a Task or Workflow"
|
283
84
|
end,
|
@@ -287,103 +88,30 @@ module CMDx
|
|
287
88
|
|
288
89
|
end
|
289
90
|
|
290
|
-
|
291
|
-
#
|
292
|
-
#
|
293
|
-
#
|
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
|
91
|
+
# Each group is evaluated for conditional execution, and if the group should
|
92
|
+
# execute, all tasks in the group are called in sequence. If any task returns
|
93
|
+
# a status that matches the workflow halt criteria, execution is halted and
|
94
|
+
# the result is thrown.
|
364
95
|
#
|
365
|
-
#
|
366
|
-
# # - TaskA execution completes with failed status
|
367
|
-
# # - Workflow halts (default behavior)
|
368
|
-
# # - TaskB, TaskC, TaskD never execute
|
369
|
-
# # - Workflow result shows failed status
|
96
|
+
# @return [void]
|
370
97
|
#
|
371
|
-
# @
|
372
|
-
# provided and handles all the complexity of group processing, conditional
|
373
|
-
# evaluation, and halt behavior.
|
98
|
+
# @raise [Fault] if a task fails and its status matches the workflow halt criteria
|
374
99
|
#
|
375
|
-
# @
|
376
|
-
#
|
377
|
-
#
|
100
|
+
# @example Execute workflow
|
101
|
+
# workflow = MyWorkflow.new(user_id: 123)
|
102
|
+
# workflow.call
|
378
103
|
def call
|
379
104
|
self.class.workflow_groups.each do |group|
|
380
|
-
next unless
|
105
|
+
next unless cmdx_eval(group.options)
|
381
106
|
|
382
|
-
workflow_halt =
|
107
|
+
workflow_halt = Array(
|
108
|
+
group.options[:workflow_halt] ||
|
109
|
+
cmd_setting(:workflow_halt)
|
110
|
+
).map(&:to_s)
|
383
111
|
|
384
112
|
group.tasks.each do |task|
|
385
113
|
task_result = task.call(context)
|
386
|
-
next unless
|
114
|
+
next unless workflow_halt.include?(task_result.status)
|
387
115
|
|
388
116
|
throw!(task_result)
|
389
117
|
end
|
data/lib/cmdx.rb
CHANGED
@@ -26,9 +26,9 @@ loader.ignore("#{__dir__}/locales")
|
|
26
26
|
loader.setup
|
27
27
|
|
28
28
|
# Pre-load core extensions to avoid circular dependencies
|
29
|
+
require_relative "cmdx/core_ext/module"
|
29
30
|
require_relative "cmdx/core_ext/object"
|
30
31
|
require_relative "cmdx/core_ext/hash"
|
31
|
-
require_relative "cmdx/core_ext/module"
|
32
32
|
|
33
33
|
# Pre-load configuration to make module methods available
|
34
34
|
# This is acceptable since configuration is fundamental to the framework
|