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.
Files changed (170) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/prompts/docs.md +9 -0
  3. data/.cursor/prompts/rspec.md +21 -0
  4. data/.cursor/prompts/yardoc.md +13 -0
  5. data/.rubocop.yml +2 -0
  6. data/CHANGELOG.md +29 -3
  7. data/README.md +2 -1
  8. data/docs/ai_prompts.md +269 -195
  9. data/docs/basics/call.md +126 -60
  10. data/docs/basics/chain.md +190 -160
  11. data/docs/basics/context.md +242 -154
  12. data/docs/basics/setup.md +302 -32
  13. data/docs/callbacks.md +382 -119
  14. data/docs/configuration.md +211 -49
  15. data/docs/deprecation.md +245 -0
  16. data/docs/getting_started.md +161 -39
  17. data/docs/internationalization.md +590 -70
  18. data/docs/interruptions/exceptions.md +135 -118
  19. data/docs/interruptions/faults.md +152 -127
  20. data/docs/interruptions/halt.md +134 -80
  21. data/docs/logging.md +183 -120
  22. data/docs/middlewares.md +165 -392
  23. data/docs/outcomes/result.md +140 -112
  24. data/docs/outcomes/states.md +134 -99
  25. data/docs/outcomes/statuses.md +204 -146
  26. data/docs/parameters/coercions.md +251 -289
  27. data/docs/parameters/defaults.md +224 -169
  28. data/docs/parameters/definitions.md +289 -141
  29. data/docs/parameters/namespacing.md +250 -161
  30. data/docs/parameters/validations.md +247 -159
  31. data/docs/testing.md +196 -203
  32. data/docs/workflows.md +146 -101
  33. data/lib/cmdx/.DS_Store +0 -0
  34. data/lib/cmdx/callback.rb +39 -55
  35. data/lib/cmdx/callback_registry.rb +80 -73
  36. data/lib/cmdx/chain.rb +65 -122
  37. data/lib/cmdx/chain_inspector.rb +23 -116
  38. data/lib/cmdx/chain_serializer.rb +34 -146
  39. data/lib/cmdx/coercion.rb +57 -0
  40. data/lib/cmdx/coercion_registry.rb +113 -0
  41. data/lib/cmdx/coercions/array.rb +18 -36
  42. data/lib/cmdx/coercions/big_decimal.rb +21 -33
  43. data/lib/cmdx/coercions/boolean.rb +21 -40
  44. data/lib/cmdx/coercions/complex.rb +18 -31
  45. data/lib/cmdx/coercions/date.rb +20 -39
  46. data/lib/cmdx/coercions/date_time.rb +22 -39
  47. data/lib/cmdx/coercions/float.rb +19 -32
  48. data/lib/cmdx/coercions/hash.rb +22 -41
  49. data/lib/cmdx/coercions/integer.rb +20 -33
  50. data/lib/cmdx/coercions/rational.rb +20 -32
  51. data/lib/cmdx/coercions/string.rb +23 -31
  52. data/lib/cmdx/coercions/time.rb +24 -40
  53. data/lib/cmdx/coercions/virtual.rb +14 -31
  54. data/lib/cmdx/configuration.rb +101 -162
  55. data/lib/cmdx/context.rb +34 -166
  56. data/lib/cmdx/core_ext/hash.rb +42 -67
  57. data/lib/cmdx/core_ext/module.rb +35 -79
  58. data/lib/cmdx/core_ext/object.rb +63 -98
  59. data/lib/cmdx/correlator.rb +59 -154
  60. data/lib/cmdx/error.rb +37 -202
  61. data/lib/cmdx/errors.rb +153 -216
  62. data/lib/cmdx/fault.rb +68 -150
  63. data/lib/cmdx/faults.rb +26 -137
  64. data/lib/cmdx/immutator.rb +22 -110
  65. data/lib/cmdx/lazy_struct.rb +110 -186
  66. data/lib/cmdx/log_formatters/json.rb +14 -40
  67. data/lib/cmdx/log_formatters/key_value.rb +14 -40
  68. data/lib/cmdx/log_formatters/line.rb +14 -48
  69. data/lib/cmdx/log_formatters/logstash.rb +14 -57
  70. data/lib/cmdx/log_formatters/pretty_json.rb +14 -50
  71. data/lib/cmdx/log_formatters/pretty_key_value.rb +13 -46
  72. data/lib/cmdx/log_formatters/pretty_line.rb +16 -54
  73. data/lib/cmdx/log_formatters/raw.rb +19 -49
  74. data/lib/cmdx/logger.rb +22 -79
  75. data/lib/cmdx/logger_ansi.rb +31 -72
  76. data/lib/cmdx/logger_serializer.rb +74 -103
  77. data/lib/cmdx/middleware.rb +56 -60
  78. data/lib/cmdx/middleware_registry.rb +82 -77
  79. data/lib/cmdx/middlewares/correlate.rb +41 -226
  80. data/lib/cmdx/middlewares/timeout.rb +46 -185
  81. data/lib/cmdx/parameter.rb +167 -183
  82. data/lib/cmdx/parameter_evaluator.rb +231 -0
  83. data/lib/cmdx/parameter_inspector.rb +37 -55
  84. data/lib/cmdx/parameter_registry.rb +65 -84
  85. data/lib/cmdx/parameter_serializer.rb +32 -76
  86. data/lib/cmdx/railtie.rb +24 -107
  87. data/lib/cmdx/result.rb +254 -259
  88. data/lib/cmdx/result_ansi.rb +28 -80
  89. data/lib/cmdx/result_inspector.rb +34 -70
  90. data/lib/cmdx/result_logger.rb +23 -77
  91. data/lib/cmdx/result_serializer.rb +59 -125
  92. data/lib/cmdx/rspec/matchers.rb +28 -0
  93. data/lib/cmdx/rspec/result_matchers/be_executed.rb +42 -0
  94. data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +94 -0
  95. data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +94 -0
  96. data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +59 -0
  97. data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +57 -0
  98. data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +87 -0
  99. data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +51 -0
  100. data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +58 -0
  101. data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +59 -0
  102. data/lib/cmdx/rspec/result_matchers/have_context.rb +86 -0
  103. data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +54 -0
  104. data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +52 -0
  105. data/lib/cmdx/rspec/result_matchers/have_metadata.rb +114 -0
  106. data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +66 -0
  107. data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +64 -0
  108. data/lib/cmdx/rspec/result_matchers/have_runtime.rb +78 -0
  109. data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +76 -0
  110. data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +62 -0
  111. data/lib/cmdx/rspec/task_matchers/have_callback.rb +85 -0
  112. data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +68 -0
  113. data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +92 -0
  114. data/lib/cmdx/rspec/task_matchers/have_middleware.rb +46 -0
  115. data/lib/cmdx/rspec/task_matchers/have_parameter.rb +181 -0
  116. data/lib/cmdx/task.rb +336 -427
  117. data/lib/cmdx/task_deprecator.rb +52 -0
  118. data/lib/cmdx/task_processor.rb +246 -0
  119. data/lib/cmdx/task_serializer.rb +34 -69
  120. data/lib/cmdx/utils/ansi_color.rb +13 -89
  121. data/lib/cmdx/utils/log_timestamp.rb +13 -42
  122. data/lib/cmdx/utils/monotonic_runtime.rb +11 -63
  123. data/lib/cmdx/utils/name_affix.rb +21 -71
  124. data/lib/cmdx/validator.rb +57 -0
  125. data/lib/cmdx/validator_registry.rb +108 -0
  126. data/lib/cmdx/validators/exclusion.rb +55 -94
  127. data/lib/cmdx/validators/format.rb +31 -85
  128. data/lib/cmdx/validators/inclusion.rb +65 -110
  129. data/lib/cmdx/validators/length.rb +117 -133
  130. data/lib/cmdx/validators/numeric.rb +123 -130
  131. data/lib/cmdx/validators/presence.rb +38 -79
  132. data/lib/cmdx/version.rb +1 -7
  133. data/lib/cmdx/workflow.rb +58 -330
  134. data/lib/cmdx.rb +1 -1
  135. data/lib/generators/cmdx/install_generator.rb +14 -31
  136. data/lib/generators/cmdx/task_generator.rb +39 -55
  137. data/lib/generators/cmdx/templates/install.rb +24 -6
  138. data/lib/generators/cmdx/workflow_generator.rb +41 -66
  139. data/lib/locales/ar.yml +0 -1
  140. data/lib/locales/cs.yml +0 -1
  141. data/lib/locales/da.yml +0 -1
  142. data/lib/locales/de.yml +0 -1
  143. data/lib/locales/el.yml +0 -1
  144. data/lib/locales/en.yml +0 -1
  145. data/lib/locales/es.yml +0 -1
  146. data/lib/locales/fi.yml +0 -1
  147. data/lib/locales/fr.yml +0 -1
  148. data/lib/locales/he.yml +0 -1
  149. data/lib/locales/hi.yml +0 -1
  150. data/lib/locales/it.yml +0 -1
  151. data/lib/locales/ja.yml +0 -1
  152. data/lib/locales/ko.yml +0 -1
  153. data/lib/locales/nl.yml +0 -1
  154. data/lib/locales/no.yml +0 -1
  155. data/lib/locales/pl.yml +0 -1
  156. data/lib/locales/pt.yml +0 -1
  157. data/lib/locales/ru.yml +0 -1
  158. data/lib/locales/sv.yml +0 -1
  159. data/lib/locales/th.yml +0 -1
  160. data/lib/locales/tr.yml +0 -1
  161. data/lib/locales/vi.yml +0 -1
  162. data/lib/locales/zh.yml +0 -1
  163. metadata +36 -8
  164. data/lib/cmdx/parameter_validator.rb +0 -81
  165. data/lib/cmdx/parameter_value.rb +0 -244
  166. data/lib/cmdx/parameters_inspector.rb +0 -72
  167. data/lib/cmdx/parameters_serializer.rb +0 -115
  168. data/lib/cmdx/rspec/result_matchers.rb +0 -917
  169. data/lib/cmdx/rspec/task_matchers.rb +0 -570
  170. 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 automatically raised when using the bang `call!` method on tasks that
