cmdx 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/prompts/docs.md +9 -0
  3. data/.cursor/prompts/rspec.md +13 -12
  4. data/.cursor/prompts/yardoc.md +11 -6
  5. data/CHANGELOG.md +13 -2
  6. data/README.md +1 -0
  7. data/docs/ai_prompts.md +269 -195
  8. data/docs/basics/call.md +124 -58
  9. data/docs/basics/chain.md +190 -160
  10. data/docs/basics/context.md +242 -154
  11. data/docs/basics/setup.md +302 -32
  12. data/docs/callbacks.md +390 -94
  13. data/docs/configuration.md +181 -65
  14. data/docs/deprecation.md +245 -0
  15. data/docs/getting_started.md +161 -39
  16. data/docs/internationalization.md +590 -70
  17. data/docs/interruptions/exceptions.md +135 -118
  18. data/docs/interruptions/faults.md +150 -125
  19. data/docs/interruptions/halt.md +134 -80
  20. data/docs/logging.md +181 -118
  21. data/docs/middlewares.md +150 -377
  22. data/docs/outcomes/result.md +140 -112
  23. data/docs/outcomes/states.md +134 -99
  24. data/docs/outcomes/statuses.md +204 -146
  25. data/docs/parameters/coercions.md +232 -281
  26. data/docs/parameters/defaults.md +224 -169
  27. data/docs/parameters/definitions.md +289 -141
  28. data/docs/parameters/namespacing.md +250 -161
  29. data/docs/parameters/validations.md +260 -133
  30. data/docs/testing.md +191 -197
  31. data/docs/workflows.md +143 -98
  32. data/lib/cmdx/callback.rb +23 -19
  33. data/lib/cmdx/callback_registry.rb +1 -3
  34. data/lib/cmdx/chain_inspector.rb +23 -23
  35. data/lib/cmdx/chain_serializer.rb +38 -19
  36. data/lib/cmdx/coercion.rb +20 -12
  37. data/lib/cmdx/coercion_registry.rb +51 -32
  38. data/lib/cmdx/configuration.rb +84 -31
  39. data/lib/cmdx/context.rb +32 -21
  40. data/lib/cmdx/core_ext/hash.rb +13 -13
  41. data/lib/cmdx/core_ext/module.rb +1 -1
  42. data/lib/cmdx/core_ext/object.rb +12 -12
  43. data/lib/cmdx/correlator.rb +60 -39
  44. data/lib/cmdx/errors.rb +105 -131
  45. data/lib/cmdx/fault.rb +66 -45
  46. data/lib/cmdx/immutator.rb +20 -21
  47. data/lib/cmdx/lazy_struct.rb +78 -70
  48. data/lib/cmdx/log_formatters/json.rb +1 -1
  49. data/lib/cmdx/log_formatters/key_value.rb +1 -1
  50. data/lib/cmdx/log_formatters/line.rb +1 -1
  51. data/lib/cmdx/log_formatters/logstash.rb +1 -1
  52. data/lib/cmdx/log_formatters/pretty_json.rb +1 -1
  53. data/lib/cmdx/log_formatters/pretty_key_value.rb +1 -1
  54. data/lib/cmdx/log_formatters/pretty_line.rb +1 -1
  55. data/lib/cmdx/log_formatters/raw.rb +2 -2
  56. data/lib/cmdx/logger.rb +19 -14
  57. data/lib/cmdx/logger_ansi.rb +33 -17
  58. data/lib/cmdx/logger_serializer.rb +85 -24
  59. data/lib/cmdx/middleware.rb +39 -21
  60. data/lib/cmdx/middleware_registry.rb +4 -3
  61. data/lib/cmdx/parameter.rb +151 -89
  62. data/lib/cmdx/parameter_inspector.rb +34 -21
  63. data/lib/cmdx/parameter_registry.rb +36 -30
  64. data/lib/cmdx/parameter_serializer.rb +21 -14
  65. data/lib/cmdx/result.rb +136 -135
  66. data/lib/cmdx/result_ansi.rb +31 -17
  67. data/lib/cmdx/result_inspector.rb +32 -27
  68. data/lib/cmdx/result_logger.rb +23 -14
  69. data/lib/cmdx/result_serializer.rb +65 -27
  70. data/lib/cmdx/task.rb +234 -113
  71. data/lib/cmdx/task_deprecator.rb +22 -25
  72. data/lib/cmdx/task_processor.rb +89 -88
  73. data/lib/cmdx/task_serializer.rb +27 -14
  74. data/lib/cmdx/utils/monotonic_runtime.rb +2 -4
  75. data/lib/cmdx/validator.rb +25 -16
  76. data/lib/cmdx/validator_registry.rb +53 -31
  77. data/lib/cmdx/validators/exclusion.rb +1 -1
  78. data/lib/cmdx/validators/format.rb +2 -2
  79. data/lib/cmdx/validators/inclusion.rb +2 -2
  80. data/lib/cmdx/validators/length.rb +2 -2
  81. data/lib/cmdx/validators/numeric.rb +3 -3
  82. data/lib/cmdx/validators/presence.rb +2 -2
  83. data/lib/cmdx/version.rb +1 -1
  84. data/lib/cmdx/workflow.rb +54 -33
  85. data/lib/generators/cmdx/task_generator.rb +6 -6
  86. data/lib/generators/cmdx/workflow_generator.rb +6 -6
  87. metadata +3 -1
