cmdx 0.5.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/.DS_Store +0 -0
  3. data/.cursor/rules/cursor-instructions.mdc +6 -0
  4. data/.rubocop.yml +16 -1
  5. data/.ruby-version +1 -1
  6. data/CHANGELOG.md +31 -1
  7. data/README.md +72 -25
  8. data/docs/ai_prompts.md +309 -0
  9. data/docs/basics/call.md +225 -14
  10. data/docs/basics/chain.md +271 -0
  11. data/docs/basics/context.md +232 -33
  12. data/docs/basics/setup.md +76 -12
  13. data/docs/callbacks.md +273 -0
  14. data/docs/configuration.md +158 -28
  15. data/docs/getting_started.md +134 -22
  16. data/docs/interruptions/exceptions.md +189 -11
  17. data/docs/interruptions/faults.md +187 -44
  18. data/docs/interruptions/halt.md +179 -35
  19. data/docs/logging.md +194 -53
  20. data/docs/middlewares.md +735 -0
  21. data/docs/outcomes/result.md +296 -10
  22. data/docs/outcomes/states.md +203 -31
  23. data/docs/outcomes/statuses.md +275 -30
  24. data/docs/parameters/coercions.md +402 -29
  25. data/docs/parameters/defaults.md +249 -25
  26. data/docs/parameters/definitions.md +238 -72
  27. data/docs/parameters/namespacing.md +250 -27
  28. data/docs/parameters/validations.md +193 -168
  29. data/docs/testing.md +550 -0
  30. data/docs/tips_and_tricks.md +95 -43
  31. data/docs/workflows.md +319 -0
  32. data/lib/cmdx/.DS_Store +0 -0
  33. data/lib/cmdx/callback.rb +69 -0
  34. data/lib/cmdx/callback_registry.rb +106 -0
  35. data/lib/cmdx/chain.rb +190 -0
  36. data/lib/cmdx/chain_inspector.rb +149 -0
  37. data/lib/cmdx/chain_serializer.rb +175 -0
  38. data/lib/cmdx/coercions/array.rb +37 -0
  39. data/lib/cmdx/coercions/big_decimal.rb +33 -0
  40. data/lib/cmdx/coercions/boolean.rb +41 -1
  41. data/lib/cmdx/coercions/complex.rb +31 -0
  42. data/lib/cmdx/coercions/date.rb +39 -0
  43. data/lib/cmdx/coercions/date_time.rb +39 -0
  44. data/lib/cmdx/coercions/float.rb +31 -0
  45. data/lib/cmdx/coercions/hash.rb +42 -0
  46. data/lib/cmdx/coercions/integer.rb +32 -0
  47. data/lib/cmdx/coercions/rational.rb +31 -0
  48. data/lib/cmdx/coercions/string.rb +31 -0
  49. data/lib/cmdx/coercions/time.rb +39 -0
  50. data/lib/cmdx/coercions/virtual.rb +31 -0
  51. data/lib/cmdx/configuration.rb +217 -9
  52. data/lib/cmdx/context.rb +173 -2
  53. data/lib/cmdx/core_ext/hash.rb +72 -0
  54. data/lib/cmdx/core_ext/module.rb +94 -0
  55. data/lib/cmdx/core_ext/object.rb +105 -0
  56. data/lib/cmdx/correlator.rb +217 -0
  57. data/lib/cmdx/error.rb +210 -8
  58. data/lib/cmdx/errors.rb +256 -1
  59. data/lib/cmdx/fault.rb +177 -2
  60. data/lib/cmdx/faults.rb +158 -2
  61. data/lib/cmdx/immutator.rb +121 -2
  62. data/lib/cmdx/lazy_struct.rb +261 -18
  63. data/lib/cmdx/log_formatters/json.rb +46 -0
  64. data/lib/cmdx/log_formatters/key_value.rb +46 -0
  65. data/lib/cmdx/log_formatters/line.rb +54 -0
  66. data/lib/cmdx/log_formatters/logstash.rb +64 -0
  67. data/lib/cmdx/log_formatters/pretty_json.rb +57 -0
  68. data/lib/cmdx/log_formatters/pretty_key_value.rb +51 -0
  69. data/lib/cmdx/log_formatters/pretty_line.rb +60 -0
  70. data/lib/cmdx/log_formatters/raw.rb +54 -0
  71. data/lib/cmdx/logger.rb +85 -0
  72. data/lib/cmdx/logger_ansi.rb +93 -7
  73. data/lib/cmdx/logger_serializer.rb +116 -0
  74. data/lib/cmdx/middleware.rb +74 -0
  75. data/lib/cmdx/middleware_registry.rb +106 -0
  76. data/lib/cmdx/middlewares/correlate.rb +266 -0
  77. data/lib/cmdx/middlewares/timeout.rb +232 -0
  78. data/lib/cmdx/parameter.rb +228 -1
  79. data/lib/cmdx/parameter_inspector.rb +61 -0
  80. data/lib/cmdx/parameter_registry.rb +125 -0
  81. data/lib/cmdx/parameter_serializer.rb +83 -0
  82. data/lib/cmdx/parameter_validator.rb +62 -0
  83. data/lib/cmdx/parameter_value.rb +109 -1
  84. data/lib/cmdx/parameters_inspector.rb +59 -0
  85. data/lib/cmdx/parameters_serializer.rb +102 -0
  86. data/lib/cmdx/railtie.rb +123 -3
  87. data/lib/cmdx/result.rb +367 -25
  88. data/lib/cmdx/result_ansi.rb +105 -9
  89. data/lib/cmdx/result_inspector.rb +76 -0
  90. data/lib/cmdx/result_logger.rb +90 -3
  91. data/lib/cmdx/result_serializer.rb +137 -0
  92. data/lib/cmdx/rspec/result_matchers.rb +917 -0
  93. data/lib/cmdx/rspec/task_matchers.rb +570 -0
  94. data/lib/cmdx/task.rb +405 -37
  95. data/lib/cmdx/task_serializer.rb +74 -2
  96. data/lib/cmdx/utils/ansi_color.rb +95 -0
  97. data/lib/cmdx/utils/log_timestamp.rb +48 -0
  98. data/lib/cmdx/utils/monotonic_runtime.rb +71 -4
  99. data/lib/cmdx/utils/name_affix.rb +78 -0
  100. data/lib/cmdx/validators/custom.rb +82 -0
  101. data/lib/cmdx/validators/exclusion.rb +94 -0
  102. data/lib/cmdx/validators/format.rb +102 -8
  103. data/lib/cmdx/validators/inclusion.rb +104 -0
  104. data/lib/cmdx/validators/length.rb +128 -0
  105. data/lib/cmdx/validators/numeric.rb +128 -0
  106. data/lib/cmdx/validators/presence.rb +93 -7
  107. data/lib/cmdx/version.rb +7 -1
  108. data/lib/cmdx/workflow.rb +394 -0
  109. data/lib/cmdx.rb +25 -64
  110. data/lib/generators/cmdx/install_generator.rb +37 -1
  111. data/lib/generators/cmdx/task_generator.rb +69 -1
  112. data/lib/generators/cmdx/templates/install.rb +8 -12
  113. data/lib/generators/cmdx/workflow_generator.rb +109 -0
  114. metadata +54 -15
  115. data/docs/basics/run.md +0 -34
  116. data/docs/batch.md +0 -53
  117. data/docs/example.md +0 -82
  118. data/docs/hooks.md +0 -62
  119. data/lib/cmdx/batch.rb +0 -43
  120. data/lib/cmdx/parameters.rb +0 -35
  121. data/lib/cmdx/run.rb +0 -39
  122. data/lib/cmdx/run_inspector.rb +0 -26
  123. data/lib/cmdx/run_serializer.rb +0 -20
  124. data/lib/cmdx/task_hook.rb +0 -18
  125. data/lib/generators/cmdx/batch_generator.rb +0 -30
  126. /data/lib/generators/cmdx/templates/{batch.rb.tt → workflow.rb.tt} +0 -0
