cmdx 0.4.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/.DS_Store +0 -0
  3. data/.cursor/rules/cursor-instructions.mdc +6 -0
  4. data/.rubocop.yml +16 -1
  5. data/.ruby-version +1 -1
  6. data/CHANGELOG.md +42 -1
  7. data/README.md +72 -25
  8. data/docs/ai_prompts.md +309 -0
  9. data/docs/basics/call.md +225 -14
  10. data/docs/basics/chain.md +271 -0
  11. data/docs/basics/context.md +232 -33
  12. data/docs/basics/setup.md +76 -12
  13. data/docs/callbacks.md +273 -0
  14. data/docs/configuration.md +158 -28
  15. data/docs/getting_started.md +134 -22
  16. data/docs/interruptions/exceptions.md +189 -11
  17. data/docs/interruptions/faults.md +187 -44
  18. data/docs/interruptions/halt.md +179 -35
  19. data/docs/logging.md +194 -53
  20. data/docs/middlewares.md +735 -0
  21. data/docs/outcomes/result.md +296 -10
  22. data/docs/outcomes/states.md +212 -19
  23. data/docs/outcomes/statuses.md +284 -18
  24. data/docs/parameters/coercions.md +402 -29
  25. data/docs/parameters/defaults.md +249 -25
  26. data/docs/parameters/definitions.md +238 -72
  27. data/docs/parameters/namespacing.md +250 -27
  28. data/docs/parameters/validations.md +193 -168
  29. data/docs/testing.md +550 -0
  30. data/docs/tips_and_tricks.md +95 -43
  31. data/docs/workflows.md +319 -0
  32. data/lib/cmdx/.DS_Store +0 -0
  33. data/lib/cmdx/callback.rb +69 -0
  34. data/lib/cmdx/callback_registry.rb +106 -0
  35. data/lib/cmdx/chain.rb +190 -0
  36. data/lib/cmdx/chain_inspector.rb +149 -0
  37. data/lib/cmdx/chain_serializer.rb +175 -0
  38. data/lib/cmdx/coercions/array.rb +37 -0
  39. data/lib/cmdx/coercions/big_decimal.rb +33 -0
  40. data/lib/cmdx/coercions/boolean.rb +41 -1
  41. data/lib/cmdx/coercions/complex.rb +31 -0
  42. data/lib/cmdx/coercions/date.rb +39 -0
  43. data/lib/cmdx/coercions/date_time.rb +39 -0
  44. data/lib/cmdx/coercions/float.rb +31 -0
  45. data/lib/cmdx/coercions/hash.rb +42 -0
  46. data/lib/cmdx/coercions/integer.rb +32 -0
  47. data/lib/cmdx/coercions/rational.rb +31 -0
  48. data/lib/cmdx/coercions/string.rb +31 -0
  49. data/lib/cmdx/coercions/time.rb +39 -0
  50. data/lib/cmdx/coercions/virtual.rb +31 -0
  51. data/lib/cmdx/configuration.rb +217 -9
  52. data/lib/cmdx/context.rb +173 -2
  53. data/lib/cmdx/core_ext/hash.rb +72 -0
  54. data/lib/cmdx/core_ext/module.rb +94 -0
  55. data/lib/cmdx/core_ext/object.rb +105 -0
  56. data/lib/cmdx/correlator.rb +217 -0
  57. data/lib/cmdx/error.rb +210 -8
  58. data/lib/cmdx/errors.rb +256 -1
  59. data/lib/cmdx/fault.rb +177 -2
  60. data/lib/cmdx/faults.rb +158 -2
  61. data/lib/cmdx/immutator.rb +121 -2
  62. data/lib/cmdx/lazy_struct.rb +261 -18
  63. data/lib/cmdx/log_formatters/json.rb +46 -0
  64. data/lib/cmdx/log_formatters/key_value.rb +46 -0
  65. data/lib/cmdx/log_formatters/line.rb +54 -0
  66. data/lib/cmdx/log_formatters/logstash.rb +64 -0
  67. data/lib/cmdx/log_formatters/pretty_json.rb +57 -0
  68. data/lib/cmdx/log_formatters/pretty_key_value.rb +51 -0
  69. data/lib/cmdx/log_formatters/pretty_line.rb +60 -0
  70. data/lib/cmdx/log_formatters/raw.rb +54 -0
  71. data/lib/cmdx/logger.rb +85 -0
  72. data/lib/cmdx/logger_ansi.rb +93 -7
  73. data/lib/cmdx/logger_serializer.rb +116 -0
  74. data/lib/cmdx/middleware.rb +74 -0
  75. data/lib/cmdx/middleware_registry.rb +106 -0
  76. data/lib/cmdx/middlewares/correlate.rb +266 -0
  77. data/lib/cmdx/middlewares/timeout.rb +232 -0
  78. data/lib/cmdx/parameter.rb +228 -1
  79. data/lib/cmdx/parameter_inspector.rb +61 -0
  80. data/lib/cmdx/parameter_registry.rb +125 -0
  81. data/lib/cmdx/parameter_serializer.rb +83 -0
  82. data/lib/cmdx/parameter_validator.rb +62 -0
  83. data/lib/cmdx/parameter_value.rb +109 -1
  84. data/lib/cmdx/parameters_inspector.rb +59 -0
  85. data/lib/cmdx/parameters_serializer.rb +102 -0
  86. data/lib/cmdx/railtie.rb +123 -3
  87. data/lib/cmdx/result.rb +399 -20
  88. data/lib/cmdx/result_ansi.rb +105 -9
  89. data/lib/cmdx/result_inspector.rb +76 -0
  90. data/lib/cmdx/result_logger.rb +90 -3
  91. data/lib/cmdx/result_serializer.rb +137 -0
  92. data/lib/cmdx/rspec/result_matchers.rb +917 -0
  93. data/lib/cmdx/rspec/task_matchers.rb +570 -0
  94. data/lib/cmdx/task.rb +409 -34
  95. data/lib/cmdx/task_serializer.rb +74 -2
  96. data/lib/cmdx/utils/ansi_color.rb +95 -0
  97. data/lib/cmdx/utils/log_timestamp.rb +48 -0
  98. data/lib/cmdx/utils/monotonic_runtime.rb +71 -4
  99. data/lib/cmdx/utils/name_affix.rb +78 -0
  100. data/lib/cmdx/validators/custom.rb +82 -0
  101. data/lib/cmdx/validators/exclusion.rb +94 -0
  102. data/lib/cmdx/validators/format.rb +102 -8
  103. data/lib/cmdx/validators/inclusion.rb +104 -0
  104. data/lib/cmdx/validators/length.rb +128 -0
  105. data/lib/cmdx/validators/numeric.rb +128 -0
  106. data/lib/cmdx/validators/presence.rb +93 -7
  107. data/lib/cmdx/version.rb +7 -1
  108. data/lib/cmdx/workflow.rb +394 -0
  109. data/lib/cmdx.rb +25 -64
  110. data/lib/generators/cmdx/install_generator.rb +37 -1
  111. data/lib/generators/cmdx/task_generator.rb +69 -1
  112. data/lib/generators/cmdx/templates/install.rb +8 -12
  113. data/lib/generators/cmdx/workflow_generator.rb +109 -0
  114. metadata +54 -15
  115. data/docs/basics/run.md +0 -34
  116. data/docs/batch.md +0 -53
  117. data/docs/example.md +0 -82
  118. data/docs/hooks.md +0 -59
  119. data/lib/cmdx/batch.rb +0 -43
  120. data/lib/cmdx/parameters.rb +0 -34
  121. data/lib/cmdx/run.rb +0 -38
  122. data/lib/cmdx/run_inspector.rb +0 -26
  123. data/lib/cmdx/run_serializer.rb +0 -16
  124. data/lib/cmdx/task_hook.rb +0 -18
  125. data/lib/generators/cmdx/batch_generator.rb +0 -30
  126. /data/lib/generators/cmdx/templates/{batch.rb.tt → workflow.rb.tt} +0 -0
