cmdx 0.5.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.
- checksums.yaml +4 -4
- data/.DS_Store +0 -0
- data/.cursor/rules/cursor-instructions.mdc +6 -0
- data/.rubocop.yml +16 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +31 -1
- data/README.md +72 -25
- data/docs/ai_prompts.md +309 -0
- data/docs/basics/call.md +225 -14
- data/docs/basics/chain.md +271 -0
- data/docs/basics/context.md +232 -33
- data/docs/basics/setup.md +76 -12
- data/docs/callbacks.md +273 -0
- data/docs/configuration.md +158 -28
- data/docs/getting_started.md +134 -22
- data/docs/interruptions/exceptions.md +189 -11
- data/docs/interruptions/faults.md +187 -44
- data/docs/interruptions/halt.md +179 -35
- data/docs/logging.md +194 -53
- data/docs/middlewares.md +735 -0
- data/docs/outcomes/result.md +296 -10
- data/docs/outcomes/states.md +203 -31
- data/docs/outcomes/statuses.md +275 -30
- data/docs/parameters/coercions.md +402 -29
- data/docs/parameters/defaults.md +249 -25
- data/docs/parameters/definitions.md +238 -72
- data/docs/parameters/namespacing.md +250 -27
- data/docs/parameters/validations.md +193 -168
- data/docs/testing.md +550 -0
- data/docs/tips_and_tricks.md +95 -43
- data/docs/workflows.md +319 -0
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/callback.rb +69 -0
- data/lib/cmdx/callback_registry.rb +106 -0
- data/lib/cmdx/chain.rb +190 -0
- data/lib/cmdx/chain_inspector.rb +149 -0
- data/lib/cmdx/chain_serializer.rb +175 -0
- data/lib/cmdx/coercions/array.rb +37 -0
- data/lib/cmdx/coercions/big_decimal.rb +33 -0
- data/lib/cmdx/coercions/boolean.rb +41 -1
- data/lib/cmdx/coercions/complex.rb +31 -0
- data/lib/cmdx/coercions/date.rb +39 -0
- data/lib/cmdx/coercions/date_time.rb +39 -0
- data/lib/cmdx/coercions/float.rb +31 -0
- data/lib/cmdx/coercions/hash.rb +42 -0
- data/lib/cmdx/coercions/integer.rb +32 -0
- data/lib/cmdx/coercions/rational.rb +31 -0
- data/lib/cmdx/coercions/string.rb +31 -0
- data/lib/cmdx/coercions/time.rb +39 -0
- data/lib/cmdx/coercions/virtual.rb +31 -0
- data/lib/cmdx/configuration.rb +217 -9
- data/lib/cmdx/context.rb +173 -2
- data/lib/cmdx/core_ext/hash.rb +72 -0
- data/lib/cmdx/core_ext/module.rb +94 -0
- data/lib/cmdx/core_ext/object.rb +105 -0
- data/lib/cmdx/correlator.rb +217 -0
- data/lib/cmdx/error.rb +210 -8
- data/lib/cmdx/errors.rb +256 -1
- data/lib/cmdx/fault.rb +177 -2
- data/lib/cmdx/faults.rb +158 -2
- data/lib/cmdx/immutator.rb +121 -2
- data/lib/cmdx/lazy_struct.rb +261 -18
- data/lib/cmdx/log_formatters/json.rb +46 -0
- data/lib/cmdx/log_formatters/key_value.rb +46 -0
- data/lib/cmdx/log_formatters/line.rb +54 -0
- data/lib/cmdx/log_formatters/logstash.rb +64 -0
- data/lib/cmdx/log_formatters/pretty_json.rb +57 -0
- data/lib/cmdx/log_formatters/pretty_key_value.rb +51 -0
- data/lib/cmdx/log_formatters/pretty_line.rb +60 -0
- data/lib/cmdx/log_formatters/raw.rb +54 -0
- data/lib/cmdx/logger.rb +85 -0
- data/lib/cmdx/logger_ansi.rb +93 -7
- data/lib/cmdx/logger_serializer.rb +116 -0
- data/lib/cmdx/middleware.rb +74 -0
- data/lib/cmdx/middleware_registry.rb +106 -0
- data/lib/cmdx/middlewares/correlate.rb +266 -0
- data/lib/cmdx/middlewares/timeout.rb +232 -0
- data/lib/cmdx/parameter.rb +228 -1
- data/lib/cmdx/parameter_inspector.rb +61 -0
- data/lib/cmdx/parameter_registry.rb +125 -0
- data/lib/cmdx/parameter_serializer.rb +83 -0
- data/lib/cmdx/parameter_validator.rb +62 -0
- data/lib/cmdx/parameter_value.rb +109 -1
- data/lib/cmdx/parameters_inspector.rb +59 -0
- data/lib/cmdx/parameters_serializer.rb +102 -0
- data/lib/cmdx/railtie.rb +123 -3
- data/lib/cmdx/result.rb +367 -25
- data/lib/cmdx/result_ansi.rb +105 -9
- data/lib/cmdx/result_inspector.rb +76 -0
- data/lib/cmdx/result_logger.rb +90 -3
- data/lib/cmdx/result_serializer.rb +137 -0
- data/lib/cmdx/rspec/result_matchers.rb +917 -0
- data/lib/cmdx/rspec/task_matchers.rb +570 -0
- data/lib/cmdx/task.rb +405 -37
- data/lib/cmdx/task_serializer.rb +74 -2
- data/lib/cmdx/utils/ansi_color.rb +95 -0
- data/lib/cmdx/utils/log_timestamp.rb +48 -0
- data/lib/cmdx/utils/monotonic_runtime.rb +71 -4
- data/lib/cmdx/utils/name_affix.rb +78 -0
- data/lib/cmdx/validators/custom.rb +82 -0
- data/lib/cmdx/validators/exclusion.rb +94 -0
- data/lib/cmdx/validators/format.rb +102 -8
- data/lib/cmdx/validators/inclusion.rb +104 -0
- data/lib/cmdx/validators/length.rb +128 -0
- data/lib/cmdx/validators/numeric.rb +128 -0
- data/lib/cmdx/validators/presence.rb +93 -7
- data/lib/cmdx/version.rb +7 -1
- data/lib/cmdx/workflow.rb +394 -0
- data/lib/cmdx.rb +25 -64
- data/lib/generators/cmdx/install_generator.rb +37 -1
- data/lib/generators/cmdx/task_generator.rb +69 -1
- data/lib/generators/cmdx/templates/install.rb +8 -12
- data/lib/generators/cmdx/workflow_generator.rb +109 -0
- metadata +54 -15
- data/docs/basics/run.md +0 -34
- data/docs/batch.md +0 -53
- data/docs/example.md +0 -82
- data/docs/hooks.md +0 -62
- data/lib/cmdx/batch.rb +0 -43
- data/lib/cmdx/parameters.rb +0 -35
- data/lib/cmdx/run.rb +0 -39
- data/lib/cmdx/run_inspector.rb +0 -26
- data/lib/cmdx/run_serializer.rb +0 -20
- data/lib/cmdx/task_hook.rb +0 -18
- data/lib/generators/cmdx/batch_generator.rb +0 -30
- /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,
|
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, :
|
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, "
|
208
|
+
raise ArgumentError, "block required" unless block_given?
|
34
209
|
|
35
210
|
temp_fault = Class.new(self) do
|
36
211
|
def self.===(other)
|