@@ -1,6 +1,6 @@
1
1
  # Parameters - Validations
2
2
 
3
- Parameter values can be validated using built-in validators or custom validation logic. All validators support internationalization (i18n) and integrate seamlessly with CMDx's error handling system.
3
+ Parameter validations ensure data integrity by applying constraints to task inputs. All validators integrate with CMDx's error handling system and support internationalization for consistent error messaging across different locales.
4
4
 
5
5
  ## Table of Contents
6
6
 
@@ -8,53 +8,71 @@ Parameter values can be validated using built-in validators or custom validation
8
8
  - [Common Options](#common-options)
9
9
  - [Presence](#presence)
10
10
  - [Format](#format)
11
- - [Exclusion](#exclusion)
12
11
  - [Inclusion](#inclusion)
12
+ - [Exclusion](#exclusion)
13
13
  - [Length](#length)
14
14
  - [Numeric](#numeric)
15
- - [Validation Results](#validation-results)
15
+ - [Error Handling](#error-handling)
16
+ - [Conditional Validation](#conditional-validation)
16
17
 
17
18
  ## TLDR
18
19
 
19
- - **Built-in validators** - `presence`, `format`, `inclusion`, `exclusion`, `length`, `numeric`
20
- - **Common options** - All support `:allow_nil`, `:if`, `:unless`, `:message`
21
- - **Usage** - Add to parameter definitions: `required :email, presence: true, format: { with: /@/ }`
22
- - **Conditional** - Use `:if` and `:unless` for conditional validation
20
+ ```ruby
21
+ # Basic validation
22
+ required :email, presence: true, format: { with: /@/ }
23
+ required :status, inclusion: { in: %w[pending active] }
24
+ required :password, length: { min: 8 }
23
25
 
24
- ## Common Options
26
+ # Conditional validation
27
+ optional :phone, presence: { if: :phone_required? }
28
+ required :age, numeric: { min: 18, unless: :minor_allowed? }
25
29
 
26
- All validators support these common options:
30
+ # Custom messages
31
+ required :username, exclusion: { in: %w[admin root], message: "reserved name" }
32
+ ```
27
33
 
28
- | Option | Description |
29
- | ------------ | ----------- |
30
- | `:allow_nil` | Skip validation if the parameter value is `nil` |
31
- | `:if` | Callable method, proc or string to determine if validation should occur |
32
- | `:unless` | Callable method, proc, or string to determine if validation should not occur |
33
- | `:message` | Error message for violations. Fallback for specific error keys not provided |
34
+ ## Common Options
34
35
 
35
36
  > [!NOTE]
36
- > Validators on `optional` parameters only execute when arguments are supplied.
37
+ > Validators on `optional` parameters only execute when arguments are provided.
38
+
39
+ All validators support these common options:
40
+
41
+ | Option | Description |
42
+ |--------|-------------|
43
+ | `:allow_nil` | Skip validation when value is `nil` |
44
+ | `:if` | Method, proc, or string determining when to validate |
45
+ | `:unless` | Method, proc, or string determining when to skip validation |
46
+ | `:message` | Custom error message for validation failures |
37
47
 
38
48
  ## Presence
39
49
 
40
50
  Validates that parameter values are not empty using intelligent type checking:
51
+
41
52
  - **Strings**: Must contain non-whitespace characters
42
- - **Collections**: Must not be empty (arrays, hashes, etc.)
53
+ - **Collections**: Must not be empty (arrays, hashes, sets)
43
54
  - **Other objects**: Must not be `nil`
44
55
 
45
56
  > [!TIP]
46
- > For boolean fields where valid values are `true` and `false`, use `inclusion: { in: [true, false] }` instead of presence validation.
57
+ > For boolean fields accepting `true` and `false`, use `inclusion: { in: [true, false] }` instead of presence validation.
47
58
 
48
59
  ```ruby
49
60
  class CreateUserTask < CMDx::Task
50
61
  required :email, presence: true
51
- optional :phone, presence: { message: "cannot be blank" }
62
+ required :name, presence: { message: "cannot be blank" }
52
63
  required :active, inclusion: { in: [true, false] }
53
64
 
54
65
  def call
55
- User.create!(email: email, phone: phone, active: active)
66
+ User.create!(email: email, name: name, active: active)
56
67
  end
57
68
  end
69
+
70
+ # Valid inputs
71
+ CreateUserTask.call(email: "user@example.com", name: "John", active: true)
72
+
73
+ # Invalid inputs
74
+ CreateUserTask.call(email: "", name: " ", active: nil)
75
+ # → ValidationError: "email can't be blank. name cannot be blank. active must be one of: true, false"
58
76
  ```
59
77
 
60
78
  ## Format
@@ -65,10 +83,11 @@ Validates parameter values against regular expression patterns. Supports positiv
65
83
  class RegisterUserTask < CMDx::Task
66
84
  required :email, format: { with: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i }
67
85
  required :username, format: { without: /\A(admin|root|system)\z/i }
86
+
68
87
  optional :password, format: {
69
88
  with: /\A(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}\z/,
70
89
  without: /password|123456/i,
71
- if: :strong_password_required?
90
+ if: :secure_password_required?
72
91
  }
73
92
 
74
93
  def call
@@ -77,199 +96,307 @@ class RegisterUserTask < CMDx::Task
77
96
 
78
97
  private
79
98
 
80
- def strong_password_required?
81
- context.account.security_policy.strong_passwords?
99
+ def secure_password_required?
100
+ context.security_policy.enforce_strong_passwords?
82
101
  end
83
102
  end
84
103
  ```
85
104
 
86
105
  **Options:**
87
106
 
88
- | Option | Description |
89
- | ---------- | ----------- |
90
- | `:with` | Regular expression that value must match |
107
+ | Option | Description |
108
+ |--------|-------------|
109
+ | `:with` | Regular expression that value must match |
91
110
  | `:without` | Regular expression that value must not match |
92
111
 
93
- ## Exclusion
112
+ ## Inclusion
94
113
 
95
- Validates that parameter values are not in a specific enumerable (array, range, etc.).
114
+ > [!IMPORTANT]
115
+ > Validates that parameter values are within a specific set of allowed values (array, range, or other enumerable).
96
116
 
97
117
  ```ruby
98
- class ProcessPaymentTask < CMDx::Task
99
- required :payment_method, exclusion: { in: %w[cash check] }
100
- required :amount, exclusion: { in: 0.0..0.99, in_message: "must be at least $1.00" }
101
- optional :discount_percent, exclusion: { in: 90..100 }
118
+ class UpdateOrderTask < CMDx::Task
119
+ required :status, inclusion: { in: %w[pending processing shipped delivered] }
120
+ required :priority, inclusion: { in: 1..5 }
121
+
122
+ optional :shipping_method, inclusion: {
123
+ in: %w[standard express overnight],
124
+ unless: :digital_product?
125
+ }
102
126
 
103
127
  def call
104
- charge_payment
128
+ update_order_attributes
129
+ end
130
+
131
+ private
132
+
133
+ def digital_product?
134
+ context.order.items.all?(&:digital?)
105
135
  end
106
136
  end
107
137
  ```
108
138
 
109
139
  **Options:**
110
140
 
111
- | Option | Description |
112
- | ------------ | ----------- |
113
- | `:in` | Enumerable of forbidden values |
114
- | `:within` | Alias for `:in` |
141
+ | Option | Description |
142
+ |--------|-------------|
143
+ | `:in` | Enumerable of allowed values |
144
+ | `:within` | Alias for `:in` |
115
145
 
116
- **Error Messages:**
146
+ **Custom Error Messages:**
117
147
 
118
- | Option | Description |
119
- | ----------------- | ----------- |
120
- | `:of_message` | Error when value is in array (default: "must not be one of: %{values}") |
121
- | `:in_message` | Error when value is in range (default: "must not be within %{min} and %{max}") |
148
+ | Option | Description |
149
+ |--------|-------------|
150
+ | `:of_message` | Error for array validation (default: "must be one of: %{values}") |
151
+ | `:in_message` | Error for range validation (default: "must be within %{min} and %{max}") |
122
152
  | `:within_message` | Alias for `:in_message` |
123
153
 
124
- ## Inclusion
154
+ ## Exclusion
125
155
 
126
- Validates that parameter values are in a specific enumerable (array, range, etc.).
156
+ Validates that parameter values are not within a specific set of forbidden values.
127
157
 
128
158
  ```ruby
129
- class UpdateOrderTask < CMDx::Task
130
- required :status, inclusion: { in: %w[pending processing shipped delivered] }
131
- required :priority, inclusion: { in: 1..5 }
132
- optional :shipping_method, inclusion: {
133
- in: %w[standard express overnight],
134
- unless: :digital_order?
159
+ class ProcessPaymentTask < CMDx::Task
160
+ required :payment_method, exclusion: { in: %w[cash check] }
161
+ required :amount, exclusion: { in: 0.0..0.99, in_message: "must be at least $1.00" }
162
+
163
+ optional :promo_code, exclusion: {
164
+ in: %w[EXPIRED INVALID],
165
+ of_message: "is not valid"
135
166
  }
136
167
 
137
168
  def call
138
- update_order_status
139
- end
140
-
141
- private
142
-
143
- def digital_order?
144
- context.order.digital_items_only?
169
+ charge_payment_method
145
170
  end
146
171
  end
172
+
173
+ # Valid usage
174
+ ProcessPaymentTask.call(
175
+ payment_method: "credit_card",
176
+ amount: 29.99,
177
+ promo_code: "SAVE20"
178
+ )
147
179
  ```
148
180
 
149
181
  **Options:**
150
182
 
151
- | Option | Description |
152
- | ------------ | ----------- |
153
- | `:in` | Enumerable of allowed values |
154
- | `:within` | Alias for `:in` |
183
+ | Option | Description |
184
+ |--------|-------------|
185
+ | `:in` | Enumerable of forbidden values |
186
+ | `:within` | Alias for `:in` |
155
187
 
156
- **Error Messages:**
188
+ **Custom Error Messages:**
157
189
 
158
- | Option | Description |
159
- | ----------------- | ----------- |
160
- | `:of_message` | Error when value not in array (default: "must be one of: %{values}") |
161
- | `:in_message` | Error when value not in range (default: "must be within %{min} and %{max}") |
190
+ | Option | Description |
191
+ |--------|-------------|
192
+ | `:of_message` | Error for array validation (default: "must not be one of: %{values}") |
193
+ | `:in_message` | Error for range validation (default: "must not be within %{min} and %{max}") |
162
194
  | `:within_message` | Alias for `:in_message` |
163
195
 
164
196
  ## Length
165
197
 
166
- Validates parameter length/size. Works with any object responding to `#size` or `#length`. Only one constraint option can be used at a time, except `:min` and `:max` which can be combined.
198
+ Validates parameter length for any object responding to `#size` or `#length`. Only one constraint option can be used at a time, except `:min` and `:max` which can be combined.
167
199
 
168
200
  ```ruby
169
201
  class CreatePostTask < CMDx::Task
170
202
  required :title, length: { within: 5..100 }
171
- required :body, length: { min: 20 }
172
- optional :summary, length: { max: 200 }
203
+ required :content, length: { min: 50 }
173
204
  required :slug, length: { min: 3, max: 50 }
174
- required :category_code, length: { is: 3 }
205
+
206
+ optional :summary, length: { max: 200, allow_nil: true }
207
+ optional :category_code, length: { is: 3 }
175
208
 
176
209
  def call
177
- create_blog_post
210
+ Post.create!(title: title, content: content, slug: slug)
178
211
  end
179
212
  end
180
213
  ```
181
214
 
182
- **Options:**
215
+ **Constraint Options:**
183
216
 
184
- | Option | Description |
185
- | ------------- | ----------- |
186
- | `:within` | Range specifying min and max size |
187
- | `:not_within` | Range specifying forbidden size range |
188
- | `:in` | Alias for `:within` |
189
- | `:not_in` | Alias for `:not_within` |
190
- | `:min` | Minimum size required |
191
- | `:max` | Maximum size allowed |
192
- | `:is` | Exact size required |
193
- | `:is_not` | Size that is forbidden |
217
+ | Option | Description |
218
+ |--------|-------------|
219
+ | `:within` / `:in` | Range specifying min and max length |
220
+ | `:not_within` / `:not_in` | Range specifying forbidden length range |
221
+ | `:min` | Minimum length required |
222
+ | `:max` | Maximum length allowed |
223
+ | `:is` | Exact length required |
224
+ | `:is_not` | Length that is forbidden |
194
225
 
195
226
  **Error Messages:**
196
227
 
197
- | Option | Description |
198
- | --------------------- | ----------- |
199
- | `:within_message` | "length must be within %{min} and %{max}" |
228
+ | Option | Description |
229
+ |--------|-------------|
230
+ | `:within_message` | "length must be within %{min} and %{max}" |
200
231
  | `:not_within_message` | "length must not be within %{min} and %{max}" |
201
- | `:min_message` | "length must be at least %{min}" |
202
- | `:max_message` | "length must be at most %{max}" |
203
- | `:is_message` | "length must be %{is}" |
204
- | `:is_not_message` | "length must not be %{is_not}" |
232
+ | `:min_message` | "length must be at least %{min}" |
233
+ | `:max_message` | "length must be at most %{max}" |
234
+ | `:is_message` | "length must be %{is}" |
235
+ | `:is_not_message` | "length must not be %{is_not}" |
205
236
 
206
237
  ## Numeric
207
238
 
208
- Validates numeric values against constraints. Works with any numeric type. Only one constraint option can be used at a time, except `:min` and `:max` which can be combined.
239
+ Validates numeric values against constraints. Works with any numeric type including integers, floats, and decimals.
209
240
 
210
241
  ```ruby
211
242
  class ProcessOrderTask < CMDx::Task
212
243
  required :quantity, numeric: { within: 1..100 }
213
244
  required :price, numeric: { min: 0.01 }
214
- optional :discount_percent, numeric: { max: 50 }
215
- required :tax_rate, numeric: { min: 0, max: 0.15 }
216
- required :api_version, numeric: { is: 2 }
245
+ required :tax_rate, numeric: { min: 0, max: 0.25 }
246
+
247
+ optional :discount, numeric: { max: 50, allow_nil: true }
248
+ optional :api_version, numeric: { is: 2 }
217
249
 
218
250
  def call
219
251
  calculate_order_total
220
252
  end
221
253
  end
254
+
255
+ # Error example
256
+ ProcessOrderTask.call(
257
+ quantity: 0, # Below minimum
258
+ price: -5.00, # Below minimum
259
+ tax_rate: 0.30 # Above maximum
260
+ )
261
+ # → ValidationError: "quantity must be within 1 and 100. price must be at least 0.01. tax_rate must be at most 0.25"
222
262
  ```
223
263
 
224
- **Options:**
264
+ **Constraint Options:**
225
265
 
226
- | Option | Description |
227
- | ------------- | ----------- |
228
- | `:within` | Range specifying min and max value |
229
- | `:not_within` | Range specifying forbidden value range |
230
- | `:in` | Alias for `:within` |
231
- | `:not_in` | Alias for `:not_within` |
232
- | `:min` | Minimum value required |
233
- | `:max` | Maximum value allowed |
234
- | `:is` | Exact value required |
235
- | `:is_not` | Value that is forbidden |
266
+ | Option | Description |
267
+ |--------|-------------|
268
+ | `:within` / `:in` | Range specifying min and max value |
269
+ | `:not_within` / `:not_in` | Range specifying forbidden value range |
270
+ | `:min` | Minimum value required |
271
+ | `:max` | Maximum value allowed |
272
+ | `:is` | Exact value required |
273
+ | `:is_not` | Value that is forbidden |
236
274
 
237
- **Error Messages:**
275
+ ## Error Handling
238
276
 
239
- | Option | Description |
240
- | --------------------- | ----------- |
241
- | `:within_message` | "must be within %{min} and %{max}" |
242
- | `:not_within_message` | "must not be within %{min} and %{max}" |
243
- | `:min_message` | "must be at least %{min}" |
244
- | `:max_message` | "must be at most %{max}" |
245
- | `:is_message` | "must be %{is}" |
246
- | `:is_not_message` | "must not be %{is_not}" |
277
+ > [!WARNING]
278
+ > Validation failures cause tasks to enter a failed state with detailed error information including parameter paths and specific violation messages.
247
279
 
248
- ## Validation Results
280
+ ```ruby
281
+ class CreateUserTask < CMDx::Task
282
+ required :email, format: { with: /@/, message: "must be valid" }
283
+ required :username, presence: true, length: { min: 3 }
284
+ required :age, numeric: { min: 13, max: 120 }
249
285
 
250
- When validation fails, tasks enter a failed state with detailed error information:
286
+ def call
287
+ # Process user
288
+ end
289
+ end
290
+
291
+ result = CreateUserTask.call(
292
+ email: "invalid-email",
293
+ username: "",
294
+ age: 5
295
+ )
296
+
297
+ result.state # → "interrupted"
298
+ result.status # → "failed"
299
+ result.failed? # → true
300
+
301
+ # Detailed error information
302
+ result.metadata
303
+ # {
304
+ # reason: "email must be valid. username can't be blank. username length must be at least 3. age must be at least 13.",
305
+ # messages: {
306
+ # email: ["must be valid"],
307
+ # username: ["can't be blank", "length must be at least 3"],
308
+ # age: ["must be at least 13"]
309
+ # }
310
+ # }
311
+
312
+ # Access specific parameter errors
313
+ result.metadata[:messages][:email] # → ["must be valid"]
314
+ result.metadata[:messages][:username] # → ["can't be blank", "length must be at least 3"]
315
+ ```
316
+
317
+ ### Nested Parameter Validation
251
318
 
252
319
  ```ruby
253
- class CreateUserTask < CMDx::Task
254
- required :email, format: { with: /@/, message: "format is invalid" }
255
- required :username, presence: { message: "cannot be empty" }
320
+ class ProcessOrderTask < CMDx::Task
321
+ required :order, type: :hash do
322
+ required :customer_email, format: { with: /@/ }
323
+ required :items, type: :array, length: { min: 1 }
324
+
325
+ optional :shipping, type: :hash do
326
+ required :method, inclusion: { in: %w[standard express] }
327
+ required :address, presence: true
328
+ end
329
+ end
330
+
331
+ def call
332
+ # Process validated order
333
+ end
256
334
  end
257
335
 
258
- result = CreateUserTask.call(email: "invalid", username: "")
259
-
260
- result.state #=> "interrupted"
261
- result.status #=> "failed"
262
- result.metadata #=> {
263
- #=> reason: "email format is invalid. username cannot be empty.",
264
- #=> messages: {
265
- #=> email: ["format is invalid"],
266
- #=> username: ["cannot be empty"]
267
- #=> }
268
- #=> }
269
-
270
- # Accessing individual error messages
271
- result.metadata[:messages][:email] #=> ["format is invalid"]
272
- result.metadata[:messages][:username] #=> ["cannot be empty"]
336
+ # Nested validation errors
337
+ result = ProcessOrderTask.call(
338
+ order: {
339
+ customer_email: "invalid",
340
+ items: [],
341
+ shipping: {
342
+ method: "invalid",
343
+ address: ""
344
+ }
345
+ }
346
+ )
347
+
348
+ result.metadata[:messages]
349
+ # {
350
+ # "order.customer_email" => ["is invalid"],
351
+ # "order.items" => ["length must be at least 1"],
352
+ # "order.shipping.method" => ["must be one of: standard, express"],
353
+ # "order.shipping.address" => ["can't be blank"]
354
+ # }
355
+ ```
356
+
357
+ ## Conditional Validation
358
+
359
+ > [!TIP]
360
+ > Use `:if` and `:unless` options to apply validations conditionally based on runtime context or other parameter values.
361
+
362
+ ```ruby
363
+ class UserRegistrationTask < CMDx::Task
364
+ required :email, presence: true, format: { with: /@/ }
365
+ required :user_type, inclusion: { in: %w[individual business] }
366
+
367
+ # Conditional validations based on user type
368
+ optional :company_name, presence: { if: :business_user? }
369
+ optional :tax_id, format: { with: /\A\d{2}-\d{7}\z/, if: :business_user? }
370
+
371
+ # Conditional validation with procs
372
+ optional :phone, presence: {
373
+ if: proc { |task| task.context.require_phone_verification? }
374
+ }
375
+
376
+ # Multiple conditions
377
+ optional :parent_email, presence: {
378
+ if: :minor_user?,
379
+ format: { with: /@/, unless: :parent_present? }
380
+ }
381
+
382
+ def call
383
+ create_user_account
384
+ end
385
+
386
+ private
387
+
388
+ def business_user?
389
+ user_type == "business"
390
+ end
391
+
392
+ def minor_user?
393
+ context.user_age < 18
394
+ end
395
+
396
+ def parent_present?
397
+ context.parent_guardian_present?
398
+ end
399
+ end
273
400
  ```
274
401
 
275
402
  ---