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.
Files changed (157) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/prompts/rspec.md +20 -0
  3. data/.cursor/prompts/yardoc.md +8 -0
  4. data/.rubocop.yml +2 -0
  5. data/CHANGELOG.md +17 -2
  6. data/README.md +1 -1
  7. data/docs/basics/call.md +2 -2
  8. data/docs/basics/chain.md +1 -1
  9. data/docs/callbacks.md +3 -36
  10. data/docs/configuration.md +58 -12
  11. data/docs/interruptions/exceptions.md +1 -1
  12. data/docs/interruptions/faults.md +2 -2
  13. data/docs/logging.md +4 -4
  14. data/docs/middlewares.md +43 -43
  15. data/docs/parameters/coercions.md +49 -38
  16. data/docs/parameters/defaults.md +1 -1
  17. data/docs/parameters/validations.md +0 -39
  18. data/docs/testing.md +11 -12
  19. data/docs/workflows.md +4 -4
  20. data/lib/cmdx/.DS_Store +0 -0
  21. data/lib/cmdx/callback.rb +36 -56
  22. data/lib/cmdx/callback_registry.rb +82 -73
  23. data/lib/cmdx/chain.rb +65 -122
  24. data/lib/cmdx/chain_inspector.rb +22 -115
  25. data/lib/cmdx/chain_serializer.rb +17 -148
  26. data/lib/cmdx/coercion.rb +49 -0
  27. data/lib/cmdx/coercion_registry.rb +94 -0
  28. data/lib/cmdx/coercions/array.rb +18 -36
  29. data/lib/cmdx/coercions/big_decimal.rb +21 -33
  30. data/lib/cmdx/coercions/boolean.rb +21 -40
  31. data/lib/cmdx/coercions/complex.rb +18 -31
  32. data/lib/cmdx/coercions/date.rb +20 -39
  33. data/lib/cmdx/coercions/date_time.rb +22 -39
  34. data/lib/cmdx/coercions/float.rb +19 -32
  35. data/lib/cmdx/coercions/hash.rb +22 -41
  36. data/lib/cmdx/coercions/integer.rb +20 -33
  37. data/lib/cmdx/coercions/rational.rb +20 -32
  38. data/lib/cmdx/coercions/string.rb +23 -31
  39. data/lib/cmdx/coercions/time.rb +24 -40
  40. data/lib/cmdx/coercions/virtual.rb +14 -31
  41. data/lib/cmdx/configuration.rb +57 -171
  42. data/lib/cmdx/context.rb +22 -165
  43. data/lib/cmdx/core_ext/hash.rb +42 -67
  44. data/lib/cmdx/core_ext/module.rb +35 -79
  45. data/lib/cmdx/core_ext/object.rb +63 -98
  46. data/lib/cmdx/correlator.rb +40 -156
  47. data/lib/cmdx/error.rb +37 -202
  48. data/lib/cmdx/errors.rb +165 -202
  49. data/lib/cmdx/fault.rb +55 -158
  50. data/lib/cmdx/faults.rb +26 -137
  51. data/lib/cmdx/immutator.rb +22 -109
  52. data/lib/cmdx/lazy_struct.rb +103 -187
  53. data/lib/cmdx/log_formatters/json.rb +14 -40
  54. data/lib/cmdx/log_formatters/key_value.rb +14 -40
  55. data/lib/cmdx/log_formatters/line.rb +14 -48
  56. data/lib/cmdx/log_formatters/logstash.rb +14 -57
  57. data/lib/cmdx/log_formatters/pretty_json.rb +14 -50
  58. data/lib/cmdx/log_formatters/pretty_key_value.rb +13 -46
  59. data/lib/cmdx/log_formatters/pretty_line.rb +16 -54
  60. data/lib/cmdx/log_formatters/raw.rb +19 -49
  61. data/lib/cmdx/logger.rb +20 -82
  62. data/lib/cmdx/logger_ansi.rb +18 -75
  63. data/lib/cmdx/logger_serializer.rb +24 -114
  64. data/lib/cmdx/middleware.rb +38 -60
  65. data/lib/cmdx/middleware_registry.rb +81 -77
  66. data/lib/cmdx/middlewares/correlate.rb +41 -226
  67. data/lib/cmdx/middlewares/timeout.rb +46 -185
  68. data/lib/cmdx/parameter.rb +120 -198
  69. data/lib/cmdx/parameter_evaluator.rb +231 -0
  70. data/lib/cmdx/parameter_inspector.rb +25 -56
  71. data/lib/cmdx/parameter_registry.rb +59 -84
  72. data/lib/cmdx/parameter_serializer.rb +23 -74
  73. data/lib/cmdx/railtie.rb +24 -107
  74. data/lib/cmdx/result.rb +254 -260
  75. data/lib/cmdx/result_ansi.rb +19 -85
  76. data/lib/cmdx/result_inspector.rb +27 -68
  77. data/lib/cmdx/result_logger.rb +18 -81
  78. data/lib/cmdx/result_serializer.rb +28 -132
  79. data/lib/cmdx/rspec/matchers.rb +28 -0
  80. data/lib/cmdx/rspec/result_matchers/be_executed.rb +42 -0
  81. data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +94 -0
  82. data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +94 -0
  83. data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +59 -0
  84. data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +57 -0
  85. data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +87 -0
  86. data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +51 -0
  87. data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +58 -0
  88. data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +59 -0
  89. data/lib/cmdx/rspec/result_matchers/have_context.rb +86 -0
  90. data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +54 -0
  91. data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +52 -0
  92. data/lib/cmdx/rspec/result_matchers/have_metadata.rb +114 -0
  93. data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +66 -0
  94. data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +64 -0
  95. data/lib/cmdx/rspec/result_matchers/have_runtime.rb +78 -0
  96. data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +76 -0
  97. data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +62 -0
  98. data/lib/cmdx/rspec/task_matchers/have_callback.rb +85 -0
  99. data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +68 -0
  100. data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +92 -0
  101. data/lib/cmdx/rspec/task_matchers/have_middleware.rb +46 -0
  102. data/lib/cmdx/rspec/task_matchers/have_parameter.rb +181 -0
  103. data/lib/cmdx/task.rb +213 -425
  104. data/lib/cmdx/task_deprecator.rb +55 -0
  105. data/lib/cmdx/task_processor.rb +245 -0
  106. data/lib/cmdx/task_serializer.rb +22 -70
  107. data/lib/cmdx/utils/ansi_color.rb +13 -89
  108. data/lib/cmdx/utils/log_timestamp.rb +13 -42
  109. data/lib/cmdx/utils/monotonic_runtime.rb +13 -63
  110. data/lib/cmdx/utils/name_affix.rb +21 -71
  111. data/lib/cmdx/validator.rb +48 -0
  112. data/lib/cmdx/validator_registry.rb +86 -0
  113. data/lib/cmdx/validators/exclusion.rb +55 -94
  114. data/lib/cmdx/validators/format.rb +31 -85
  115. data/lib/cmdx/validators/inclusion.rb +65 -110
  116. data/lib/cmdx/validators/length.rb +117 -133
  117. data/lib/cmdx/validators/numeric.rb +123 -130
  118. data/lib/cmdx/validators/presence.rb +38 -79
  119. data/lib/cmdx/version.rb +1 -7
  120. data/lib/cmdx/workflow.rb +46 -339
  121. data/lib/cmdx.rb +1 -1
  122. data/lib/generators/cmdx/install_generator.rb +14 -31
  123. data/lib/generators/cmdx/task_generator.rb +39 -55
  124. data/lib/generators/cmdx/templates/install.rb +24 -6
  125. data/lib/generators/cmdx/workflow_generator.rb +41 -66
  126. data/lib/locales/ar.yml +0 -1
  127. data/lib/locales/cs.yml +0 -1
  128. data/lib/locales/da.yml +0 -1
  129. data/lib/locales/de.yml +0 -1
  130. data/lib/locales/el.yml +0 -1
  131. data/lib/locales/en.yml +0 -1
  132. data/lib/locales/es.yml +0 -1
  133. data/lib/locales/fi.yml +0 -1
  134. data/lib/locales/fr.yml +0 -1
  135. data/lib/locales/he.yml +0 -1
  136. data/lib/locales/hi.yml +0 -1
  137. data/lib/locales/it.yml +0 -1
  138. data/lib/locales/ja.yml +0 -1
  139. data/lib/locales/ko.yml +0 -1
  140. data/lib/locales/nl.yml +0 -1
  141. data/lib/locales/no.yml +0 -1
  142. data/lib/locales/pl.yml +0 -1
  143. data/lib/locales/pt.yml +0 -1
  144. data/lib/locales/ru.yml +0 -1
  145. data/lib/locales/sv.yml +0 -1
  146. data/lib/locales/th.yml +0 -1
  147. data/lib/locales/tr.yml +0 -1
  148. data/lib/locales/vi.yml +0 -1
  149. data/lib/locales/zh.yml +0 -1
  150. metadata +34 -8
  151. data/lib/cmdx/parameter_validator.rb +0 -81
  152. data/lib/cmdx/parameter_value.rb +0 -244
  153. data/lib/cmdx/parameters_inspector.rb +0 -72
  154. data/lib/cmdx/parameters_serializer.rb +0 -115
  155. data/lib/cmdx/rspec/result_matchers.rb +0 -917
  156. data/lib/cmdx/rspec/task_matchers.rb +0 -570
  157. 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.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