data/lib/cmdx/errors.rb CHANGED
@@ -1,22 +1,151 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CMDx
4
+ ##
5
+ # Errors provides a collection-like interface for managing validation and execution errors
6
+ # within CMDx tasks. It offers Rails-inspired methods for adding, querying, and formatting
7
+ # error messages, making it easy to accumulate and present multiple error conditions.
8
+ #
9
+ # The Errors class is designed to work seamlessly with CMDx's parameter validation system,
10
+ # automatically collecting validation failures and providing convenient methods for
11
+ # error reporting and user feedback.
12
+ #
13
+ #
14
+ # @example Basic error management
15
+ # errors = Errors.new
16
+ # errors.add(:email, "is required")
17
+ # errors.add(:email, "is invalid format")
18
+ # errors.add(:password, "is too short")
19
+ #
20
+ # errors.empty? #=> false
21
+ # errors.size #=> 2 (attributes with errors)
22
+ # errors[:email] #=> ["is required", "is invalid format"]
23
+ # errors.full_messages #=> ["email is required", "email is invalid format", "password is too short"]
24
+ #
25
+ # @example Task integration
26
+ # class CreateUserTask < CMDx::Task
27
+ # required :email, type: :string
28
+ # required :password, type: :string
29
+ #
30
+ # def call
31
+ # validate_email
32
+ # validate_password
33
+ #
34
+ # if errors.present?
35
+ # fail!(reason: "Validation failed", validation_errors: errors.full_messages)
36
+ # end
37
+ #
38
+ # create_user
39
+ # end
40
+ #
41
+ # private
42
+ #
43
+ # def validate_email
44
+ # errors.add(:email, "is required") if email.blank?
45
+ # errors.add(:email, "is invalid") unless email.include?("@")
46
+ # end
47
+ #
48
+ # def validate_password
49
+ # errors.add(:password, "is too short") if password.length < 8
50
+ # end
51
+ # end
52
+ #
53
+ # @example Error querying and formatting
54
+ # errors = Errors.new
55
+ # errors.add(:name, "cannot be blank")
56
+ # errors.add(:age, "must be a number")
57
+ #
58
+ # # Checking for specific errors
59
+ # errors.key?(:name) #=> true
60
+ # errors.added?(:name, "cannot be blank") #=> true
61
+ # errors.of_kind?(:age, "must be positive") #=> false
62
+ #
63
+ # # Getting messages
64
+ # errors.messages_for(:name) #=> ["cannot be blank"]
65
+ # errors.full_messages_for(:name) #=> ["name cannot be blank"]
66
+ #
67
+ # # Converting to hash
68
+ # errors.to_hash #=> {:name => ["cannot be blank"], :age => ["must be a number"]}
69
+ # errors.to_hash(true) #=> {:name => ["name cannot be blank"], :age => ["age must be a number"]}
70
+ #
71
+ # @example Rails-style validation
72
+ # class ValidateUserTask < CMDx::Task
73
+ # required :user_data, type: :hash
74
+ #
75
+ # def call
76
+ # user = user_data
77
+ #
78
+ # errors.add(:email, "can't be blank") if user[:email].blank?
79
+ # errors.add(:email, "is invalid") unless valid_email?(user[:email])
80
+ # errors.add(:age, "must be at least 18") if user[:age] && user[:age] < 18
81
+ #
82
+ # return if errors.invalid?
83
+ #
84
+ # context.validated_user = user
85
+ # end
86
+ # end
87
+ #
88
+ # @see Task Task base class with errors attribute
89
+ # @see Parameter Parameter validation integration
90
+ # @see ValidationError Individual validation errors
91
+ # @since 1.0.0
4
92
  class Errors