10
- # encounter `skip!` or `fail!` conditions. They carry the full context of the
11
- # interrupted task, including the result object with its metadata and execution state.
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
- __cmdx_attr_delegator :task, :chain, :context,
83
- to: :result
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 object containing fault details
21
+ # @param result [CMDx::Result] the task result that caused the fault
96
22
  #
97
- # @example Creating a fault from a failed result
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 Fault with I18n message
104
- # # With custom locale configuration
105
- # fault = Fault.new(result)
106
- # fault.message #=> Localized message from I18n
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
- # @param result [Result] the result object to build a fault from
120
- # @return [Fault] a fault instance of the appropriate subclass
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
- # @example Building a skipped fault
123
- # result = MyTask.call(param: "value")
124
- # result.skip!(reason: "Not needed")
125
- # fault = Fault.build(result)
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 Building a failed fault
129
- # result = MyTask.call(param: "invalid")
130
- # result.fail!(reason: "Validation error")
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
- # @param tasks [Array<Class>] task classes to match against
143
- # @return [Class] a temporary fault class with custom matching logic
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
- # @example Matching specific task types
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
- # OrderWorkflow.call!(orders: orders)
148
- # rescue CMDx::Fault.for?(ProcessOrderTask, ValidateOrderTask) => e
149
- # # Only handle faults from these specific task types
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 Multiple task matching
154
- # payment_tasks = [ProcessPaymentTask, ValidateCardTask, ChargeCardTask]
79
+ # @example Match faults from multiple task types
80
+ # UserTaskFaults = CMDx::Fault.for?(CreateUserTask, UpdateUserTask, DeleteUserTask)
81
+ #
155
82
  # begin