@@ -1,79 +1,131 @@
1
1
  # Tips & Tricks
2
2
 
3
- ## Configuration
3
+ This guide covers advanced patterns and optimization techniques for getting the most out of CMDx in production applications.
4
4
 
5
- Configure `CMDx` to get the most out of your Rails application.
5
+ ## Table of Contents
6
6
 
7
- ```ruby
8
- CMDx.configure do |config|
9
- # Redirect your logs through the app defined logger:
10
- config.logger = Rails.logger
11
-
12
- # Adjust the log level to write depending on the environment:
13
- config.logger.level = Rails.env.development? ? Logger::DEBUG : Logger::INFO
7
+ - [Project Organization](#project-organization)
8
+ - [Directory Structure](#directory-structure)
9
+ - [Naming Conventions](#naming-conventions)
10
+ - [Parameter Optimization](#parameter-optimization)
11
+ - [Efficient Parameter Definitions](#efficient-parameter-definitions)
12
+ - [Monitoring and Observability](#monitoring-and-observability)
13
+ - [ActiveRecord Query Tagging](#activerecord-query-tagging)
14
14
 
15
- # Structure log lines using a pre-built or custom formatter:
16
- config.logger.formatter = CMDx::LogFormatters::Logstash.new
17
- end
18
- ```
15
+ ## Project Organization
19
16
 