5
93
 
6
- __cmdx_attr_delegator :clear, :delete, :each, :empty?, :key?, :keys, :size, :values, to: :errors
94
+ __cmdx_attr_delegator :clear, :delete, :each, :empty?, :key?, :keys, :size, :values,
95
+ to: :errors
7
96
 
97
+ ##
98
+ # @!attribute [r] errors
99
+ # @return [Hash] internal hash storing error messages by attribute
8
100
  attr_reader :errors
9
101
 
102
+ ##
103
+ # @!method attribute_names
104
+ # @return [Array<Symbol>] list of attributes that have errors
10
105
  alias attribute_names keys
106
+
107
+ ##
108
+ # @!method blank?
109
+ # @return [Boolean] true if no errors are present
11
110
  alias blank? empty?
111
+
112
+ ##
113
+ # @!method valid?
114
+ # @return [Boolean] true if no errors are present
12
115
  alias valid? empty?
116
+
117
+ ##
118
+ # Alias for {#key?}. Checks if an attribute has error messages.
13
119
  alias has_key? key?
120
+
121
+ ##
122
+ # Alias for {#key?}. Checks if an attribute has error messages.
14
123
  alias include? key?
15
124
 
125
+ ##
126
+ # Initializes a new Errors collection.
127
+ # Creates an empty hash to store error messages by attribute.
128
+ #
129
+ # @example
130
+ # errors = Errors.new
131
+ # errors.empty? #=> true
16
132
  def initialize
