cmdx 0.4.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.
- checksums.yaml +4 -4
- data/.DS_Store +0 -0
- data/.cursor/rules/cursor-instructions.mdc +6 -0
- data/.rubocop.yml +16 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +42 -1
- data/README.md +72 -25
- data/docs/ai_prompts.md +309 -0
- data/docs/basics/call.md +225 -14
- data/docs/basics/chain.md +271 -0
- data/docs/basics/context.md +232 -33
- data/docs/basics/setup.md +76 -12
- data/docs/callbacks.md +273 -0
- data/docs/configuration.md +158 -28
- data/docs/getting_started.md +134 -22
- data/docs/interruptions/exceptions.md +189 -11
- data/docs/interruptions/faults.md +187 -44
- data/docs/interruptions/halt.md +179 -35
- data/docs/logging.md +194 -53
- data/docs/middlewares.md +735 -0
- data/docs/outcomes/result.md +296 -10
- data/docs/outcomes/states.md +212 -19
- data/docs/outcomes/statuses.md +284 -18
- data/docs/parameters/coercions.md +402 -29
- data/docs/parameters/defaults.md +249 -25
- data/docs/parameters/definitions.md +238 -72
- data/docs/parameters/namespacing.md +250 -27
- data/docs/parameters/validations.md +193 -168
- data/docs/testing.md +550 -0
- data/docs/tips_and_tricks.md +95 -43
- data/docs/workflows.md +319 -0
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/callback.rb +69 -0
- data/lib/cmdx/callback_registry.rb +106 -0
- data/lib/cmdx/chain.rb +190 -0
- data/lib/cmdx/chain_inspector.rb +149 -0
- data/lib/cmdx/chain_serializer.rb +175 -0
- data/lib/cmdx/coercions/array.rb +37 -0
- data/lib/cmdx/coercions/big_decimal.rb +33 -0
- data/lib/cmdx/coercions/boolean.rb +41 -1
- data/lib/cmdx/coercions/complex.rb +31 -0
- data/lib/cmdx/coercions/date.rb +39 -0
- data/lib/cmdx/coercions/date_time.rb +39 -0
- data/lib/cmdx/coercions/float.rb +31 -0
- data/lib/cmdx/coercions/hash.rb +42 -0
- data/lib/cmdx/coercions/integer.rb +32 -0
- data/lib/cmdx/coercions/rational.rb +31 -0
- data/lib/cmdx/coercions/string.rb +31 -0
- data/lib/cmdx/coercions/time.rb +39 -0
- data/lib/cmdx/coercions/virtual.rb +31 -0
- data/lib/cmdx/configuration.rb +217 -9
- data/lib/cmdx/context.rb +173 -2
- data/lib/cmdx/core_ext/hash.rb +72 -0
- data/lib/cmdx/core_ext/module.rb +94 -0
- data/lib/cmdx/core_ext/object.rb +105 -0
- data/lib/cmdx/correlator.rb +217 -0
- data/lib/cmdx/error.rb +210 -8
- data/lib/cmdx/errors.rb +256 -1
- data/lib/cmdx/fault.rb +177 -2
- data/lib/cmdx/faults.rb +158 -2
- data/lib/cmdx/immutator.rb +121 -2
- data/lib/cmdx/lazy_struct.rb +261 -18
- data/lib/cmdx/log_formatters/json.rb +46 -0
- data/lib/cmdx/log_formatters/key_value.rb +46 -0
- data/lib/cmdx/log_formatters/line.rb +54 -0
- data/lib/cmdx/log_formatters/logstash.rb +64 -0
- data/lib/cmdx/log_formatters/pretty_json.rb +57 -0
- data/lib/cmdx/log_formatters/pretty_key_value.rb +51 -0
- data/lib/cmdx/log_formatters/pretty_line.rb +60 -0
- data/lib/cmdx/log_formatters/raw.rb +54 -0
- data/lib/cmdx/logger.rb +85 -0
- data/lib/cmdx/logger_ansi.rb +93 -7
- data/lib/cmdx/logger_serializer.rb +116 -0
- data/lib/cmdx/middleware.rb +74 -0
- data/lib/cmdx/middleware_registry.rb +106 -0
- data/lib/cmdx/middlewares/correlate.rb +266 -0
- data/lib/cmdx/middlewares/timeout.rb +232 -0
- data/lib/cmdx/parameter.rb +228 -1
- data/lib/cmdx/parameter_inspector.rb +61 -0
- data/lib/cmdx/parameter_registry.rb +125 -0
- data/lib/cmdx/parameter_serializer.rb +83 -0
- data/lib/cmdx/parameter_validator.rb +62 -0
- data/lib/cmdx/parameter_value.rb +109 -1
- data/lib/cmdx/parameters_inspector.rb +59 -0
- data/lib/cmdx/parameters_serializer.rb +102 -0
- data/lib/cmdx/railtie.rb +123 -3
- data/lib/cmdx/result.rb +399 -20
- data/lib/cmdx/result_ansi.rb +105 -9
- data/lib/cmdx/result_inspector.rb +76 -0
- data/lib/cmdx/result_logger.rb +90 -3
- data/lib/cmdx/result_serializer.rb +137 -0
- data/lib/cmdx/rspec/result_matchers.rb +917 -0
- data/lib/cmdx/rspec/task_matchers.rb +570 -0
- data/lib/cmdx/task.rb +409 -34
- data/lib/cmdx/task_serializer.rb +74 -2
- data/lib/cmdx/utils/ansi_color.rb +95 -0
- data/lib/cmdx/utils/log_timestamp.rb +48 -0
- data/lib/cmdx/utils/monotonic_runtime.rb +71 -4
- data/lib/cmdx/utils/name_affix.rb +78 -0
- data/lib/cmdx/validators/custom.rb +82 -0
- data/lib/cmdx/validators/exclusion.rb +94 -0
- data/lib/cmdx/validators/format.rb +102 -8
- data/lib/cmdx/validators/inclusion.rb +104 -0
- data/lib/cmdx/validators/length.rb +128 -0
- data/lib/cmdx/validators/numeric.rb +128 -0
- data/lib/cmdx/validators/presence.rb +93 -7
- data/lib/cmdx/version.rb +7 -1
- data/lib/cmdx/workflow.rb +394 -0
- data/lib/cmdx.rb +25 -64
- data/lib/generators/cmdx/install_generator.rb +37 -1
- data/lib/generators/cmdx/task_generator.rb +69 -1
- data/lib/generators/cmdx/templates/install.rb +8 -12
- data/lib/generators/cmdx/workflow_generator.rb +109 -0
- metadata +54 -15
- data/docs/basics/run.md +0 -34
- data/docs/batch.md +0 -53
- data/docs/example.md +0 -82
- data/docs/hooks.md +0 -59
- data/lib/cmdx/batch.rb +0 -43
- data/lib/cmdx/parameters.rb +0 -34
- data/lib/cmdx/run.rb +0 -38
- data/lib/cmdx/run_inspector.rb +0 -26
- data/lib/cmdx/run_serializer.rb +0 -16
- data/lib/cmdx/task_hook.rb +0 -18
- data/lib/generators/cmdx/batch_generator.rb +0 -30
- /data/lib/generators/cmdx/templates/{batch.rb.tt → workflow.rb.tt} +0 -0
data/docs/tips_and_tricks.md
CHANGED
@@ -1,79 +1,131 @@
|
|
1
1
|
# Tips & Tricks
|
2
2
|
|
3
|
-
|
3
|
+
This guide covers advanced patterns and optimization techniques for getting the most out of CMDx in production applications.
|
4
4
|
|
5
|
-
|
5
|
+
## Table of Contents
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
16
|
-
config.logger.formatter = CMDx::LogFormatters::Logstash.new
|
17
|
-
end
|
18
|
-
```
|
15
|
+
## Project Organization
|
19
16
|
|
20
|
-
|
17
|
+
### Directory Structure
|
21
18
|
|
22
|
-
|
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
|
-
/
|
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
|
-
-
|
30
|
+
- send_email_task.rb
|
31
|
+
- send_sms_task.rb
|
30
32
|
- post_slack_message_task.rb
|
31
|
-
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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:
|
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, :
|
62
|
-
|
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
|
-
|
67
|
-
|
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
|
-
|
74
|
-
|
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:** [
|
79
|
-
- **Next:** [
|
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
|