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.
- 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 +31 -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 +203 -31
- data/docs/outcomes/statuses.md +275 -30
- 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 +367 -25
- 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 +405 -37
- 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 -62
- data/lib/cmdx/batch.rb +0 -43
- data/lib/cmdx/parameters.rb +0 -35
- data/lib/cmdx/run.rb +0 -39
- data/lib/cmdx/run_inspector.rb +0 -26
- data/lib/cmdx/run_serializer.rb +0 -20
- 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/configuration.md
CHANGED
@@ -1,57 +1,187 @@
|
|
1
1
|
# Configuration
|
2
2
|
|
3
|
-
|
3
|
+
CMDx provides a flexible configuration system that allows customization at both global and task levels. Configuration follows a hierarchy where global settings serve as defaults that can be overridden at the task level.
|
4
4
|
|
5
|
-
##
|
5
|
+
## Table of Contents
|
6
6
|
|
7
|
-
|
8
|
-
|
7
|
+
- [Configuration Hierarchy](#configuration-hierarchy)
|
8
|
+
- [Global Configuration](#global-configuration)
|
9
|
+
- [Configuration Options](#configuration-options)
|
10
|
+
- [Global Middlewares](#global-middlewares)
|
11
|
+
- [Global Callbacks](#global-callbacks)
|
12
|
+
- [Task Settings](#task-settings)
|
13
|
+
- [Available Task Settings](#available-task-settings)
|
14
|
+
- [Workflow Configuration](#workflow-configuration)
|
15
|
+
- [Configuration Management](#configuration-management)
|
16
|
+
- [Accessing Configuration](#accessing-configuration)
|
17
|
+
- [Resetting Configuration](#resetting-configuration)
|
18
|
+
|
19
|
+
## Configuration Hierarchy
|
20
|
+
|
21
|
+
CMDx follows a three-tier configuration hierarchy:
|
22
|
+
|
23
|
+
1. **Global Configuration**: Framework-wide defaults
|
24
|
+
2. **Task Settings**: Class-level overrides via `task_settings!`
|
25
|
+
3. **Runtime Parameters**: Instance-specific overrides during execution
|
26
|
+
|
27
|
+
> [!IMPORTANT]
|
28
|
+
> Task-level settings take precedence over global configuration. Settings are inherited from superclasses and can be overridden in subclasses.
|
29
|
+
|
30
|
+
## Global Configuration
|
31
|
+
|
32
|
+
Generate a configuration file using the Rails generator:
|
33
|
+
|
34
|
+
```bash
|
35
|
+
rails g cmdx:install
|
36
|
+
```
|
37
|
+
|
38
|
+
This creates `config/initializers/cmdx.rb` with default settings:
|
9
39
|
|
10
40
|
```ruby
|
11
41
|
CMDx.configure do |config|
|
12
|
-
#
|
13
|
-
# This option can accept an array of statuses.
|
42
|
+
# Halt execution and raise fault on these result statuses when using `call!`
|
14
43
|
config.task_halt = CMDx::Result::FAILED
|
15
44
|
|
16
|
-
#
|
17
|
-
|
18
|
-
|
19
|
-
# Define which statuses a batch task will halt execution from proceeding to the next step.
|
20
|
-
# By default skipped tasks are treated as a NOOP so processing is continued.
|
21
|
-
# This option can accept an array of statuses.
|
22
|
-
config.batch_halt = CMDx::Result::FAILED
|
45
|
+
# Stop workflow execution when tasks return these statuses
|
46
|
+
# Note: Skipped tasks continue processing by default
|
47
|
+
config.workflow_halt = CMDx::Result::FAILED
|
23
48
|
|
24
|
-
#
|
25
|
-
# TIP: remember to account for all defined tasks when setting this value
|
26
|
-
config.batch_timeout = nil
|
27
|
-
|
28
|
-
# A list of available log formatter can be found at:
|
49
|
+
# Logger with formatter - see available formatters at:
|
29
50
|
# https://github.com/drexed/cmdx/tree/main/lib/cmdx/log_formatters
|
30
51
|
config.logger = Logger.new($stdout, formatter: CMDx::LogFormatters::Line.new)
|
31
52
|
end
|
32
53
|
```
|
33
54
|
|
34
|
-
|
55
|
+
### Configuration Options
|
56
|
+
|
57
|
+
| Option | Type | Default | Description |
|
58
|
+
|---------------|-----------------------|----------------|-------------|
|
59
|
+
| `task_halt` | String, Array<String> | `"failed"` | Result statuses that cause `call!` to raise faults |
|
60
|
+
| `workflow_halt` | String, Array<String> | `"failed"` | Result statuses that halt workflow execution |
|
61
|
+
| `logger` | Logger | Line formatter | Logger instance for task execution logging |
|
62
|
+
| `middlewares` | MiddlewareRegistry | Empty registry | Global middleware registry applied to all tasks |
|
63
|
+
| `callbacks` | CallbackRegistry | Empty registry | Global callback registry applied to all tasks |
|
64
|
+
|
65
|
+
### Global Middlewares
|
35
66
|
|
36
|
-
|
67
|
+
Configure middlewares that automatically apply to all tasks in your application:
|
37
68
|
|
38
69
|
```ruby
|
39
|
-
|
70
|
+
CMDx.configure do |config|
|
71
|
+
# Add middlewares without arguments
|
72
|
+
config.middlewares.use CMDx::Middlewares::Timeout
|
73
|
+
|
74
|
+
# Add middlewares with arguments
|
75
|
+
config.middlewares.use CMDx::Middlewares::Timeout, seconds: 30
|
76
|
+
|
77
|
+
# Add middleware instances
|
78
|
+
config.middlewares.use CMDx::Middlewares::Timeout.new(seconds: 30)
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
### Global Callbacks
|
83
|
+
|
84
|
+
Configure callbacks that automatically apply to all tasks in your application:
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
CMDx.configure do |config|
|
88
|
+
# Add method callbacks
|
89
|
+
config.callbacks.register :before_execution, :log_task_start
|
90
|
+
config.callbacks.register :after_execution, :log_task_end
|
91
|
+
|
92
|
+
# Add callback instances
|
93
|
+
config.callbacks.register :on_success, NotificationCallback.new([:slack])
|
94
|
+
config.callbacks.register :on_failure, AlertCallback.new(severity: :critical)
|
95
|
+
|
96
|
+
# Add conditional callbacks
|
97
|
+
config.callbacks.register :on_failure, :page_admin, if: :production?
|
98
|
+
config.callbacks.register :before_validation, :skip_validation, unless: :validate_params?
|
99
|
+
|
100
|
+
# Add proc callbacks
|
101
|
+
config.callbacks.register :on_complete, proc { |task, callback_type|
|
102
|
+
Metrics.increment("task.#{task.class.name.underscore}.completed")
|
103
|
+
}
|
104
|
+
end
|
105
|
+
```
|
40
106
|
|
41
|
-
|
42
|
-
|
107
|
+
## Task Settings
|
108
|
+
|
109
|
+
Override global configuration for specific tasks or workflows using `task_settings!`:
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
class ProcessPaymentTask < CMDx::Task
|
113
|
+
task_settings!(
|
114
|
+
task_halt: ["failed"], # Only halt on failures
|
115
|
+
tags: ["payments", "critical"], # Add logging tags
|
116
|
+
logger: Rails.logger, # Use Rails logger
|
117
|
+
log_level: :info, # Set log level
|
118
|
+
log_formatter: CMDx::LogFormatters::Json.new # JSON formatter
|
119
|
+
)
|
43
120
|
|
44
121
|
def call
|
45
|
-
#
|
122
|
+
# Process payment logic
|
46
123
|
end
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
### Available Task Settings
|
128
|
+
|
129
|
+
| Setting | Type | Description |
|
130
|
+
|-----------------|-----------------------|-------------|
|
131
|
+
| `task_halt` | String, Array<String> | Result statuses that cause `call!` to raise faults |
|
132
|
+
| `workflow_halt` | String, Array<String> | Result statuses that halt workflow execution |
|
133
|
+
| `tags` | Array<String> | Tags automatically appended to logs |
|
134
|
+
| `logger` | Logger | Custom logger instance |
|
135
|
+
| `log_level` | Symbol | Log level (`:debug`, `:info`, `:warn`, `:error`, `:fatal`) |
|
136
|
+
| `log_formatter` | LogFormatter | Custom log formatter |
|
47
137
|
|
138
|
+
### Workflow Configuration
|
139
|
+
|
140
|
+
Configure halt behavior for workflows:
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
class OrderProcessingWorkflow < CMDx::Workflow
|
144
|
+
# Strict workflow - halt on any failure
|
145
|
+
task_settings!(workflow_halt: ["failed", "skipped"])
|
146
|
+
|
147
|
+
process ValidateOrderTask
|
148
|
+
process ChargePaymentTask
|
149
|
+
process FulfillOrderTask
|
48
150
|
end
|
49
151
|
```
|
50
152
|
|
51
|
-
|
52
|
-
|
153
|
+
## Configuration Management
|
154
|
+
|
155
|
+
### Accessing Configuration
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
# Global configuration
|
159
|
+
CMDx.configuration.logger #=> <Logger instance>
|
160
|
+
CMDx.configuration.task_halt #=> "failed"
|
161
|
+
CMDx.configuration.middlewares #=> <MiddlewareRegistry instance>
|
162
|
+
CMDx.configuration.callbacks #=> <CallbackRegistry instance>
|
163
|
+
|
164
|
+
# Task-specific settings
|
165
|
+
class AnalyzeDataTask < CMDx::Task
|
166
|
+
task_settings!(tags: ["analytics"])
|
167
|
+
|
168
|
+
def call
|
169
|
+
tags = task_setting(:tags) # Gets ["analytics"]
|
170
|
+
halt_statuses = task_setting(:task_halt) # Gets global default
|
171
|
+
end
|
172
|
+
end
|
173
|
+
```
|
174
|
+
|
175
|
+
### Resetting Configuration
|
176
|
+
|
177
|
+
Reset configuration to defaults (useful for testing):
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
CMDx.reset_configuration!
|
181
|
+
CMDx.configuration.task_halt #=> "failed" (default)
|
182
|
+
```
|
53
183
|
|
54
184
|
---
|
55
185
|
|
56
|
-
- **Prev:** [
|
57
|
-
- **Next:** [Basics - Setup](
|
186
|
+
- **Prev:** [Getting Started](getting_started.md)
|
187
|
+
- **Next:** [Basics - Setup](basics/setup.md)
|
data/docs/getting_started.md
CHANGED
@@ -1,47 +1,159 @@
|
|
1
|
-
# Getting
|
1
|
+
# Getting Started
|
2
2
|
|
3
|
-
|
3
|
+
CMDx is a Ruby framework for building maintainable, observable business logic through composable
|
4
|
+
command objects. Design robust workflows with automatic parameter validation, structured error
|
5
|
+
handling, comprehensive logging, and intelligent execution flow control that scales from simple
|
6
|
+
tasks to complex multi-step processes.
|
4
7
|
|
5
|
-
|
6
|
-
- Provide easy branching, nesting, and composition of complex tasks
|
7
|
-
- Supply intent, severity, and reasoning to halting execution of tasks
|
8
|
-
- Demystify root causes of complex multi-level tasks with exhaustive tracing
|
8
|
+
## Table of Contents
|
9
9
|
|
10
|
-
|
10
|
+
- [Installation](#installation)
|
11
|
+
- [Quick Setup](#quick-setup)
|
12
|
+
- [Execution](#execution)
|
13
|
+
- [Result Handling](#result-handling)
|
14
|
+
- [Exception Handling](#exception-handling)
|
15
|
+
- [Building Workflows](#building-workflows)
|
16
|
+
- [Code Generation](#code-generation)
|
17
|
+
|
18
|
+
## Installation
|
19
|
+
|
20
|
+
Add CMDx to your Gemfile:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
gem 'cmdx'
|
24
|
+
```
|
25
|
+
|
26
|
+
For Rails applications, generate the configuration:
|
27
|
+
|
28
|
+
```bash
|
29
|
+
rails generate cmdx:install
|
30
|
+
```
|
31
|
+
|
32
|
+
> [!NOTE]
|
33
|
+
> This creates `config/initializers/cmdx.rb` with default settings.
|
34
|
+
|
35
|
+
## Quick Setup
|
11
36
|
|
12
37
|
```ruby
|
13
38
|
class ProcessOrderTask < CMDx::Task
|
39
|
+
required :order_id, type: :integer
|
40
|
+
optional :send_email, type: :boolean, default: true
|
14
41
|
|
15
42
|
def call
|
16
|
-
|
17
|
-
skip!(reason: "Order is processing") if context.order.processing?
|
43
|
+
context.order = Order.find(order_id)
|
18
44
|
|
19
|
-
|
20
|
-
|
45
|
+
if context.order.canceled?
|
46
|
+
fail!(reason: "Order canceled", canceled_at: context.order.canceled_at)
|
47
|
+
elsif context.order.completed?
|
48
|
+
skip!(reason: "Already processed")
|
49
|
+
else
|
50
|
+
context.order.update!(status: 'completed', completed_at: Time.now)
|
51
|
+
EmailService.send_confirmation(context.order) if send_email
|
52
|
+
end
|
21
53
|
end
|
22
|
-
|
23
54
|
end
|
24
55
|
```
|
25
56
|
|
57
|
+
> [!TIP]
|
58
|
+
> Use **present tense verbs** for clarity: `ProcessOrderTask`, `SendEmailTask`, `ValidatePaymentTask`
|
59
|
+
|
26
60
|
## Execution
|
27
61
|
|
62
|
+
Execute tasks using class methods:
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
# Returns Result object
|
66
|
+
result = ProcessOrderTask.call(order_id: 123)
|
67
|
+
|
68
|
+
# Raises exceptions on failure/skip, else returns Result object
|
69
|
+
result = ProcessOrderTask.call!(order_id: 123, send_email: false)
|
70
|
+
```
|
71
|
+
|
72
|
+
## Result Handling
|
73
|
+
|
74
|
+
Check execution outcomes:
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
result = ProcessOrderTask.call(order_id: 123)
|
78
|
+
|
79
|
+
case result.status
|
80
|
+
when 'success'
|
81
|
+
redirect_to order_path(result.context.order), notice: "Order processed!"
|
82
|
+
when 'skipped'
|
83
|
+
redirect_to order_path(result.context.order), notice: result.metadata[:reason]
|
84
|
+
when 'failed'
|
85
|
+
redirect_to orders_path, alert: "Error: #{result.metadata[:reason]}"
|
86
|
+
end
|
87
|
+
|
88
|
+
# Access execution metadata
|
89
|
+
puts "Runtime: #{result.runtime}s, Task ID: #{result.id}"
|
90
|
+
```
|
91
|
+
|
92
|
+
## Exception Handling
|
93
|
+
|
94
|
+
Use `call!` for exception-based control flow:
|
95
|
+
|
28
96
|
```ruby
|
29
|
-
|
97
|
+
begin
|
98
|
+
result = ProcessOrderTask.call!(order_id: 123)
|
99
|
+
redirect_to order_path(result.context.order), notice: "Success!"
|
100
|
+
rescue CMDx::Skipped => e
|
101
|
+
redirect_to orders_path, notice: e.result.metadata[:reason]
|
102
|
+
rescue CMDx::Failed => e
|
103
|
+
redirect_to order_path(123), alert: e.result.metadata[:reason]
|
104
|
+
end
|
30
105
|
```
|
31
106
|
|
32
|
-
##
|
107
|
+
## Building Workflows
|
108
|
+
|
109
|
+
Combine tasks using workflows:
|
33
110
|
|
34
111
|
```ruby
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
112
|
+
class OrderProcessingWorkflow < CMDx::Workflow
|
113
|
+
required :order_id, type: :integer
|
114
|
+
|
115
|
+
before_execution :log_workflow_start
|
116
|
+
on_failed :notify_support
|
117
|
+
|
118
|
+
process ValidateOrderTask
|
119
|
+
process ChargePaymentTask
|
120
|
+
process UpdateInventoryTask
|
121
|
+
process SendConfirmationTask, if: proc { context.payment_successful? }
|
122
|
+
|
123
|
+
# NO call method
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def log_workflow_start
|
128
|
+
Rails.logger.info "Starting order workflow for order #{order_id}"
|
129
|
+
end
|
130
|
+
|
131
|
+
def notify_support
|
132
|
+
SupportNotifier.alert("Order workflow failed", context: context.to_h)
|
133
|
+
end
|
41
134
|
end
|
135
|
+
|
136
|
+
result = ProcessOrderWorkflow.call(order_id: 123)
|
42
137
|
```
|
43
138
|
|
139
|
+
## Code Generation
|
140
|
+
|
141
|
+
Generate tasks and workflows with proper structure:
|
142
|
+
|
143
|
+
```bash
|
144
|
+
# Generate individual task
|
145
|
+
rails generate cmdx:task ProcessOrder
|
146
|
+
# Creates: app/cmds/process_order_task.rb
|
147
|
+
|
148
|
+
# Generate task workflow
|
149
|
+
rails generate cmdx:workflow OrderDeliveryWorkflow
|
150
|
+
# Creates: app/cmds/order_delivery_workflow.rb
|
151
|
+
```
|
152
|
+
|
153
|
+
> [!NOTE]
|
154
|
+
> Generators automatically handle naming conventions and inherit from `ApplicationTask`/`ApplicationWorkflow` when available.
|
155
|
+
|
44
156
|
---
|
45
157
|
|
46
|
-
- **Prev:** [
|
47
|
-
- **Next:** [Configuration](
|
158
|
+
- **Prev:** [Tips and Tricks](tips_and_tricks.md)
|
159
|
+
- **Next:** [Configuration](configuration.md)
|
@@ -1,29 +1,207 @@
|
|
1
1
|
# Interruptions - Exceptions
|
2
2
|
|
3
|
-
|
3
|
+
CMDx provides robust exception handling that differs between the `call` and `call!`
|
4
|
+
methods. Understanding how unhandled exceptions are processed is crucial for
|
5
|
+
building reliable task execution flows and implementing proper error handling strategies.
|
4
6
|
|
5
|
-
|
6
|
-
|
7
|
+
## Table of Contents
|
8
|
+
|
9
|
+
- [Exception Handling Behavior](#exception-handling-behavior)
|
10
|
+
- [Bang Call (`call!`)](#bang-call-call)
|
11
|
+
- [Exception Classification](#exception-classification)
|
12
|
+
|
13
|
+
## Exception Handling Behavior
|
14
|
+
|
15
|
+
### Non-bang Call (`call`)
|
16
|
+
|
17
|
+
The `call` method captures **all** unhandled exceptions and converts them to
|
18
|
+
failed results, ensuring that no exceptions escape the task execution boundary.
|
19
|
+
This provides consistent, predictable behavior for result processing.
|
7
20
|
|
8
21
|
```ruby
|
9
|
-
|
22
|
+
class ProcessUserOrderTask < CMDx::Task
|
23
|
+
|
24
|
+
def call
|
25
|
+
# This will raise a NoMethodError
|
26
|
+
undefined_method_call
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
result = ProcessUserOrderTask.call
|
10
32
|
result.state #=> "interrupted"
|
11
33
|
result.status #=> "failed"
|
34
|
+
result.failed? #=> true
|
12
35
|
result.metadata #=> {
|
13
|
-
#=> reason: "[
|
14
|
-
#=> original_exception: <
|
36
|
+
#=> reason: "[NoMethodError] undefined method `undefined_method_call`",
|
37
|
+
#=> original_exception: <NoMethodError>
|
15
38
|
#=> }
|
16
39
|
```
|
17
40
|
|
18
|
-
|
41
|
+
> [!NOTE]
|
42
|
+
> The `call` method ensures no exceptions escape task execution, making it ideal
|
43
|
+
> for workflow processing and scenarios where you need guaranteed result objects.
|
44
|
+
|
45
|
+
### Exception Metadata Structure
|
46
|
+
|
47
|
+
Captured exceptions populate result metadata with structured information:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
class ConnectDatabaseTask < CMDx::Task
|
51
|
+
|
52
|
+
def call
|
53
|
+
# Simulate a database connection error
|
54
|
+
raise ActiveRecord::ConnectionNotEstablished, "Database unavailable"
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
result = ConnectDatabaseTask.call
|
60
|
+
|
61
|
+
# Exception information in metadata
|
62
|
+
result.metadata[:reason] #=> "[ActiveRecord::ConnectionNotEstablished] Database unavailable"
|
63
|
+
result.metadata[:original_exception] #=> <ActiveRecord::ConnectionNotEstablished>
|
64
|
+
result.metadata[:original_exception].class #=> ActiveRecord::ConnectionNotEstablished
|
65
|
+
result.metadata[:original_exception].message #=> "Database unavailable"
|
66
|
+
result.metadata[:original_exception].backtrace #=> ["..."]
|
67
|
+
```
|
68
|
+
|
69
|
+
### Accessing Original Exception Details
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
result = ProcessUserOrderTask.call
|
73
|
+
|
74
|
+
if result.failed? && result.metadata[:original_exception]
|
75
|
+
original = result.metadata[:original_exception]
|
76
|
+
|
77
|
+
puts "Exception type: #{original.class}"
|
78
|
+
puts "Exception message: #{original.message}"
|
79
|
+
puts "Exception backtrace:"
|
80
|
+
puts original.backtrace.first(5).join("\n")
|
81
|
+
|
82
|
+
# Check exception type for specific handling
|
83
|
+
case original
|
84
|
+
when ActiveRecord::RecordNotFound
|
85
|
+
handle_missing_record(original)
|
86
|
+
when Net::TimeoutError
|
87
|
+
handle_timeout_error(original)
|
88
|
+
when StandardError
|
89
|
+
handle_generic_error(original)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
```
|
93
|
+
|
94
|
+
## Bang Call (`call!`)
|
95
|
+
|
96
|
+
The `call!` method allows unhandled exceptions to propagate **unless** they are
|
97
|
+
CMDx faults that match the `task_halt` configuration. This enables exception-based
|
98
|
+
control flow while still providing structured fault handling.
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
class ProcessUserOrderTask < CMDx::Task
|
102
|
+
|
103
|
+
def call
|
104
|
+
# This will raise a NoMethodError directly
|
105
|
+
undefined_method_call
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
begin
|
111
|
+
ProcessUserOrderTask.call!
|
112
|
+
rescue NoMethodError => e
|
113
|
+
puts "Caught original exception: #{e.message}"
|
114
|
+
# Handle the original exception directly
|
115
|
+
end
|
116
|
+
```
|
117
|
+
|
118
|
+
### Fault vs Exception Behavior
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
class ProcessOrderPaymentTask < CMDx::Task
|
122
|
+
|
123
|
+
def call
|
124
|
+
if context.simulate_fault
|
125
|
+
fail!(reason: "Controlled failure") # Becomes CMDx::Failed
|
126
|
+
else
|
127
|
+
raise StandardError, "Uncontrolled error" # Remains StandardError
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
# Fault behavior (controlled)
|
134
|
+
begin
|
135
|
+
ProcessOrderPaymentTask.call!(simulate_fault: true)
|
136
|
+
rescue CMDx::Failed => e
|
137
|
+
puts "Caught CMDx fault: #{e.message}"
|
138
|
+
end
|
139
|
+
|
140
|
+
# Exception behavior (uncontrolled)
|
141
|
+
begin
|
142
|
+
ProcessOrderPaymentTask.call!(simulate_fault: false)
|
143
|
+
rescue StandardError => e
|
144
|
+
puts "Caught standard exception: #{e.message}"
|
145
|
+
end
|
146
|
+
```
|
147
|
+
|
148
|
+
## Exception Classification
|
149
|
+
|
150
|
+
### Protected Exceptions
|
19
151
|
|
20
|
-
|
152
|
+
Certain CMDx-specific exceptions are always allowed to propagate and are never
|
153
|
+
converted to failed results:
|
21
154
|
|
22
155
|
```ruby
|
23
|
-
|
156
|
+
class ProcessUndefinedOrderTask < CMDx::Task
|
157
|
+
# Intentionally not implementing call method
|
158
|
+
end
|
159
|
+
|
160
|
+
# These exceptions always propagate regardless of call method
|
161
|
+
begin
|
162
|
+
ProcessUndefinedOrderTask.call
|
163
|
+
rescue CMDx::UndefinedCallError => e
|
164
|
+
puts "This exception is never converted to a failed result"
|
165
|
+
end
|
166
|
+
|
167
|
+
begin
|
168
|
+
ProcessUndefinedOrderTask.call!
|
169
|
+
rescue CMDx::UndefinedCallError => e
|
170
|
+
puts "This exception propagates normally in call! too"
|
171
|
+
end
|
24
172
|
```
|
25
173
|
|
174
|
+
### CMDx Fault Handling
|
175
|
+
|
176
|
+
CMDx faults have special handling in both call methods:
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
class ProcessOrderWithHaltTask < CMDx::Task
|
180
|
+
# Configure to halt on failures
|
181
|
+
task_settings!(task_halt: [CMDx::Result::FAILED])
|
182
|
+
|
183
|
+
def call
|
184
|
+
fail!(reason: "This is a controlled failure")
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# With call - fault becomes failed result
|
189
|
+
result = ProcessOrderWithHaltTask.call
|
190
|
+
result.failed? #=> true
|
191
|
+
|
192
|
+
# With call! - fault becomes exception (due to task_halt configuration)
|
193
|
+
begin
|
194
|
+
ProcessOrderWithHaltTask.call!
|
195
|
+
rescue CMDx::Failed => e
|
196
|
+
puts "Fault converted to exception: #{e.message}"
|
197
|
+
end
|
198
|
+
```
|
199
|
+
|
200
|
+
> [!IMPORTANT]
|
201
|
+
> Always preserve original exception information in metadata when handling
|
202
|
+
> exceptions manually. This maintains debugging capabilities and error traceability.
|
203
|
+
|
26
204
|
---
|
27
205
|
|
28
|
-
- **Prev:** [Interruptions - Faults](
|
29
|
-
- **Next:** [Outcomes - Result](
|
206
|
+
- **Prev:** [Interruptions - Faults](faults.md)
|
207
|
+
- **Next:** [Outcomes - Result](../outcomes/result.md)
|