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.
Files changed (170) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/prompts/docs.md +9 -0
  3. data/.cursor/prompts/rspec.md +21 -0
  4. data/.cursor/prompts/yardoc.md +13 -0
  5. data/.rubocop.yml +2 -0
  6. data/CHANGELOG.md +29 -3
  7. data/README.md +2 -1
  8. data/docs/ai_prompts.md +269 -195
  9. data/docs/basics/call.md +126 -60
  10. data/docs/basics/chain.md +190 -160
  11. data/docs/basics/context.md +242 -154
  12. data/docs/basics/setup.md +302 -32
  13. data/docs/callbacks.md +382 -119
  14. data/docs/configuration.md +211 -49
  15. data/docs/deprecation.md +245 -0
  16. data/docs/getting_started.md +161 -39
  17. data/docs/internationalization.md +590 -70
  18. data/docs/interruptions/exceptions.md +135 -118
  19. data/docs/interruptions/faults.md +152 -127
  20. data/docs/interruptions/halt.md +134 -80
  21. data/docs/logging.md +183 -120
  22. data/docs/middlewares.md +165 -392
  23. data/docs/outcomes/result.md +140 -112
  24. data/docs/outcomes/states.md +134 -99
  25. data/docs/outcomes/statuses.md +204 -146
  26. data/docs/parameters/coercions.md +251 -289
  27. data/docs/parameters/defaults.md +224 -169
  28. data/docs/parameters/definitions.md +289 -141
  29. data/docs/parameters/namespacing.md +250 -161
  30. data/docs/parameters/validations.md +247 -159
  31. data/docs/testing.md +196 -203
  32. data/docs/workflows.md +146 -101
  33. data/lib/cmdx/.DS_Store +0 -0
  34. data/lib/cmdx/callback.rb +39 -55
  35. data/lib/cmdx/callback_registry.rb +80 -73
  36. data/lib/cmdx/chain.rb +65 -122
  37. data/lib/cmdx/chain_inspector.rb +23 -116
  38. data/lib/cmdx/chain_serializer.rb +34 -146
  39. data/lib/cmdx/coercion.rb +57 -0
  40. data/lib/cmdx/coercion_registry.rb +113 -0
  41. data/lib/cmdx/coercions/array.rb +18 -36
  42. data/lib/cmdx/coercions/big_decimal.rb +21 -33
  43. data/lib/cmdx/coercions/boolean.rb +21 -40
  44. data/lib/cmdx/coercions/complex.rb +18 -31
  45. data/lib/cmdx/coercions/date.rb +20 -39
  46. data/lib/cmdx/coercions/date_time.rb +22 -39
  47. data/lib/cmdx/coercions/float.rb +19 -32
  48. data/lib/cmdx/coercions/hash.rb +22 -41
  49. data/lib/cmdx/coercions/integer.rb +20 -33
  50. data/lib/cmdx/coercions/rational.rb +20 -32
  51. data/lib/cmdx/coercions/string.rb +23 -31
  52. data/lib/cmdx/coercions/time.rb +24 -40
  53. data/lib/cmdx/coercions/virtual.rb +14 -31
  54. data/lib/cmdx/configuration.rb +101 -162
  55. data/lib/cmdx/context.rb +34 -166
  56. data/lib/cmdx/core_ext/hash.rb +42 -67
  57. data/lib/cmdx/core_ext/module.rb +35 -79
  58. data/lib/cmdx/core_ext/object.rb +63 -98
  59. data/lib/cmdx/correlator.rb +59 -154
  60. data/lib/cmdx/error.rb +37 -202
  61. data/lib/cmdx/errors.rb +153 -216
  62. data/lib/cmdx/fault.rb +68 -150
  63. data/lib/cmdx/faults.rb +26 -137
  64. data/lib/cmdx/immutator.rb +22 -110
  65. data/lib/cmdx/lazy_struct.rb +110 -186
  66. data/lib/cmdx/log_formatters/json.rb +14 -40
  67. data/lib/cmdx/log_formatters/key_value.rb +14 -40
  68. data/lib/cmdx/log_formatters/line.rb +14 -48
  69. data/lib/cmdx/log_formatters/logstash.rb +14 -57
  70. data/lib/cmdx/log_formatters/pretty_json.rb +14 -50
  71. data/lib/cmdx/log_formatters/pretty_key_value.rb +13 -46
  72. data/lib/cmdx/log_formatters/pretty_line.rb +16 -54
  73. data/lib/cmdx/log_formatters/raw.rb +19 -49
  74. data/lib/cmdx/logger.rb +22 -79
  75. data/lib/cmdx/logger_ansi.rb +31 -72
  76. data/lib/cmdx/logger_serializer.rb +74 -103
  77. data/lib/cmdx/middleware.rb +56 -60
  78. data/lib/cmdx/middleware_registry.rb +82 -77
  79. data/lib/cmdx/middlewares/correlate.rb +41 -226
  80. data/lib/cmdx/middlewares/timeout.rb +46 -185
  81. data/lib/cmdx/parameter.rb +167 -183
  82. data/lib/cmdx/parameter_evaluator.rb +231 -0
  83. data/lib/cmdx/parameter_inspector.rb +37 -55
  84. data/lib/cmdx/parameter_registry.rb +65 -84
  85. data/lib/cmdx/parameter_serializer.rb +32 -76
  86. data/lib/cmdx/railtie.rb +24 -107
  87. data/lib/cmdx/result.rb +254 -259
  88. data/lib/cmdx/result_ansi.rb +28 -80
  89. data/lib/cmdx/result_inspector.rb +34 -70
  90. data/lib/cmdx/result_logger.rb +23 -77
  91. data/lib/cmdx/result_serializer.rb +59 -125
  92. data/lib/cmdx/rspec/matchers.rb +28 -0
  93. data/lib/cmdx/rspec/result_matchers/be_executed.rb +42 -0
  94. data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +94 -0
  95. data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +94 -0
  96. data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +59 -0
  97. data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +57 -0
  98. data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +87 -0
  99. data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +51 -0
  100. data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +58 -0
  101. data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +59 -0
  102. data/lib/cmdx/rspec/result_matchers/have_context.rb +86 -0
  103. data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +54 -0
  104. data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +52 -0
  105. data/lib/cmdx/rspec/result_matchers/have_metadata.rb +114 -0
  106. data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +66 -0
  107. data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +64 -0
  108. data/lib/cmdx/rspec/result_matchers/have_runtime.rb +78 -0
  109. data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +76 -0
  110. data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +62 -0
  111. data/lib/cmdx/rspec/task_matchers/have_callback.rb +85 -0
  112. data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +68 -0
  113. data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +92 -0
  114. data/lib/cmdx/rspec/task_matchers/have_middleware.rb +46 -0
  115. data/lib/cmdx/rspec/task_matchers/have_parameter.rb +181 -0
  116. data/lib/cmdx/task.rb +336 -427
  117. data/lib/cmdx/task_deprecator.rb +52 -0
  118. data/lib/cmdx/task_processor.rb +246 -0
  119. data/lib/cmdx/task_serializer.rb +34 -69
  120. data/lib/cmdx/utils/ansi_color.rb +13 -89
  121. data/lib/cmdx/utils/log_timestamp.rb +13 -42
  122. data/lib/cmdx/utils/monotonic_runtime.rb +11 -63
  123. data/lib/cmdx/utils/name_affix.rb +21 -71
  124. data/lib/cmdx/validator.rb +57 -0
  125. data/lib/cmdx/validator_registry.rb +108 -0
  126. data/lib/cmdx/validators/exclusion.rb +55 -94
  127. data/lib/cmdx/validators/format.rb +31 -85
  128. data/lib/cmdx/validators/inclusion.rb +65 -110
  129. data/lib/cmdx/validators/length.rb +117 -133
  130. data/lib/cmdx/validators/numeric.rb +123 -130
  131. data/lib/cmdx/validators/presence.rb +38 -79
  132. data/lib/cmdx/version.rb +1 -7
  133. data/lib/cmdx/workflow.rb +58 -330
  134. data/lib/cmdx.rb +1 -1
  135. data/lib/generators/cmdx/install_generator.rb +14 -31
  136. data/lib/generators/cmdx/task_generator.rb +39 -55
  137. data/lib/generators/cmdx/templates/install.rb +24 -6
  138. data/lib/generators/cmdx/workflow_generator.rb +41 -66
  139. data/lib/locales/ar.yml +0 -1
  140. data/lib/locales/cs.yml +0 -1
  141. data/lib/locales/da.yml +0 -1
  142. data/lib/locales/de.yml +0 -1
  143. data/lib/locales/el.yml +0 -1
  144. data/lib/locales/en.yml +0 -1
  145. data/lib/locales/es.yml +0 -1
  146. data/lib/locales/fi.yml +0 -1
  147. data/lib/locales/fr.yml +0 -1
  148. data/lib/locales/he.yml +0 -1
  149. data/lib/locales/hi.yml +0 -1
  150. data/lib/locales/it.yml +0 -1
  151. data/lib/locales/ja.yml +0 -1
  152. data/lib/locales/ko.yml +0 -1
  153. data/lib/locales/nl.yml +0 -1
  154. data/lib/locales/no.yml +0 -1
  155. data/lib/locales/pl.yml +0 -1
  156. data/lib/locales/pt.yml +0 -1
  157. data/lib/locales/ru.yml +0 -1
  158. data/lib/locales/sv.yml +0 -1
  159. data/lib/locales/th.yml +0 -1
  160. data/lib/locales/tr.yml +0 -1
  161. data/lib/locales/vi.yml +0 -1
  162. data/lib/locales/zh.yml +0 -1
  163. metadata +36 -8
  164. data/lib/cmdx/parameter_validator.rb +0 -81
  165. data/lib/cmdx/parameter_value.rb +0 -244
  166. data/lib/cmdx/parameters_inspector.rb +0 -72
  167. data/lib/cmdx/parameters_serializer.rb +0 -115
  168. data/lib/cmdx/rspec/result_matchers.rb +0 -917
  169. data/lib/cmdx/rspec/task_matchers.rb +0 -570
  170. data/lib/cmdx/validators/custom.rb +0 -102