17
133
  @errors = {}
18
134
  end
19
135
 
136
+ ##
137
+ # Adds an error message to the specified attribute.
138
+ # Messages are stored in arrays and automatically deduplicated.
139
+ #
140
+ # @param key [Symbol, String] the attribute name
141
+ # @param value [String] the error message
142
+ # @return [Array<String>] the updated array of messages for the attribute
143
+ #
144
+ # @example Adding multiple errors
145
+ # errors.add(:email, "is required")
146
+ # errors.add(:email, "is invalid format")
147
+ # errors.add(:email, "is required") # Duplicate - ignored
148
+ # errors[:email] #=> ["is required", "is invalid format"]
20
149
  def add(key, value)
21
150
  errors[key] ||= []
22
151
  errors[key] << value
@@ -24,6 +153,18 @@ module CMDx
24
153
  end
25
154
  alias []= add
26
155
 
156
+ ##
157
+ # Checks if a specific error message has been added to an attribute.
158
+ #
159
+ # @param key [Symbol, String] the attribute name
160
+ # @param val [String] the error message to check for
161
+ # @return [Boolean] true if the specific error exists
162
+ #
163
+ # @example
164
+ # errors.add(:name, "is required")
165
+ # errors.added?(:name, "is required") #=> true
166
+ # errors.added?(:name, "is too long") #=> false
167
+ # errors.of_kind?(:name, "is required") #=> true (alias)
27
168
  def added?(key, val)
28
169
  return false unless key?(key)
29
170
 
@@ -31,16 +172,53 @@ module CMDx
31
172
  end
32
173
  alias of_kind? added?
33
174
 
175
+ ##
176
+ # Iterates over each error, yielding the attribute and message.
177
+ # Flattens the error structure so each message is yielded individually.
178
+ #
179
+ # @yieldparam key [Symbol] the attribute name
180
+ # @yieldparam val [String] the error message
181
+ # @return [Enumerator] if no block given
182
+ #
183
+ # @example
184
+ # errors.add(:name, "is required")
185
+ # errors.add(:email, "is invalid")
186
+ # errors.each { |attr, msg| puts "#{attr}: #{msg}" }
187
+ # # Output:
188
+ # # name: is required
189
+ # # email: is invalid
34
190
  def each
35
191
  errors.each_key do |key|
36
192
  errors[key].each { |val| yield(key, val) }
37
193
  end
38
194
  end
39
195
 
196
+ ##
197
+ # Generates a full error message by combining attribute name and message.
198
+ #
199
+ # @param key [Symbol, String] the attribute name
200
+ # @param value [String] the error message
201
+ # @return [String] formatted full message
202
+ #
203
+ # @example
204
+ # errors.full_message(:email, "is required") #=> "email is required"
205
+ # errors.full_message(:age, "must be positive") #=> "age must be positive"
40
206
  def full_message(key, value)
