cmdx 1.13.0 → 1.14.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +84 -76
  3. data/LICENSE.txt +3 -20
  4. data/README.md +8 -7
  5. data/lib/cmdx/attribute.rb +21 -5
  6. data/lib/cmdx/context.rb +16 -0
  7. data/lib/cmdx/executor.rb +9 -9
  8. data/lib/cmdx/result.rb +27 -7
  9. data/lib/cmdx/task.rb +19 -0
  10. data/lib/cmdx/version.rb +1 -1
  11. data/mkdocs.yml +62 -36
  12. metadata +3 -57
  13. data/.cursor/prompts/docs.md +0 -12
  14. data/.cursor/prompts/llms.md +0 -8
  15. data/.cursor/prompts/rspec.md +0 -24
  16. data/.cursor/prompts/yardoc.md +0 -15
  17. data/.cursor/rules/cursor-instructions.mdc +0 -68
  18. data/.irbrc +0 -18
  19. data/.rspec +0 -4
  20. data/.rubocop.yml +0 -95
  21. data/.ruby-version +0 -1
  22. data/.yard-lint.yml +0 -174
  23. data/.yardopts +0 -7
  24. data/docs/.DS_Store +0 -0
  25. data/docs/assets/favicon.ico +0 -0
  26. data/docs/assets/favicon.svg +0 -1
  27. data/docs/attributes/coercions.md +0 -155
  28. data/docs/attributes/defaults.md +0 -77
  29. data/docs/attributes/definitions.md +0 -283
  30. data/docs/attributes/naming.md +0 -68
  31. data/docs/attributes/transformations.md +0 -63
  32. data/docs/attributes/validations.md +0 -336
  33. data/docs/basics/chain.md +0 -108
  34. data/docs/basics/context.md +0 -121
  35. data/docs/basics/execution.md +0 -152
  36. data/docs/basics/setup.md +0 -107
  37. data/docs/callbacks.md +0 -157
  38. data/docs/configuration.md +0 -314
  39. data/docs/deprecation.md +0 -143
  40. data/docs/getting_started.md +0 -137
  41. data/docs/index.md +0 -134
  42. data/docs/internationalization.md +0 -126
  43. data/docs/interruptions/exceptions.md +0 -52
  44. data/docs/interruptions/faults.md +0 -169
  45. data/docs/interruptions/halt.md +0 -216
  46. data/docs/logging.md +0 -90
  47. data/docs/middlewares.md +0 -191
  48. data/docs/outcomes/result.md +0 -197
  49. data/docs/outcomes/states.md +0 -66
  50. data/docs/outcomes/statuses.md +0 -65
  51. data/docs/retries.md +0 -121
  52. data/docs/stylesheets/extra.css +0 -42
  53. data/docs/tips_and_tricks.md +0 -157
  54. data/docs/workflows.md +0 -226
  55. data/examples/active_record_database_transaction.md +0 -27
  56. data/examples/active_record_query_tagging.md +0 -46
  57. data/examples/flipper_feature_flags.md +0 -50
  58. data/examples/paper_trail_whatdunnit.md +0 -39
  59. data/examples/redis_idempotency.md +0 -71
  60. data/examples/sentry_error_tracking.md +0 -46
  61. data/examples/sidekiq_async_execution.md +0 -29
  62. data/examples/stoplight_circuit_breaker.md +0 -36
  63. data/src/cmdx-dark-logo.png +0 -0
  64. data/src/cmdx-favicon.svg +0 -1
  65. data/src/cmdx-light-logo.png +0 -0
  66. data/src/cmdx-logo.svg +0 -1
