cmdx 1.1.2 → 1.5.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/.DS_Store +0 -0
- data/.cursor/prompts/docs.md +4 -1
- data/.cursor/prompts/llms.md +20 -0
- data/.cursor/prompts/rspec.md +4 -1
- data/.cursor/prompts/yardoc.md +3 -2
- data/.cursor/rules/cursor-instructions.mdc +55 -1
- data/.irbrc +6 -0
- data/.rubocop.yml +29 -18
- data/CHANGELOG.md +11 -132
- data/LLM.md +3317 -0
- data/README.md +68 -44
- data/docs/attributes/coercions.md +162 -0
- data/docs/attributes/defaults.md +90 -0
- data/docs/attributes/definitions.md +281 -0
- data/docs/attributes/naming.md +78 -0
- data/docs/attributes/validations.md +309 -0
- data/docs/basics/chain.md +56 -249
- data/docs/basics/context.md +56 -289
- data/docs/basics/execution.md +114 -0
- data/docs/basics/setup.md +37 -334
- data/docs/callbacks.md +89 -467
- data/docs/deprecation.md +91 -174
- data/docs/getting_started.md +212 -202
- data/docs/internationalization.md +11 -647
- data/docs/interruptions/exceptions.md +23 -198
- data/docs/interruptions/faults.md +71 -151
- data/docs/interruptions/halt.md +109 -186
- data/docs/logging.md +44 -256
- data/docs/middlewares.md +113 -426
- data/docs/outcomes/result.md +81 -228
- data/docs/outcomes/states.md +33 -221
- data/docs/outcomes/statuses.md +21 -311
- data/docs/tips_and_tricks.md +120 -70
- data/docs/workflows.md +99 -283
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/attribute.rb +229 -0
- data/lib/cmdx/attribute_registry.rb +94 -0
- data/lib/cmdx/attribute_value.rb +193 -0
- data/lib/cmdx/callback_registry.rb +69 -77
- data/lib/cmdx/chain.rb +56 -73
- data/lib/cmdx/coercion_registry.rb +52 -68
- data/lib/cmdx/coercions/array.rb +19 -18
- data/lib/cmdx/coercions/big_decimal.rb +20 -24
- data/lib/cmdx/coercions/boolean.rb +26 -25
- data/lib/cmdx/coercions/complex.rb +21 -22
- data/lib/cmdx/coercions/date.rb +25 -23
- data/lib/cmdx/coercions/date_time.rb +24 -25
- data/lib/cmdx/coercions/float.rb +25 -22
- data/lib/cmdx/coercions/hash.rb +31 -32
- data/lib/cmdx/coercions/integer.rb +30 -24
- data/lib/cmdx/coercions/rational.rb +29 -24
- data/lib/cmdx/coercions/string.rb +19 -22
- data/lib/cmdx/coercions/symbol.rb +37 -0
- data/lib/cmdx/coercions/time.rb +26 -25
- data/lib/cmdx/configuration.rb +49 -108
- data/lib/cmdx/context.rb +222 -44
- data/lib/cmdx/deprecator.rb +61 -0
- data/lib/cmdx/errors.rb +42 -252
- data/lib/cmdx/exceptions.rb +39 -0
- data/lib/cmdx/faults.rb +78 -39
- data/lib/cmdx/freezer.rb +51 -0
- data/lib/cmdx/identifier.rb +30 -0
- data/lib/cmdx/locale.rb +52 -0
- data/lib/cmdx/log_formatters/json.rb +21 -22
- data/lib/cmdx/log_formatters/key_value.rb +20 -22
- data/lib/cmdx/log_formatters/line.rb +15 -22
- data/lib/cmdx/log_formatters/logstash.rb +22 -23
- data/lib/cmdx/log_formatters/raw.rb +16 -22
- data/lib/cmdx/middleware_registry.rb +70 -74
- data/lib/cmdx/middlewares/correlate.rb +90 -54
- data/lib/cmdx/middlewares/runtime.rb +58 -0
- data/lib/cmdx/middlewares/timeout.rb +48 -68
- data/lib/cmdx/railtie.rb +12 -45
- data/lib/cmdx/result.rb +229 -314
- data/lib/cmdx/task.rb +194 -366
- data/lib/cmdx/utils/call.rb +49 -0
- data/lib/cmdx/utils/condition.rb +71 -0
- data/lib/cmdx/utils/format.rb +61 -0
- data/lib/cmdx/validator_registry.rb +63 -72
- data/lib/cmdx/validators/exclusion.rb +38 -67
- data/lib/cmdx/validators/format.rb +48 -49
- data/lib/cmdx/validators/inclusion.rb +43 -74
- data/lib/cmdx/validators/length.rb +101 -162
- data/lib/cmdx/validators/numeric.rb +95 -170
- data/lib/cmdx/validators/presence.rb +37 -50
- data/lib/cmdx/version.rb +1 -1
- data/lib/cmdx/worker.rb +178 -0
- data/lib/cmdx/workflow.rb +85 -81
- data/lib/cmdx.rb +19 -13
- data/lib/generators/cmdx/install_generator.rb +14 -13
- data/lib/generators/cmdx/task_generator.rb +25 -50
- data/lib/generators/cmdx/templates/install.rb +11 -46
- data/lib/generators/cmdx/templates/task.rb.tt +3 -2
- data/lib/locales/en.yml +18 -4
- data/src/cmdx-logo.png +0 -0
- metadata +32 -116
- data/docs/ai_prompts.md +0 -393
- data/docs/basics/call.md +0 -317
- data/docs/configuration.md +0 -344
- data/docs/parameters/coercions.md +0 -396
- data/docs/parameters/defaults.md +0 -335
- data/docs/parameters/definitions.md +0 -446
- data/docs/parameters/namespacing.md +0 -378
- data/docs/parameters/validations.md +0 -405
- data/docs/testing.md +0 -553
- data/lib/cmdx/callback.rb +0 -53
- data/lib/cmdx/chain_inspector.rb +0 -56
- data/lib/cmdx/chain_serializer.rb +0 -63
- data/lib/cmdx/coercion.rb +0 -57
- data/lib/cmdx/coercions/virtual.rb +0 -29
- data/lib/cmdx/core_ext/hash.rb +0 -83
- data/lib/cmdx/core_ext/module.rb +0 -98
- data/lib/cmdx/core_ext/object.rb +0 -125
- data/lib/cmdx/correlator.rb +0 -122
- data/lib/cmdx/error.rb +0 -67
- data/lib/cmdx/fault.rb +0 -140
- data/lib/cmdx/immutator.rb +0 -52
- data/lib/cmdx/lazy_struct.rb +0 -246
- data/lib/cmdx/log_formatters/pretty_json.rb +0 -40
- data/lib/cmdx/log_formatters/pretty_key_value.rb +0 -38
- data/lib/cmdx/log_formatters/pretty_line.rb +0 -41
- data/lib/cmdx/logger.rb +0 -49
- data/lib/cmdx/logger_ansi.rb +0 -68
- data/lib/cmdx/logger_serializer.rb +0 -116
- data/lib/cmdx/middleware.rb +0 -70
- data/lib/cmdx/parameter.rb +0 -312
- data/lib/cmdx/parameter_evaluator.rb +0 -231
- data/lib/cmdx/parameter_inspector.rb +0 -66
- data/lib/cmdx/parameter_registry.rb +0 -106
- data/lib/cmdx/parameter_serializer.rb +0 -59
- data/lib/cmdx/result_ansi.rb +0 -71
- data/lib/cmdx/result_inspector.rb +0 -71
- data/lib/cmdx/result_logger.rb +0 -59
- data/lib/cmdx/result_serializer.rb +0 -104
- data/lib/cmdx/rspec/matchers.rb +0 -28
- data/lib/cmdx/rspec/result_matchers/be_executed.rb +0 -42
- data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +0 -94
- data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +0 -94
- data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +0 -59
- data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +0 -57
- data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +0 -87
- data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +0 -51
- data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +0 -58
- data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +0 -59
- data/lib/cmdx/rspec/result_matchers/have_context.rb +0 -86
- data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +0 -54
- data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +0 -52
- data/lib/cmdx/rspec/result_matchers/have_metadata.rb +0 -114
- data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +0 -66
- data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +0 -64
- data/lib/cmdx/rspec/result_matchers/have_runtime.rb +0 -78
- data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +0 -76
- data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +0 -62
- data/lib/cmdx/rspec/task_matchers/have_callback.rb +0 -85
- data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +0 -68
- data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +0 -92
- data/lib/cmdx/rspec/task_matchers/have_middleware.rb +0 -46
- data/lib/cmdx/rspec/task_matchers/have_parameter.rb +0 -181
- data/lib/cmdx/task_deprecator.rb +0 -58
- data/lib/cmdx/task_processor.rb +0 -246
- data/lib/cmdx/task_serializer.rb +0 -57
- data/lib/cmdx/utils/ansi_color.rb +0 -73
- data/lib/cmdx/utils/log_timestamp.rb +0 -36
- data/lib/cmdx/utils/monotonic_runtime.rb +0 -34
- data/lib/cmdx/utils/name_affix.rb +0 -52
- data/lib/cmdx/validator.rb +0 -57
- data/lib/generators/cmdx/templates/workflow.rb.tt +0 -7
- data/lib/generators/cmdx/workflow_generator.rb +0 -84
- data/lib/locales/ar.yml +0 -35
- data/lib/locales/cs.yml +0 -35
- data/lib/locales/da.yml +0 -35
- data/lib/locales/de.yml +0 -35
- data/lib/locales/el.yml +0 -35
- data/lib/locales/es.yml +0 -35
- data/lib/locales/fi.yml +0 -35
- data/lib/locales/fr.yml +0 -35
- data/lib/locales/he.yml +0 -35
- data/lib/locales/hi.yml +0 -35
- data/lib/locales/it.yml +0 -35
- data/lib/locales/ja.yml +0 -35
- data/lib/locales/ko.yml +0 -35
- data/lib/locales/nl.yml +0 -35
- data/lib/locales/no.yml +0 -35
- data/lib/locales/pl.yml +0 -35
- data/lib/locales/pt.yml +0 -35
- data/lib/locales/ru.yml +0 -35
- data/lib/locales/sv.yml +0 -35
- data/lib/locales/th.yml +0 -35
- data/lib/locales/tr.yml +0 -35
- data/lib/locales/vi.yml +0 -35
- data/lib/locales/zh.yml +0 -35
data/lib/cmdx/errors.rb
CHANGED
@@ -1,284 +1,74 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module CMDx
|
4
|
-
#
|
5
|
-
# Provides
|
6
|
-
#
|
4
|
+
# Collection of validation and execution errors organized by attribute.
|
5
|
+
# Provides methods to add, query, and format error messages for different
|
6
|
+
# attributes in a task or workflow execution.
|
7
7
|
class Errors
|
8
8
|
|
9
|
-
|
10
|
-
to: :errors
|
9
|
+
extend Forwardable
|
11
10
|
|
12
|
-
|
13
|
-
attr_reader :errors
|
11
|
+
attr_reader :messages
|
14
12
|
|
15
|
-
|
16
|
-
alias attribute_names keys
|
13
|
+
def_delegators :messages, :empty?
|
17
14
|
|
18
|
-
#
|
19
|
-
alias blank? empty?
|
20
|
-
|
21
|
-
# @return [Boolean] true if no errors are present
|
22
|
-
alias valid? empty?
|
23
|
-
|
24
|
-
# Alias for {#key?}. Checks if an attribute has error messages.
|
25
|
-
alias has_key? key?
|
26
|
-
|
27
|
-
# Alias for {#key?}. Checks if an attribute has error messages.
|
28
|
-
alias include? key?
|
29
|
-
|
30
|
-
# Creates a new empty errors collection.
|
31
|
-
#
|
32
|
-
# @return [Errors] a new errors instance with empty internal hash
|
33
|
-
#
|
34
|
-
# @example Create new errors collection
|
35
|
-
# errors = CMDx::Errors.new
|
36
|
-
# errors.empty? # => true
|
15
|
+
# Initialize a new error collection.
|
37
16
|
def initialize
|
38
|
-
@
|
39
|
-
end
|
40
|
-
|
41
|
-
# Adds an error message to the specified attribute. Automatically handles
|
42
|
-
# array initialization and prevents duplicate messages for the same attribute.
|
43
|
-
#
|
44
|
-
# @param key [Symbol, String] the attribute name to associate the error with
|
45
|
-
# @param value [String, Object] the error message or error object to add
|
46
|
-
#
|
47
|
-
# @return [Array] the updated array of error messages for the attribute
|
48
|
-
#
|
49
|
-
# @example Add error to attribute
|
50
|
-
# errors.add(:name, "can't be blank")
|
51
|
-
# errors.add(:name, "is too short")
|
52
|
-
# errors.messages_for(:name) # => ["can't be blank", "is too short"]
|
53
|
-
#
|
54
|
-
# @example Prevent duplicate errors
|
55
|
-
# errors.add(:email, "is invalid")
|
56
|
-
# errors.add(:email, "is invalid")
|
57
|
-
# errors.messages_for(:email) # => ["is invalid"]
|
58
|
-
def add(key, value)
|
59
|
-
errors[key] ||= []
|
60
|
-
errors[key] << value
|
61
|
-
errors[key].uniq!
|
17
|
+
@messages = {}
|
62
18
|
end
|
63
|
-
alias []= add
|
64
19
|
|
65
|
-
#
|
66
|
-
#
|
67
|
-
# @param key [Symbol, String] the attribute name to check
|
68
|
-
# @param val [String, Object] the error message to look for
|
69
|
-
#
|
70
|
-
# @return [Boolean] true if the error exists for the attribute, false otherwise
|
71
|
-
#
|
72
|
-
# @example Check for specific error
|
73
|
-
# errors.add(:name, "can't be blank")
|
74
|
-
# errors.added?(:name, "can't be blank") # => true
|
75
|
-
# errors.added?(:name, "is invalid") # => false
|
76
|
-
#
|
77
|
-
# @example Check non-existent attribute
|
78
|
-
# errors.added?(:nonexistent, "error") # => false
|
79
|
-
def added?(key, val)
|
80
|
-
return false unless key?(key)
|
81
|
-
|
82
|
-
errors[key].include?(val)
|
83
|
-
end
|
84
|
-
alias of_kind? added?
|
85
|
-
|
86
|
-
# Iterates over each error, yielding the attribute name and error message.
|
87
|
-
#
|
88
|
-
# @yield [key, value] gives the attribute name and error message for each error
|
89
|
-
# @yieldparam key [Symbol, String] the attribute name
|
90
|
-
# @yieldparam value [String, Object] the error message
|
91
|
-
#
|
92
|
-
# @return [Hash] the errors hash when no block given
|
93
|
-
#
|
94
|
-
# @example Iterate over all errors
|
95
|
-
# errors.add(:name, "can't be blank")
|
96
|
-
# errors.add(:email, "is invalid")
|
97
|
-
# errors.each { |attr, msg| puts "#{attr}: #{msg}" }
|
98
|
-
# # Output:
|
99
|
-
# # name: can't be blank
|
100
|
-
# # email: is invalid
|
101
|
-
def each
|
102
|
-
errors.each_key do |key|
|
103
|
-
errors[key].each { |val| yield(key, val) }
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
# Formats an error message by combining the attribute name and error value.
|
108
|
-
#
|
109
|
-
# @param key [Symbol, String] the attribute name
|
110
|
-
# @param value [String, Object] the error message
|
111
|
-
#
|
112
|
-
# @return [String] the formatted full error message
|
113
|
-
#
|
114
|
-
# @example Format error message
|
115
|
-
# errors.full_message(:name, "can't be blank") # => "name can't be blank"
|
116
|
-
# errors.full_message(:email, "is invalid") # => "email is invalid"
|
117
|
-
def full_message(key, value)
|
118
|
-
"#{key} #{value}"
|
119
|
-
end
|
120
|
-
|
121
|
-
# Returns all error messages formatted with their attribute names.
|
122
|
-
#
|
123
|
-
# @return [Array<String>] array of formatted error messages
|
20
|
+
# Add an error message for a specific attribute.
|
124
21
|
#
|
125
|
-
# @
|
126
|
-
#
|
127
|
-
# errors.add(:email, "is invalid")
|
128
|
-
# errors.full_messages # => ["name can't be blank", "email is invalid"]
|
22
|
+
# @param attribute [Symbol] The attribute name associated with the error
|
23
|
+
# @param message [String] The error message to add
|
129
24
|
#
|
130
|
-
# @example
|
131
|
-
# errors
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
end
|
137
|
-
alias to_a full_messages
|
138
|
-
|
139
|
-
# Returns formatted error messages for a specific attribute.
|
140
|
-
#
|
141
|
-
# @param key [Symbol, String] the attribute name to get messages for
|
142
|
-
#
|
143
|
-
# @return [Array<String>] array of formatted error messages for the attribute
|
144
|
-
#
|
145
|
-
# @example Get messages for existing attribute
|
146
|
-
# errors.add(:name, "can't be blank")
|
147
|
-
# errors.add(:name, "is too short")
|
148
|
-
# errors.full_messages_for(:name) # => ["name can't be blank", "name is too short"]
|
149
|
-
#
|
150
|
-
# @example Get messages for non-existent attribute
|
151
|
-
# errors.full_messages_for(:nonexistent) # => []
|
152
|
-
def full_messages_for(key)
|
153
|
-
return [] unless key?(key)
|
154
|
-
|
155
|
-
errors[key].map { |val| full_message(key, val) }
|
156
|
-
end
|
157
|
-
|
158
|
-
# Checks if the errors collection contains any validation errors.
|
159
|
-
#
|
160
|
-
# @return [Boolean] true if there are any errors present, false otherwise
|
161
|
-
#
|
162
|
-
# @example Check invalid state
|
163
|
-
# errors.add(:name, "can't be blank")
|
164
|
-
# errors.invalid? # => true
|
165
|
-
#
|
166
|
-
# @example Check valid state
|
167
|
-
# errors.invalid? # => false
|
168
|
-
def invalid?
|
169
|
-
!valid?
|
170
|
-
end
|
171
|
-
|
172
|
-
# Transforms each error using the provided block and returns results as an array.
|
173
|
-
#
|
174
|
-
# @yield [key, value] gives the attribute name and error message for transformation
|
175
|
-
# @yieldparam key [Symbol, String] the attribute name
|
176
|
-
# @yieldparam value [String, Object] the error message
|
177
|
-
# @yieldreturn [Object] the transformed value to include in result array
|
178
|
-
#
|
179
|
-
# @return [Array] array of transformed error values
|
180
|
-
#
|
181
|
-
# @example Transform errors to uppercase messages
|
182
|
-
# errors.add(:name, "can't be blank")
|
183
|
-
# errors.add(:email, "is invalid")
|
184
|
-
# errors.map { |attr, msg| msg.upcase } # => ["CAN'T BE BLANK", "IS INVALID"]
|
185
|
-
#
|
186
|
-
# @example Create custom error objects
|
187
|
-
# errors.map { |attr, msg| { attribute: attr, message: msg } }
|
188
|
-
# # => [{ attribute: :name, message: "can't be blank" }]
|
189
|
-
def map
|
190
|
-
errors.each_with_object([]) do |(key, _arr), memo|
|
191
|
-
memo.concat(errors[key].map { |val| yield(key, val) })
|
192
|
-
end
|
193
|
-
end
|
25
|
+
# @example
|
26
|
+
# errors = CMDx::Errors.new
|
27
|
+
# errors.add(:email, "must be valid format")
|
28
|
+
# errors.add(:email, "cannot be blank")
|
29
|
+
def add(attribute, message)
|
30
|
+
return if message.empty?
|
194
31
|
|
195
|
-
|
196
|
-
|
197
|
-
# @param hash [Hash] hash of errors to merge, with attribute keys and message arrays as values
|
198
|
-
#
|
199
|
-
# @return [Hash] the updated internal errors hash
|
200
|
-
#
|
201
|
-
# @example Merge additional errors
|
202
|
-
# errors.add(:name, "can't be blank")
|
203
|
-
# other_errors = { email: ["is invalid"], name: ["is too short"] }
|
204
|
-
# errors.merge!(other_errors)
|
205
|
-
# errors.messages_for(:name) # => ["can't be blank", "is too short"]
|
206
|
-
# errors.messages_for(:email) # => ["is invalid"]
|
207
|
-
#
|
208
|
-
# @example Merge with duplicate prevention
|
209
|
-
# errors.add(:name, "can't be blank")
|
210
|
-
# duplicate_errors = { name: ["can't be blank", "is required"] }
|
211
|
-
# errors.merge!(duplicate_errors)
|
212
|
-
# errors.messages_for(:name) # => ["can't be blank", "is required"]
|
213
|
-
def merge!(hash)
|
214
|
-
errors.merge!(hash) do |_, arr1, arr2|
|
215
|
-
arr3 = arr1 + arr2
|
216
|
-
arr3.uniq!
|
217
|
-
arr3
|
218
|
-
end
|
32
|
+
messages[attribute] ||= Set.new
|
33
|
+
messages[attribute] << message
|
219
34
|
end
|
220
35
|
|
221
|
-
#
|
222
|
-
#
|
223
|
-
# @param key [Symbol, String] the attribute name to get messages for
|
36
|
+
# Check if there are any errors for a specific attribute.
|
224
37
|
#
|
225
|
-
# @
|
38
|
+
# @param attribute [Symbol] The attribute name to check for errors
|
226
39
|
#
|
227
|
-
# @
|
228
|
-
# errors.add(:name, "can't be blank")
|
229
|
-
# errors.add(:name, "is too short")
|
230
|
-
# errors.messages_for(:name) # => ["can't be blank", "is too short"]
|
40
|
+
# @return [Boolean] true if the attribute has errors, false otherwise
|
231
41
|
#
|
232
|
-
# @example
|
233
|
-
# errors.
|
234
|
-
|
235
|
-
|
42
|
+
# @example
|
43
|
+
# errors.for?(:email) # => true
|
44
|
+
# errors.for?(:name) # => false
|
45
|
+
def for?(attribute)
|
46
|
+
return false unless messages.key?(attribute)
|
236
47
|
|
237
|
-
|
48
|
+
!messages[attribute].empty?
|
238
49
|
end
|
239
|
-
alias [] messages_for
|
240
50
|
|
241
|
-
#
|
51
|
+
# Convert errors to a hash format with arrays of messages.
|
242
52
|
#
|
243
|
-
# @return [
|
53
|
+
# @return [Hash{Symbol => Array<String>}] Hash with attribute keys and message arrays
|
244
54
|
#
|
245
|
-
# @example
|
246
|
-
# errors.
|
247
|
-
|
248
|
-
|
249
|
-
# @example Check empty collection
|
250
|
-
# errors.present? # => false
|
251
|
-
def present?
|
252
|
-
!blank?
|
55
|
+
# @example
|
56
|
+
# errors.to_h # => { email: ["must be valid format", "cannot be blank"] }
|
57
|
+
def to_h
|
58
|
+
messages.transform_values(&:to_a)
|
253
59
|
end
|
254
60
|
|
255
|
-
#
|
256
|
-
#
|
257
|
-
# @param full_messages [Boolean] whether to format messages with attribute names
|
61
|
+
# Convert errors to a human-readable string format.
|
258
62
|
#
|
259
|
-
# @return [
|
260
|
-
# @option return [Array<String>] attribute_name array of error messages (raw or formatted)
|
63
|
+
# @return [String] Formatted error messages joined with periods
|
261
64
|
#
|
262
|
-
# @example
|
263
|
-
# errors.
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
# errors.to_hash(true) # => { :name => ["name can't be blank"], :email => ["email is invalid"] }
|
269
|
-
#
|
270
|
-
# @example Empty errors collection
|
271
|
-
# errors.to_hash # => {}
|
272
|
-
def to_hash(full_messages = false)
|
273
|
-
return errors unless full_messages
|
274
|
-
|
275
|
-
errors.each_with_object({}) do |(key, arr), memo|
|
276
|
-
memo[key] = arr.map { |val| full_message(key, val) }
|
277
|
-
end
|
65
|
+
# @example
|
66
|
+
# errors.to_s # => "email must be valid format. email cannot be blank"
|
67
|
+
def to_s
|
68
|
+
messages.each_with_object([]) do |(attribute, messages), memo|
|
69
|
+
messages.each { |message| memo << "#{attribute} #{message}" }
|
70
|
+
end.join(". ")
|
278
71
|
end
|
279
|
-
alias messages to_hash
|
280
|
-
alias group_by_attribute to_hash
|
281
|
-
alias as_json to_hash
|
282
72
|
|
283
73
|
end
|
284
74
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMDx
|
4
|
+
|
5
|
+
# Base exception class for all CMDx-related errors.
|
6
|
+
#
|
7
|
+
# This serves as the root exception class for all errors raised by the CMDx
|
8
|
+
# framework. It inherits from StandardError and provides a common base for
|
9
|
+
# handling CMDx-specific exceptions.
|
10
|
+
Error = Class.new(StandardError)
|
11
|
+
|
12
|
+
# Raised when attribute coercion fails during task execution.
|
13
|
+
#
|
14
|
+
# This error occurs when a attribute value cannot be converted to the expected
|
15
|
+
# type using the registered coercion handlers. It indicates that the provided
|
16
|
+
# value is incompatible with the attribute's defined type.
|
17
|
+
CoercionError = Class.new(Error)
|
18
|
+
|
19
|
+
# Raised when a deprecated task is used.
|
20
|
+
#
|
21
|
+
# This error occurs when a deprecated task is called. It indicates that the
|
22
|
+
# task is no longer supported and should be replaced with a newer alternative.
|
23
|
+
DeprecationError = Class.new(Error)
|
24
|
+
|
25
|
+
# Raised when an abstract method is called without being implemented.
|
26
|
+
#
|
27
|
+
# This error occurs when a subclass fails to implement required abstract
|
28
|
+
# methods such as 'task' in tasks. It indicates incomplete implementation
|
29
|
+
# of required functionality.
|
30
|
+
UndefinedMethodError = Class.new(Error)
|
31
|
+
|
32
|
+
# Raised when attribute validation fails during task execution.
|
33
|
+
#
|
34
|
+
# This error occurs when a attribute value doesn't meet the validation criteria
|
35
|
+
# defined by the validator. It indicates that the provided value violates
|
36
|
+
# business rules or data integrity constraints.
|
37
|
+
ValidationError = Class.new(Error)
|
38
|
+
|
39
|
+
end
|
data/lib/cmdx/faults.rb
CHANGED
@@ -2,55 +2,94 @@
|
|
2
2
|
|
3
3
|
module CMDx
|
4
4
|
|
5
|
+
# Base fault class for handling task execution failures and interruptions.
|
6
|
+
#
|
7
|
+
# Faults represent error conditions that occur during task execution, providing
|
8
|
+
# a structured way to handle and categorize different types of failures.
|
9
|
+
# Each fault contains a reference to the result object that caused the fault.
|
10
|
+
class Fault < Error
|
11
|
+
|
12
|
+
extend Forwardable
|
13
|
+
|
14
|
+
attr_reader :result
|
15
|
+
|
16
|
+
def_delegators :result, :task, :context, :chain
|
17
|
+
|
18
|
+
# Initialize a new fault with the given result.
|
19
|
+
#
|
20
|
+
# @param result [Result] the result object that caused this fault
|
21
|
+
#
|
22
|
+
# @raise [ArgumentError] if result is nil or invalid
|
23
|
+
#
|
24
|
+
# @example
|
25
|
+
# fault = Fault.new(task_result)
|
26
|
+
# fault.result.reason # => "Task validation failed"
|
27
|
+
def initialize(result)
|
28
|
+
@result = result
|
29
|
+
|
30
|
+
super(result.reason)
|
31
|
+
end
|
32
|
+
|
33
|
+
class << self
|
34
|
+
|
35
|
+
# Create a fault class that matches specific task types.
|
36
|
+
#
|
37
|
+
# @param tasks [Array<Class>] array of task classes to match against
|
38
|
+
#
|
39
|
+
# @return [Class] a new fault class that matches the specified tasks
|
40
|
+
#
|
41
|
+
# @example
|
42
|
+
# Fault.for?(UserTask, AdminUserTask)
|
43
|
+
# # => true if fault.task is a UserTask or AdminUserTask
|
44
|
+
def for?(*tasks)
|
45
|
+
temp_fault = Class.new(self) do
|
46
|
+
def self.===(other)
|
47
|
+
other.is_a?(superclass) && @tasks.any? { |task| other.task.is_a?(task) }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
temp_fault.tap { |c| c.instance_variable_set(:@tasks, tasks) }
|
52
|
+
end
|
53
|
+
|
54
|
+
# Create a fault class that matches based on a custom block.
|
55
|
+
#
|
56
|
+
# @param block [Proc] block that determines if a fault matches
|
57
|
+
#
|
58
|
+
# @return [Class] a new fault class that matches based on the block
|
59
|
+
#
|
60
|
+
# @raise [ArgumentError] if no block is provided
|
61
|
+
#
|
62
|
+
# @example
|
63
|
+
# Fault.matches? { |fault| fault.result.metadata[:critical] }
|
64
|
+
# # => true if fault has critical metadata
|
65
|
+
def matches?(&block)
|
66
|
+
raise ArgumentError, "block required" unless block_given?
|
67
|
+
|
68
|
+
temp_fault = Class.new(self) do
|
69
|
+
def self.===(other)
|
70
|
+
other.is_a?(superclass) && @block.call(other)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
temp_fault.tap { |c| c.instance_variable_set(:@block, block) }
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
5
81
|
# Fault raised when a task is intentionally skipped during execution.
|
6
82
|
#
|
7
83
|
# This fault occurs when a task determines it should not execute based on
|
8
84
|
# its current context or conditions. Skipped tasks are not considered failures
|
9
85
|
# but rather intentional bypasses of task execution logic.
|
10
|
-
|
11
|
-
# @example Task that skips based on conditions
|
12
|
-
# class ProcessPaymentTask < CMDx::Task
|
13
|
-
# def call
|
14
|
-
# skip!(reason: "Payment already processed") if payment_exists?
|
15
|
-
# end
|
16
|
-
# end
|
17
|
-
#
|
18
|
-
# result = ProcessPaymentTask.call(payment_id: 123)
|
19
|
-
# # raises CMDx::Skipped when payment already exists
|
20
|
-
#
|
21
|
-
# @example Catching skipped faults
|
22
|
-
# begin
|
23
|
-
# MyTask.call!(data: "invalid")
|
24
|
-
# rescue CMDx::Skipped => e
|
25
|
-
# puts "Task was skipped: #{e.message}"
|
26
|
-
# end
|
27
|
-
Skipped = Class.new(Fault)
|
86
|
+
SkipFault = Class.new(Fault)
|
28
87
|
|
29
88
|
# Fault raised when a task execution fails due to errors or validation failures.
|
30
89
|
#
|
31
90
|
# This fault occurs when a task encounters an error condition, validation failure,
|
32
91
|
# or any other condition that prevents successful completion. Failed tasks indicate
|
33
92
|
# that the intended operation could not be completed successfully.
|
34
|
-
|
35
|
-
# @example Task that fails due to validation
|
36
|
-
# class ValidateUserTask < CMDx::Task
|
37
|
-
# required :email, type: :string
|
38
|
-
#
|
39
|
-
# def call
|
40
|
-
# fail!(reason: "Invalid email format") unless valid_email?
|
41
|
-
# end
|
42
|
-
# end
|
43
|
-
#
|
44
|
-
# result = ValidateUserTask.call(email: "invalid-email")
|
45
|
-
# # raises CMDx::Failed when email is invalid
|
46
|
-
#
|
47
|
-
# @example Catching failed faults
|
48
|
-
# begin
|
49
|
-
# RiskyTask.call!(data: "problematic")
|
50
|
-
# rescue CMDx::Failed => e
|
51
|
-
# puts "Task failed: #{e.message}"
|
52
|
-
# puts "Original task: #{e.task.class.name}"
|
53
|
-
# end
|
54
|
-
Failed = Class.new(Fault)
|
93
|
+
FailFault = Class.new(Fault)
|
55
94
|
|
56
95
|
end
|
data/lib/cmdx/freezer.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMDx
|
4
|
+
# Provides freezing functionality for CMDx tasks and their associated objects.
|
5
|
+
#
|
6
|
+
# The Freezer module is responsible for making task objects immutable after execution
|
7
|
+
# to prevent accidental modifications and ensure data integrity. It can be disabled
|
8
|
+
# via environment variable for testing or debugging purposes.
|
9
|
+
module Freezer
|
10
|
+
|
11
|
+
extend self
|
12
|
+
|
13
|
+
# Freezes a task and its associated objects to prevent modifications.
|
14
|
+
#
|
15
|
+
# This method makes the task, result, context, and chain immutable after execution.
|
16
|
+
# Freezing can be skipped by setting the SKIP_CMDX_FREEZING environment variable.
|
17
|
+
#
|
18
|
+
# @param task [Task] The task instance to freeze
|
19
|
+
# @option ENV["SKIP_CMDX_FREEZING"] [String, Boolean] Set to "true" or true to skip freezing
|
20
|
+
#
|
21
|
+
# @raise [RuntimeError] If attempting to stub on frozen objects
|
22
|
+
#
|
23
|
+
# @example Freeze a completed task
|
24
|
+
# task = MyTask.new
|
25
|
+
# task.execute
|
26
|
+
# CMDx::Freezer.immute(task)
|
27
|
+
# # task, result, context, and chain are now frozen
|
28
|
+
# @example Skip freezing for testing
|
29
|
+
# ENV["SKIP_CMDX_FREEZING"] = "true"
|
30
|
+
# CMDx::Freezer.immute(task)
|
31
|
+
# # No freezing occurs
|
32
|
+
def immute(task)
|
33
|
+
# Stubbing on frozen objects is not allowed
|
34
|
+
skip_freezing = ENV.fetch("SKIP_CMDX_FREEZING", false)
|
35
|
+
return if Coercions::Boolean.call(skip_freezing)
|
36
|
+
|
37
|
+
task.freeze
|
38
|
+
task.result.freeze
|
39
|
+
|
40
|
+
# Freezing the context and chain can only be done
|
41
|
+
# once the outer-most task has completed.
|
42
|
+
return unless task.result.index.zero?
|
43
|
+
|
44
|
+
task.context.freeze
|
45
|
+
task.chain.freeze
|
46
|
+
|
47
|
+
Chain.clear
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMDx
|
4
|
+
# Generates unique identifiers for tasks, workflows, and other CMDx components.
|
5
|
+
#
|
6
|
+
# The Identifier module provides a consistent way to generate unique identifiers
|
7
|
+
# across the CMDx system, with fallback support for different Ruby versions.
|
8
|
+
module Identifier
|
9
|
+
|
10
|
+
extend self
|
11
|
+
|
12
|
+
# Generates a unique identifier string.
|
13
|
+
#
|
14
|
+
# @return [String] A unique identifier string (UUID v7 if available, otherwise UUID v4)
|
15
|
+
#
|
16
|
+
# @raise [StandardError] If SecureRandom is unavailable or fails to generate an identifier
|
17
|
+
#
|
18
|
+
# @example Generate a unique identifier
|
19
|
+
# CMDx::Identifier.generate
|
20
|
+
# # => "01890b2c-1234-5678-9abc-def123456789"
|
21
|
+
def generate
|
22
|
+
if SecureRandom.respond_to?(:uuid_v7)
|
23
|
+
SecureRandom.uuid_v7
|
24
|
+
else
|
25
|
+
SecureRandom.uuid
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
data/lib/cmdx/locale.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMDx
|
4
|
+
# Provides internationalization and localization support for CMDx.
|
5
|
+
# Handles translation lookups with fallback to default English messages
|
6
|
+
# when I18n gem is not available.
|
7
|
+
module Locale
|
8
|
+
|
9
|
+
extend self
|
10
|
+
|
11
|
+
EN = YAML.load_file(CMDx.gem_path.join("lib/locales/en.yml")).freeze
|
12
|
+
private_constant :EN
|
13
|
+
|
14
|
+
# Translates a key to the current locale with optional interpolation.
|
15
|
+
# Falls back to English translations if I18n gem is unavailable.
|
16
|
+
#
|
17
|
+
# @param key [String, Symbol] The translation key (supports dot notation)
|
18
|
+
# @param options [Hash] Translation options
|
19
|
+
# @option options [String] :default Fallback message if translation missing
|
20
|
+
# @option options [String] :locale Target locale (when I18n available)
|
21
|
+
# @option options [Hash] :scope Translation scope (when I18n available)
|
22
|
+
# @option options [Object] :* Any other options passed to I18n.t or string interpolation
|
23
|
+
#
|
24
|
+
# @return [String] The translated message
|
25
|
+
#
|
26
|
+
# @raise [ArgumentError] When interpolation fails due to missing keys
|
27
|
+
#
|
28
|
+
# @example Basic translation
|
29
|
+
# Locale.translate("errors.invalid_input")
|
30
|
+
# # => "Invalid input provided"
|
31
|
+
# @example With interpolation
|
32
|
+
# Locale.translate("welcome.message", name: "John")
|
33
|
+
# # => "Welcome, John!"
|
34
|
+
# @example With fallback
|
35
|
+
# Locale.translate("missing.key", default: "Custom fallback message")
|
36
|
+
# # => "Custom fallback message"
|
37
|
+
def translate(key, **options)
|
38
|
+
options[:default] ||= EN.dig("en", *key.to_s.split("."))
|
39
|
+
return ::I18n.t(key, **options) if defined?(::I18n)
|
40
|
+
|
41
|
+
case message = options.delete(:default)
|
42
|
+
when NilClass then "Translation missing: #{key}"
|
43
|
+
when String then message % options
|
44
|
+
else message
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# @see #translate
|
49
|
+
alias t translate
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|