41
207
  "#{key} #{value}"
42
208
  end
43
209
 
210
+ ##
211
+ # Returns an array of all full error messages.
212
+ # Combines attribute names with their error messages.
213
+ #
214
+ # @return [Array<String>] array of formatted error messages
215
+ #
216
+ # @example
217
+ # errors.add(:email, "is required")
218
+ # errors.add(:email, "is invalid")
219
+ # errors.add(:password, "is too short")
220
+ # errors.full_messages
221
+ # #=> ["email is required", "email is invalid", "password is too short"]
44
222
  def full_messages
45
223
  errors.each_with_object([]) do |(key, arr), memo|
46
224
  arr.each { |val| memo << full_message(key, val) }
@@ -48,16 +226,54 @@ module CMDx
48
226
  end
49
227
  alias to_a full_messages
50
228
 
229
+ ##
230
+ # Returns full error messages for a specific attribute.
231
+ #
232
+ # @param key [Symbol, String] the attribute name
233
+ # @return [Array<String>] array of full messages for the attribute
234
+ #
235
+ # @example
236
+ # errors.add(:email, "is required")
237
+ # errors.add(:email, "is invalid")
238
+ # errors.full_messages_for(:email)
239
+ # #=> ["email is required", "email is invalid"]
240
+ # errors.full_messages_for(:missing) #=> []
51
241
  def full_messages_for(key)
52
242
  return [] unless key?(key)
53
243
 
54
244
  errors[key].map { |val| full_message(key, val) }
55
245
  end
56
246
 
247
+ ##
248
+ # Checks if any errors are present.
249
+ #
250
+ # @return [Boolean] true if errors exist
251
+ #
252
+ # @example
253
+ # errors = Errors.new
254
+ # errors.invalid? #=> false
255
+ # errors.add(:name, "is required")
256
+ # errors.invalid? #=> true
57
257
  def invalid?
58
258
  !valid?
59
259
  end
60
260
 
261
+ ##
262
+ # Merges another hash of errors into this collection.
263
+ # Combines arrays of messages for attributes that exist in both collections.
264
+ #
265
+ # @param hash [Hash] hash of attribute => messages to merge
266
+ # @return [Hash] the updated errors hash
267
+ #
268
+ # @example
269
+ # errors1 = Errors.new
270
+ # errors1.add(:email, "is required")
271
+ #
272
+ # errors2 = { email: ["is invalid"], password: ["is too short"] }
273
+ # errors1.merge!(errors2)
274
+ #
275
+ # errors1[:email] #=> ["is required", "is invalid"]
276
+ # errors1[:password] #=> ["is too short"]
61
277
  def merge!(hash)
62
278
  errors.merge!(hash) do |_, arr1, arr2|
63
279
  arr3 = arr1 + arr2
@@ -66,6 +282,17 @@ module CMDx
66
282
  end
67
283
  end
68
284
 
285
+ ##
286
+ # Returns error messages for a specific attribute.
287
+ #
288
+ # @param key [Symbol, String] the attribute name
289
+ # @return [Array<String>] array of error messages for the attribute
290
+ #
291
+ # @example
292
+ # errors.add(:email, "is required")
293
+ # errors.add(:email, "is invalid")
294
+ # errors.messages_for(:email) #=> ["is required", "is invalid"]
295
+ # errors[:email] #=> ["is required", "is invalid"] (alias)
69
296
  def messages_for(key)
70
297
  return [] unless key?(key)
71
298
 
@@ -73,10 +300,38 @@ module CMDx
73
300
  end
74
301
  alias [] messages_for
75
302
 
303
+ ##
304
+ # Checks if any errors are present.
305
+ #
306
+ # @return [Boolean] true if errors exist
307
+ #
308
+ # @example
309
+ # errors = Errors.new
310
+ # errors.present? #=> false
311
+ # errors.add(:name, "is required")
312
+ # errors.present? #=> true
76
313
  def present?
77
314
  !blank?
78
315
  end
79
316
 
