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.
Files changed (192) hide show
  1. checksums.yaml +4 -4
  2. data/.DS_Store +0 -0
  3. data/.cursor/prompts/docs.md +4 -1
  4. data/.cursor/prompts/llms.md +20 -0
  5. data/.cursor/prompts/rspec.md +4 -1
  6. data/.cursor/prompts/yardoc.md +3 -2
  7. data/.cursor/rules/cursor-instructions.mdc +55 -1
  8. data/.irbrc +6 -0
  9. data/.rubocop.yml +29 -18
  10. data/CHANGELOG.md +11 -132
  11. data/LLM.md +3317 -0
  12. data/README.md +68 -44
  13. data/docs/attributes/coercions.md +162 -0
  14. data/docs/attributes/defaults.md +90 -0
  15. data/docs/attributes/definitions.md +281 -0
  16. data/docs/attributes/naming.md +78 -0
  17. data/docs/attributes/validations.md +309 -0
  18. data/docs/basics/chain.md +56 -249
  19. data/docs/basics/context.md +56 -289
  20. data/docs/basics/execution.md +114 -0
  21. data/docs/basics/setup.md +37 -334
  22. data/docs/callbacks.md +89 -467
  23. data/docs/deprecation.md +91 -174
  24. data/docs/getting_started.md +212 -202
  25. data/docs/internationalization.md +11 -647
  26. data/docs/interruptions/exceptions.md +23 -198
  27. data/docs/interruptions/faults.md +71 -151
  28. data/docs/interruptions/halt.md +109 -186
  29. data/docs/logging.md +44 -256
  30. data/docs/middlewares.md +113 -426
  31. data/docs/outcomes/result.md +81 -228
  32. data/docs/outcomes/states.md +33 -221
  33. data/docs/outcomes/statuses.md +21 -311
  34. data/docs/tips_and_tricks.md +120 -70
  35. data/docs/workflows.md +99 -283
  36. data/lib/cmdx/.DS_Store +0 -0
  37. data/lib/cmdx/attribute.rb +229 -0
  38. data/lib/cmdx/attribute_registry.rb +94 -0
  39. data/lib/cmdx/attribute_value.rb +193 -0
  40. data/lib/cmdx/callback_registry.rb +69 -77
  41. data/lib/cmdx/chain.rb +56 -73
  42. data/lib/cmdx/coercion_registry.rb +52 -68
  43. data/lib/cmdx/coercions/array.rb +19 -18
  44. data/lib/cmdx/coercions/big_decimal.rb +20 -24
  45. data/lib/cmdx/coercions/boolean.rb +26 -25
  46. data/lib/cmdx/coercions/complex.rb +21 -22
  47. data/lib/cmdx/coercions/date.rb +25 -23
  48. data/lib/cmdx/coercions/date_time.rb +24 -25
  49. data/lib/cmdx/coercions/float.rb +25 -22
  50. data/lib/cmdx/coercions/hash.rb +31 -32
  51. data/lib/cmdx/coercions/integer.rb +30 -24
  52. data/lib/cmdx/coercions/rational.rb +29 -24
  53. data/lib/cmdx/coercions/string.rb +19 -22
  54. data/lib/cmdx/coercions/symbol.rb +37 -0
  55. data/lib/cmdx/coercions/time.rb +26 -25
  56. data/lib/cmdx/configuration.rb +49 -108
  57. data/lib/cmdx/context.rb +222 -44
  58. data/lib/cmdx/deprecator.rb +61 -0
  59. data/lib/cmdx/errors.rb +42 -252
  60. data/lib/cmdx/exceptions.rb +39 -0
  61. data/lib/cmdx/faults.rb +78 -39
  62. data/lib/cmdx/freezer.rb +51 -0
  63. data/lib/cmdx/identifier.rb +30 -0
  64. data/lib/cmdx/locale.rb +52 -0
  65. data/lib/cmdx/log_formatters/json.rb +21 -22
  66. data/lib/cmdx/log_formatters/key_value.rb +20 -22
  67. data/lib/cmdx/log_formatters/line.rb +15 -22
  68. data/lib/cmdx/log_formatters/logstash.rb +22 -23
  69. data/lib/cmdx/log_formatters/raw.rb +16 -22
  70. data/lib/cmdx/middleware_registry.rb +70 -74
  71. data/lib/cmdx/middlewares/correlate.rb +90 -54
  72. data/lib/cmdx/middlewares/runtime.rb +58 -0
  73. data/lib/cmdx/middlewares/timeout.rb +48 -68
  74. data/lib/cmdx/railtie.rb +12 -45
  75. data/lib/cmdx/result.rb +229 -314
  76. data/lib/cmdx/task.rb +194 -366
  77. data/lib/cmdx/utils/call.rb +49 -0
  78. data/lib/cmdx/utils/condition.rb +71 -0
  79. data/lib/cmdx/utils/format.rb +61 -0
  80. data/lib/cmdx/validator_registry.rb +63 -72
  81. data/lib/cmdx/validators/exclusion.rb +38 -67
  82. data/lib/cmdx/validators/format.rb +48 -49
  83. data/lib/cmdx/validators/inclusion.rb +43 -74
  84. data/lib/cmdx/validators/length.rb +101 -162
  85. data/lib/cmdx/validators/numeric.rb +95 -170
  86. data/lib/cmdx/validators/presence.rb +37 -50
  87. data/lib/cmdx/version.rb +1 -1
  88. data/lib/cmdx/worker.rb +178 -0
  89. data/lib/cmdx/workflow.rb +85 -81
  90. data/lib/cmdx.rb +19 -13
  91. data/lib/generators/cmdx/install_generator.rb +14 -13
  92. data/lib/generators/cmdx/task_generator.rb +25 -50
  93. data/lib/generators/cmdx/templates/install.rb +11 -46
  94. data/lib/generators/cmdx/templates/task.rb.tt +3 -2
  95. data/lib/locales/en.yml +18 -4
  96. data/src/cmdx-logo.png +0 -0
  97. metadata +32 -116
  98. data/docs/ai_prompts.md +0 -393
  99. data/docs/basics/call.md +0 -317
  100. data/docs/configuration.md +0 -344
  101. data/docs/parameters/coercions.md +0 -396
  102. data/docs/parameters/defaults.md +0 -335
  103. data/docs/parameters/definitions.md +0 -446
  104. data/docs/parameters/namespacing.md +0 -378
  105. data/docs/parameters/validations.md +0 -405
  106. data/docs/testing.md +0 -553
  107. data/lib/cmdx/callback.rb +0 -53
  108. data/lib/cmdx/chain_inspector.rb +0 -56
  109. data/lib/cmdx/chain_serializer.rb +0 -63
  110. data/lib/cmdx/coercion.rb +0 -57
  111. data/lib/cmdx/coercions/virtual.rb +0 -29
  112. data/lib/cmdx/core_ext/hash.rb +0 -83
  113. data/lib/cmdx/core_ext/module.rb +0 -98
  114. data/lib/cmdx/core_ext/object.rb +0 -125
  115. data/lib/cmdx/correlator.rb +0 -122
  116. data/lib/cmdx/error.rb +0 -67
  117. data/lib/cmdx/fault.rb +0 -140
  118. data/lib/cmdx/immutator.rb +0 -52
  119. data/lib/cmdx/lazy_struct.rb +0 -246
  120. data/lib/cmdx/log_formatters/pretty_json.rb +0 -40
  121. data/lib/cmdx/log_formatters/pretty_key_value.rb +0 -38
  122. data/lib/cmdx/log_formatters/pretty_line.rb +0 -41
  123. data/lib/cmdx/logger.rb +0 -49
  124. data/lib/cmdx/logger_ansi.rb +0 -68
  125. data/lib/cmdx/logger_serializer.rb +0 -116
  126. data/lib/cmdx/middleware.rb +0 -70
  127. data/lib/cmdx/parameter.rb +0 -312
  128. data/lib/cmdx/parameter_evaluator.rb +0 -231
  129. data/lib/cmdx/parameter_inspector.rb +0 -66
  130. data/lib/cmdx/parameter_registry.rb +0 -106
  131. data/lib/cmdx/parameter_serializer.rb +0 -59
  132. data/lib/cmdx/result_ansi.rb +0 -71
  133. data/lib/cmdx/result_inspector.rb +0 -71
  134. data/lib/cmdx/result_logger.rb +0 -59
  135. data/lib/cmdx/result_serializer.rb +0 -104
  136. data/lib/cmdx/rspec/matchers.rb +0 -28
  137. data/lib/cmdx/rspec/result_matchers/be_executed.rb +0 -42
  138. data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +0 -94
  139. data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +0 -94
  140. data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +0 -59
  141. data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +0 -57
  142. data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +0 -87
  143. data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +0 -51
  144. data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +0 -58
  145. data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +0 -59
  146. data/lib/cmdx/rspec/result_matchers/have_context.rb +0 -86
  147. data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +0 -54
  148. data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +0 -52
  149. data/lib/cmdx/rspec/result_matchers/have_metadata.rb +0 -114
  150. data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +0 -66
  151. data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +0 -64
  152. data/lib/cmdx/rspec/result_matchers/have_runtime.rb +0 -78
  153. data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +0 -76
  154. data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +0 -62
  155. data/lib/cmdx/rspec/task_matchers/have_callback.rb +0 -85
  156. data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +0 -68
  157. data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +0 -92
  158. data/lib/cmdx/rspec/task_matchers/have_middleware.rb +0 -46
  159. data/lib/cmdx/rspec/task_matchers/have_parameter.rb +0 -181
  160. data/lib/cmdx/task_deprecator.rb +0 -58
  161. data/lib/cmdx/task_processor.rb +0 -246
  162. data/lib/cmdx/task_serializer.rb +0 -57
  163. data/lib/cmdx/utils/ansi_color.rb +0 -73
  164. data/lib/cmdx/utils/log_timestamp.rb +0 -36
  165. data/lib/cmdx/utils/monotonic_runtime.rb +0 -34
  166. data/lib/cmdx/utils/name_affix.rb +0 -52
  167. data/lib/cmdx/validator.rb +0 -57
  168. data/lib/generators/cmdx/templates/workflow.rb.tt +0 -7
  169. data/lib/generators/cmdx/workflow_generator.rb +0 -84
  170. data/lib/locales/ar.yml +0 -35
  171. data/lib/locales/cs.yml +0 -35
  172. data/lib/locales/da.yml +0 -35
  173. data/lib/locales/de.yml +0 -35
  174. data/lib/locales/el.yml +0 -35
  175. data/lib/locales/es.yml +0 -35
  176. data/lib/locales/fi.yml +0 -35
  177. data/lib/locales/fr.yml +0 -35
  178. data/lib/locales/he.yml +0 -35
  179. data/lib/locales/hi.yml +0 -35
  180. data/lib/locales/it.yml +0 -35
  181. data/lib/locales/ja.yml +0 -35
  182. data/lib/locales/ko.yml +0 -35
  183. data/lib/locales/nl.yml +0 -35
  184. data/lib/locales/no.yml +0 -35
  185. data/lib/locales/pl.yml +0 -35
  186. data/lib/locales/pt.yml +0 -35
  187. data/lib/locales/ru.yml +0 -35
  188. data/lib/locales/sv.yml +0 -35
  189. data/lib/locales/th.yml +0 -35
  190. data/lib/locales/tr.yml +0 -35
  191. data/lib/locales/vi.yml +0 -35
  192. 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