156
- # PaymentWorkflow.call!(payment_data: data)
157
- # rescue CMDx::Failed.for?(*payment_tasks) => e
158
- # # Handle failures from any payment-related task
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
- # Creates a fault matcher with custom matching logic via a block.
173
- # This enables sophisticated fault matching based on any aspect of the fault,
174
- # including result metadata, task state, or context values.
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 Matching by error code
110
+ # @example Match faults by custom criteria
181
111
  # begin
182
- # ProcessOrderTask.call!(order_id: 123)
183
- # rescue CMDx::Fault.matches? { |f| f.result.metadata[:error_code] == "PAYMENT_DECLINED" } => e
184
- # # Handle specific payment errors
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 Matching by context values
189
- # begin
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
- # WorkflowProcessor.call!(items: items)
199
- # rescue CMDx::Fault.matches? { |f|
200
- # f.result.failed? &&
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
- # Skipped faults are typically used for:
12
- # - Conditional logic where certain conditions make execution unnecessary
13
- # - Early returns when prerequisites are not met
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
- # context.order = Order.find(order_id)
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
- # # Non-bang call returns result
30
- # result = ProcessOrderTask.call(order_id: 123)
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
- # # Bang call raises exception
21
+ # @example Catching skipped faults
35
22
  # begin
36
- # ProcessOrderTask.call!(order_id: 123)
23
+ # MyTask.call!(data: "invalid")
37
24
  # rescue CMDx::Skipped => e
38
- # puts "Skipped: #{e.message}"
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
- # Failed faults are typically used for:
77
- # - Validation errors that prevent successful execution
78
- # - Business rule violations that constitute failures
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
- # unless valid_payment_method?
93
- # fail!(reason: "Invalid payment method", code: "INVALID_METHOD")
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
- # if User.exists?(email: email)
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
- # @example Handling specific failure types
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
- # ProcessOrderTask.call!(order_id: 123)
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
- # # Handle all other failures
141
- # log_failure_and_notify_support(e)
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
@@ -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
- # The Immutator is automatically called during the task termination phase as part
10
- # of the execution lifecycle. It freezes the task instance, its result, and
11
- # associated objects to maintain data integrity and enforce the single-use pattern
12
- # of CMDx tasks.
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
- # @example First task in chain (freezes everything)
101
- # task = ProcessOrderTask.call(order_id: 123)
102
- # # task.result.index == 0
103
- # Immutator.call(task)
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
- # @example Subsequent task in chain (selective freezing)
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
- # @example Test environment (no freezing)
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
- # @note This method is automatically called by the task execution framework
120
- # and should not typically be called directly by user code.
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
- # @note Freezing is skipped entirely in test environments to prevent conflicts
123
- # with test frameworks that need to stub or mock objects.
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)