20
- ## Setup
17
+ ### Directory Structure
21
18
 
22
- While not required, a common setup involves creating an `app/cmds` directory
23
- to place all of your tasks and batches under, eg:
19
+ Create a well-organized command structure for maintainable applications:
24
20
 
25
21
  ```txt
26
22
  /app
27
- /cmds
23
+ /commands
24
+ /orders
25
+ - process_order_task.rb
26
+ - validate_order_task.rb
27
+ - fulfill_order_task.rb
28
+ - order_processing_workflow.rb
28
29
  /notifications
29
- - deliver_email_task.rb
30
+ - send_email_task.rb
31
+ - send_sms_task.rb
30
32
  - post_slack_message_task.rb
31
- - send_carrier_pigeon_task.rb
32
- - batch_deliver_all.rb
33
- - process_order_task.rb
34
- - application_batch.rb
33
+ - notification_delivery_workflow.rb
34
+ /payments
35
+ - charge_payment_task.rb
36
+ - refund_payment_task.rb
37
+ - validate_payment_method_task.rb
35
38
  - application_task.rb
39
+ - application_workflow.rb
36
40
  ```
37
41
 
38
- > [!TIP]
39
- > Prefix batches with `batch_` and suffix tasks with `_task` to they convey their function.
40
- > Use a verb+noun naming structure to convey the work that will be performed, eg:
41
- > `BatchDeliverNotifications` or `DeliverEmailTask`
42
-
43
- ## Parameters
42
+ ### Naming Conventions
44
43
 
45
- Use the Rails `with_options` as an elegant way to factor duplication
46
- out of options passed to a series of parameter definitions. The following
47
- are a few common example:
44
+ Follow consistent naming patterns for clarity and maintainability:
48
45
 
49
46
  ```ruby
50
- class UpdateUserDetailsTask < CMDx::Task
47
+ # Tasks: Verb + Noun + Task
48
+ class ProcessOrderTask < CMDx::Task; end
49
+ class SendEmailTask < CMDx::Task; end
50
+ class ValidatePaymentTask < CMDx::Task; end
51
+
52
+ # Workflows: Noun + Verb + Workflow
53
+ class OrderProcessingWorkflow < CMDx::Workflow; end
54
+ class NotificationDeliveryWorkflow < CMDx::Workflow; end
55
+
56
+ # Use present tense verbs for actions
57
+ class CreateUserTask < CMDx::Task; end # ✓ Good
58
+ class CreatingUserTask < CMDx::Task; end # ❌ Avoid
59
+ class UserCreationTask < CMDx::Task; end # ❌ Avoid
60
+ ```
51
61
 
52
- # Apply `type: :string, presence: true` to this set of parameters:
62
+ ## Parameter Optimization
63
+
64
+ ### Efficient Parameter Definitions
65
+
66
+ Use Rails `with_options` to reduce duplication and improve readability:
67
+
68
+ ```ruby
69
+ class UpdateUserProfileTask < CMDx::Task
70
+ # Apply common options to multiple parameters
53
71
  with_options(type: :string, presence: true) do
54
- required :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i }
72
+ required :email, format: { with: URI::MailTo::EMAIL_REGEXP }
55
73
  optional :first_name, :last_name
74
+ optional :phone, format: { with: /\A\+?[\d\s\-\(\)]+\z/ }
56
75
  end
57
76
 
77
+ # Nested parameters with shared prefix
58
78
  required :address do
59
- # Apply the `address_*` prefix to this set of nested parameters:
60
79
  with_options(prefix: :address_) do
61
- required :city, :country
62
- optional :state
80
+ required :street, :city, :postal_code, type: :string
81
+ required :country, type: :string, inclusion: { in: VALID_COUNTRIES }
82
+ optional :state, type: :string
63
83
  end
64
84
  end
65
85
 
66
- def call
67
- # Do work
86
+ # Shared validation rules
87
+ with_options(type: :integer, numericality: { greater_than: 0 }) do
88
+ optional :age, numericality: { less_than: 150 }
89
+ optional :years_experience, numericality: { less_than: 80 }
68
90
  end
69
91
 
92
+ def call
93
+ # Implementation
94
+ end
70
95
  end
71
96
  ```
