cmdx 0.5.0 → 1.0.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 (151) 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 +19 -1
  5. data/.ruby-version +1 -1
  6. data/CHANGELOG.md +95 -28
  7. data/README.md +73 -25
  8. data/docs/ai_prompts.md +319 -0
  9. data/docs/basics/call.md +234 -14
  10. data/docs/basics/chain.md +280 -0
  11. data/docs/basics/context.md +241 -33
  12. data/docs/basics/setup.md +85 -12
  13. data/docs/callbacks.md +283 -0
  14. data/docs/configuration.md +155 -30
  15. data/docs/getting_started.md +145 -22
  16. data/docs/internationalization.md +148 -0
  17. data/docs/interruptions/exceptions.md +198 -11
  18. data/docs/interruptions/faults.md +196 -44
  19. data/docs/interruptions/halt.md +188 -35
  20. data/docs/logging.md +204 -53
  21. data/docs/middlewares.md +745 -0
  22. data/docs/outcomes/result.md +305 -10
  23. data/docs/outcomes/states.md +212 -31
  24. data/docs/outcomes/statuses.md +284 -30
  25. data/docs/parameters/coercions.md +411 -29
  26. data/docs/parameters/defaults.md +258 -25
  27. data/docs/parameters/definitions.md +247 -72
  28. data/docs/parameters/namespacing.md +259 -27
  29. data/docs/parameters/validations.md +173 -168
  30. data/docs/testing.md +560 -0
  31. data/docs/tips_and_tricks.md +103 -42
  32. data/docs/workflows.md +329 -0
  33. data/lib/cmdx/.DS_Store +0 -0
  34. data/lib/cmdx/callback.rb +69 -0
  35. data/lib/cmdx/callback_registry.rb +106 -0
  36. data/lib/cmdx/chain.rb +190 -0
  37. data/lib/cmdx/chain_inspector.rb +149 -0
  38. data/lib/cmdx/chain_serializer.rb +175 -0
  39. data/lib/cmdx/coercions/array.rb +37 -0
  40. data/lib/cmdx/coercions/big_decimal.rb +33 -0
  41. data/lib/cmdx/coercions/boolean.rb +41 -1
  42. data/lib/cmdx/coercions/complex.rb +31 -0
  43. data/lib/cmdx/coercions/date.rb +39 -0
  44. data/lib/cmdx/coercions/date_time.rb +39 -0
  45. data/lib/cmdx/coercions/float.rb +31 -0
  46. data/lib/cmdx/coercions/hash.rb +42 -0
  47. data/lib/cmdx/coercions/integer.rb +32 -0
  48. data/lib/cmdx/coercions/rational.rb +31 -0
  49. data/lib/cmdx/coercions/string.rb +31 -0
  50. data/lib/cmdx/coercions/time.rb +39 -0
  51. data/lib/cmdx/coercions/virtual.rb +31 -0
  52. data/lib/cmdx/configuration.rb +217 -9
  53. data/lib/cmdx/context.rb +173 -2
  54. data/lib/cmdx/core_ext/hash.rb +72 -0
  55. data/lib/cmdx/core_ext/module.rb +94 -0
  56. data/lib/cmdx/core_ext/object.rb +105 -0
  57. data/lib/cmdx/correlator.rb +217 -0
  58. data/lib/cmdx/error.rb +210 -8
  59. data/lib/cmdx/errors.rb +256 -1
  60. data/lib/cmdx/fault.rb +177 -2
  61. data/lib/cmdx/faults.rb +158 -2
  62. data/lib/cmdx/immutator.rb +121 -2
  63. data/lib/cmdx/lazy_struct.rb +261 -18
  64. data/lib/cmdx/log_formatters/json.rb +46 -0
  65. data/lib/cmdx/log_formatters/key_value.rb +46 -0
  66. data/lib/cmdx/log_formatters/line.rb +54 -0
  67. data/lib/cmdx/log_formatters/logstash.rb +64 -0
  68. data/lib/cmdx/log_formatters/pretty_json.rb +57 -0
  69. data/lib/cmdx/log_formatters/pretty_key_value.rb +51 -0
  70. data/lib/cmdx/log_formatters/pretty_line.rb +60 -0
  71. data/lib/cmdx/log_formatters/raw.rb +54 -0
  72. data/lib/cmdx/logger.rb +85 -0
  73. data/lib/cmdx/logger_ansi.rb +93 -7
  74. data/lib/cmdx/logger_serializer.rb +116 -0
  75. data/lib/cmdx/middleware.rb +74 -0
  76. data/lib/cmdx/middleware_registry.rb +106 -0
  77. data/lib/cmdx/middlewares/correlate.rb +266 -0
  78. data/lib/cmdx/middlewares/timeout.rb +232 -0
  79. data/lib/cmdx/parameter.rb +228 -1
  80. data/lib/cmdx/parameter_inspector.rb +61 -0
  81. data/lib/cmdx/parameter_registry.rb +125 -0
  82. data/lib/cmdx/parameter_serializer.rb +83 -0
  83. data/lib/cmdx/parameter_validator.rb +62 -0
  84. data/lib/cmdx/parameter_value.rb +109 -1
  85. data/lib/cmdx/parameters_inspector.rb +59 -0
  86. data/lib/cmdx/parameters_serializer.rb +102 -0
  87. data/lib/cmdx/railtie.rb +123 -3
  88. data/lib/cmdx/result.rb +367 -25
  89. data/lib/cmdx/result_ansi.rb +105 -9
  90. data/lib/cmdx/result_inspector.rb +76 -0
  91. data/lib/cmdx/result_logger.rb +90 -3
  92. data/lib/cmdx/result_serializer.rb +137 -0
  93. data/lib/cmdx/rspec/result_matchers.rb +917 -0
  94. data/lib/cmdx/rspec/task_matchers.rb +570 -0
  95. data/lib/cmdx/task.rb +405 -37
  96. data/lib/cmdx/task_serializer.rb +74 -2
  97. data/lib/cmdx/utils/ansi_color.rb +95 -0
  98. data/lib/cmdx/utils/log_timestamp.rb +48 -0
  99. data/lib/cmdx/utils/monotonic_runtime.rb +71 -4
  100. data/lib/cmdx/utils/name_affix.rb +78 -0
  101. data/lib/cmdx/validators/custom.rb +82 -0
  102. data/lib/cmdx/validators/exclusion.rb +94 -0
  103. data/lib/cmdx/validators/format.rb +102 -8
  104. data/lib/cmdx/validators/inclusion.rb +104 -0
  105. data/lib/cmdx/validators/length.rb +128 -0
  106. data/lib/cmdx/validators/numeric.rb +128 -0
  107. data/lib/cmdx/validators/presence.rb +93 -7
  108. data/lib/cmdx/version.rb +7 -1
  109. data/lib/cmdx/workflow.rb +394 -0
  110. data/lib/cmdx.rb +25 -64
  111. data/lib/generators/cmdx/install_generator.rb +37 -1
  112. data/lib/generators/cmdx/task_generator.rb +69 -1
  113. data/lib/generators/cmdx/templates/install.rb +43 -15
  114. data/lib/generators/cmdx/workflow_generator.rb +109 -0
  115. data/lib/locales/ar.yml +36 -0
  116. data/lib/locales/cs.yml +36 -0
  117. data/lib/locales/da.yml +36 -0
  118. data/lib/locales/de.yml +36 -0
  119. data/lib/locales/el.yml +36 -0
  120. data/lib/locales/en.yml +20 -20
  121. data/lib/locales/es.yml +20 -20
  122. data/lib/locales/fi.yml +36 -0
  123. data/lib/locales/fr.yml +36 -0
  124. data/lib/locales/he.yml +36 -0
  125. data/lib/locales/hi.yml +36 -0
  126. data/lib/locales/it.yml +36 -0
  127. data/lib/locales/ja.yml +36 -0
  128. data/lib/locales/ko.yml +36 -0
  129. data/lib/locales/nl.yml +36 -0
  130. data/lib/locales/no.yml +36 -0
  131. data/lib/locales/pl.yml +36 -0
  132. data/lib/locales/pt.yml +36 -0
  133. data/lib/locales/ru.yml +36 -0
  134. data/lib/locales/sv.yml +36 -0
  135. data/lib/locales/th.yml +36 -0
  136. data/lib/locales/tr.yml +36 -0
  137. data/lib/locales/vi.yml +36 -0
  138. data/lib/locales/zh.yml +36 -0
  139. metadata +77 -15
  140. data/docs/basics/run.md +0 -34
  141. data/docs/batch.md +0 -53
  142. data/docs/example.md +0 -82
  143. data/docs/hooks.md +0 -62
  144. data/lib/cmdx/batch.rb +0 -43
  145. data/lib/cmdx/parameters.rb +0 -35
  146. data/lib/cmdx/run.rb +0 -39
  147. data/lib/cmdx/run_inspector.rb +0 -26
  148. data/lib/cmdx/run_serializer.rb +0 -20
  149. data/lib/cmdx/task_hook.rb +0 -18
  150. data/lib/generators/cmdx/batch_generator.rb +0 -30
  151. /data/lib/generators/cmdx/templates/{batch.rb.tt → workflow.rb.tt} +0 -0