- # Container for collecting and managing validation and execution errors by attribute.
5
- # Provides a comprehensive API for adding, querying, and formatting error messages
6
- # with support for multiple errors per attribute and various output formats.
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
- cmdx_attr_delegator :clear, :delete, :empty?, :key?, :keys, :size, :values,
10
- to: :errors
9
+ extend Forwardable
11
10
 
12
- # @return [Hash] internal hash storing error messages by attribute
13
- attr_reader :errors
11
+ attr_reader :messages
14
12
 
15
- # @return [Array<Symbol>] list of attributes that have errors
16
- alias attribute_names keys
13
+ def_delegators :messages, :empty?
17
14
 
18
- # @return [Boolean] true if no errors are present
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
- @errors = {}
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
- # Checks if a specific error message has been added to an attribute.
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
- # @example Get all formatted messages
126
- # errors.add(:name, "can't be blank")
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 Empty errors collection
131
- # errors.full_messages # => []
132
- def full_messages
133
- errors.each_with_object([]) do |(key, arr), memo|
134
- arr.each { |val| memo << full_message(key, val) }
135
- end
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
- # Merges another errors hash into this collection, combining arrays for duplicate keys.
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
- # Returns the raw error messages for a specific attribute without formatting.
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
- # @return [Array] array of raw error messages for the attribute
38
+ # @param attribute [Symbol] The attribute name to check for errors
226
39
  #
227
- # @example Get raw messages for existing attribute
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 Get messages for non-existent attribute
233
- # errors.messages_for(:nonexistent) # => []
234
- def messages_for(key)
235
- return [] unless key?(key)
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
- errors[key]
48
+ !messages[attribute].empty?
238
49
  end
239
- alias [] messages_for
240
50
 
241
- # Checks if the errors collection contains any validation errors.
51
+ # Convert errors to a hash format with arrays of messages.
242
52
  #
243
- # @return [Boolean] true if there are any errors present, false otherwise
53
+ # @return [Hash{Symbol => Array<String>}] Hash with attribute keys and message arrays
244
54
  #
245
- # @example Check for errors presence
246
- # errors.add(:name, "can't be blank")
247
- # errors.present? # => true
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
- # Converts the errors collection to a hash format, optionally with full formatted messages.
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 [Hash] hash representation of errors
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 Get raw errors hash
263
- # errors.add(:name, "can't be blank")
264
- # errors.add(:email, "is invalid")
265
- # errors.to_hash # => { :name => ["can't be blank"], :email => ["is invalid"] }
266
- #
267
- # @example Get formatted errors hash
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
@@ -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
@@ -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