72
97
 
73
- [Learn More](https://api.rubyonrails.org/classes/Object.html#method-i-with_options)
74
- about its usages on the official Rails docs.
98
+ ## Monitoring and Observability
99
+
100
+ ### ActiveRecord Query Tagging
101
+
102
+ Automatically tag SQL queries for better debugging:
103
+
104
+ ```ruby
105
+ # config/application.rb
106
+ config.active_record.query_log_tags_enabled = true
107
+ config.active_record.query_log_tags << :cmdx_task_class
108
+ config.active_record.query_log_tags << :cmdx_chain_id
109
+
110
+ # app/commands/application_task.rb
111
+ class ApplicationTask < CMDx::Task
112
+ before_execution :set_execution_context
113
+
114
+ private
115
+
116
+ def set_execution_context
117
+ ActiveSupport::ExecutionContext.set(
118
+ cmdx_task_class: self.class.name,
119
+ cmdx_chain_id: chain.id
120
+ )
121
+ end
122
+ end
123
+
124
+ # SQL queries will now include comments like:
125
+ # /*cmdx_task_class:ProcessOrderTask,cmdx_chain_id:018c2b95-b764-7615*/ SELECT * FROM orders WHERE id = 1
126
+ ```
75
127
 
76
128
  ---
77
129
 
78
- - **Prev:** [Logging](https://github.com/drexed/cmdx/blob/main/docs/logging.md)
79
- - **Next:** [Example](https://github.com/drexed/cmdx/blob/main/docs/example.md)
130
+ - **Prev:** [AI Prompts](ai_prompts.md)
131
+ - **Next:** [Getting Started](getting_started.md)
data/docs/workflows.md ADDED
@@ -0,0 +1,319 @@
1
+ # Workflow
2
+
3
+ A CMDx::Workflow orchestrates sequential execution of multiple tasks in a linear pipeline. Workflows provide a declarative DSL for composing complex business workflows from individual task components, with support for conditional execution, context propagation, and configurable halt behavior.
4
+
5
+ Workflows inherit from Task, gaining all task capabilities including callbacks, parameter validation, result tracking, and configuration. The key difference is that workflows coordinate other tasks rather than implementing business logic directly.
6
+
7
+ ## Table of Contents
8
+
9
+ - [Basic Usage](#basic-usage)
10
+ - [Task Declaration](#task-declaration)
11
+ - [Context Propagation](#context-propagation)
12
+ - [Conditional Execution](#conditional-execution)
13
+ - [Halt Behavior](#halt-behavior)
14
+ - [Default Behavior](#default-behavior)
15
+ - [Class-Level Configuration](#class-level-configuration)
16
+ - [Group-Level Configuration](#group-level-configuration)
17
+ - [Available Result Statuses](#available-result-statuses)
18
+ - [Process Method Options](#process-method-options)
19
+ - [Condition Callables](#condition-callables)
20
+ - [Nested Workflows](#nested-workflows)
21
+ - [Task Settings Integration](#task-settings-integration)
22
+ - [Generator](#generator)
23
+
24
+ ## Basic Usage
25
+
26
+ > [!WARNING]
27
+ > Do **NOT** define a `call` method in workflow classes. The workflow class automatically provides the call logic.
28
+
29
+ ```ruby
30
+ class OrderProcessingWorkflow < CMDx::Workflow
31
+ # Sequential task execution
32
+ process ValidateOrderTask
33
+ process CalculateTaxTask
34
+ process ChargePaymentTask
35
+ process FulfillOrderTask
36
+ end
37
+
38
+ # Execute the workflow
39
+ result = WorkflowProcessOrders.call(order: order, user: current_user)
40
+
41
+ if result.success?
42
+ redirect_to success_path
43
+ elsif result.failed?
44
+ flash[:error] = "Order processing failed: #{result.metadata[:reason]}"
45
+ redirect_to cart_path
46
+ end
47
+ ```
48
+
49
+ ## Task Declaration
50
+
51
+ Tasks are declared using the `process` method and organized into groups with shared execution options:
52
+
53
+ ```ruby
54
+ class NotificationDeliveryWorkflow < CMDx::Workflow
55
+ # Single task declaration
56
+ process PrepareNotificationTask
57
+
58
+ # Multiple tasks in one declaration (grouped)
59
+ process SendEmailTask, SendSmsTask, SendPushTask
60
+
61
+ # Tasks with conditions
62
+ process SendWebhookTask, if: proc { context.webhook_enabled? }
63
+ process SendSlackTask, unless: :slack_disabled?
64
+
65
+ private
66
+
67
+ def slack_disabled?
68
+ !context.user.slack_enabled?
69
+ end
70
+ end
71
+ ```
72
+
73
+ > [!IMPORTANT]
74
+ > Process steps are executed in the order they are declared (FIFO: first in, first out).
75
+
76
+ ## Context Propagation
77
+
78
+ The context object is shared across all tasks in the workflow, creating a data pipeline:
79
+
80
+ ```ruby
81
+ class EcommerceProcessingWorkflow < CMDx::Workflow
82
+ process ValidateOrderTask # Sets context.validation_result
83
+ process CalculateTaxTask # Uses context.order, sets context.tax_amount
84
+ process ChargePaymentTask # Uses context.tax_amount, sets context.payment_id
85
+ process FulfillOrderTask # Uses context.payment_id, sets context.tracking_number
86
+ end
87
+
88
+ result = WorkflowProcessEcommerce.call(order: order)
89
+ # Final context contains data from all executed tasks
90
+ result.context.validation_result # From ValidateOrderTask
91
+ result.context.tax_amount # From CalculateTaxTask
92
+ result.context.payment_id # From ChargePaymentTask
93
+ result.context.tracking_number # From FulfillOrderTask
94
+ ```
95
+
96
+ ## Conditional Execution
97
+
98
+ Tasks can be executed conditionally using `:if` and `:unless` options. Conditions can be procs, lambdas, or method names:
99
+
100
+ ```ruby
101
+ class UserProcessingWorkflow < CMDx::Workflow
102
+ process ValidateUserTask
103
+
104
+ # Proc condition
105
+ process UpgradeToPremiumTask, if: proc { context.user.premium? }
106
+
107
+ # Lambda condition
108
+ process ProcessInternationalTask, unless: -> { context.user.domestic? }
109
+
110
+ # Method condition
111
+ process LogDebugInfoTask, if: :debug_enabled?
112
+
113
+ # Complex condition
114
+ process SendSpecialOfferTask, if: proc {
115
+ context.user.active? &&
116
+ context.feature_enabled?(:offers) &&
117
+ Time.now.hour.between?(9, 17)
118
+ }
119
+
120
+ private
121
+
122
+ def debug_enabled?
123
+ Rails.env.development?
124
+ end
125
+ end
126
+ ```
127
+
128
+ ## Halt Behavior
129
+
130
+ Workflows control execution flow through halt behavior, which determines when to stop processing based on task results.
131
+
132
+ ### Default Behavior
133
+
134
+ By default, workflows halt on `FAILED` status but continue on `SKIPPED`. This reflects the philosophy that skipped tasks are bypass mechanisms, not execution blockers.
135
+
136
+ ```ruby
137
+ class DataProcessingWorkflow < CMDx::Workflow
138
+ process LoadDataTask # If this fails, workflow stops
139
+ process ValidateDataTask # If this is skipped, workflow continues
140
+ process SaveDataTask # This only runs if LoadDataTask and ValidateDataTask don't fail
141
+ end
142
+ ```
143
+
144
+ ### Class-Level Configuration
145
+
146
+ Configure halt behavior for the entire workflow using `task_settings!`:
147
+
148
+ ```ruby
149
+ class CriticalDataProcessingWorkflow < CMDx::Workflow
150
+ # Halt on both failed and skipped results
151
+ task_settings!(workflow_halt: [CMDx::Result::FAILED, CMDx::Result::SKIPPED])
152
+
153
+ process LoadCriticalDataTask
154
+ process ValidateCriticalDataTask
155
+ end
156
+
157
+ class OptionalDataProcessingWorkflow < CMDx::Workflow
158
+ # Never halt, always continue
159
+ task_settings!(workflow_halt: [])
160
+
161
+ process TryLoadDataTask
162
+ process TryValidateDataTask
163
+ process TrySaveDataTask
164
+ end
165
+ ```
166
+
167
+ ### Group-Level Configuration
168
+
169
+ Different groups can have different halt behavior:
170
+
171
+ ```ruby
172
+ class UserAccountProcessingWorkflow < CMDx::Workflow
173
+ # Critical tasks - halt on any failure or skip
174
+ process CreateUserTask, ValidateUserTask,
175
+ workflow_halt: [CMDx::Result::FAILED, CMDx::Result::SKIPPED]
176
+
177
+ # Optional tasks - never halt execution
178
+ process SendWelcomeEmailTask, CreateProfileTask, workflow_halt: []
179
+
180
+ # Notification tasks - use default behavior (halt on failed only)
181
+ process NotifyAdminTask, LogUserCreationTask
182
+ end
183
+ ```
184
+
185
+ ### Available Result Statuses
186
+
187
+ The following result statuses can be used in `workflow_halt` arrays:
188
+
189
+ - `CMDx::Result::SUCCESS` - Task completed successfully
190
+ - `CMDx::Result::SKIPPED` - Task was skipped intentionally
191
+ - `CMDx::Result::FAILED` - Task failed due to error or validation
192
+
193
+ ## Process Method Options
194
+
195
+ The `process` method supports the following options:
196
+
197
+ | Option | Description |
198
+ | ------------- | ----------- |
199
+ | `:if` | Specifies a callable method, proc or string to determine if processing steps should occur. |
200
+ | `:unless` | Specifies a callable method, proc, or string to determine if processing steps should not occur. |
201
+ | `:workflow_halt` | Sets which result statuses processing of further steps should be prevented. (default: `CMDx::Result::FAILED`) |
202
+
203
+ ### Condition Callables
204
+
205
+ Conditions can be provided in several formats:
206
+
207
+ ```ruby
208
+ class AccountProcessingWorkflow < CMDx::Workflow
209
+ # Proc - executed in workflow instance context
210
+ process UpgradeAccountTask, if: proc { context.user.admin? }
211
+
212
+ # Lambda - executed in workflow instance context
213
+ process MaintenanceModeTask, unless: -> { context.maintenance_mode? }
214
+
215
+ # Symbol - method name called on workflow instance
216
+ process AdvancedFeatureTask, if: :feature_enabled?
217
+
218
+ # String - method name called on workflow instance
219
+ process OptionalTask, unless: "skip_task?"
220
+
221
+ private
222
+
223
+ def feature_enabled?
224
+ context.features.include?(:advanced)
225
+ end
226
+
227
+ def skip_task?
228
+ context.skip_optional_tasks?
229
+ end
230
+ end
231
+ ```
232
+
233
+ ## Nested Workflows
234
+
235
+ Workflows can process other workflows, creating hierarchical workflows:
236
+
237
+ ```ruby
238
+ class DataPreProcessingWorkflow < CMDx::Workflow
239
+ process ValidateInputTask
240
+ process SanitizeDataTask
241
+ end
242
+
243
+ class DataProcessingWorkflow < CMDx::Workflow
244
+ process TransformDataTask
245
+ process ApplyBusinessLogicTask
246
+ end
247
+
248
+ class DataPostProcessingWorkflow < CMDx::Workflow
249
+ process GenerateReportTask
250
+ process SendNotificationTask
251
+ end
252
+
253
+ class CompleteDataProcessingWorkflow < CMDx::Workflow
254
+ process DataPreProcessingWorkflow
255
+ process DataProcessingWorkflow, if: proc { context.pre_processing_successful? }
256
+ process DataPostProcessingWorkflow, unless: proc { context.skip_post_processing? }
257
+ end
258
+ ```
259
+
260
+ ## Task Settings Integration
261
+
262
+ Workflows support all task settings and can be configured like regular tasks:
263
+
264
+ ```ruby
265
+ class PaymentProcessingWorkflow < CMDx::Workflow
266
+ # Configure workflow-specific settings
267
+ task_settings!(
268
+ workflow_halt: [CMDx::Result::FAILED],
269
+ log_level: :debug,
270
+ tags: [:critical, :payment]
271
+ )
272
+
273
+ # Parameter validation
274
+ required :order_id, type: :integer
275
+ optional :notify_user, type: :boolean, default: true
276
+
277
+ # Callbacks
278
+ before_execution :setup_context
279
+ after_execution :cleanup_resources
280
+
281
+ process ValidateOrderTask
282
+ process ProcessPaymentTask
283
+ process NotifyUserTask, if: proc { context.notify_user }
284
+
285
+ private
286
+
287
+ def setup_context
288
+ context.start_time = Time.now
289
+ end
290
+
291
+ def cleanup_resources
292
+ context.temp_files&.each(&:delete)
293
+ end
294
+ end
295
+ ```
296
+
297
+ ## Generator
298
+
299
+ Generate a new workflow using the Rails generator:
300
+
301
+ ```bash
302
+ rails g cmdx:workflow ProcessOrder
303
+ ```
304
+
305
+ This creates a workflow template file under `app/cmds`:
306
+
307
+ ```ruby
308
+ class OrderProcessingWorkflow < ApplicationWorkflow
309
+ process # TODO
310
+ end
311
+ ```
312
+
313
+ > [!NOTE]
314
+ > The generator creates workflow files in `app/commands/workflow_[name].rb`, inherits from `ApplicationWorkflow` if available (otherwise `CMDx::Workflow`) and handles proper naming conventions.
315
+
316
+ ---
317
+
318
+ - **Prev:** [Middlewares](middlewares.md)
319
+ - **Next:** [Logging](logging.md)
data/lib/cmdx/.DS_Store CHANGED
Binary file
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ ##
5
+ # Base class for CMDx callbacks that provides lifecycle execution points.
6
+ #
7
+ # Callback components can wrap or observe task execution at specific lifecycle
8
+ # points like before validation, on success, after execution, etc.
9
+ # Each callback must implement the `call` method which receives the
10
+ # task instance and callback context.
11
+ #
12
+ # @example Basic callback implementation
13
+ # class LoggingCallback < CMDx::Callback
14
+ # def call(task, callback_type)
15
+ # puts "Executing #{callback_type} callback for #{task.class.name}"
16
+ # task.logger.info("Callback executed: #{callback_type}")
17
+ # end
18
+ # end
19
+ #
20
+ # @example Callback with initialization parameters
21
+ # class NotificationCallback < CMDx::Callback
22
+ # def initialize(channels)
23
+ # @channels = channels
24
+ # end
25
+ #
26
+ # def call(task, callback_type)
27
+ # return unless callback_type == :on_success
28
+ #
29
+ # @channels.each do |channel|
30
+ # NotificationService.send(channel, "Task #{task.class.name} completed")
31
+ # end
32
+ # end
33
+ # end
34
+ #
35
+ # @example Conditional callback execution
36
+ # class ErrorReportingCallback < CMDx::Callback
37
+ # def call(task, callback_type)
38
+ # return unless callback_type == :on_failure
39
+ # return unless task.result.failed?
40
+ #
41
+ # ErrorReporter.notify(
42
+ # task.errors.full_messages.join(", "),
43
+ # context: task.context.to_h
44
+ # )
45
+ # end
46
+ # end
47
+ #
48
+ # @see CallbackRegistry Callback management
49
+ # @see Task Callback integration
50
+ # @since 1.0.0
51
+ class Callback
52
+
53
+ ##
54
+ # Executes the callback logic.
55
+ #
56
+ # This method must be implemented by subclasses to define the callback
57
+ # behavior. The method receives the task instance and the callback type
58
+ # being executed.
59
+ #
60
+ # @param task [Task] the task instance being executed
61
+ # @param callback_type [Symbol] the type of callback being executed
62
+ # @return [void]
63
+ # @abstract Subclasses must implement this method
64
+ def call(_task, _callback_type)
65
+ raise UndefinedCallError, "call method not defined in #{self.class.name}"
66
+ end
67
+
68
+ end
69
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ ##
5
+ # The CallbackRegistry collection provides a lifecycle callback system that executes
6
+ # registered callbacks at specific points during task execution. Callbacks can be
7
+ # conditionally executed based on task state and support both method references
8
+ # and callable objects.
9
+ #
10
+ # The CallbackRegistry collection extends Hash to provide specialized functionality for
11
+ # managing collections of callback definitions within CMDx tasks. It handles
12
+ # callback registration, conditional execution, and inspection.
13
+ #
14
+ # @example Basic callback usage
15
+ # callback_registry = CallbackRegistry.new
16
+ # callback_registry.register(:before_validation, :check_permissions)
17
+ # callback_registry.register(:on_success, :log_success, if: :important?)
18
+ # callback_registry.register(:on_failure, proc { alert_admin }, unless: :test_env?)
19
+ #
20
+ # callback_registry.call(task, :before_validation)
21
+ #
22
+ # @example Hash-like operations
23
+ # callback_registry[:before_validation] = [[:check_permissions, {}]]
24
+ # callback_registry.keys # => [:before_validation]
25
+ # callback_registry.empty? # => false
26
+ # callback_registry.each { |callback_name, callbacks| puts "#{callback_name}: #{callbacks}" }
27
+ #
28
+ # @see Callback Base callback execution class
29
+ # @see Task Task lifecycle callbacks
30
+ # @since 1.0.0
31
+ class CallbackRegistry < Hash
32
+
33
+ ##
34
+ # Initializes a new CallbackRegistry.
35
+ #
36
+ # @param registry [CallbackRegistry, Hash, nil] Optional registry to copy from
37
+ #
38
+ # @example Initialize empty registry
39
+ # registry = CallbackRegistry.new
40
+ #
41
+ # @example Initialize with existing registry
42
+ # global_callbacks = CallbackRegistry.new
43
+ # task_callbacks = CallbackRegistry.new(global_callbacks)
44
+ def initialize(registry = nil)
45
+ super()
46
+
47
+ registry&.each do |callback_type, callback_definitions|
48
+ self[callback_type] = callback_definitions.dup
49
+ end
50
+ end
51
+
52
+ # Registers a callback for the given callback type.
53
+ #
54
+ # @param callback [Symbol] The callback type (e.g., :before_validation, :on_success)
55
+ # @param callables [Array<Symbol, Proc, #call>] Methods or callables to execute
56
+ # @param options [Hash] Conditions for callback execution
57
+ # @option options [Symbol, Proc, #call] :if condition that must be truthy
58
+ # @option options [Symbol, Proc, #call] :unless condition that must be falsy
59
+ # @param block [Proc] Block to execute as part of the callback
60
+ # @return [CallbackRegistry] self for method chaining
61
+ #
62
+ # @example Register method callback
63
+ # registry.register(:before_validation, :check_permissions)
64
+ #
65
+ # @example Register conditional callback
66
+ # registry.register(:on_failure, :alert_admin, if: :critical?)
67
+ #
68
+ # @example Register proc callback
69
+ # registry.register(:on_success, proc { log_completion })
70
+ def register(callback, *callables, **options, &block)
71
+ callables << block if block_given?
72
+ (self[callback] ||= []).push([callables, options]).uniq!
73
+ self
74
+ end
75
+
76
+ # Executes all callbacks registered for a specific callback type on the given task.
77
+ # Each callback is evaluated for its conditions (if/unless) before execution.
78
+ #
79
+ # @param task [Task] The task instance to execute callbacks on
80
+ # @param callback [Symbol] The callback type to execute (e.g., :before_validation, :on_success)
81
+ # @return [void]
82
+ #
83
+ # @example Execute callbacks
84
+ # registry.call(task, :before_validation)
85
+ #
86
+ # @example Execute conditional callbacks
87
+ # # Only executes if task.critical? returns true
88
+ # registry.call(task, :on_failure) # where registry has on_failure :alert, if: :critical?
89
+ def call(task, callback)
90
+ return unless key?(callback)
91
+
92
+ Array(self[callback]).each do |callables, options|
93
+ next unless task.__cmdx_eval(options)
94
+
95
+ Array(callables).each do |c|
96
+ if c.is_a?(Callback)
97
+ c.call(task, callback)
98
+ else
99
+ task.__cmdx_try(c)
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ end
106
+ end