cmdx 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.cursor/prompts/rspec.md +20 -0
- data/.cursor/prompts/yardoc.md +8 -0
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +17 -2
- data/README.md +1 -1
- data/docs/basics/call.md +2 -2
- data/docs/basics/chain.md +1 -1
- data/docs/callbacks.md +3 -36
- data/docs/configuration.md +58 -12
- data/docs/interruptions/exceptions.md +1 -1
- data/docs/interruptions/faults.md +2 -2
- data/docs/logging.md +4 -4
- data/docs/middlewares.md +43 -43
- data/docs/parameters/coercions.md +49 -38
- data/docs/parameters/defaults.md +1 -1
- data/docs/parameters/validations.md +0 -39
- data/docs/testing.md +11 -12
- data/docs/workflows.md +4 -4
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/callback.rb +36 -56
- data/lib/cmdx/callback_registry.rb +82 -73
- data/lib/cmdx/chain.rb +65 -122
- data/lib/cmdx/chain_inspector.rb +22 -115
- data/lib/cmdx/chain_serializer.rb +17 -148
- data/lib/cmdx/coercion.rb +49 -0
- data/lib/cmdx/coercion_registry.rb +94 -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 +57 -171
- data/lib/cmdx/context.rb +22 -165
- 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 +40 -156
- data/lib/cmdx/error.rb +37 -202
- data/lib/cmdx/errors.rb +165 -202
- data/lib/cmdx/fault.rb +55 -158
- data/lib/cmdx/faults.rb +26 -137
- data/lib/cmdx/immutator.rb +22 -109
- data/lib/cmdx/lazy_struct.rb +103 -187
- 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 +20 -82
- data/lib/cmdx/logger_ansi.rb +18 -75
- data/lib/cmdx/logger_serializer.rb +24 -114
- data/lib/cmdx/middleware.rb +38 -60
- data/lib/cmdx/middleware_registry.rb +81 -77
- data/lib/cmdx/middlewares/correlate.rb +41 -226
- data/lib/cmdx/middlewares/timeout.rb +46 -185
- data/lib/cmdx/parameter.rb +120 -198
- data/lib/cmdx/parameter_evaluator.rb +231 -0
- data/lib/cmdx/parameter_inspector.rb +25 -56
- data/lib/cmdx/parameter_registry.rb +59 -84
- data/lib/cmdx/parameter_serializer.rb +23 -74
- data/lib/cmdx/railtie.rb +24 -107
- data/lib/cmdx/result.rb +254 -260
- data/lib/cmdx/result_ansi.rb +19 -85
- data/lib/cmdx/result_inspector.rb +27 -68
- data/lib/cmdx/result_logger.rb +18 -81
- data/lib/cmdx/result_serializer.rb +28 -132
- 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 +213 -425
- data/lib/cmdx/task_deprecator.rb +55 -0
- data/lib/cmdx/task_processor.rb +245 -0
- data/lib/cmdx/task_serializer.rb +22 -70
- 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 +13 -63
- data/lib/cmdx/utils/name_affix.rb +21 -71
- data/lib/cmdx/validator.rb +48 -0
- data/lib/cmdx/validator_registry.rb +86 -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 +46 -339
- 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 +34 -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.0"
|
12
6
|
|
13
7
|
end
|
data/lib/cmdx/workflow.rb
CHANGED
@@ -1,285 +1,63 @@
|
|
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
|
+
# Orchestrates sequential execution of multiple tasks and workflows.
|
9
5
|
#
|
10
|
-
#
|
11
|
-
#
|
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
|
6
|
+
# Workflow provides a way to chain multiple tasks together with conditional
|
7
|
+
# execution logic and halt behavior. Tasks are organized into groups that can
|
8
|
+
# be conditionally executed based on options, and execution can be halted
|
9
|
+
# based on task results.
|
140
10
|
class Workflow < Task
|
141
11
|
|
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.
|
12
|
+
# Container for holding a group of tasks and their execution options.
|
146
13
|
#
|
147
14
|
# @!attribute [r] tasks
|
148
|
-
# @return [Array<
|
15
|
+
# @return [Array<Task>] the tasks in this group
|
149
16
|
# @!attribute [r] options
|
150
|
-
# @return [Hash] execution options
|
151
|
-
#
|
152
|
-
# @example Group creation
|
153
|
-
# group = CMDx::Workflow::Group.new(
|
154
|
-
# [TaskA, TaskB, TaskC],
|
155
|
-
# { if: proc { condition }, workflow_halt: ["failed"] }
|
156
|
-
# )
|
17
|
+
# @return [Hash] the execution options for this group
|
157
18
|
Group = Struct.new(:tasks, :options)
|
158
19
|
|
159
20
|
class << self
|
160
21
|
|
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
|
22
|
+
# Returns the collection of workflow groups defined for this workflow.
|
167
23
|
#
|
168
|
-
# @
|
169
|
-
# class MyWorkflow < CMDx::Workflow
|
170
|
-
# process TaskA, TaskB
|
171
|
-
# process TaskC, if: proc { condition }
|
172
|
-
# end
|
24
|
+
# @return [Array<Group>] array of workflow groups to be executed
|
173
25
|
#
|
174
|
-
#
|
175
|
-
# MyWorkflow.workflow_groups
|
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
|
26
|
+
# @example Access workflow groups
|
27
|
+
# MyWorkflow.workflow_groups #=> [#<Group:...>, #<Group:...>]
|
183
28
|
def workflow_groups
|
184
29
|
@workflow_groups ||= []
|
185
30
|
end
|
186
31
|
|
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
|
32
|
+
# Defines a group of tasks to be executed as part of this workflow.
|
194
33
|
#
|
195
|
-
#
|
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
|
34
|
+
# @param tasks [Array<Task>] tasks to include in this workflow group
|
207
35
|
# @param options [Hash] execution options for this group
|
36
|
+
# @option options [Symbol, Array<Symbol>] :workflow_halt status values that will halt workflow execution
|
37
|
+
# @option options [Proc] :if conditional proc that determines if this group should execute
|
38
|
+
# @option options [Proc] :unless conditional proc that determines if this group should be skipped
|
208
39
|
#
|
209
|
-
# @
|
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
|
40
|
+
# @return [void]
|
240
41
|
#
|
241
|
-
# @
|
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]
|
42
|
+
# @raise [TypeError] if any task is not a Task or Workflow subclass
|
246
43
|
#
|
247
|
-
#
|
248
|
-
#
|
44
|
+
# @example Define a simple workflow group
|
45
|
+
# MyWorkflow.process CreateUserTask, SendEmailTask
|
249
46
|
#
|
250
|
-
#
|
251
|
-
#
|
252
|
-
# end
|
47
|
+
# @example Define a conditional workflow group
|
48
|
+
# MyWorkflow.process NotifyAdminTask, if: ->(workflow) { workflow.context.admin.active? }
|
253
49
|
#
|
254
|
-
# @example
|
255
|
-
#
|
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
|
50
|
+
# @example Define a workflow group with halt behavior
|
51
|
+
# MyWorkflow.process ValidateInputTask, ProcessDataTask, workflow_halt: :failed
|
277
52
|
def process(*tasks, **options)
|
278
53
|
workflow_groups << Group.new(
|
279
54
|
tasks.flatten.map do |task|
|
280
|
-
|
55
|
+
unless task.is_a?(Class) && (task <= Task)
|
56
|
+
raise TypeError,
|
57
|
+
"must be a Task or Workflow"
|
58
|
+
end
|
281
59
|
|
282
|
-
|
60
|
+
task
|
283
61
|
end,
|
284
62
|
options
|
285
63
|
)
|
@@ -287,103 +65,32 @@ module CMDx
|
|
287
65
|
|
288
66
|
end
|
289
67
|
|
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
|
68
|
+
# Executes all workflow groups in sequence.
|
358
69
|
#
|
359
|
-
#
|
360
|
-
#
|
361
|
-
#
|
362
|
-
#
|
363
|
-
# # - Workflow continues to completion
|
70
|
+
# Each group is evaluated for conditional execution, and if the group should
|
71
|
+
# execute, all tasks in the group are called in sequence. If any task returns
|
72
|
+
# a status that matches the workflow halt criteria, execution is halted and
|
73
|
+
# the result is thrown.
|
364
74
|
#
|
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
|
75
|
+
# @return [void]
|
370
76
|
#
|
371
|
-
# @
|
372
|
-
# provided and handles all the complexity of group processing, conditional
|
373
|
-
# evaluation, and halt behavior.
|
77
|
+
# @raise [Fault] if a task fails and its status matches the workflow halt criteria
|
374
78
|
#
|
375
|
-
# @
|
376
|
-
#
|
377
|
-
#
|
79
|
+
# @example Execute workflow with halt on failure
|
80
|
+
# workflow = MyWorkflow.new(user_id: 123)
|
81
|
+
# workflow.call # Executes all groups until halt condition is met
|
378
82
|
def call
|
379
83
|
self.class.workflow_groups.each do |group|
|
380
|
-
next unless
|
84
|
+
next unless cmdx_eval(group.options)
|
381
85
|
|
382
|
-
workflow_halt =
|
86
|
+
workflow_halt = Array(
|
87
|
+
group.options[:workflow_halt] ||
|
88
|
+
cmd_setting(:workflow_halt)
|
89
|
+
).map(&:to_s)
|
383
90
|
|
384
91
|
group.tasks.each do |task|
|
385
92
|
task_result = task.call(context)
|
386
|
-
next unless
|
93
|
+
next unless workflow_halt.include?(task_result.status)
|
387
94
|
|
388
95
|
throw!(task_result)
|
389
96
|
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
|
@@ -1,48 +1,31 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Cmdx
|
4
|
-
|
5
|
-
# Rails generator for creating CMDx initializer configuration.
|
4
|
+
# Rails generator for creating CMDx initializer configuration file.
|
6
5
|
#
|
7
|
-
# This generator creates a
|
8
|
-
# global
|
9
|
-
#
|
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
|
6
|
+
# This generator creates a new initializer file at config/initializers/cmdx.rb
|
7
|
+
# with global configuration settings for the CMDx framework. The generated
|
8
|
+
# initializer provides a centralized location for configuring CMDx behavior
|
9
|
+
# such as logging, error handling, and default parameter settings.
|
21
10
|
class InstallGenerator < Rails::Generators::Base
|
22
11
|
|
23
12
|
source_root File.expand_path("templates", __dir__)
|
24
13
|
|
25
14
|
desc "Creates CMDx initializer with global configuration settings"
|
26
15
|
|
27
|
-
|
28
|
-
# Copies the CMDx configuration template to the Rails initializers directory.
|
16
|
+
# Copies the CMDx initializer template to the Rails application.
|
29
17
|
#
|
30
|
-
# Creates a new initializer file at
|
31
|
-
# default configuration
|
32
|
-
#
|
33
|
-
# - Timeout settings
|
34
|
-
# - Workflow execution controls
|
35
|
-
# - Logger configuration
|
18
|
+
# Creates a new initializer file at config/initializers/cmdx.rb by copying
|
19
|
+
# the install.rb template. This file contains the default CMDx configuration
|
20
|
+
# that can be customized for the specific application needs.
|
36
21
|
#
|
37
22
|
# @return [void]
|
38
|
-
# @raise [Thor::Error] if the destination file cannot be created
|
39
23
|
#
|
40
|
-
# @
|
41
|
-
#
|
42
|
-
#
|
43
|
-
|
44
|
-
#
|
45
|
-
# end
|
24
|
+
# @raise [Thor::Error] if the destination file cannot be created or already exists without force
|
25
|
+
#
|
26
|
+
# @example Generate CMDx initializer
|
27
|
+
# rails generate cmdx:install
|
28
|
+
# # Creates config/initializers/cmdx.rb
|
46
29
|
def copy_initializer_file
|
47
30
|
copy_file("install.rb", "config/initializers/cmdx.rb")
|
48
31
|
end
|