- # 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
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<Class>] array of task classes to execute
15
+ # @return [Array<Task>] the tasks in this group
149
16
  # @!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
- # )
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
- # @example Accessing workflow groups
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
- # 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
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
- # - **`: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
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
- # @option options [Proc, Symbol, String] :if condition that must be truthy
210
- # @option options [Proc, Symbol, String] :unless condition that must be falsy
211
- # @option options [Array<Symbol>] :workflow_halt result statuses that halt execution
212
- #
213
- # @raise [TypeError] if any task doesn't inherit from Task
214
- #
215
- # @example Basic task declaration
216
- # class SimpleWorkflow < CMDx::Workflow
217
- # process TaskA
218
- # process TaskB, TaskC
219
- # end
220
- #
221
- # @example Conditional execution
222
- # class ConditionalWorkflow < CMDx::Workflow
223
- # process AlwaysTask
224
- #
225
- # # Proc condition
226
- # process PremiumTask, if: proc { context.user.premium? }
227
- #
228
- # # Lambda condition
229
- # process InternationalTask, unless: -> { context.domestic_only? }
230
- #
231
- # # Method condition
232
- # process DebugTask, if: :debug_enabled?
233
- #
234
- # private
235
- #
236
- # def debug_enabled?
237
- # Rails.env.development?
238
- # end
239
- # end
40
+ # @return [void]
240
41
  #
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]
42
+ # @raise [TypeError] if any task is not a Task or Workflow subclass
246
43
  #