317
+ ##
318
+ # Converts the errors collection to a hash representation.
319
+ #
320
+ # @param full_messages [Boolean] whether to include full formatted messages
321
+ # @return [Hash] hash representation of errors
322
+ #
323
+ # @example Raw messages
324
+ # errors.add(:email, "is required")
325
+ # errors.to_hash #=> {:email => ["is required"]}
326
+ #
327
+ # @example Full messages
328
+ # errors.add(:email, "is required")
329
+ # errors.to_hash(true) #=> {:email => ["email is required"]}
330
+ #
331
+ # @example Method aliases
332
+ # errors.messages #=> same as to_hash
333
+ # errors.group_by_attribute #=> same as to_hash
334
+ # errors.as_json #=> same as to_hash
80
335
  def to_hash(full_messages = false)
81
336
  return errors unless full_messages
82
337
 
data/lib/cmdx/fault.rb CHANGED
@@ -1,12 +1,109 @@
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.
8
+ #
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
4
80
  class Fault < Error
5
81
 
6
- __cmdx_attr_delegator :task, :run, :context, to: :result
82
+ __cmdx_attr_delegator :task, :chain, :context,
83
+ to: :result
7
84
 
85
+ ##
86
+ # @!attribute [r] result
87
+ # @return [Result] the result object that caused this fault
8
88
  attr_reader :result
9
89
 
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.
94
+ #
95
+ # @param result [Result] the result object containing fault details
96
+ #
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">
102
+ #
103
+ # @example Fault with I18n message
104
+ # # With custom locale configuration
105
+ # fault = Fault.new(result)
106
+ # fault.message #=> Localized message from I18n
10
107
  def initialize(result)
11
108
  @result = result
12
109
  super(result.metadata[:reason] || I18n.t("cmdx.faults.unspecified", default: "no reason given"))
@@ -14,11 +111,53 @@ module CMDx
14
111
 
15
112
  class << self
16
113
 
114
+ ##
115
+ # 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
+ #
119
+ # @param result [Result] the result object to build a fault from
120
+ # @return [Fault] a fault instance of the appropriate subclass
121
+ #
122
+ # @example Building a skipped fault
123
+ # result = MyTask.call(param: "value")
124
+ # result.skip!(reason: "Not needed")
125
+ # fault = Fault.build(result)
126
+ # fault.class #=> CMDx::Skipped
127
+ #
128
+ # @example Building a failed fault
129
+ # result = MyTask.call(param: "invalid")
130
+ # result.fail!(reason: "Validation error")
131
+ # fault = Fault.build(result)
132
+ # fault.class #=> CMDx::Failed
17
133
  def build(result)
18
134
  fault = CMDx.const_get(result.status.capitalize)
19
135
  fault.new(result)
20
136
  end
21
137
 
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.
141
+ #
142
+ # @param tasks [Array<Class>] task classes to match against
143
+ # @return [Class] a temporary fault class with custom matching logic
144
+ #
145
+ # @example Matching specific task types
146
+ # 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)
151
+ # end
152
+ #
153
+ # @example Multiple task matching
154
+ # payment_tasks = [ProcessPaymentTask, ValidateCardTask, ChargeCardTask]
155
+ # 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)
160
+ # end
22
161
  def for?(*tasks)
23
162
  temp_fault = Class.new(self) do
24
163
  def self.===(other)
@@ -29,8 +168,44 @@ module CMDx
29
168
  temp_fault.tap { |c| c.instance_variable_set(:@tasks, tasks) }
30
169
  end
31
170
 
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.
175
+ #
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
+ # @raise [ArgumentError] if no block is provided
179
+ #
180
+ # @example Matching by error code
181
+ # 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)
186
+ # end
187
+ #
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
195
+ #
196
+ # @example Complex matching logic
197
+ # 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)
206
+ # end
32
207
  def matches?(&block)
33
- raise ArgumentError, "a block is required" unless block_given?
208
+ raise ArgumentError, "block required" unless block_given?
34
209
 
35
210
  temp_fault = Class.new(self) do
36
211
  def self.===(other)