@@ -1,216 +0,0 @@
1
- # Interruptions - Halt
2
-
3
- Stop task execution intentionally using `skip!` or `fail!`. Both methods signal clear intent about why execution stopped.
4
-
5
- ## Skipping
6
-
7
- Use `skip!` when the task doesn't need to run. It's a no-op, not an error.
8
-
9
- !!! warning "Important"
10
-
11
- Skipped tasks are considered "good" outcomes—they succeeded by doing nothing.
12
-
13
- ```ruby
14
- class ProcessInventory < CMDx::Task
15
- def work
16
- # Without a reason
17
- skip! if Array(ENV["DISABLED_TASKS"]).include?(self.class.name)
18
-
19
- # With a reason
20
- skip!("Warehouse closed") unless Time.now.hour.between?(8, 18)
21
-
22
- inventory = Inventory.find(context.inventory_id)
23
-
24
- if inventory.already_counted?
25
- skip!("Inventory already counted today")
26
- else
27
- inventory.count!
28
- end
29
- end
30
- end
31
-
32
- result = ProcessInventory.execute(inventory_id: 456)
33
-
34
- # Executed
35
- result.status #=> "skipped"
36
-
37
- # Without a reason
38
- result.reason #=> "Unspecified"
39
-
40
- # With a reason
41
- result.reason #=> "Warehouse closed"
42
- ```
43
-
44
- ## Failing
45
-
46
- Use `fail!` when the task can't complete successfully. It signals controlled, intentional failure:
47
-
48
- ```ruby
49
- class ProcessRefund < CMDx::Task
50
- def work
51
- # Without a reason
52
- fail! if Array(ENV["DISABLED_TASKS"]).include?(self.class.name)
53
-
54
- refund = Refund.find(context.refund_id)
55
-
56
- # With a reason
57
- if refund.expired?
58
- fail!("Refund period has expired")
59
- elsif !refund.amount.positive?
60
- fail!("Refund amount must be positive")
61
- else
62
- refund.process!
63
- end
64
- end
65
- end
66
-
67
- result = ProcessRefund.execute(refund_id: 789)
68
-
69
- # Executed
70
- result.status #=> "failed"
71
-
72
- # Without a reason
73
- result.reason #=> "Unspecified"
74
-
75
- # With a reason
76
- result.reason #=> "Refund period has expired"
77
- ```
78
-
79
- ## Metadata Enrichment
80
-
81
- Enrich halt calls with metadata for better debugging and error handling:
82
-
83
- ```ruby
84
- class ProcessRenewal < CMDx::Task
85
- def work
86
- license = License.find(context.license_id)
87
-
88
- if license.already_renewed?
89
- # Without metadata
90
- skip!("License already renewed")
91
- end
92
-
93
- unless license.renewal_eligible?
94
- # With metadata
95
- fail!(
96
- "License not eligible for renewal",
97
- error_code: "LICENSE.NOT_ELIGIBLE",
98
- retry_after: Time.current + 30.days
99
- )
100
- end
101
-
102
- process_renewal
103
- end
104
- end
105
-
106
- result = ProcessRenewal.execute(license_id: 567)
107
-
108
- # Without metadata
109
- result.metadata #=> {}
110
-
111
- # With metadata
112
- result.metadata #=> {
113
- # error_code: "LICENSE.NOT_ELIGIBLE",
114
- # retry_after: <Time 30 days from now>
115
- # }
116
- ```
117
-
118
- ## State Transitions
119
-
120
- Halt methods trigger specific state and status transitions:
121
-
122
- | Method | State | Status | Outcome |
123
- |--------|-------|--------|---------|
124
- | `skip!` | `interrupted` | `skipped` | `good? = true`, `bad? = true` |
125
- | `fail!` | `interrupted` | `failed` | `good? = false`, `bad? = true` |
126
-
127
- ```ruby
128
- result = ProcessRenewal.execute(license_id: 567)
129
-
130
- # State information
131
- result.state #=> "interrupted"
132
- result.status #=> "skipped" or "failed"
133
- result.interrupted? #=> true
134
- result.complete? #=> false
135
-
136
- # Outcome categorization
137
- result.good? #=> true for skipped, false for failed
138
- result.bad? #=> true for both skipped and failed
139
- ```
140
-
141
- ## Execution Behavior
142
-
143
- Halt methods behave differently depending on the call method used:
144
-
145
- ### Non-bang execution
146
-
147
- Returns result object without raising exceptions:
148
-
149
- ```ruby
150
- result = ProcessRefund.execute(refund_id: 789)
151
-
152
- case result.status
153
- when "success"
154
- puts "Refund processed: $#{result.context.refund.amount}"
155
- when "skipped"
156
- puts "Refund skipped: #{result.reason}"
157
- when "failed"
158
- puts "Refund failed: #{result.reason}"
159
- handle_refund_error(result.metadata[:error_code])
160
- end
161
- ```
162
-
163
- ### Bang execution
164
-
165
- Raises exceptions for halt conditions based on `task_breakpoints` configuration:
166
-
167
- ```ruby
168
- begin
169
- result = ProcessRefund.execute!(refund_id: 789)
170
- puts "Success: Refund processed"
171
- rescue CMDx::SkipFault => e
172
- puts "Skipped: #{e.message}"
173
- rescue CMDx::FailFault => e
174
- puts "Failed: #{e.message}"
175
- handle_refund_failure(e.result.metadata[:error_code])
176
- end
177
- ```
178
-
179
- ## Best Practices
180
-
181
- Always provide a reason for better debugging and clearer exception messages:
182
-
183
- ```ruby
184
- # Good: Clear, specific reason
185
- skip!("Document processing paused for compliance review")
186
- fail!("File format not supported by processor", code: "FORMAT_UNSUPPORTED")
187
-
188
- # Acceptable: Generic, non-specific reason
189
- skip!("Paused")
190
- fail!("Unsupported")
191
-
192
- # Bad: Default, cannot determine reason
193
- skip! #=> "Unspecified"
194
- fail! #=> "Unspecified"
195
- ```
196
-
197
- ## Manual Errors
198
-
199
- For rare cases, manually add errors before halting:
200
-
201
- !!! warning "Important"
202
-
203
- Manual errors don't stop execution—you still need to call `fail!` or `skip!`.
204
-
205
- ```ruby
206
- class ProcessRenewal < CMDx::Task
207
- def work
208
- if document.nonrenewable?
209
- errors.add(:document, "not renewable")
210
- fail!("document could not be renewed")
211
- else
212
- document.renew!
213
- end
214
- end
215
- end
216
- ```
data/docs/logging.md DELETED
@@ -1,90 +0,0 @@
1
- # Logging
2
-
3
- CMDx automatically logs every task execution with structured data, making debugging and monitoring effortless. Choose from multiple formatters to match your logging infrastructure.
4
-
5
- ## Formatters
6
-
7
- Choose the format that works best for your logging system:
8
-
9
- | Formatter | Use Case | Output Style |
10
- |-----------|----------|--------------|
11
- | `Line` | Traditional logging | Single-line format |
12
- | `Json` | Structured systems | Compact JSON |
13
- | `KeyValue` | Log parsing | `key=value` pairs |
14
- | `Logstash` | ELK stack | JSON with @version/@timestamp |
15
- | `Raw` | Minimal output | Message content only |
16
-
17
- Sample output:
18
-
19
- ```log
20
- <!-- Success (INFO level) -->
21
- I, [2025-12-23T17:04:07.292614Z #20108] INFO -- cmdx: {index: 1, chain_id: "019b4c2b-087b-79be-8ef2-96c11b659df5", type: "Task", tags: [], class: "GenerateInvoice", dry_run: false, id: "019b4c2b-0878-704d-ba0b-daa5410123ec", state: "complete", status: "success", outcome: "success", metadata: {runtime: 187}}
22
-
23
- <!-- Skipped (INFO level) -->
24
- I, [2025-12-23T17:04:11.496881Z #20139] INFO -- cmdx: {index: 2, chain_id: "019b4c2b-18e8-7af6-a38b-63b042c4fbed", type: "Task", tags: [], class: "ValidateCustomer", dry_run: false, id: "019b4c2b-18e5-7230-af7e-5b4a4bd7cda2", state: "interrupted", status: "skipped", outcome: "skipped", metadata: {}, reason: "Customer already validated", cause: #<CMDx::SkipFault: Customer already validated>, rolled_back: false}
25
-
26
- <!-- Failed (INFO level) -->
27
- I, [2025-12-23T17:04:15.875306Z #20173] INFO -- cmdx: {index: 3, chain_id: "019b4c2b-2a02-7dbc-b713-b20a7379704f", type: "Task", tags: [], class: "CalculateTax", dry_run: false, id: "019b4c2b-2a00-70b7-9fab-2f14db9139ef", state: "interrupted", status: "failed", outcome: "failed", metadata: {error_code: "TAX_SERVICE_UNAVAILABLE"}, reason: "Validation failed", cause: #<CMDx::FailFault: Validation failed>, rolled_back: false}
28
-
29
- <!-- Failed Chain -->
30
- I, [2025-12-23T17:04:20.972539Z #20209] INFO -- cmdx: {index: 0, chain_id: "019b4c2b-3de9-71f7-bcc3-2a98836bcfd7", type: "Workflow", tags: [], class: "BillingWorkflow", dry_run: false, id: "019b4c2b-3de6-70b9-9c16-5be13b1a463c", state: "interrupted", status: "failed", outcome: "interrupted", metadata: {}, reason: "Validation failed", cause: #<CMDx::FailFault: Validation failed>, rolled_back: false, threw_failure: {index: 3, chain_id: "019b4c2b-3de9-71f7-bcc3-2a98836bcfd7", type: "Task", tags: [], class: "CalculateTax", id: "019b4c2b-3dec-70b3-969b-c5b7896e3b27", state: "interrupted", status: "failed", outcome: "failed", metadata: {error_code: "TAX_SERVICE_UNAVAILABLE"}, reason: "Validation failed", cause: #<CMDx::FailFault: Validation failed>, rolled_back: false}, caused_failure: {index: 3, chain_id: "019b4c2b-3de9-71f7-bcc3-2a98836bcfd7", type: "Task", tags: [], class: "CalculateTax", id: "019b4c2b-3dec-70b3-969b-c5b7896e3b27", state: "interrupted", status: "failed", outcome: "failed", metadata: {error_code: "TAX_SERVICE_UNAVAILABLE"}, reason: "Validation failed", cause: #<CMDx::FailFault: Validation failed>, rolled_back: false}}
31
- ```
32
-
33
- !!! tip
34
-
35
- Use logging as a low-level event stream to track all tasks in a request. Combine with correlation for powerful distributed tracing.
36
-
37
- ## Structure
38
-
39
- Every log entry includes rich metadata. Available fields depend on execution context and outcome.
40
-
41
- ### Core Fields
42
-
43
- | Field | Description | Example |
44
- |-------|-------------|---------|
45
- | `severity` | Log level | `INFO`, `WARN`, `ERROR` |
46
- | `timestamp` | ISO 8601 execution time | `2022-07-17T18:43:15.000000` |
47
- | `pid` | Process ID | `3784` |
48
-
49
- ### Task Information
50
-
51
- | Field | Description | Example |
52
- |-------|-------------|---------|
53
- | `index` | Execution sequence position | `0`, `1`, `2` |
54
- | `chain_id` | Unique execution chain ID | `018c2b95-b764-7615...` |
55
- | `type` | Execution unit type | `Task`, `Workflow` |
56
- | `class` | Task class name | `GenerateInvoiceTask` |
57
- | `id` | Unique task instance ID | `018c2b95-b764-7615...` |
58
- | `tags` | Custom categorization | `["billing", "financial"]` |
59
-
60
- ### Execution Data
61
-
62
- | Field | Description | Example |
63
- |-------|-------------|---------|
64
- | `state` | Lifecycle state | `complete`, `interrupted` |
65
- | `status` | Business outcome | `success`, `skipped`, `failed` |
66
- | `outcome` | Final classification | `success`, `interrupted` |
67
- | `metadata` | Custom task data | `{order_id: 123, amount: 99.99}` |
68
-
69
- ### Failure Chain
70
-
71
- | Field | Description |
72
- |-------|-------------|
73
- | `reason` | Reason given for the stoppage |
74
- | `caused` | Cause exception details |
75
- | `caused_failure` | Original failing task details |
76
- | `threw_failure` | Task that propagated the failure |
77
-
78
- ## Usage
79
-
80
- Access the framework logger directly within tasks:
81
-
82
- ```ruby
83
- class ProcessSubscription < CMDx::Task
84
- def work
85
- logger.debug { "Activated feature flags: #{Features.active_flags}" }
86
- # Your logic here...
87
- logger.info("Subscription processed")
88
- end
89
- end
90
- ```
data/docs/middlewares.md DELETED
@@ -1,191 +0,0 @@
1
- # Middlewares
2
-
3
- Wrap task execution with middleware for cross-cutting concerns like authentication, caching, timeouts, and monitoring. Think Rack middleware, but for your business logic.
4
-
5
- See [Global Configuration](getting_started.md#middlewares) for framework-wide setup.
6
-
7
- ## Execution Order
8
-
9
- Middleware wraps task execution in layers, like an onion:
10
-
11
- !!! note
12
-
13
- First registered = outermost wrapper. They execute in registration order.
14
-
15
- ```ruby
16
- class ProcessCampaign < CMDx::Task
17
- register :middleware, AuditMiddleware # 1st: outermost wrapper
18
- register :middleware, AuthorizationMiddleware # 2nd: middle wrapper
19
- register :middleware, CacheMiddleware # 3rd: innermost wrapper
20
-
21
- def work
22
- # Your logic here...
23
- end
24
- end
25
-
26
- # Execution flow:
27
- # 1. AuditMiddleware (before)
28
- # 2. AuthorizationMiddleware (before)
29
- # 3. CacheMiddleware (before)
30
- # 4. [task execution]
31
- # 5. CacheMiddleware (after)
32
- # 6. AuthorizationMiddleware (after)
33
- # 7. AuditMiddleware (after)
34
- ```
35
-
36
- ## Declarations
37
-
38
- ### Proc or Lambda
39
-
40
- Use anonymous functions for simple middleware logic:
41
-
42
- ```ruby
43
- class ProcessCampaign < CMDx::Task
44
- # Proc
45
- register :middleware, proc do |task, options, &block|
46
- result = block.call
47
- Analytics.track(result.status)
48
- result
49
- end
50
-
51
- # Lambda
52
- register :middleware, ->(task, options, &block) {
53
- result = block.call
54
- Analytics.track(result.status)
55
- result
56
- }
57
- end
58
- ```
59
-
60
- ### Class or Module
61
-
62
- For complex middleware logic, use classes or modules:
63
-
64
- ```ruby
65
- class TelemetryMiddleware
66
- def call(task, options)
67
- result = yield
68
- Telemetry.record(result.status)
69
- ensure
70
- result # Always return result
71
- end
72
- end
73
-
74
- class ProcessCampaign < CMDx::Task
75
- # Class or Module
76
- register :middleware, TelemetryMiddleware
77
-
78
- # Instance
79
- register :middleware, TelemetryMiddleware.new
80
-
81
- # With options
82
- register :middleware, MonitoringMiddleware, service_key: ENV["MONITORING_KEY"]
83
- register :middleware, MonitoringMiddleware.new(ENV["MONITORING_KEY"])
84
- end
85
- ```
86
-
87
- ## Removals
88
-
89
- Remove class or module-based middleware globally or per-task:
90
-
91
- !!! warning
92
-
93
- Each `deregister` call removes one middleware. Use multiple calls for batch removals.
94
-
95
- ```ruby
96
- class ProcessCampaign < CMDx::Task
97
- # Class or Module (no instances)
98
- deregister :middleware, TelemetryMiddleware
99
- end
100
- ```
101
-
102
- ## Built-in
103
-
104
- ### Timeout
105
-
106
- Prevent tasks from running too long:
107
-
108
- ```ruby
109
- class ProcessReport < CMDx::Task
110
- # Default timeout: 3 seconds
111
- register :middleware, CMDx::Middlewares::Timeout
112
-
113
- # Seconds (takes Numeric, Symbol, Proc, Lambda, Class, Module)
114
- register :middleware, CMDx::Middlewares::Timeout, seconds: :max_processing_time
115
-
116
- # If or Unless (takes Symbol, Proc, Lambda, Class, Module)
117
- register :middleware, CMDx::Middlewares::Timeout, unless: -> { self.class.name.include?("Quick") }
118
-
119
- def work
120
- # Your logic here...
121
- end
122
-
123
- private
124
-
125
- def max_processing_time
126
- Rails.env.production? ? 2 : 10
127
- end
128
- end
129
-
130
- # Slow task
131
- result = ProcessReport.execute
132
-
133
- result.state #=> "interrupted"
134
- result.status #=> "failure"
135
- result.reason #=> "[CMDx::TimeoutError] execution exceeded 3 seconds"
136
- result.cause #=> <CMDx::TimeoutError>
137
- result.metadata #=> { limit: 3 }
138
- ```
139
-
140
- ### Correlate
141
-
142
- Add correlation IDs for distributed tracing and request tracking:
143
-
144
- ```ruby
145
- class ProcessExport < CMDx::Task
146
- # Default correlation ID generation
147
- register :middleware, CMDx::Middlewares::Correlate
148
-
149
- # Seconds (takes Object, Symbol, Proc, Lambda, Class, Module)
150
- register :middleware, CMDx::Middlewares::Correlate, id: proc { |task| task.context.session_id }
151
-
152
- # If or Unless (takes Symbol, Proc, Lambda, Class, Module)
153
- register :middleware, CMDx::Middlewares::Correlate, if: :correlation_enabled?
154
-
155
- def work
156
- # Your logic here...
157
- end
158
-
159
- private
160
-
161
- def correlation_enabled?
162
- ENV["CORRELATION_ENABLED"] == "true"
163
- end
164
- end
165
-
166
- result = ProcessExport.execute
167
- result.metadata #=> { correlation_id: "550e8400-e29b-41d4-a716-446655440000" }
168
- ```
169
-
170
- ### Runtime
171
-
172
- Track task execution time in milliseconds using a monotonic clock:
173
-
174
- ```ruby
175
- class PerformanceMonitoringCheck
176
- def call(task)
177
- task.context.tenant.monitoring_enabled?
178
- end
179
- end
180
-
181
- class ProcessExport < CMDx::Task
182
- # Default timeout is 3 seconds
183
- register :middleware, CMDx::Middlewares::Runtime
184
-
185
- # If or Unless (takes Symbol, Proc, Lambda, Class, Module)
186
- register :middleware, CMDx::Middlewares::Runtime, if: PerformanceMonitoringCheck
187
- end
188
-
189
- result = ProcessExport.execute
190
- result.metadata #=> { runtime: 1247 } (ms)
191
- ```
@@ -1,197 +0,0 @@
1
- # Outcomes - Result
2
-
3
- Results are your window into task execution. They expose everything: outcome, state, timing, context, and metadata.
4
-
5
- ## Result Attributes
6
-
7
- Access essential execution information:
8
-
9
- !!! warning "Important"
10
-
11
- Results are immutable after execution completes.
12
-
13
- ```ruby
14
- result = BuildApplication.execute(version: "1.2.3")
15
-
16
- # Object data
17
- result.task #=> <BuildApplication>
18
- result.context #=> <CMDx::Context>
19
- result.chain #=> <CMDx::Chain>
20
-
21
- # Execution data
22
- result.state #=> "interrupted"
23
- result.status #=> "failed"
24
-
25
- # Fault data
26
- result.reason #=> "Build tool not found"
27
- result.cause #=> <CMDx::FailFault>
28
- result.metadata #=> { error_code: "BUILD_TOOL.NOT_FOUND" }
29
- ```
30
-
31
- ## Lifecycle Information
32
-
33
- Check execution state, status, and rollback with predicate methods:
34
-
35
- ```ruby
36
- result = BuildApplication.execute(version: "1.2.3")
37
-
38
- # State predicates (execution lifecycle)
39
- result.complete? #=> true (successful completion)
40
- result.interrupted? #=> false (no interruption)
41
- result.executed? #=> true (execution finished)
42
-
43
- # Status predicates (execution outcome)
44
- result.success? #=> true (successful execution)
45
- result.failed? #=> false (no failure)
46
- result.skipped? #=> false (not skipped)
47
-
48
- # Outcome categorization
49
- result.good? #=> true (success or skipped)
50
- result.bad? #=> false (skipped or failed)
51
-
52
- # Rollback Status
53
- result.rolled_back? #=> true (execution was rolled back)
54
- ```
55
-
56
- ## Outcome Analysis
57
-
58
- Get a unified outcome string combining state and status:
59
-
60
- ```ruby
61
- result = BuildApplication.execute(version: "1.2.3")
62
-
63
- result.outcome #=> "success" (state and status)
64
- ```
65
-
66
- ## Chain Analysis
67
-
68
- Trace fault origins and propagation:
69
-
70
- ```ruby
71
- result = DeploymentWorkflow.execute(app_name: "webapp")
72
-
73
- if result.failed?
74
- # Find the original cause of failure
75
- if original_failure = result.caused_failure
76
- puts "Root cause: #{original_failure.task.class.name}"
77
- puts "Reason: #{original_failure.reason}"
78
- end
79
-
80
- # Find what threw the failure to this result
81
- if throwing_task = result.threw_failure
82
- puts "Failure source: #{throwing_task.task.class.name}"
83
- puts "Reason: #{throwing_task.reason}"
84
- end
85
-
86
- # Failure classification
87
- result.caused_failure? #=> true if this result was the original cause
88
- result.threw_failure? #=> true if this result threw a failure
89
- result.thrown_failure? #=> true if this result received a thrown failure
90
- end
91
- ```
92
-
93
- ## Index and Position
94
-
95
- Results track their position within execution chains:
96
-
97
- ```ruby
98
- result = BuildApplication.execute(version: "1.2.3")
99
-
100
- # Position in execution sequence
101
- result.index #=> 0 (first task in chain)
102
-
103
- # Access via chain
104
- result.chain.results[result.index] == result #=> true
105
- ```
106
-
107
- ## Block Yield
108
-
109
- Execute code with direct result access:
110
-
111
- ```ruby
112
- BuildApplication.execute(version: "1.2.3") do |result|
113
- if result.success?
114
- notify_deployment_ready(result)
115
- elsif result.failed?
116
- handle_build_failure(result)
117
- else
118
- log_skip_reason(result)
119
- end
120
- end
121
- ```
122
-
123
- ## Handlers
124
-
125
- Handle outcomes with functional-style methods. Handlers return the result for chaining:
126
-
127
- ```ruby
128
- result = BuildApplication.execute(version: "1.2.3")
129
-
130
- # Status-based handlers
131
- result
132
- .on(:success) { |result| notify_deployment_ready(result) }
133
- .on(:failed) { |result| handle_build_failure(result) }
134
- .on(:skipped) { |result| log_skip_reason(result) }
135
-
136
- # State-based handlers
137
- result
138
- .on(:complete) { |result| update_build_status(result) }
139
- .on(:interrupted) { |result| cleanup_partial_artifacts(result) }
140
- .on(:executed) { |result| alert_operations_team(result) } #=> .on(:complete, :interrupted)
141
-
142
- # Outcome-based handlers
143
- result
144
- .on(:good) { |result| increment_success_counter(result) } #=> .on(:success, :skipped)
145
- .on(:bad) { |result| alert_operations_team(result) } #=> .on(:failed, :skipped)
146
- ```
147
-
148
- ## Pattern Matching
149
-
150
- Use Ruby 3.0+ pattern matching for elegant outcome handling:
151
-
152
- !!! warning "Important"
153
-
154
- Pattern matching works with both array and hash deconstruction.
155
-
156
- ### Array Pattern
157
-
158
- ```ruby
159
- result = BuildApplication.execute(version: "1.2.3")
160
-
161
- case result
162
- in ["complete", "success"]
163
- redirect_to build_success_page
164
- in ["interrupted", "failed"]
165
- retry_build_with_backoff(result)
166
- in ["interrupted", "skipped"]
167
- log_skip_and_continue
168
- end
169
- ```
170
-
171
- ### Hash Pattern
172
-
173
- ```ruby
174
- result = BuildApplication.execute(version: "1.2.3")
175
-
176
- case result
177
- in { state: "complete", status: "success" }
178
- celebrate_build_success
179
- in { status: "failed", metadata: { retryable: true } }
180
- schedule_build_retry(result)
181
- in { bad: true, metadata: { reason: String => reason } }
182
- escalate_build_error("Build failed: #{reason}")
183
- end
184
- ```
185
-
186
- ### Pattern Guards
187
-
188
- ```ruby
189
- case result
190
- in { status: "failed", metadata: { attempts: n } } if n < 3
191
- retry_build_with_delay(result, n * 2)
192
- in { status: "failed", metadata: { attempts: n } } if n >= 3
193
- mark_build_permanently_failed(result)
194
- in { runtime: time } if time > performance_threshold
195
- investigate_build_performance(result)
196
- end
197
- ```