@@ -2,93 +2,52 @@
2
2
 
3
3
  module CMDx
4
4
  module Validators
5
- # Presence validator for parameter validation ensuring values are not empty.
5
+ # Validator class for ensuring values are present (not empty or nil).
6
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
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 a parameter value is present (not empty or nil).
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
- # 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
24
+ # @example Validating a non-empty string
25
+ # Validators::Presence.call("hello", presence: {})
26
+ # #=> nil (no error raised)
64
27
  #
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
28
+ # @example Validating an empty string
29
+ # Validators::Presence.call("", presence: {})
30
+ # # raises ValidationError: "cannot be empty"
69
31
  #
70
- # @return [void]
71
- # @raise [ValidationError] If value is not present according to type-specific rules
32
+ # @example Validating a whitespace-only string
33
+ # Validators::Presence.call(" ", presence: {})
34
+ # # raises ValidationError: "cannot be empty"
72
35
  #
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
36
+ # @example Validating a non-empty array
37
+ # Validators::Presence.call([1, 2, 3], presence: {})
38
+ # #=> nil (no error raised)
77
39
  #
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
40
+ # @example Validating an empty array
41
+ # Validators::Presence.call([], presence: {})
42
+ # # raises ValidationError: "cannot be empty"
83
43
  #
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
44
+ # @example Validating a nil value
45
+ # Validators::Presence.call(nil, presence: {})
46
+ # # raises ValidationError: "cannot be empty"
88
47
  #
