cmdx 1.0.1 → 1.1.1
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/.cursor/prompts/docs.md +9 -0
- data/.cursor/prompts/rspec.md +21 -0
- data/.cursor/prompts/yardoc.md +13 -0
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +29 -3
- data/README.md +2 -1
- data/docs/ai_prompts.md +269 -195
- data/docs/basics/call.md +126 -60
- data/docs/basics/chain.md +190 -160
- data/docs/basics/context.md +242 -154
- data/docs/basics/setup.md +302 -32
- data/docs/callbacks.md +382 -119
- data/docs/configuration.md +211 -49
- data/docs/deprecation.md +245 -0
- data/docs/getting_started.md +161 -39
- data/docs/internationalization.md +590 -70
- data/docs/interruptions/exceptions.md +135 -118
- data/docs/interruptions/faults.md +152 -127
- data/docs/interruptions/halt.md +134 -80
- data/docs/logging.md +183 -120
- data/docs/middlewares.md +165 -392
- data/docs/outcomes/result.md +140 -112
- data/docs/outcomes/states.md +134 -99
- data/docs/outcomes/statuses.md +204 -146
- data/docs/parameters/coercions.md +251 -289
- data/docs/parameters/defaults.md +224 -169
- data/docs/parameters/definitions.md +289 -141
- data/docs/parameters/namespacing.md +250 -161
- data/docs/parameters/validations.md +247 -159
- data/docs/testing.md +196 -203
- data/docs/workflows.md +146 -101
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/callback.rb +39 -55
- data/lib/cmdx/callback_registry.rb +80 -73
- data/lib/cmdx/chain.rb +65 -122
- data/lib/cmdx/chain_inspector.rb +23 -116
- data/lib/cmdx/chain_serializer.rb +34 -146
- data/lib/cmdx/coercion.rb +57 -0
- data/lib/cmdx/coercion_registry.rb +113 -0
- data/lib/cmdx/coercions/array.rb +18 -36
- data/lib/cmdx/coercions/big_decimal.rb +21 -33
- data/lib/cmdx/coercions/boolean.rb +21 -40
- data/lib/cmdx/coercions/complex.rb +18 -31
- data/lib/cmdx/coercions/date.rb +20 -39
- data/lib/cmdx/coercions/date_time.rb +22 -39
- data/lib/cmdx/coercions/float.rb +19 -32
- data/lib/cmdx/coercions/hash.rb +22 -41
- data/lib/cmdx/coercions/integer.rb +20 -33
- data/lib/cmdx/coercions/rational.rb +20 -32
- data/lib/cmdx/coercions/string.rb +23 -31
- data/lib/cmdx/coercions/time.rb +24 -40
- data/lib/cmdx/coercions/virtual.rb +14 -31
- data/lib/cmdx/configuration.rb +101 -162
- data/lib/cmdx/context.rb +34 -166
- data/lib/cmdx/core_ext/hash.rb +42 -67
- data/lib/cmdx/core_ext/module.rb +35 -79
- data/lib/cmdx/core_ext/object.rb +63 -98
- data/lib/cmdx/correlator.rb +59 -154
- data/lib/cmdx/error.rb +37 -202
- data/lib/cmdx/errors.rb +153 -216
- data/lib/cmdx/fault.rb +68 -150
- data/lib/cmdx/faults.rb +26 -137
- data/lib/cmdx/immutator.rb +22 -110
- data/lib/cmdx/lazy_struct.rb +110 -186
- data/lib/cmdx/log_formatters/json.rb +14 -40
- data/lib/cmdx/log_formatters/key_value.rb +14 -40
- data/lib/cmdx/log_formatters/line.rb +14 -48
- data/lib/cmdx/log_formatters/logstash.rb +14 -57
- data/lib/cmdx/log_formatters/pretty_json.rb +14 -50
- data/lib/cmdx/log_formatters/pretty_key_value.rb +13 -46
- data/lib/cmdx/log_formatters/pretty_line.rb +16 -54
- data/lib/cmdx/log_formatters/raw.rb +19 -49
- data/lib/cmdx/logger.rb +22 -79
- data/lib/cmdx/logger_ansi.rb +31 -72
- data/lib/cmdx/logger_serializer.rb +74 -103
- data/lib/cmdx/middleware.rb +56 -60
- data/lib/cmdx/middleware_registry.rb +82 -77
- data/lib/cmdx/middlewares/correlate.rb +41 -226
- data/lib/cmdx/middlewares/timeout.rb +46 -185
- data/lib/cmdx/parameter.rb +167 -183
- data/lib/cmdx/parameter_evaluator.rb +231 -0
- data/lib/cmdx/parameter_inspector.rb +37 -55
- data/lib/cmdx/parameter_registry.rb +65 -84
- data/lib/cmdx/parameter_serializer.rb +32 -76
- data/lib/cmdx/railtie.rb +24 -107
- data/lib/cmdx/result.rb +254 -259
- data/lib/cmdx/result_ansi.rb +28 -80
- data/lib/cmdx/result_inspector.rb +34 -70
- data/lib/cmdx/result_logger.rb +23 -77
- data/lib/cmdx/result_serializer.rb +59 -125
- data/lib/cmdx/rspec/matchers.rb +28 -0
- data/lib/cmdx/rspec/result_matchers/be_executed.rb +42 -0
- data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +94 -0
- data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +94 -0
- data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +59 -0
- data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +57 -0
- data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +87 -0
- data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +51 -0
- data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +58 -0
- data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +59 -0
- data/lib/cmdx/rspec/result_matchers/have_context.rb +86 -0
- data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +54 -0
- data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +52 -0
- data/lib/cmdx/rspec/result_matchers/have_metadata.rb +114 -0
- data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +66 -0
- data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +64 -0
- data/lib/cmdx/rspec/result_matchers/have_runtime.rb +78 -0
- data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +76 -0
- data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +62 -0
- data/lib/cmdx/rspec/task_matchers/have_callback.rb +85 -0
- data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +68 -0
- data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +92 -0
- data/lib/cmdx/rspec/task_matchers/have_middleware.rb +46 -0
- data/lib/cmdx/rspec/task_matchers/have_parameter.rb +181 -0
- data/lib/cmdx/task.rb +336 -427
- data/lib/cmdx/task_deprecator.rb +52 -0
- data/lib/cmdx/task_processor.rb +246 -0
- data/lib/cmdx/task_serializer.rb +34 -69
- data/lib/cmdx/utils/ansi_color.rb +13 -89
- data/lib/cmdx/utils/log_timestamp.rb +13 -42
- data/lib/cmdx/utils/monotonic_runtime.rb +11 -63
- data/lib/cmdx/utils/name_affix.rb +21 -71
- data/lib/cmdx/validator.rb +57 -0
- data/lib/cmdx/validator_registry.rb +108 -0
- data/lib/cmdx/validators/exclusion.rb +55 -94
- data/lib/cmdx/validators/format.rb +31 -85
- data/lib/cmdx/validators/inclusion.rb +65 -110
- data/lib/cmdx/validators/length.rb +117 -133
- data/lib/cmdx/validators/numeric.rb +123 -130
- data/lib/cmdx/validators/presence.rb +38 -79
- data/lib/cmdx/version.rb +1 -7
- data/lib/cmdx/workflow.rb +58 -330
- data/lib/cmdx.rb +1 -1
- data/lib/generators/cmdx/install_generator.rb +14 -31
- data/lib/generators/cmdx/task_generator.rb +39 -55
- data/lib/generators/cmdx/templates/install.rb +24 -6
- data/lib/generators/cmdx/workflow_generator.rb +41 -66
- data/lib/locales/ar.yml +0 -1
- data/lib/locales/cs.yml +0 -1
- data/lib/locales/da.yml +0 -1
- data/lib/locales/de.yml +0 -1
- data/lib/locales/el.yml +0 -1
- data/lib/locales/en.yml +0 -1
- data/lib/locales/es.yml +0 -1
- data/lib/locales/fi.yml +0 -1
- data/lib/locales/fr.yml +0 -1
- data/lib/locales/he.yml +0 -1
- data/lib/locales/hi.yml +0 -1
- data/lib/locales/it.yml +0 -1
- data/lib/locales/ja.yml +0 -1
- data/lib/locales/ko.yml +0 -1
- data/lib/locales/nl.yml +0 -1
- data/lib/locales/no.yml +0 -1
- data/lib/locales/pl.yml +0 -1
- data/lib/locales/pt.yml +0 -1
- data/lib/locales/ru.yml +0 -1
- data/lib/locales/sv.yml +0 -1
- data/lib/locales/th.yml +0 -1
- data/lib/locales/tr.yml +0 -1
- data/lib/locales/vi.yml +0 -1
- data/lib/locales/zh.yml +0 -1
- metadata +36 -8
- data/lib/cmdx/parameter_validator.rb +0 -81
- data/lib/cmdx/parameter_value.rb +0 -244
- data/lib/cmdx/parameters_inspector.rb +0 -72
- data/lib/cmdx/parameters_serializer.rb +0 -115
- data/lib/cmdx/rspec/result_matchers.rb +0 -917
- data/lib/cmdx/rspec/task_matchers.rb +0 -570
- data/lib/cmdx/validators/custom.rb +0 -102
data/lib/cmdx/fault.rb
CHANGED
@@ -1,109 +1,31 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module CMDx
|
4
|
-
|
5
|
-
# Fault serves as the base exception class for task execution interruptions in CMDx.
|
6
|
-
# It provides a structured way to halt task execution with specific reasons and metadata,
|
7
|
-
# while offering advanced exception matching capabilities for precise error handling.
|
4
|
+
# Base fault class for handling task execution failures and interruptions.
|
8
5
|
#
|
9
|
-
# Faults are
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
# ## Fault Types
|
15
|
-
#
|
16
|
-
# CMDx provides two primary fault types:
|
17
|
-
# - **CMDx::Skipped**: Raised when a task is skipped via `skip!`
|
18
|
-
# - **CMDx::Failed**: Raised when a task fails via `fail!`
|
19
|
-
#
|
20
|
-
# ## Exception Handling Patterns
|
21
|
-
#
|
22
|
-
# Faults support multiple rescue patterns for flexible error handling:
|
23
|
-
# - Standard rescue by fault type
|
24
|
-
# - Task-specific matching with `for?`
|
25
|
-
# - Custom matching with `matches?`
|
26
|
-
#
|
27
|
-
# @example Basic fault handling
|
28
|
-
# begin
|
29
|
-
# ProcessOrderTask.call!(order_id: 123)
|
30
|
-
# rescue CMDx::Skipped => e
|
31
|
-
# logger.info "Task skipped: #{e.message}"
|
32
|
-
# e.result.metadata[:reason] #=> "Order already processed"
|
33
|
-
# rescue CMDx::Failed => e
|
34
|
-
# logger.error "Task failed: #{e.message}"
|
35
|
-
# e.task.class.name #=> "ProcessOrderTask"
|
36
|
-
# end
|
37
|
-
#
|
38
|
-
# @example Task-specific fault handling
|
39
|
-
# begin
|
40
|
-
# OrderProcessingWorkflow.call!(orders: orders)
|
41
|
-
# rescue CMDx::Fault.for?(ProcessOrderTask, ValidateOrderTask) => e
|
42
|
-
# # Handle faults only from specific task types
|
43
|
-
# retry_order_processing(e.context.order_id)
|
44
|
-
# end
|
45
|
-
#
|
46
|
-
# @example Advanced fault matching
|
47
|
-
# begin
|
48
|
-
# ProcessOrderTask.call!(order_id: 123)
|
49
|
-
# rescue CMDx::Fault.matches? { |f| f.result.metadata[:code] == "INVENTORY_DEPLETED" } => e
|
50
|
-
# # Handle specific fault conditions
|
51
|
-
# schedule_restock_notification(e.context.order)
|
52
|
-
# end
|
53
|
-
#
|
54
|
-
# @example Accessing fault context
|
55
|
-
# begin
|
56
|
-
# ProcessOrderTask.call!(order_id: 123)
|
57
|
-
# rescue CMDx::Fault => e
|
58
|
-
# e.result.status #=> "failed" or "skipped"
|
59
|
-
# e.result.metadata[:reason] #=> "Insufficient inventory"
|
60
|
-
# e.task.id #=> Task instance UUID
|
61
|
-
# e.context.order_id #=> 123
|
62
|
-
# e.chain.id #=> Chain instance UUID
|
63
|
-
# end
|
64
|
-
#
|
65
|
-
# @example Fault propagation with throw!
|
66
|
-
# class ProcessOrderTask < CMDx::Task
|
67
|
-
# def call
|
68
|
-
# validation_result = ValidateOrderTask.call(context)
|
69
|
-
# throw!(validation_result) if validation_result.failed?
|
70
|
-
#
|
71
|
-
# # This will raise CMDx::Failed with validation task's metadata
|
72
|
-
# end
|
73
|
-
# end
|
74
|
-
#
|
75
|
-
# @see Result Result object containing fault details
|
76
|
-
# @see Task Task execution methods (call vs call!)
|
77
|
-
# @see CMDx::Skipped Specific fault type for skipped tasks
|
78
|
-
# @see CMDx::Failed Specific fault type for failed tasks
|
79
|
-
# @since 1.0.0
|
6
|
+
# Faults are exceptions raised when tasks encounter specific execution states
|
7
|
+
# that prevent normal completion. Unlike regular exceptions, faults carry
|
8
|
+
# rich context information including the task result, execution chain, and
|
9
|
+
# contextual data that led to the fault condition. Faults can be caught and
|
10
|
+
# handled based on specific task types or custom matching criteria.
|
80
11
|
class Fault < Error
|
81
12
|
|
82
|
-
|
83
|
-
|
13
|
+
cmdx_attr_delegator :task, :chain, :context,
|
14
|
+
to: :result
|
84
15
|
|
85
|
-
|
86
|
-
# @!attribute [r] result
|
87
|
-
# @return [Result] the result object that caused this fault
|
16
|
+
# @return [CMDx::Result] the result object that caused this fault
|
88
17
|
attr_reader :result
|
89
18
|
|
90
|
-
|
91
|
-
# Initializes a new Fault with the given result object.
|
92
|
-
# The fault message is derived from the result's metadata reason or falls back
|
93
|
-
# to a localized default message.
|
19
|
+
# Creates a new fault instance from a task execution result.
|
94
20
|
#
|
95
|
-
# @param result [Result] the result
|
21
|
+
# @param result [CMDx::Result] the task result that caused the fault
|
96
22
|
#
|
97
|
-
# @
|
98
|
-
# result = ProcessOrderTask.call(order_id: 999) # Non-existent order
|
99
|
-
# fault = Fault.new(result)
|
100
|
-
# fault.message #=> "Order not found"
|
101
|
-
# fault.result #=> <Result status: "failed">
|
23
|
+
# @return [CMDx::Fault] the newly created fault instance
|
102
24
|
#
|
103
|
-
# @example
|
104
|
-
#
|
105
|
-
# fault = Fault.new(result)
|
106
|
-
# fault.
|
25
|
+
# @example Create fault from failed task result
|
26
|
+
# result = SomeTask.call(invalid_data: true)
|
27
|
+
# fault = CMDx::Fault.new(result)
|
28
|
+
# fault.task #=> SomeTask instance
|
107
29
|
def initialize(result)
|
108
30
|
@result = result
|
109
31
|
super(result.metadata[:reason] || I18n.t("cmdx.faults.unspecified", default: "no reason given"))
|
@@ -111,52 +33,56 @@ module CMDx
|
|
111
33
|
|
112
34
|
class << self
|
113
35
|
|
114
|
-
##
|
115
36
|
# Builds a specific fault type based on the result's status.
|
116
|
-
# Dynamically creates the appropriate fault subclass (Skipped, Failed, etc.)
|
117
|
-
# based on the result's current status.
|
118
37
|
#
|
119
|
-
#
|
120
|
-
#
|
38
|
+
# Creates an instance of the appropriate fault subclass (Skipped, Failed, etc.)
|
39
|
+
# by capitalizing the result status and looking up the corresponding fault class.
|
40
|
+
# This provides dynamic fault creation based on task execution outcomes.
|
121
41
|
#
|
122
|
-
# @
|
123
|
-
#
|
124
|
-
#
|
125
|
-
#
|
42
|
+
# @param result [CMDx::Result] the task result to build a fault from
|
43
|
+
#
|
44
|
+
# @return [CMDx::Fault] an instance of the appropriate fault subclass
|
45
|
+
#
|
46
|
+
# @raise [NameError] if no fault class exists for the result status
|
47
|
+
#
|
48
|
+
# @example Build fault from skipped task result
|
49
|
+
# result = SomeTask.call # result.status is :skipped
|
50
|
+
# fault = CMDx::Fault.build(result)
|
126
51
|
# fault.class #=> CMDx::Skipped
|
127
52
|
#
|
128
|
-
# @example
|
129
|
-
# result =
|
130
|
-
#
|
131
|
-
# fault = Fault.build(result)
|
53
|
+
# @example Build fault from failed task result
|
54
|
+
# result = SomeTask.call # result.status is :failed
|
55
|
+
# fault = CMDx::Fault.build(result)
|
132
56
|
# fault.class #=> CMDx::Failed
|
133
57
|
def build(result)
|
134
58
|
fault = CMDx.const_get(result.status.capitalize)
|
135
59
|
fault.new(result)
|
136
60
|
end
|
137
61
|
|
138
|
-
|
139
|
-
# Creates a fault matcher that only matches faults from specific task classes.
|
140
|
-
# This enables precise exception handling based on the task type that caused the fault.
|
62
|
+
# Creates a fault matcher that matches faults from specific task classes.
|
141
63
|
#
|
142
|
-
#
|
143
|
-
#
|
64
|
+
# Returns a dynamically created fault class that can be used in rescue blocks
|
65
|
+
# to catch faults only when they originate from specific task types. This enables
|
66
|
+
# selective fault handling based on the task that generated the fault.
|
144
67
|
#
|
145
|
-
# @
|
68
|
+
# @param tasks [Array<Class>] one or more task classes to match against
|
69
|
+
#
|
70
|
+
# @return [Class] a fault matcher class that responds to case equality
|
71
|
+
#
|
72
|
+
# @example Catch faults from specific task types
|
146
73
|
# begin
|
147
|
-
#
|
148
|
-
# rescue CMDx::Fault.for?(
|
149
|
-
#
|
150
|
-
# handle_order_processing_error(e)
|
74
|
+
# PaymentTask.call!
|
75
|
+
# rescue CMDx::Fault.for?(PaymentTask, RefundTask) => e
|
76
|
+
# puts "Payment operation failed: #{e.message}"
|
151
77
|
# end
|
152
78
|
#
|
153
|
-
# @example
|
154
|
-
#
|
79
|
+
# @example Match faults from multiple task types
|
80
|
+
# UserTaskFaults = CMDx::Fault.for?(CreateUserTask, UpdateUserTask, DeleteUserTask)
|
81
|
+
#
|
155
82
|
# begin
|
156
|
-
#
|
157
|
-
# rescue CMDx::
|
158
|
-
#
|
159
|
-
# process_payment_failure(e)
|
83
|
+
# workflow.call!
|
84
|
+
# rescue CMDx::Fault.for?(CreateUserTask, UpdateUserTask, DeleteUserTask) => e
|
85
|
+
# handle_user_operation_failure(e)
|
160
86
|
# end
|
161
87
|
def for?(*tasks)
|
162
88
|
temp_fault = Class.new(self) do
|
@@ -168,41 +94,33 @@ module CMDx
|
|
168
94
|
temp_fault.tap { |c| c.instance_variable_set(:@tasks, tasks) }
|
169
95
|
end
|
170
96
|
|
171
|
-
|
172
|
-
#
|
173
|
-
#
|
174
|
-
#
|
97
|
+
# Creates a fault matcher using a custom block for matching criteria.
|
98
|
+
#
|
99
|
+
# Returns a dynamically created fault class that uses the provided block
|
100
|
+
# to determine if a fault should be matched. The block receives the fault
|
101
|
+
# instance and should return true if the fault matches the desired criteria.
|
102
|
+
# This enables custom fault handling logic beyond simple task type matching.
|
103
|
+
#
|
104
|
+
# @param block [Proc] a block that receives a fault and returns boolean
|
105
|
+
#
|
106
|
+
# @return [Class] a fault matcher class that responds to case equality
|
175
107
|
#
|
176
|
-
# @param block [Proc] block that receives the fault and returns true/false for matching
|
177
|
-
# @return [Class] a temporary fault class with custom matching logic
|
178
108
|
# @raise [ArgumentError] if no block is provided
|
179
109
|
#
|
180
|
-
# @example
|
110
|
+
# @example Match faults by custom criteria
|
181
111
|
# begin
|
182
|
-
#
|
183
|
-
# rescue CMDx::Fault.matches? { |
|
184
|
-
#
|
185
|
-
# retry_with_different_payment_method(e.context)
|
112
|
+
# LongRunningTask.call!
|
113
|
+
# rescue CMDx::Fault.matches? { |fault| fault.context[:timeout_exceeded] } => e
|
114
|
+
# puts "Task timed out: #{e.message}"
|
186
115
|
# end
|
187
116
|
#
|
188
|
-
# @example
|
189
|
-
#
|
190
|
-
# ProcessOrderTask.call!(order_id: 123)
|
191
|
-
# rescue CMDx::Fault.matches? { |f| f.context.order_value > 1000 } => e
|
192
|
-
# # Handle high-value order failures differently
|
193
|
-
# escalate_to_manager(e)
|
194
|
-
# end
|
117
|
+
# @example Match faults by metadata content
|
118
|
+
# ValidationFault = CMDx::Fault.matches? { |fault| fault.result.metadata[:type] == "validation_error" }
|
195
119
|
#
|
196
|
-
# @example Complex matching logic
|
197
120
|
# begin
|
198
|
-
#
|
199
|
-
# rescue
|
200
|
-
#
|
201
|
-
# f.result.metadata[:reason]&.include?("timeout") &&
|
202
|
-
# f.chain.results.count(&:failed?) < 3
|
203
|
-
# } => e
|
204
|
-
# # Retry if it's a timeout with fewer than 3 failures in the chain
|
205
|
-
# retry_with_longer_timeout(e)
|
121
|
+
# ValidateUserTask.call!
|
122
|
+
# rescue ValidationFault => e
|
123
|
+
# display_validation_errors(e.result.errors)
|
206
124
|
# end
|
207
125
|
def matches?(&block)
|
208
126
|
raise ArgumentError, "block required" unless block_given?
|
data/lib/cmdx/faults.rb
CHANGED
@@ -2,166 +2,55 @@
|
|
2
2
|
|
3
3
|
module CMDx
|
4
4
|
|
5
|
-
|
6
|
-
# Skipped is a specific fault type raised when a task execution is intentionally
|
7
|
-
# skipped via the `skip!` method. This represents a controlled interruption where
|
8
|
-
# the task determines that execution is not necessary or appropriate under the
|
9
|
-
# current conditions.
|
5
|
+
# Fault raised when a task is intentionally skipped during execution.
|
10
6
|
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
# - Business logic that determines the operation is redundant
|
15
|
-
# - Graceful handling of edge cases that don't constitute errors
|
16
|
-
#
|
17
|
-
# @example Basic skip usage
|
18
|
-
# class ProcessOrderTask < CMDx::Task
|
19
|
-
# required :order_id, type: :integer
|
7
|
+
# This fault occurs when a task determines it should not execute based on
|
8
|
+
# its current context or conditions. Skipped tasks are not considered failures
|
9
|
+
# but rather intentional bypasses of task execution logic.
|
20
10
|
#
|
11
|
+
# @example Task that skips based on conditions
|
12
|
+
# class ProcessPaymentTask < CMDx::Task
|
21
13
|
# def call
|
22
|
-
#
|
23
|
-
# skip!(reason: "Order already processed") if context.order.processed?
|
24
|
-
#
|
25
|
-
# context.order.process!
|
14
|
+
# skip!(reason: "Payment already processed") if payment_exists?
|
26
15
|
# end
|
27
16
|
# end
|
28
17
|
#
|
29
|
-
#
|
30
|
-
#
|
31
|
-
# result.skipped? #=> true
|
32
|
-
# result.metadata[:reason] #=> "Order already processed"
|
18
|
+
# result = ProcessPaymentTask.call(payment_id: 123)
|
19
|
+
# # raises CMDx::Skipped when payment already exists
|
33
20
|
#
|
34
|
-
#
|
21
|
+
# @example Catching skipped faults
|
35
22
|
# begin
|
36
|
-
#
|
23
|
+
# MyTask.call!(data: "invalid")
|
37
24
|
# rescue CMDx::Skipped => e
|
38
|
-
# puts "
|
39
|
-
# end
|
40
|
-
#
|
41
|
-
# @example Conditional skip logic
|
42
|
-
# class SendNotificationTask < CMDx::Task
|
43
|
-
# required :user_id, type: :integer
|
44
|
-
# optional :force, type: :boolean, default: false
|
45
|
-
#
|
46
|
-
# def call
|
47
|
-
# context.user = User.find(user_id)
|
48
|
-
#
|
49
|
-
# unless force || context.user.notifications_enabled?
|
50
|
-
# skip!(reason: "User has notifications disabled")
|
51
|
-
# end
|
52
|
-
#
|
53
|
-
# NotificationService.send(context.user)
|
54
|
-
# end
|
25
|
+
# puts "Task was skipped: #{e.message}"
|
55
26
|
# end
|
56
|
-
#
|
57
|
-
# @example Handling skipped tasks in workflows
|
58
|
-
# begin
|
59
|
-
# OrderProcessingWorkflow.call!(orders: orders)
|
60
|
-
# rescue CMDx::Skipped => e
|
61
|
-
# # Log skipped operations but continue processing
|
62
|
-
# logger.info "Skipped processing: #{e.message}"
|
63
|
-
# end
|
64
|
-
#
|
65
|
-
# @see Fault Base fault class with advanced matching capabilities
|
66
|
-
# @see Failed Failed fault type for error conditions
|
67
|
-
# @see Result#skip! Method for triggering skipped faults
|
68
|
-
# @since 1.0.0
|
69
27
|
Skipped = Class.new(Fault)
|
70
28
|
|
71
|
-
|
72
|
-
# Failed is a specific fault type raised when a task execution encounters an
|
73
|
-
# error condition via the `fail!` method. This represents a controlled failure
|
74
|
-
# where the task explicitly determines that execution cannot continue successfully.
|
29
|
+
# Fault raised when a task execution fails due to errors or validation failures.
|
75
30
|
#
|
76
|
-
#
|
77
|
-
#
|
78
|
-
#
|
79
|
-
# - Resource unavailability or constraint violations
|
80
|
-
# - Explicit error conditions that require attention
|
81
|
-
#
|
82
|
-
# @example Basic failure usage
|
83
|
-
# class ProcessPaymentTask < CMDx::Task
|
84
|
-
# required :payment_amount, type: :float
|
85
|
-
# required :payment_method, type: :string
|
86
|
-
#
|
87
|
-
# def call
|
88
|
-
# unless payment_amount > 0
|
89
|
-
# fail!(reason: "Payment amount must be positive", code: "INVALID_AMOUNT")
|
90
|
-
# end
|
31
|
+
# This fault occurs when a task encounters an error condition, validation failure,
|
32
|
+
# or any other condition that prevents successful completion. Failed tasks indicate
|
33
|
+
# that the intended operation could not be completed successfully.
|
91
34
|
#
|
92
|
-
#
|
93
|
-
#
|
94
|
-
# end
|
95
|
-
#
|
96
|
-
# process_payment
|
97
|
-
# end
|
98
|
-
# end
|
99
|
-
#
|
100
|
-
# # Non-bang call returns result
|
101
|
-
# result = ProcessPaymentTask.call(payment_amount: -10, payment_method: "card")
|
102
|
-
# result.failed? #=> true
|
103
|
-
# result.metadata[:reason] #=> "Payment amount must be positive"
|
104
|
-
# result.metadata[:code] #=> "INVALID_AMOUNT"
|
105
|
-
#
|
106
|
-
# # Bang call raises exception
|
107
|
-
# begin
|
108
|
-
# ProcessPaymentTask.call!(payment_amount: -10, payment_method: "card")
|
109
|
-
# rescue CMDx::Failed => e
|
110
|
-
# puts "Failed: #{e.message}"
|
111
|
-
# puts "Error code: #{e.result.metadata[:code]}"
|
112
|
-
# end
|
113
|
-
#
|
114
|
-
# @example Validation failure with detailed metadata
|
115
|
-
# class CreateUserTask < CMDx::Task
|
35
|
+
# @example Task that fails due to validation
|
36
|
+
# class ValidateUserTask < CMDx::Task
|
116
37
|
# required :email, type: :string
|
117
|
-
# required :password, type: :string
|
118
38
|
#
|
119
39
|
# def call
|
120
|
-
#
|
121
|
-
# fail!(
|
122
|
-
# "Email already exists",
|
123
|
-
# code: "EMAIL_EXISTS",
|
124
|
-
# field: "email",
|
125
|
-
# suggested_action: "Use different email or login instead"
|
126
|
-
# )
|
127
|
-
# end
|
128
|
-
#
|
129
|
-
# context.user = User.create!(email: email, password: password)
|
40
|
+
# fail!(reason: "Invalid email format") unless valid_email?
|
130
41
|
# end
|
131
42
|
# end
|
132
43
|
#
|
133
|
-
#
|
44
|
+
# result = ValidateUserTask.call(email: "invalid-email")
|
45
|
+
# # raises CMDx::Failed when email is invalid
|
46
|
+
#
|
47
|
+
# @example Catching failed faults
|
134
48
|
# begin
|
135
|
-
#
|
136
|
-
# rescue CMDx::Failed.matches? { |f| f.result.metadata[:code] == "PAYMENT_DECLINED" } => e
|
137
|
-
# # Handle payment failures specifically
|
138
|
-
# retry_with_backup_payment_method(e.context)
|
49
|
+
# RiskyTask.call!(data: "problematic")
|
139
50
|
# rescue CMDx::Failed => e
|
140
|
-
#
|
141
|
-
#
|
51
|
+
# puts "Task failed: #{e.message}"
|
52
|
+
# puts "Original task: #{e.task.class.name}"
|
142
53
|
# end
|
143
|
-
#
|
144
|
-
# @example Failure propagation in complex workflows
|
145
|
-
# class OrderFulfillmentTask < CMDx::Task
|
146
|
-
# def call
|
147
|
-
# payment_result = ProcessPaymentTask.call(context)
|
148
|
-
#
|
149
|
-
# if payment_result.failed?
|
150
|
-
# fail!(
|
151
|
-
# "Cannot fulfill order due to payment failure",
|
152
|
-
# code: "PAYMENT_REQUIRED",
|
153
|
-
# original_error: payment_result.metadata
|
154
|
-
# )
|
155
|
-
# end
|
156
|
-
#
|
157
|
-
# fulfill_order
|
158
|
-
# end
|
159
|
-
# end
|
160
|
-
#
|
161
|
-
# @see Fault Base fault class with advanced matching capabilities
|
162
|
-
# @see Skipped Skipped fault type for conditional interruptions
|
163
|
-
# @see Result#fail! Method for triggering failed faults
|
164
|
-
# @since 1.0.0
|
165
54
|
Failed = Class.new(Fault)
|
166
55
|
|
167
56
|
end
|
data/lib/cmdx/immutator.rb
CHANGED
@@ -1,126 +1,38 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module CMDx
|
4
|
-
|
5
|
-
# Immutator provides task finalization by freezing objects to prevent mutation
|
6
|
-
# after task execution is complete. This ensures task immutability and prevents
|
7
|
-
# accidental side effects or modifications to completed task instances.
|
4
|
+
# Provides object immutability functionality for tasks and their associated objects.
|
8
5
|
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
# ## Freezing Strategy
|
15
|
-
#
|
16
|
-
# The Immutator employs a selective freezing strategy:
|
17
|
-
# 1. **Task Instance**: Always frozen to prevent method calls and modifications
|
18
|
-
# 2. **Result Object**: Always frozen to preserve execution outcome
|
19
|
-
# 3. **Context Object**: Frozen only for the first task in a chain (index 0)
|
20
|
-
# 4. **Chain Object**: Frozen only for the first task in a chain (index 0)
|
21
|
-
#
|
22
|
-
# This selective approach allows subsequent tasks in a workflow or chain to continue
|
23
|
-
# using the shared context and chain objects while ensuring completed tasks remain
|
24
|
-
# immutable.
|
25
|
-
#
|
26
|
-
# ## Test Environment Handling
|
27
|
-
#
|
28
|
-
# In test environments (Rails or Rack), freezing is automatically disabled to
|
29
|
-
# prevent conflicts with test frameworks that may need to stub or mock frozen
|
30
|
-
# objects. This ensures smooth testing without compromising the immutability
|
31
|
-
# guarantees in production environments.
|
32
|
-
#
|
33
|
-
# @example Task execution with automatic freezing
|
34
|
-
# class ProcessOrderTask < CMDx::Task
|
35
|
-
# required :order_id, type: :integer
|
36
|
-
#
|
37
|
-
# def call
|
38
|
-
# context.order = Order.find(order_id)
|
39
|
-
# context.order.process!
|
40
|
-
# end
|
41
|
-
# end
|
42
|
-
#
|
43
|
-
# result = ProcessOrderTask.call(order_id: 123)
|
44
|
-
# result.frozen? #=> true
|
45
|
-
# result.task.frozen? #=> true
|
46
|
-
# result.context.frozen? #=> true (if first task in chain)
|
47
|
-
#
|
48
|
-
# @example Attempting to modify frozen task (will raise error)
|
49
|
-
# result = ProcessOrderTask.call(order_id: 123)
|
50
|
-
# result.context.new_field = "value" #=> FrozenError
|
51
|
-
# result.task.call #=> FrozenError
|
52
|
-
#
|
53
|
-
# @example Workflow execution with selective freezing
|
54
|
-
# class OrderWorkflow < CMDx::Workflow
|
55
|
-
# def call
|
56
|
-
# ProcessOrderTask.call(context)
|
57
|
-
# SendEmailTask.call(context)
|
58
|
-
# end
|
59
|
-
# end
|
60
|
-
#
|
61
|
-
# result = OrderWorkflow.call(order_id: 123)
|
62
|
-
# # First task freezes context and chain
|
63
|
-
# # Second task can still use unfrozen context for execution
|
64
|
-
# # But both task instances are individually frozen
|
65
|
-
#
|
66
|
-
# @example Test environment behavior
|
67
|
-
# # In test environment (SKIP_CMDX_FREEZING=1)
|
68
|
-
# result = ProcessOrderTask.call(order_id: 123)
|
69
|
-
# result.frozen? #=> false (freezing disabled)
|
70
|
-
# result.task.frozen? #=> false (allows stubbing/mocking)
|
71
|
-
#
|
72
|
-
# @see Task Task execution lifecycle
|
73
|
-
# @see Result Result object that gets frozen
|
74
|
-
# @see Context Context object that may get frozen
|
75
|
-
# @see Chain Chain object that may get frozen
|
76
|
-
# @since 1.0.0
|
6
|
+
# This module freezes task objects and their related components after execution
|
7
|
+
# to prevent unintended modifications. It supports conditional freezing through
|
8
|
+
# environment variable configuration, allowing developers to disable immutability
|
9
|
+
# during testing scenarios where object stubbing is required.
|
77
10
|
module Immutator
|
78
11
|
|
79
12
|
module_function
|
80
13
|
|
81
|
-
|
82
|
-
# Freezes task-related objects to ensure immutability after execution.
|
83
|
-
# This method is called automatically during task termination and implements
|
84
|
-
# a selective freezing strategy based on task position within a chain.
|
85
|
-
#
|
86
|
-
# The freezing process:
|
87
|
-
# 1. Checks if running in test environment and skips freezing if so
|
88
|
-
# 2. Always freezes the task instance and its result
|
89
|
-
# 3. Freezes context and chain only for the first task (index 0) in a chain
|
90
|
-
#
|
91
|
-
# This selective approach ensures that:
|
92
|
-
# - Completed tasks cannot be modified or re-executed
|
93
|
-
# - Results remain immutable and trustworthy
|
94
|
-
# - Shared objects (context/chain) remain available for subsequent tasks
|
95
|
-
# - Test environments can continue to function with mocking/stubbing
|
96
|
-
#
|
97
|
-
# @param task [Task] the task instance to freeze along with its associated objects
|
98
|
-
# @return [void]
|
14
|
+
# Freezes a task and its associated objects to prevent further modification.
|
99
15
|
#
|
100
|
-
#
|
101
|
-
#
|
102
|
-
#
|
103
|
-
#
|
104
|
-
# # Freezes: task, result, context, chain
|
16
|
+
# This method makes the task, its result, and related objects immutable after
|
17
|
+
# execution. If the task result index is zero (indicating the first task in a chain),
|
18
|
+
# it also freezes the context and chain objects. The freezing behavior can be
|
19
|
+
# disabled via the SKIP_CMDX_FREEZING environment variable for testing purposes.
|
105
20
|
#
|
106
|
-
# @
|
107
|
-
# # After first task has run
|
108
|
-
# task = SendEmailTask.call(context)
|
109
|
-
# # task.result.index == 1
|
110
|
-
# Immutator.call(task)
|
111
|
-
# # Freezes: task, result (context and chain remain unfrozen)
|
21
|
+
# @param task [CMDx::Task] the task instance to freeze along with its associated objects
|
112
22
|
#
|
113
|
-
# @
|
114
|
-
# ENV["RAILS_ENV"] = "test"
|
115
|
-
# task = ProcessOrderTask.call(order_id: 123)
|
116
|
-
# Immutator.call(task)
|
117
|
-
# # No objects are frozen, allows test stubbing
|
23
|
+
# @return [void] returns nil when freezing is skipped, otherwise no meaningful return value
|
118
24
|
#
|
119
|
-
# @
|
120
|
-
#
|
25
|
+
# @example Freeze a task after execution
|
26
|
+
# task = MyTask.call(user_id: 123)
|
27
|
+
# CMDx::Immutator.call(task)
|
28
|
+
# task.frozen? #=> true
|
29
|
+
# task.result.frozen? #=> true
|
121
30
|
#
|
122
|
-
# @
|
123
|
-
#
|
31
|
+
# @example Skip freezing during testing
|
32
|
+
# ENV["SKIP_CMDX_FREEZING"] = "true"
|
33
|
+
# task = MyTask.call(user_id: 123)
|
34
|
+
# CMDx::Immutator.call(task)
|
35
|
+
# task.frozen? #=> false
|
124
36
|
def call(task)
|
125
37
|
# Stubbing on frozen objects is not allowed
|
126
38
|
skip_freezing = ENV.fetch("SKIP_CMDX_FREEZING", false)
|