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,137 +0,0 @@
1
- # Getting Started
2
-
3
- CMDx is a Ruby framework for building maintainable, observable business logic through composable command objects. It brings structure, consistency, and powerful developer tools to your business processes.
4
-
5
- **Common challenges:**
6
-
7
- - Inconsistent service object patterns across your codebase
8
- - Black boxes make debugging a nightmare
9
- - Fragile error handling erodes confidence
10
-
11
- **What you get:**
12
-
13
- - Consistent, standardized architecture
14
- - Built-in flow control and error handling
15
- - Composable, reusable workflows
16
- - Comprehensive logging for observability
17
- - Attribute validation with type coercions
18
- - Sensible defaults and developer-friendly APIs
19
-
20
- ## Installation
21
-
22
- Add CMDx to your Gemfile:
23
-
24
- ```sh
25
- gem install cmdx
26
-
27
- # - or -
28
-
29
- bundle add cmdx
30
- ```
31
-
32
- ## Configuration
33
-
34
- For Rails applications, run the following command to generate a global configuration file in `config/initializers/cmdx.rb`.
35
-
36
- ```bash
37
- rails generate cmdx:install
38
- ```
39
-
40
- If not using Rails, manually copy the [configuration file](https://github.com/drexed/cmdx/blob/main/lib/generators/cmdx/templates/install.rb).
41
-
42
- ## The CERO Pattern
43
-
44
- CMDx embraces the Compose, Execute, React, Observe (CERO, pronounced "zero") pattern—a simple yet powerful approach to building reliable business logic.
45
-
46
- ```mermaid
47
- flowchart LR
48
- Compose --> Execute
49
- Execute --> React
50
- Execute -.-> Observe
51
- ```
52
-
53
- ### Compose
54
-
55
- Build reusable, single-responsibility tasks with typed attributes, validation, and callbacks. Tasks can be chained together in workflows to create complex business processes from simple building blocks.
56
-
57
- ```ruby
58
- class AnalyzeMetrics < CMDx::Task
59
- def work
60
- # Your logic here...
61
- end
62
- end
63
- ```
64
-
65
- ### Execute
66
-
67
- Invoke tasks with a consistent API that always returns a result object. Execution automatically handles validation, type coercion, error handling, and logging. Arguments are validated and coerced before your task logic runs.
68
-
69
- ```ruby
70
- # Without args
71
- result = AnalyzeMetrics.execute
72
-
73
- # With args
74
- result = AnalyzeMetrics.execute(model: "blackbox", "sensitivity" => 3)
75
- ```
76
-
77
- ### React
78
-
79
- Every execution returns a result object with a clear outcome. Check the result's state (`success?`, `failed?`, `skipped?`) and access returned values, error messages, and metadata to make informed decisions.
80
-
81
- ```ruby
82
- if result.success?
83
- # Handle success
84
- elsif result.skipped?
85
- # Handle skipped
86
- elsif result.failed?
87
- # Handle failed
88
- end
89
- ```
90
-
91
- ### Observe
92
-
93
- Every task execution generates structured logs with execution chains, runtime metrics, and contextual metadata. Logs can be automatically correlated using chain IDs, making it easy to trace complex workflows and debug issues.
94
-
95
- ```log
96
- I, [2022-07-17T18:42:37.000000 #3784] INFO -- CMDx:
97
- index=1 chain_id="018c2b95-23j4-2kj3-32kj-3n4jk3n4jknf" type="Task" class="SendAnalyzedEmail" state="complete" status="success" metadata={runtime: 347}
98
-
99
- I, [2022-07-17T18:43:15.000000 #3784] INFO -- CMDx:
100
- index=0 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="AnalyzeMetrics" state="complete" status="success" metadata={runtime: 187}
101
- ```
102
-
103
- !!! note
104
-
105
- This represents a log-only event-sourcing approach, enabling full traceability and a complete, time-ordered view of system behavior.
106
-
107
- ## Task Generator
108
-
109
- Generate new CMDx tasks quickly using the built-in generator:
110
-
111
- ```bash
112
- rails generate cmdx:task ModerateBlogPost
113
- ```
114
-
115
- This creates a new task file with the basic structure:
116
-
117
- ```ruby
118
- # app/tasks/moderate_blog_post.rb
119
- class ModerateBlogPost < CMDx::Task
120
- def work
121
- # Your logic here...
122
- end
123
- end
124
- ```
125
-
126
- !!! tip
127
-
128
- Use **present tense verbs + noun** for task names, eg: `ModerateBlogPost`, `ScheduleAppointment`, `ValidateDocument`
129
-
130
- ## Type safety
131
-
132
- CMDx includes built-in RBS (Ruby Type Signature) inline annotations throughout the codebase, providing type information for static analysis and editor support.
133
-
134
- - **Type checking** — Catch type errors before runtime using tools like Steep or TypeProf
135
- - **Better IDE support** — Enhanced autocomplete, navigation, and inline documentation
136
- - **Self-documenting code** — Clear method signatures and return types
137
- - **Refactoring confidence** — Type-aware refactoring reduces bugs
data/docs/index.md DELETED
@@ -1,134 +0,0 @@
1
- # CMDx
2
-
3
- Build business logic that's powerful, predictable, and maintainable.
4
-
5
- [![Version](https://img.shields.io/gem/v/cmdx)](https://rubygems.org/gems/cmdx)
6
- [![Build](https://github.com/drexed/cmdx/actions/workflows/ci.yml/badge.svg)](https://github.com/drexed/cmdx/actions/workflows/ci.yml)
7
- [![License](https://img.shields.io/github/license/drexed/cmdx)](https://github.com/drexed/cmdx/blob/main/LICENSE.txt)
8
-
9
- ---
10
-
11
- Say goodbye to messy service objects. CMDx (pronounced "Command X") helps you design business logic with clarity and consistency—build faster, debug easier, and ship with confidence.
12
-
13
- !!! note
14
-
15
- Documentation reflects the latest code on `main`. For version-specific documentation, please refer to the `docs/` directory within that version's tag.
16
-
17
- ## Requirements
18
-
19
- - Ruby: MRI 3.1+ or JRuby 9.4+.
20
-
21
- CMDx works with any Ruby framework. Rails support is built-in, but it's framework-agnostic at its core.
22
-
23
- ## Installation
24
-
25
- ```sh
26
- gem install cmdx
27
-
28
- # - or -
29
-
30
- bundle add cmdx
31
- ```
32
-
33
- ## Quick Example
34
-
35
- Build powerful business logic in four simple steps:
36
-
37
- ### 1. Compose
38
-
39
- === "Full Featured Task"
40
-
41
- ```ruby
42
- class AnalyzeMetrics < CMDx::Task
43
- register :middleware, CMDx::Middlewares::Correlate, id: -> { Current.request_id }
44
-
45
- on_success :track_analysis_completion!
46
-
47
- required :dataset_id, type: :integer, numeric: { min: 1 }
48
- optional :analysis_type, default: "standard"
49
-
50
- def work
51
- if dataset.nil?
52
- fail!("Dataset not found", code: 404)
53
- elsif dataset.unprocessed?
54
- skip!("Dataset not ready for analysis")
55
- else
56
- context.result = PValueAnalyzer.execute(dataset:, analysis_type:)
57
- context.analyzed_at = Time.now
58
-
59
- SendAnalyzedEmail.execute(user_id: Current.account.manager_id)
60
- end
61
- end
62
-
63
- private
64
-
65
- def dataset
66
- @dataset ||= Dataset.find_by(id: dataset_id)
67
- end
68
-
69
- def track_analysis_completion!
70
- dataset.update!(analysis_result_id: context.result.id)
71
- end
72
- end
73
- ```
74
-
75
- === "Minimum Viable Task"
76
-
77
- ```ruby
78
- class SendAnalyzedEmail < CMDx::Task
79
- def work
80
- user = User.find(context.user_id)
81
- MetricsMailer.analyzed(user).deliver_now
82
- end
83
- end
84
- ```
85
-
86
- ### 2. Execute
87
-
88
- ```ruby
89
- result = AnalyzeMetrics.execute(
90
- dataset_id: 123,
91
- "analysis_type" => "advanced"
92
- )
93
- ```
94
-
95
- ### 3. React
96
-
97
- ```ruby
98
- if result.success?
99
- puts "Metrics analyzed at #{result.context.analyzed_at}"
100
- elsif result.skipped?
101
- puts "Skipping analyzation due to: #{result.reason}"
102
- elsif result.failed?
103
- puts "Analyzation failed due to: #{result.reason} with code #{result.metadata[:code]}"
104
- end
105
- ```
106
-
107
- ### 4. Observe
108
-
109
- ```log
110
- I, [2022-07-17T18:42:37.000000 #3784] INFO -- CMDx:
111
- index=1 chain_id="018c2b95-23j4-2kj3-32kj-3n4jk3n4jknf" type="Task" class="SendAnalyzedEmail" state="complete" status="success" metadata={runtime: 347}
112
-
113
- I, [2022-07-17T18:43:15.000000 #3784] INFO -- CMDx:
114
- index=0 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="AnalyzeMetrics" state="complete" status="success" metadata={runtime: 187}
115
- ```
116
-
117
- Ready to dive in? Check out the [Getting Started](getting_started.md) guide to learn more.
118
-
119
- ## Ecosystem
120
-
121
- - [cmdx-rspec](https://github.com/drexed/cmdx-rspec) - RSpec test matchers
122
-
123
- For backwards compatibility of certain functionality:
124
-
125
- - [cmdx-i18n](https://github.com/drexed/cmdx-i18n) - 85+ translations, `v1.5.0` - `v1.6.2`
126
- - [cmdx-parallel](https://github.com/drexed/cmdx-parallel) - Parallel workflow tasks, `v1.6.1` - `v1.6.2`
127
-
128
- ## Contributing
129
-
130
- Bug reports and pull requests are welcome at <https://github.com/drexed/cmdx>. We're committed to fostering a welcoming, collaborative community. Please follow our [code of conduct](https://github.com/drexed/cmdx/blob/main/CODE_OF_CONDUCT.md).
131
-
132
- ## License
133
-
134
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -1,126 +0,0 @@
1
- # Internationalization (i18n)
2
-
3
- CMDx supports 90+ languages out of the box for all error messages, validations, coercions, and faults. Error messages automatically adapt to the current `I18n.locale`, making it easy to build applications for global audiences.
4
-
5
- ## Usage
6
-
7
- All error messages are automatically localized based on your current locale:
8
-
9
- ```ruby
10
- class ProcessQuote < CMDx::Task
11
- attribute :price, type: :float
12
-
13
- def work
14
- # Your logic here...
15
- end
16
- end
17
-
18
- I18n.with_locale(:fr) do
19
- result = ProcessQuote.execute(price: "invalid")
20
- result.metadata[:messages][:price] #=> ["impossible de contraindre en float"]
21
- end
22
- ```
23
-
24
- ## Configuration
25
-
26
- CMDx uses the `I18n` gem for localization. In Rails, locales load automatically.
27
-
28
- ### Copy Locale Files
29
-
30
- Copy locale files to your Rails application's `config/locales` directory:
31
-
32
- ```bash
33
- rails generate cmdx:locale [LOCALE]
34
-
35
- # Eg: generate french locale
36
- rails generate cmdx:locale fr
37
- ```
38
-
39
- ### Available Locales
40
-
41
- - af - Afrikaans
42
- - ar - Arabic
43
- - az - Azerbaijani
44
- - be - Belarusian
45
- - bg - Bulgarian
46
- - bn - Bengali
47
- - bs - Bosnian
48
- - ca - Catalan
49
- - cnr - Montenegrin
50
- - cs - Czech
51
- - cy - Welsh
52
- - da - Danish
53
- - de - German
54
- - dz - Dzongkha
55
- - el - Greek
56
- - en - English
57
- - eo - Esperanto
58
- - es - Spanish
59
- - et - Estonian
60
- - eu - Basque
61
- - fa - Persian
62
- - fi - Finnish
63
- - fr - French
64
- - fy - Western Frisian
65
- - gd - Scottish Gaelic
66
- - gl - Galician
67
- - he - Hebrew
68
- - hi - Hindi
69
- - hr - Croatian
70
- - hu - Hungarian
71
- - hy - Armenian
72
- - id - Indonesian
73
- - is - Icelandic
74
- - it - Italian
75
- - ja - Japanese
76
- - ka - Georgian
77
- - kk - Kazakh
78
- - km - Khmer
79
- - kn - Kannada
80
- - ko - Korean
81
- - lb - Luxembourgish
82
- - lo - Lao
83
- - lt - Lithuanian
84
- - lv - Latvian
85
- - mg - Malagasy
86
- - mk - Macedonian
87
- - ml - Malayalam
88
- - mn - Mongolian
89
- - mr-IN - Marathi (India)
90
- - ms - Malay
91
- - nb - Norwegian Bokmål
92
- - ne - Nepali
93
- - nl - Dutch
94
- - nn - Norwegian Nynorsk
95
- - oc - Occitan
96
- - or - Odia
97
- - pa - Punjabi
98
- - pl - Polish
99
- - pt - Portuguese
100
- - rm - Romansh
101
- - ro - Romanian
102
- - ru - Russian
103
- - sc - Sardinian
104
- - sk - Slovak
105
- - sl - Slovenian
106
- - sq - Albanian
107
- - sr - Serbian
108
- - st - Southern Sotho
109
- - sv - Swedish
110
- - sw - Swahili
111
- - ta - Tamil
112
- - te - Telugu
113
- - th - Thai
114
- - tl - Tagalog
115
- - tr - Turkish
116
- - tt - Tatar
117
- - ug - Uyghur
118
- - uk - Ukrainian
119
- - ur - Urdu
120
- - uz - Uzbek
121
- - vi - Vietnamese
122
- - wo - Wolof
123
- - zh-CN - Chinese (Simplified)
124
- - zh-HK - Chinese (Hong Kong)
125
- - zh-TW - Chinese (Traditional)
126
- - zh-YUE - Chinese (Yue)
@@ -1,52 +0,0 @@
1
- # Interruptions - Exceptions
2
-
3
- Exception handling differs between `execute` and `execute!`. Choose the method that matches your error handling strategy.
4
-
5
- ## Exception Handling
6
-
7
- !!! warning "Important"
8
-
9
- Prefer `skip!` and `fail!` over raising exceptions—they signal intent more clearly.
10
-
11
- ### Non-bang execution
12
-
13
- Captures all exceptions and returns them as failed results:
14
-
15
- ```ruby
16
- class CompressDocument < CMDx::Task
17
- def work
18
- document = Document.find(context.document_id)
19
- document.compress!
20
- end
21
- end
22
-
23
- result = CompressDocument.execute(document_id: "unknown-doc-id")
24
- result.state #=> "interrupted"
25
- result.status #=> "failed"
26
- result.failed? #=> true
27
- result.reason #=> "[ActiveRecord::NotFoundError] record not found"
28
- result.cause #=> <ActiveRecord::NotFoundError>
29
- ```
30
-
31
- !!! note
32
-
33
- Use `exception_handler` with `execute` to send exceptions to APM tools before they become failed results.
34
-
35
- ### Bang execution
36
-
37
- Lets exceptions propagate naturally for standard Ruby error handling:
38
-
39
- ```ruby
40
- class CompressDocument < CMDx::Task
41
- def work
42
- document = Document.find(context.document_id)
43
- document.compress!
44
- end
45
- end
46
-
47
- begin
48
- CompressDocument.execute!(document_id: "unknown-doc-id")
49
- rescue ActiveRecord::NotFoundError => e
50
- puts "Handle exception: #{e.message}"
51
- end
52
- ```
@@ -1,169 +0,0 @@
1
- # Interruptions - Faults
2
-
3
- Faults are exceptions raised by `execute!` when tasks halt. They carry rich context about execution state, enabling sophisticated error handling patterns.
4
-
5
- ## Fault Types
6
-
7
- | Type | Triggered By | Use Case |
8
- |------|--------------|----------|
9
- | `CMDx::Fault` | Base class | Catch-all for any interruption |
10
- | `CMDx::SkipFault` | `skip!` method | Optional processing, early returns |
11
- | `CMDx::FailFault` | `fail!` method | Validation errors, processing failures |
12
-
13
- !!! warning "Important"
14
-
15
- All faults inherit from `CMDx::Fault` and expose result, task, context, and chain data.
16
-
17
- ## Fault Handling
18
-
19
- ```ruby
20
- begin
21
- ProcessTicket.execute!(ticket_id: 456)
22
- rescue CMDx::SkipFault => e
23
- logger.info "Ticket processing skipped: #{e.message}"
24
- schedule_retry(e.context.ticket_id)
25
- rescue CMDx::FailFault => e
26
- logger.error "Ticket processing failed: #{e.message}"
27
- notify_admin(e.context.assigned_agent, e.result.metadata[:error_code])
28
- rescue CMDx::Fault => e
29
- logger.warn "Ticket processing interrupted: #{e.message}"
30
- rollback_changes
31
- end
32
- ```
33
-
34
- ## Data Access
35
-
36
- Access rich execution data from fault exceptions:
37
-
38
- ```ruby
39
- begin
40
- LicenseActivation.execute!(license_key: key, machine_id: machine)
41
- rescue CMDx::Fault => e
42
- # Result information
43
- e.result.state #=> "interrupted"
44
- e.result.status #=> "failed" or "skipped"
45
- e.result.reason #=> "License key already activated"
46
-
47
- # Task information
48
- e.task.class #=> <LicenseActivation>
49
- e.task.id #=> "abc123..."
50
-
51
- # Context data
52
- e.context.license_key #=> "ABC-123-DEF"
53
- e.context.machine_id #=> "[FILTERED]"
54
-
55
- # Chain information
56
- e.chain.id #=> "def456..."
57
- e.chain.size #=> 3
58
- end
59
- ```
60
-
61
- ## Advanced Matching
62
-
63
- ### Task-Specific Matching
64
-
65
- Handle faults only from specific tasks using `for?`:
66
-
67
- ```ruby
68
- begin
69
- DocumentWorkflow.execute!(document_data: data)
70
- rescue CMDx::FailFault.for?(FormatValidator, ContentProcessor) => e
71
- # Handle only document-related failures
72
- retry_with_alternate_parser(e.context)
73
- rescue CMDx::SkipFault.for?(VirusScanner, ContentFilter) => e
74
- # Handle security-related skips
75
- quarantine_for_review(e.context.document_id)
76
- end
77
- ```
78
-
79
- ### Custom Logic Matching
80
-
81
- ```ruby
82
- begin
83
- ReportGenerator.execute!(report: report_data)
84
- rescue CMDx::Fault.matches? { |f| f.context.data_size > 10_000 } => e
85
- escalate_large_dataset_failure(e)
86
- rescue CMDx::FailFault.matches? { |f| f.result.metadata[:attempt_count] > 3 } => e
87
- abandon_report_generation(e)
88
- rescue CMDx::Fault.matches? { |f| f.result.metadata[:error_type] == "memory" } => e
89
- increase_memory_and_retry(e)
90
- end
91
- ```
92
-
93
- ## Fault Propagation
94
-
95
- Propagate failures with `throw!` to preserve context and maintain the error chain:
96
-
97
- ### Basic Propagation
98
-
99
- ```ruby
100
- class ReportGenerator < CMDx::Task
101
- def work
102
- # Throw if skipped or failed
103
- validation_result = DataValidator.execute(context)
104
- throw!(validation_result)
105
-
106
- # Only throw if skipped
107
- check_permissions = CheckPermissions.execute(context)
108
- throw!(check_permissions) if check_permissions.skipped?
109
-
110
- # Only throw if failed
111
- data_result = DataProcessor.execute(context)
112
- throw!(data_result) if data_result.failed?
113
-
114
- # Continue processing
115
- generate_report
116
- end
117
- end
118
- ```
119
-
120
- ### Additional Metadata
121
-
122
- ```ruby
123
- class BatchProcessor < CMDx::Task
124
- def work
125
- step_result = FileValidation.execute(context)
126
-
127
- if step_result.failed?
128
- throw!(step_result, {
129
- batch_stage: "validation",
130
- can_retry: true,
131
- next_step: "file_repair"
132
- })
133
- end
134
-
135
- continue_batch
136
- end
137
- end
138
- ```
139
-
140
- ## Chain Analysis
141
-
142
- Trace fault origins and propagation through the execution chain:
143
-
144
- ```ruby
145
- result = DocumentWorkflow.execute(invalid_data)
146
-
147
- if result.failed?
148
- # Trace the original failure
149
- original = result.caused_failure
150
- if original
151
- puts "Original failure: #{original.task.class.name}"
152
- puts "Reason: #{original.reason}"
153
- end
154
-
155
- # Find what propagated the failure
156
- thrower = result.threw_failure
157
- puts "Propagated by: #{thrower.task.class.name}" if thrower
158
-
159
- # Analyze failure type
160
- case
161
- when result.caused_failure?
162
- puts "This task was the original source"
163
- when result.threw_failure?
164
- puts "This task propagated a failure"
165
- when result.thrown_failure?
166
- puts "This task failed due to propagation"
167
- end
168
- end
169
- ```