89
- # @example Custom error message
90
- # Presence.call("", presence: { message: "is required" })
91
- # # => raises ValidationError: "is required"
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.dig(:presence, :message) if options[:presence].is_a?(Hash)
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
- # 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.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. 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
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 task classes to execute
16
+ # @return [Array<Class>] array of Task or Workflow classes to execute
149
17
  # @!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
- # )
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
- # @return [Array<Group>] array of task groups in declaration order
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
- # @example Accessing workflow groups
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: proc { condition }
34
+ # process TaskC, if: :condition_met?
172
35
  # end
173
36
  #
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
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
- # @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
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
- # @raise [TypeError] if any task doesn't inherit from Task
49
+ # @param tasks [Array<Class>] Task or Workflow classes to execute in sequence
50
+ # @param options [Hash] execution configuration options
214
51
  #
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? }
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
- # # Method condition
232
- # process DebugTask, if: :debug_enabled?
56
+ # @return [void]
233
57
  #
234
- # private
58
+ # @raise [TypeError] when tasks contain objects that are not Task or Workflow classes
235
59
  #
236
- # def debug_enabled?
237
- # Rails.env.development?
238
- # end
60
+ # @example Declare sequential tasks
61
+ # class UserRegistrationWorkflow < CMDx::Workflow
62
+ # process CreateUserTask, SendWelcomeEmailTask
239
63
  # end
240
64
  #
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
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 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? }
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
- # 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
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
- # # 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
96
+ # @return [void]
370
97
  #
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.
98
+ # @raise [Fault] if a task fails and its status matches the workflow halt criteria
374
99
  #
375
- # @see Task#call Base task execution method
376
- # @see Context Shared data object
377
- # @see Result Task execution results
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 __cmdx_eval(group.options)
105
+ next unless cmdx_eval(group.options)
381
106
 
382
- workflow_halt = group.options[:workflow_halt] || task_setting(: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 Array(workflow_halt).include?(task_result.status)
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