247
- # # Optional tasks - never halt
248
- # process OptionalTaskA, OptionalTaskB, workflow_halt: []
44
+ # @example Define a simple workflow group
45
+ # MyWorkflow.process CreateUserTask, SendEmailTask
249
46
  #
250
- # # Default behavior tasks
251
- # process NormalTaskA, NormalTaskB # Halts on FAILED only
252
- # end
47
+ # @example Define a conditional workflow group
48
+ # MyWorkflow.process NotifyAdminTask, if: ->(workflow) { workflow.context.admin.active? }
253
49
  #
254
- # @example Complex conditions
255
- # class ComplexWorkflow < CMDx::Workflow
256
- # process BaseTask
257
- #
258
- # # Multiple conditions can be combined in proc
259
- # process ConditionalTask, if: proc {
260
- # context.user.active? &&
261
- # context.feature_enabled?(:new_feature) &&
262
- # Time.now.hour.between?(9, 17)
263
- # }
264
- #
265
- # # Conditional with custom halt behavior
266
- # process RiskyTask,
267
- # unless: :safe_mode?,
268
- # workflow_halt: [CMDx::Result::FAILED, CMDx::Result::SKIPPED]
269
- # end
270
- #
271
- # @example Nested workflow processing
272
- # class MasterWorkflow < CMDx::Workflow
273
- # process PreProcessingWorkflow
274
- # process CoreWorkflow, if: proc { context.pre_processing_successful? }
275
- # process PostProcessingWorkflow, unless: proc { context.skip_post_processing? }
276
- # end
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
- next task if task <= Task
55
+ unless task.is_a?(Class) && (task <= Task)
56
+ raise TypeError,
57
+ "must be a Task or Workflow"
58
+ end
281
59
 
282
- raise TypeError, "must be a Task or Workflow"
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
- # # 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
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
- # # 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
75
+ # @return [void]
370
76
  #
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.
77
+ # @raise [Fault] if a task fails and its status matches the workflow halt criteria
374
78
  #
375
- # @see Task#call Base task execution method
376
- # @see Context Shared data object
377
- # @see Result Task execution results
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 __cmdx_eval(group.options)
84
+ next unless cmdx_eval(group.options)
381
85
 
382
- workflow_halt = group.options[:workflow_halt] || task_setting(: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 Array(workflow_halt).include?(task_result.status)
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 configuration initializer file that sets up
8
- # global CMDx settings for task execution, workflow processing, logging,
9
- # and error handling behaviors.
10
- #
11
- # The generated initializer provides sensible defaults that can be
12
- # customized for specific application requirements.
13
- #
14
- # @example Generate CMDx initializer
15
- # rails generate cmdx:install
16
- #
17
- # @example Generated file location
18
- # config/initializers/cmdx.rb
19
- #
20
- # @since 1.0.0
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 `config/initializers/cmdx.rb` with
31
- # default configuration settings for:
32
- # - Task halt behaviors
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
- # @example Generated initializer content
41
- # CMDx.configure do |config|
42
- # config.task_halt = CMDx::Result::FAILED
43
-
44
- # # ... additional settings
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