@@ -1,79 +1,140 @@
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
7
+ - [TLDR](#tldr)
8
+ - [Project Organization](#project-organization)
9
+ - [Directory Structure](#directory-structure)
10
+ - [Naming Conventions](#naming-conventions)
11
+ - [Parameter Optimization](#parameter-optimization)
12
+ - [Efficient Parameter Definitions](#efficient-parameter-definitions)
13
+ - [Monitoring and Observability](#monitoring-and-observability)
14
+ - [ActiveRecord Query Tagging](#activerecord-query-tagging)
11
15
 
12
- # Adjust the log level to write depending on the environment:
13
- config.logger.level = Rails.env.development? ? Logger::DEBUG : Logger::INFO
16
+ ## TLDR
14
17
 
15
- # Structure log lines using a pre-built or custom formatter:
16
- config.logger.formatter = CMDx::LogFormatters::Logstash.new
17
- end
18
- ```
18
+ - **Organization** - Group commands by domain in `/app/commands` with descriptive subdirectories
19
+ - **Naming** - Tasks use "Verb + Noun + Task", workflows use "Noun + Verb + Workflow"
20
+ - **Parameter optimization** - Use `with_options` to reduce duplication in parameter definitions
21
+ - **Monitoring** - Enable ActiveRecord query tagging for better debugging and observability
22
+ - **Base classes** - Create `ApplicationTask` and `ApplicationWorkflow` for shared configuration
19
23
 
20
- ## Setup
24
+ ## Project Organization
21
25
 
22
- While not required, a common setup involves creating an `app/cmds` directory
23
- to place all of your tasks and batches under, eg:
26
+ ### Directory Structure
27
+
28
+ Create a well-organized command structure for maintainable applications:
24
29
 
25
30
  ```txt
26
31
  /app
27
- /cmds
32
+ /commands
33
+ /orders
34
+ - process_order_task.rb
35
+ - validate_order_task.rb
36
+ - fulfill_order_task.rb
37
+ - order_processing_workflow.rb
28
38
  /notifications
29
- - deliver_email_task.rb
39
+ - send_email_task.rb
40
+ - send_sms_task.rb
30
41
  - 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
42
+ - notification_delivery_workflow.rb
43
+ /payments
44
+ - charge_payment_task.rb
45
+ - refund_payment_task.rb
46
+ - validate_payment_method_task.rb
35
47
  - application_task.rb
48
+ - application_workflow.rb
36
49
  ```
37
50
 
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
51
+ ### Naming Conventions
44
52
 
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:
53
+ Follow consistent naming patterns for clarity and maintainability:
48
54
 
49
55
  ```ruby
50
- class UpdateUserDetailsTask < CMDx::Task
56
+ # Tasks: Verb + Noun + Task
57
+ class ProcessOrderTask < CMDx::Task; end
58
+ class SendEmailTask < CMDx::Task; end
59
+ class ValidatePaymentTask < CMDx::Task; end
60
+
61
+ # Workflows: Noun + Verb + Workflow
62
+ class OrderProcessingWorkflow < CMDx::Workflow; end
63
+ class NotificationDeliveryWorkflow < CMDx::Workflow; end
64
+
65
+ # Use present tense verbs for actions
66
+ class CreateUserTask < CMDx::Task; end # ✓ Good
67
+ class CreatingUserTask < CMDx::Task; end # ❌ Avoid
68
+ class UserCreationTask < CMDx::Task; end # ❌ Avoid
69
+ ```
51
70
 
52
- # Apply `type: :string, presence: true` to this set of parameters:
71
+ ## Parameter Optimization
72
+
73
+ ### Efficient Parameter Definitions
74
+
75
+ Use Rails `with_options` to reduce duplication and improve readability:
76
+
77
+ ```ruby
78
+ class UpdateUserProfileTask < CMDx::Task
79
+ # Apply common options to multiple parameters
53
80
  with_options(type: :string, presence: true) do
54
- required :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i }
81
+ required :email, format: { with: URI::MailTo::EMAIL_REGEXP }
55
82
  optional :first_name, :last_name
83
+ optional :phone, format: { with: /\A\+?[\d\s\-\(\)]+\z/ }
56
84
  end
57
85
 
86
+ # Nested parameters with shared prefix
58
87
  required :address do
59
- # Apply the `address_*` prefix to this set of nested parameters:
60
88
  with_options(prefix: :address_) do
61
- required :city, :country
62
- optional :state
89
+ required :street, :city, :postal_code, type: :string
90
+ required :country, type: :string, inclusion: { in: VALID_COUNTRIES }
91
+ optional :state, type: :string
63
92
  end
64
93
  end
65
94
 
66
- def call
67
- # Do work
95
+ # Shared validation rules
96
+ with_options(type: :integer, numericality: { greater_than: 0 }) do
97
+ optional :age, numericality: { less_than: 150 }
98
+ optional :years_experience, numericality: { less_than: 80 }
68
99
  end
69
100
 
101
+ def call
102
+ # Implementation
103
+ end
70
104
  end
71
105
  ```
72
106
 
73
- [Learn More](https://api.rubyonrails.org/classes/Object.html#method-i-with_options)
74
- about its usages on the official Rails docs.
107
+ ## Monitoring and Observability
108
+
109
+ ### ActiveRecord Query Tagging
110
+
111
+ Automatically tag SQL queries for better debugging:
112
+
113
+ ```ruby
114
+ # config/application.rb
115
+ config.active_record.query_log_tags_enabled = true
116
+ config.active_record.query_log_tags << :cmdx_task_class
117
+ config.active_record.query_log_tags << :cmdx_chain_id
118
+
119
+ # app/commands/application_task.rb
120
+ class ApplicationTask < CMDx::Task
121
+ before_execution :set_execution_context
122
+
123
+ private
124
+
125
+ def set_execution_context
126
+ ActiveSupport::ExecutionContext.set(
127
+ cmdx_task_class: self.class.name,
128
+ cmdx_chain_id: chain.id
129
+ )
130
+ end
131
+ end
132
+
133
+ # SQL queries will now include comments like:
134
+ # /*cmdx_task_class:ProcessOrderTask,cmdx_chain_id:018c2b95-b764-7615*/ SELECT * FROM orders WHERE id = 1
135
+ ```
75
136
 
76
137
  ---
77
138
 
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)
139
+ - **Prev:** [AI Prompts](ai_prompts.md)
140
+ - **Next:** [Getting Started](getting_started.md)
data/docs/workflows.md ADDED
@@ -0,0 +1,329 @@
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
+ - [TLDR](#tldr)
10
+ - [Basic Usage](#basic-usage)
11
+ - [Task Declaration](#task-declaration)
12
+ - [Context Propagation](#context-propagation)
13
+ - [Conditional Execution](#conditional-execution)
14
+ - [Halt Behavior](#halt-behavior)
15
+ - [Default Behavior](#default-behavior)
16
+ - [Class-Level Configuration](#class-level-configuration)
17
+ - [Group-Level Configuration](#group-level-configuration)
18
+ - [Available Result Statuses](#available-result-statuses)
19
+ - [Process Method Options](#process-method-options)
20
+ - [Condition Callables](#condition-callables)
21
+ - [Nested Workflows](#nested-workflows)
22
+ - [Task Settings Integration](#task-settings-integration)
23
+ - [Generator](#generator)
24
+
25
+ ## TLDR
26
+
27
+ - **Purpose** - Orchestrate sequential execution of multiple tasks in linear pipeline
28
+ - **Declaration** - Use `process` method to declare tasks in execution order
29
+ - **Context sharing** - Context object shared across all tasks for data pipeline
30
+ - **Conditional execution** - Support `:if` and `:unless` options for conditional tasks
31
+ - **Halt behavior** - Configurable stopping on failed/skipped results (default: halt on failed only)
32
+ - **No call method** - Workflows automatically provide execution logic, don't define `call`
33
+
34
+ ## Basic Usage
35
+
36
+ > [!WARNING]
37
+ > Do **NOT** define a `call` method in workflow classes. The workflow class automatically provides the call logic.
38
+
39
+ ```ruby
40
+ class OrderProcessingWorkflow < CMDx::Workflow
41
+ # Sequential task execution
42
+ process ValidateOrderTask
43
+ process CalculateTaxTask
44
+ process ChargePaymentTask
45
+ process FulfillOrderTask
46
+ end
47
+
48
+ # Execute the workflow
49
+ result = WorkflowProcessOrders.call(order: order, user: current_user)
50
+
51
+ if result.success?
52
+ redirect_to success_path
53
+ elsif result.failed?
54
+ flash[:error] = "Order processing failed: #{result.metadata[:reason]}"
55
+ redirect_to cart_path
56
+ end
57
+ ```
58
+
59
+ ## Task Declaration
60
+
61
+ Tasks are declared using the `process` method and organized into groups with shared execution options:
62
+
63
+ ```ruby
64
+ class NotificationDeliveryWorkflow < CMDx::Workflow
65
+ # Single task declaration
66
+ process PrepareNotificationTask
67
+
68
+ # Multiple tasks in one declaration (grouped)
69
+ process SendEmailTask, SendSmsTask, SendPushTask
70
+
71
+ # Tasks with conditions
72
+ process SendWebhookTask, if: proc { context.webhook_enabled? }
73
+ process SendSlackTask, unless: :slack_disabled?
74
+
75
+ private
76
+
77
+ def slack_disabled?
78
+ !context.user.slack_enabled?
79
+ end
80
+ end
81
+ ```
82
+
83
+ > [!IMPORTANT]
84
+ > Process steps are executed in the order they are declared (FIFO: first in, first out).
85
+
86
+ ## Context Propagation
87
+
88
+ The context object is shared across all tasks in the workflow, creating a data pipeline:
89
+
90
+ ```ruby
91
+ class EcommerceProcessingWorkflow < CMDx::Workflow
92
+ process ValidateOrderTask # Sets context.validation_result
93
+ process CalculateTaxTask # Uses context.order, sets context.tax_amount
94
+ process ChargePaymentTask # Uses context.tax_amount, sets context.payment_id
95
+ process FulfillOrderTask # Uses context.payment_id, sets context.tracking_number
96
+ end
97
+
98
+ result = WorkflowProcessEcommerce.call(order: order)
99
+ # Final context contains data from all executed tasks
100
+ result.context.validation_result # From ValidateOrderTask
101
+ result.context.tax_amount # From CalculateTaxTask
102
+ result.context.payment_id # From ChargePaymentTask
103
+ result.context.tracking_number # From FulfillOrderTask
104
+ ```
105
+
106
+ ## Conditional Execution
107
+
108
+ Tasks can be executed conditionally using `:if` and `:unless` options. Conditions can be procs, lambdas, or method names:
109
+
110
+ ```ruby
111
+ class UserProcessingWorkflow < CMDx::Workflow
112
+ process ValidateUserTask
113
+
114
+ # Proc condition
115
+ process UpgradeToPremiumTask, if: proc { context.user.premium? }
116
+
117
+ # Lambda condition
118
+ process ProcessInternationalTask, unless: -> { context.user.domestic? }
119
+
120
+ # Method condition
121
+ process LogDebugInfoTask, if: :debug_enabled?
122
+
123
+ # Complex condition
124
+ process SendSpecialOfferTask, if: proc {
125
+ context.user.active? &&
126
+ context.feature_enabled?(:offers) &&
127
+ Time.now.hour.between?(9, 17)
128
+ }
129
+
130
+ private
131
+
132
+ def debug_enabled?
133
+ Rails.env.development?
134
+ end
135
+ end
136
+ ```
137
+
138
+ ## Halt Behavior
139
+
140
+ Workflows control execution flow through halt behavior, which determines when to stop processing based on task results.
141
+
142
+ ### Default Behavior
143
+
144
+ By default, workflows halt on `FAILED` status but continue on `SKIPPED`. This reflects the philosophy that skipped tasks are bypass mechanisms, not execution blockers.
145
+
146
+ ```ruby
147
+ class DataProcessingWorkflow < CMDx::Workflow
148
+ process LoadDataTask # If this fails, workflow stops
149
+ process ValidateDataTask # If this is skipped, workflow continues
150
+ process SaveDataTask # This only runs if LoadDataTask and ValidateDataTask don't fail
151
+ end
152
+ ```
153
+
154
+ ### Class-Level Configuration
155
+
156
+ Configure halt behavior for the entire workflow using `task_settings!`:
157
+
158
+ ```ruby
159
+ class CriticalDataProcessingWorkflow < CMDx::Workflow
160
+ # Halt on both failed and skipped results
161
+ task_settings!(workflow_halt: [CMDx::Result::FAILED, CMDx::Result::SKIPPED])
162
+
163
+ process LoadCriticalDataTask
164
+ process ValidateCriticalDataTask
165
+ end
166
+
167
+ class OptionalDataProcessingWorkflow < CMDx::Workflow
168
+ # Never halt, always continue
169
+ task_settings!(workflow_halt: [])
170
+
171
+ process TryLoadDataTask
172
+ process TryValidateDataTask
173
+ process TrySaveDataTask
174
+ end
175
+ ```
176
+
177
+ ### Group-Level Configuration
178
+
179
+ Different groups can have different halt behavior:
180
+
181
+ ```ruby
182
+ class UserAccountProcessingWorkflow < CMDx::Workflow
183
+ # Critical tasks - halt on any failure or skip
184
+ process CreateUserTask, ValidateUserTask,
185
+ workflow_halt: [CMDx::Result::FAILED, CMDx::Result::SKIPPED]
186
+
187
+ # Optional tasks - never halt execution
188
+ process SendWelcomeEmailTask, CreateProfileTask, workflow_halt: []
189
+
190
+ # Notification tasks - use default behavior (halt on failed only)
191
+ process NotifyAdminTask, LogUserCreationTask
192
+ end
193
+ ```
194
+
195
+ ### Available Result Statuses
196
+
197
+ The following result statuses can be used in `workflow_halt` arrays:
198
+
199
+ - `CMDx::Result::SUCCESS` - Task completed successfully
200
+ - `CMDx::Result::SKIPPED` - Task was skipped intentionally
201
+ - `CMDx::Result::FAILED` - Task failed due to error or validation
202
+
203
+ ## Process Method Options
204
+
205
+ The `process` method supports the following options:
206
+
207
+ | Option | Description |
208
+ | ------------- | ----------- |
209
+ | `:if` | Specifies a callable method, proc or string to determine if processing steps should occur. |
210
+ | `:unless` | Specifies a callable method, proc, or string to determine if processing steps should not occur. |
211
+ | `:workflow_halt` | Sets which result statuses processing of further steps should be prevented. (default: `CMDx::Result::FAILED`) |
212
+
213
+ ### Condition Callables
214
+
215
+ Conditions can be provided in several formats:
216
+
217
+ ```ruby
218
+ class AccountProcessingWorkflow < CMDx::Workflow
219
+ # Proc - executed in workflow instance context
220
+ process UpgradeAccountTask, if: proc { context.user.admin? }
221
+
222
+ # Lambda - executed in workflow instance context
223
+ process MaintenanceModeTask, unless: -> { context.maintenance_mode? }
224
+
225
+ # Symbol - method name called on workflow instance
226
+ process AdvancedFeatureTask, if: :feature_enabled?
227
+
228
+ # String - method name called on workflow instance
229
+ process OptionalTask, unless: "skip_task?"
230
+
231
+ private
232
+
233
+ def feature_enabled?
234
+ context.features.include?(:advanced)
235
+ end
236
+
237
+ def skip_task?
238
+ context.skip_optional_tasks?
239
+ end
240
+ end
241
+ ```
242
+
243
+ ## Nested Workflows
244
+
245
+ Workflows can process other workflows, creating hierarchical workflows:
246
+
247
+ ```ruby
248
+ class DataPreProcessingWorkflow < CMDx::Workflow
249
+ process ValidateInputTask
250
+ process SanitizeDataTask
251
+ end
252
+
253
+ class DataProcessingWorkflow < CMDx::Workflow
254
+ process TransformDataTask
255
+ process ApplyBusinessLogicTask
256
+ end
257
+
258
+ class DataPostProcessingWorkflow < CMDx::Workflow
259
+ process GenerateReportTask
260
+ process SendNotificationTask
261
+ end
262
+
263
+ class CompleteDataProcessingWorkflow < CMDx::Workflow
264
+ process DataPreProcessingWorkflow
265
+ process DataProcessingWorkflow, if: proc { context.pre_processing_successful? }
266
+ process DataPostProcessingWorkflow, unless: proc { context.skip_post_processing? }
267
+ end
268
+ ```
269
+
270
+ ## Task Settings Integration
271
+
272
+ Workflows support all task settings and can be configured like regular tasks:
273
+
274
+ ```ruby
275
+ class PaymentProcessingWorkflow < CMDx::Workflow
276
+ # Configure workflow-specific settings
277
+ task_settings!(
278
+ workflow_halt: [CMDx::Result::FAILED],
279
+ log_level: :debug,
280
+ tags: [:critical, :payment]
281
+ )
282
+
283
+ # Parameter validation
284
+ required :order_id, type: :integer
285
+ optional :notify_user, type: :boolean, default: true
286
+
287
+ # Callbacks
288
+ before_execution :setup_context
289
+ after_execution :cleanup_resources
290
+
291
+ process ValidateOrderTask
292
+ process ProcessPaymentTask
293
+ process NotifyUserTask, if: proc { context.notify_user }
294
+
295
+ private
296
+
297
+ def setup_context
298
+ context.start_time = Time.now
299
+ end
300
+
301
+ def cleanup_resources
302
+ context.temp_files&.each(&:delete)
303
+ end
304
+ end
305
+ ```
306
+
307
+ ## Generator
308
+
309
+ Generate a new workflow using the Rails generator:
310
+
311
+ ```bash
312
+ rails g cmdx:workflow ProcessOrder
313
+ ```
314
+
315
+ This creates a workflow template file under `app/cmds`:
316
+
317
+ ```ruby
318
+ class OrderProcessingWorkflow < ApplicationWorkflow
319
+ process # TODO
320
+ end
321
+ ```
322
+
323
+ > [!NOTE]
324
+ > The generator creates workflow files in `app/commands/workflow_[name].rb`, inherits from `ApplicationWorkflow` if available (otherwise `CMDx::Workflow`) and handles proper naming conventions.
325
+
326
+ ---
327
+
328
+ - **Prev:** [Middlewares](middlewares.md)
329
+ - **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