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.
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 +42 -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 +212 -19
  23. data/docs/outcomes/statuses.md +284 -18
  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 +399 -20
  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 +409 -34
  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 -59
  119. data/lib/cmdx/batch.rb +0 -43
  120. data/lib/cmdx/parameters.rb +0 -34
  121. data/lib/cmdx/run.rb +0 -38
  122. data/lib/cmdx/run_inspector.rb +0 -26
  123. data/lib/cmdx/run_serializer.rb +0 -16
  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,57 +1,187 @@
1
1
  # Configuration
2
2
 
3
- Configuration of tasks can be done at global and task levels.
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
- ## Global settings
5
+ ## Table of Contents
6
6
 
7
- Run `rails g cmdx:install` to generate a configuration file within the
8
- `config/initializers` directory. These settings will be preloaded into all tasks.
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
- # Define which statuses a bang `call!` will halt and raise a fault.
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
- # Enable task timeouts to prevent call execution beyond a defined threshold.
17
- config.task_timeout = nil
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
- # Enable batch timeouts to prevent call execution beyond a defined threshold.
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
- ## Task settings
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
- Finely tune tasks via class level settings.
67
+ Configure middlewares that automatically apply to all tasks in your application:
37
68
 
38
69
  ```ruby
39
- class ProcessOrderTask < CMDx::Task
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
- # Task level settings:
42
- task_settings!(task_timeout: 1, tags: ["v1", "DEPRECATED"])
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
- # Do work...
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
- > [!NOTE]
52
- > `tags` is a task level setting only. Tags will automatically be appended to logs.
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:** [Configuration](https://github.com/drexed/cmdx/blob/main/docs/configuration.md)
57
- - **Next:** [Basics - Setup](https://github.com/drexed/cmdx/blob/main/docs/basics/setup.md)
186
+ - **Prev:** [Getting Started](getting_started.md)
187
+ - **Next:** [Basics - Setup](basics/setup.md)
@@ -1,47 +1,159 @@
1
- # Getting Start
1
+ # Getting Started
2
2
 
3
- `CMDx` is a framework for expressive processing of business logic.
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
- Goals:
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
- ## Setup
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
- fail!(reason: "Order was canceled") if context.order.canceled?
17
- skip!(reason: "Order is processing") if context.order.processing?
43
+ context.order = Order.find(order_id)
18
44
 
19
- inform_partner_warehouses
20
- send_confirmation_email
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
- result = ProcessOrderTask.call(order: order)
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
- ## Result
107
+ ## Building Workflows
108
+
109
+ Combine tasks using workflows:
33
110
 
34
111
  ```ruby
35
- if result.failed?
36
- flash[:error] = "Failed! #{result.metadata[:reason]}"
37
- elsif result.skipped?
38
- flash[:notice] = "FYI: #{result.metadata[:reason]}"
39
- else
40
- flash[:success] = "Order successfully processed"
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:** [Example](https://github.com/drexed/cmdx/blob/main/docs/example.md)
47
- - **Next:** [Configuration](https://github.com/drexed/cmdx/blob/main/docs/configuration.md)
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
- ## Non-bang call
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
- Any unhandled exception will be caught and halted using `fail!`.
6
- The original exception will be passed as metadata of the result object.
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
- result = ProcessOrderTask.call
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: "[RuntimeError] method xyz is not defined",
14
- #=> original_exception: <RuntimeError message="method xyz is not defined">
36
+ #=> reason: "[NoMethodError] undefined method `undefined_method_call`",
37
+ #=> original_exception: <NoMethodError>
15
38
  #=> }
16
39
  ```
17
40
 
18
- ## Bang call
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
- Any unhandled exception from a `call!` method will be raised as is.
152
+ Certain CMDx-specific exceptions are always allowed to propagate and are never
153
+ converted to failed results:
21
154
 
22
155
  ```ruby
23
- ProcessOrderTask.call! #=> raises NoMethodError, "method xyz is not defined"
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](https://github.com/drexed/cmdx/blob/main/docs/interruptions/faults.md)
29
- - **Next:** [Outcomes - Result](https://github.com/drexed/cmdx/blob/main/docs/outcomes/result.md)
206
+ - **Prev:** [Interruptions - Faults](faults.md)
207
+ - **Next:** [Outcomes - Result](../outcomes/result.md)