cmdx 1.0.1 → 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 (170) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/prompts/docs.md +9 -0
  3. data/.cursor/prompts/rspec.md +21 -0
  4. data/.cursor/prompts/yardoc.md +13 -0
  5. data/.rubocop.yml +2 -0
  6. data/CHANGELOG.md +29 -3
  7. data/README.md +2 -1
  8. data/docs/ai_prompts.md +269 -195
  9. data/docs/basics/call.md +126 -60
  10. data/docs/basics/chain.md +190 -160
  11. data/docs/basics/context.md +242 -154
  12. data/docs/basics/setup.md +302 -32
  13. data/docs/callbacks.md +382 -119
  14. data/docs/configuration.md +211 -49
  15. data/docs/deprecation.md +245 -0
  16. data/docs/getting_started.md +161 -39
  17. data/docs/internationalization.md +590 -70
  18. data/docs/interruptions/exceptions.md +135 -118
  19. data/docs/interruptions/faults.md +152 -127
  20. data/docs/interruptions/halt.md +134 -80
  21. data/docs/logging.md +183 -120
  22. data/docs/middlewares.md +165 -392
  23. data/docs/outcomes/result.md +140 -112
  24. data/docs/outcomes/states.md +134 -99
  25. data/docs/outcomes/statuses.md +204 -146
  26. data/docs/parameters/coercions.md +251 -289
  27. data/docs/parameters/defaults.md +224 -169
  28. data/docs/parameters/definitions.md +289 -141
  29. data/docs/parameters/namespacing.md +250 -161
  30. data/docs/parameters/validations.md +247 -159
  31. data/docs/testing.md +196 -203
  32. data/docs/workflows.md +146 -101
  33. data/lib/cmdx/.DS_Store +0 -0
  34. data/lib/cmdx/callback.rb +39 -55
  35. data/lib/cmdx/callback_registry.rb +80 -73
  36. data/lib/cmdx/chain.rb +65 -122
  37. data/lib/cmdx/chain_inspector.rb +23 -116
  38. data/lib/cmdx/chain_serializer.rb +34 -146
  39. data/lib/cmdx/coercion.rb +57 -0
  40. data/lib/cmdx/coercion_registry.rb +113 -0
  41. data/lib/cmdx/coercions/array.rb +18 -36
  42. data/lib/cmdx/coercions/big_decimal.rb +21 -33
  43. data/lib/cmdx/coercions/boolean.rb +21 -40
  44. data/lib/cmdx/coercions/complex.rb +18 -31
  45. data/lib/cmdx/coercions/date.rb +20 -39
  46. data/lib/cmdx/coercions/date_time.rb +22 -39
  47. data/lib/cmdx/coercions/float.rb +19 -32
  48. data/lib/cmdx/coercions/hash.rb +22 -41
  49. data/lib/cmdx/coercions/integer.rb +20 -33
  50. data/lib/cmdx/coercions/rational.rb +20 -32
  51. data/lib/cmdx/coercions/string.rb +23 -31
  52. data/lib/cmdx/coercions/time.rb +24 -40
  53. data/lib/cmdx/coercions/virtual.rb +14 -31
  54. data/lib/cmdx/configuration.rb +101 -162
  55. data/lib/cmdx/context.rb +34 -166
  56. data/lib/cmdx/core_ext/hash.rb +42 -67
  57. data/lib/cmdx/core_ext/module.rb +35 -79
  58. data/lib/cmdx/core_ext/object.rb +63 -98
  59. data/lib/cmdx/correlator.rb +59 -154
  60. data/lib/cmdx/error.rb +37 -202
  61. data/lib/cmdx/errors.rb +153 -216
  62. data/lib/cmdx/fault.rb +68 -150
  63. data/lib/cmdx/faults.rb +26 -137
  64. data/lib/cmdx/immutator.rb +22 -110
  65. data/lib/cmdx/lazy_struct.rb +110 -186
  66. data/lib/cmdx/log_formatters/json.rb +14 -40
  67. data/lib/cmdx/log_formatters/key_value.rb +14 -40
  68. data/lib/cmdx/log_formatters/line.rb +14 -48
  69. data/lib/cmdx/log_formatters/logstash.rb +14 -57
  70. data/lib/cmdx/log_formatters/pretty_json.rb +14 -50
  71. data/lib/cmdx/log_formatters/pretty_key_value.rb +13 -46
  72. data/lib/cmdx/log_formatters/pretty_line.rb +16 -54
  73. data/lib/cmdx/log_formatters/raw.rb +19 -49
  74. data/lib/cmdx/logger.rb +22 -79
  75. data/lib/cmdx/logger_ansi.rb +31 -72
  76. data/lib/cmdx/logger_serializer.rb +74 -103
  77. data/lib/cmdx/middleware.rb +56 -60
  78. data/lib/cmdx/middleware_registry.rb +82 -77
  79. data/lib/cmdx/middlewares/correlate.rb +41 -226
  80. data/lib/cmdx/middlewares/timeout.rb +46 -185
  81. data/lib/cmdx/parameter.rb +167 -183
  82. data/lib/cmdx/parameter_evaluator.rb +231 -0
  83. data/lib/cmdx/parameter_inspector.rb +37 -55
  84. data/lib/cmdx/parameter_registry.rb +65 -84
  85. data/lib/cmdx/parameter_serializer.rb +32 -76
  86. data/lib/cmdx/railtie.rb +24 -107
  87. data/lib/cmdx/result.rb +254 -259
  88. data/lib/cmdx/result_ansi.rb +28 -80
  89. data/lib/cmdx/result_inspector.rb +34 -70
  90. data/lib/cmdx/result_logger.rb +23 -77
  91. data/lib/cmdx/result_serializer.rb +59 -125
  92. data/lib/cmdx/rspec/matchers.rb +28 -0
  93. data/lib/cmdx/rspec/result_matchers/be_executed.rb +42 -0
  94. data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +94 -0
  95. data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +94 -0
  96. data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +59 -0
  97. data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +57 -0
  98. data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +87 -0
  99. data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +51 -0
  100. data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +58 -0
  101. data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +59 -0
  102. data/lib/cmdx/rspec/result_matchers/have_context.rb +86 -0
  103. data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +54 -0
  104. data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +52 -0
  105. data/lib/cmdx/rspec/result_matchers/have_metadata.rb +114 -0
  106. data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +66 -0
  107. data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +64 -0
  108. data/lib/cmdx/rspec/result_matchers/have_runtime.rb +78 -0
  109. data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +76 -0
  110. data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +62 -0
  111. data/lib/cmdx/rspec/task_matchers/have_callback.rb +85 -0
  112. data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +68 -0
  113. data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +92 -0
  114. data/lib/cmdx/rspec/task_matchers/have_middleware.rb +46 -0
  115. data/lib/cmdx/rspec/task_matchers/have_parameter.rb +181 -0
  116. data/lib/cmdx/task.rb +336 -427
  117. data/lib/cmdx/task_deprecator.rb +52 -0
  118. data/lib/cmdx/task_processor.rb +246 -0
  119. data/lib/cmdx/task_serializer.rb +34 -69
  120. data/lib/cmdx/utils/ansi_color.rb +13 -89
  121. data/lib/cmdx/utils/log_timestamp.rb +13 -42
  122. data/lib/cmdx/utils/monotonic_runtime.rb +11 -63
  123. data/lib/cmdx/utils/name_affix.rb +21 -71
  124. data/lib/cmdx/validator.rb +57 -0
  125. data/lib/cmdx/validator_registry.rb +108 -0
  126. data/lib/cmdx/validators/exclusion.rb +55 -94
  127. data/lib/cmdx/validators/format.rb +31 -85
  128. data/lib/cmdx/validators/inclusion.rb +65 -110
  129. data/lib/cmdx/validators/length.rb +117 -133
  130. data/lib/cmdx/validators/numeric.rb +123 -130
  131. data/lib/cmdx/validators/presence.rb +38 -79
  132. data/lib/cmdx/version.rb +1 -7
  133. data/lib/cmdx/workflow.rb +58 -330
  134. data/lib/cmdx.rb +1 -1
  135. data/lib/generators/cmdx/install_generator.rb +14 -31
  136. data/lib/generators/cmdx/task_generator.rb +39 -55
  137. data/lib/generators/cmdx/templates/install.rb +24 -6
  138. data/lib/generators/cmdx/workflow_generator.rb +41 -66
  139. data/lib/locales/ar.yml +0 -1
  140. data/lib/locales/cs.yml +0 -1
  141. data/lib/locales/da.yml +0 -1
  142. data/lib/locales/de.yml +0 -1
  143. data/lib/locales/el.yml +0 -1
  144. data/lib/locales/en.yml +0 -1
  145. data/lib/locales/es.yml +0 -1
  146. data/lib/locales/fi.yml +0 -1
  147. data/lib/locales/fr.yml +0 -1
  148. data/lib/locales/he.yml +0 -1
  149. data/lib/locales/hi.yml +0 -1
  150. data/lib/locales/it.yml +0 -1
  151. data/lib/locales/ja.yml +0 -1
  152. data/lib/locales/ko.yml +0 -1
  153. data/lib/locales/nl.yml +0 -1
  154. data/lib/locales/no.yml +0 -1
  155. data/lib/locales/pl.yml +0 -1
  156. data/lib/locales/pt.yml +0 -1
  157. data/lib/locales/ru.yml +0 -1
  158. data/lib/locales/sv.yml +0 -1
  159. data/lib/locales/th.yml +0 -1
  160. data/lib/locales/tr.yml +0 -1
  161. data/lib/locales/vi.yml +0 -1
  162. data/lib/locales/zh.yml +0 -1
  163. metadata +36 -8
  164. data/lib/cmdx/parameter_validator.rb +0 -81
  165. data/lib/cmdx/parameter_value.rb +0 -244
  166. data/lib/cmdx/parameters_inspector.rb +0 -72
  167. data/lib/cmdx/parameters_serializer.rb +0 -115
  168. data/lib/cmdx/rspec/result_matchers.rb +0 -917
  169. data/lib/cmdx/rspec/task_matchers.rb +0 -570
  170. data/lib/cmdx/validators/custom.rb +0 -102
@@ -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,55 +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
- - [Custom](#custom)
16
- - [Validation Results](#validation-results)
15
+ - [Error Handling](#error-handling)
16
+ - [Conditional Validation](#conditional-validation)
17
17
 
18
18
  ## TLDR
19
19
 
20
- - **Built-in validators** - `presence`, `format`, `inclusion`, `exclusion`, `length`, `numeric`
21
- - **Common options** - All support `:allow_nil`, `:if`, `:unless`, `:message`
22
- - **Usage** - Add to parameter definitions: `required :email, presence: true, format: { with: /@/ }`
23
- - **Conditional** - Use `:if` and `:unless` for conditional validation
24
- - **Custom validators** - Use `custom: { validator: CustomValidator }` for complex logic
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 }
25
25
 
26
- ## Common Options
26
+ # Conditional validation
27
+ optional :phone, presence: { if: :phone_required? }
28
+ required :age, numeric: { min: 18, unless: :minor_allowed? }
27
29
 
28
- All validators support these common options:
30
+ # Custom messages
31
+ required :username, exclusion: { in: %w[admin root], message: "reserved name" }
32
+ ```
29
33
 
30
- | Option | Description |
31
- | ------------ | ----------- |
32
- | `:allow_nil` | Skip validation if the parameter value is `nil` |
33
- | `:if` | Callable method, proc or string to determine if validation should occur |
34
- | `:unless` | Callable method, proc, or string to determine if validation should not occur |
35
- | `:message` | Error message for violations. Fallback for specific error keys not provided |
34
+ ## Common Options
36
35
 
37
36
  > [!NOTE]
38
- > 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 |
39
47
 
40
48
  ## Presence
41
49
 
42
50
  Validates that parameter values are not empty using intelligent type checking:
51
+
43
52
  - **Strings**: Must contain non-whitespace characters
44
- - **Collections**: Must not be empty (arrays, hashes, etc.)
53
+ - **Collections**: Must not be empty (arrays, hashes, sets)
45
54
  - **Other objects**: Must not be `nil`
46
55
 
47
56
  > [!TIP]
48
- > 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.
49
58
 
50
59
  ```ruby
51
60
  class CreateUserTask < CMDx::Task
52
61
  required :email, presence: true
53
- optional :phone, presence: { message: "cannot be blank" }
62
+ required :name, presence: { message: "cannot be blank" }
54
63
  required :active, inclusion: { in: [true, false] }
55
64
 
56
65
  def call
57
- User.create!(email: email, phone: phone, active: active)
66
+ User.create!(email: email, name: name, active: active)
58
67
  end
59
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"
60
76
  ```
61
77
 
62
78
  ## Format
@@ -67,10 +83,11 @@ Validates parameter values against regular expression patterns. Supports positiv
67
83
  class RegisterUserTask < CMDx::Task
68
84
  required :email, format: { with: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i }
69
85
  required :username, format: { without: /\A(admin|root|system)\z/i }
86
+
70
87
  optional :password, format: {
71
88
  with: /\A(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}\z/,
72
89
  without: /password|123456/i,
73
- if: :strong_password_required?
90
+ if: :secure_password_required?
74
91
  }
75
92
 
76
93
  def call
@@ -79,236 +96,307 @@ class RegisterUserTask < CMDx::Task
79
96
 
80
97
  private
81
98
 
82
- def strong_password_required?
83
- context.account.security_policy.strong_passwords?
99
+ def secure_password_required?
100
+ context.security_policy.enforce_strong_passwords?
84
101
  end
85
102
  end
86
103
  ```
87
104
 
88
105
  **Options:**
89
106
 
90
- | Option | Description |
91
- | ---------- | ----------- |
92
- | `:with` | Regular expression that value must match |
107
+ | Option | Description |
108
+ |--------|-------------|
109
+ | `:with` | Regular expression that value must match |
93
110
  | `:without` | Regular expression that value must not match |
94
111
 
95
- ## Exclusion
112
+ ## Inclusion
96
113
 
97
- 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).
98
116
 
99
117
  ```ruby
100
- class ProcessPaymentTask < CMDx::Task
101
- required :payment_method, exclusion: { in: %w[cash check] }
102
- required :amount, exclusion: { in: 0.0..0.99, in_message: "must be at least $1.00" }
103
- 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
+ }
104
126
 
105
127
  def call
106
- charge_payment
128
+ update_order_attributes
129
+ end
130
+
131
+ private
132
+
133
+ def digital_product?
134
+ context.order.items.all?(&:digital?)
107
135
  end
108
136
  end
109
137
  ```
110
138
 
111
139
  **Options:**
112
140
 
113
- | Option | Description |
114
- | ------------ | ----------- |
115
- | `:in` | Enumerable of forbidden values |
116
- | `:within` | Alias for `:in` |
141
+ | Option | Description |
142
+ |--------|-------------|
143
+ | `:in` | Enumerable of allowed values |
144
+ | `:within` | Alias for `:in` |
117
145
 
118
- **Error Messages:**
146
+ **Custom Error Messages:**
119
147
 
120
- | Option | Description |
121
- | ----------------- | ----------- |
122
- | `:of_message` | Error when value is in array (default: "must not be one of: %{values}") |
123
- | `: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}") |
124
152
  | `:within_message` | Alias for `:in_message` |
125
153
 
126
- ## Inclusion
154
+ ## Exclusion
127
155
 
128
- 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.
129
157
 
130
158
  ```ruby
131
- class UpdateOrderTask < CMDx::Task
132
- required :status, inclusion: { in: %w[pending processing shipped delivered] }
133
- required :priority, inclusion: { in: 1..5 }
134
- optional :shipping_method, inclusion: {
135
- in: %w[standard express overnight],
136
- 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"
137
166
  }
138
167
 
139
168
  def call
140
- update_order_status
141
- end
142
-
143
- private
144
-
145
- def digital_order?
146
- context.order.digital_items_only?
169
+ charge_payment_method
147
170
  end
148
171
  end
172
+
173
+ # Valid usage
174
+ ProcessPaymentTask.call(
175
+ payment_method: "credit_card",
176
+ amount: 29.99,
177
+ promo_code: "SAVE20"
178
+ )
149
179
  ```
150
180
 
151
181
  **Options:**
152
182
 
153
- | Option | Description |
154
- | ------------ | ----------- |
155
- | `:in` | Enumerable of allowed values |
156
- | `:within` | Alias for `:in` |
183
+ | Option | Description |
184
+ |--------|-------------|
185
+ | `:in` | Enumerable of forbidden values |
186
+ | `:within` | Alias for `:in` |
157
187
 
158
- **Error Messages:**
188
+ **Custom Error Messages:**
159
189
 
160
- | Option | Description |
161
- | ----------------- | ----------- |
162
- | `:of_message` | Error when value not in array (default: "must be one of: %{values}") |
163
- | `: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}") |
164
194
  | `:within_message` | Alias for `:in_message` |
165
195
 
166
196
  ## Length
167
197
 
168
- 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.
169
199
 
170
200
  ```ruby
171
201
  class CreatePostTask < CMDx::Task
172
202
  required :title, length: { within: 5..100 }
173
- required :body, length: { min: 20 }
174
- optional :summary, length: { max: 200 }
203
+ required :content, length: { min: 50 }
175
204
  required :slug, length: { min: 3, max: 50 }
176
- required :category_code, length: { is: 3 }
205
+
206
+ optional :summary, length: { max: 200, allow_nil: true }
207
+ optional :category_code, length: { is: 3 }
177
208
 
178
209
  def call
179
- create_blog_post
210
+ Post.create!(title: title, content: content, slug: slug)
180
211
  end
181
212
  end
182
213
  ```
183
214
 
184
- **Options:**
215
+ **Constraint Options:**
185
216
 
186
- | Option | Description |
187
- | ------------- | ----------- |
188
- | `:within` | Range specifying min and max size |
189
- | `:not_within` | Range specifying forbidden size range |
190
- | `:in` | Alias for `:within` |
191
- | `:not_in` | Alias for `:not_within` |
192
- | `:min` | Minimum size required |
193
- | `:max` | Maximum size allowed |
194
- | `:is` | Exact size required |
195
- | `: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 |
196
225
 
197
226
  **Error Messages:**
198
227
 
199
- | Option | Description |
200
- | --------------------- | ----------- |
201
- | `:within_message` | "length must be within %{min} and %{max}" |
228
+ | Option | Description |
229
+ |--------|-------------|
230
+ | `:within_message` | "length must be within %{min} and %{max}" |
202
231
  | `:not_within_message` | "length must not be within %{min} and %{max}" |
203
- | `:min_message` | "length must be at least %{min}" |
204
- | `:max_message` | "length must be at most %{max}" |
205
- | `:is_message` | "length must be %{is}" |
206
- | `: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}" |
207
236
 
208
237
  ## Numeric
209
238
 
210
- 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.
211
240
 
212
241
  ```ruby
213
242
  class ProcessOrderTask < CMDx::Task
214
243
  required :quantity, numeric: { within: 1..100 }
215
244
  required :price, numeric: { min: 0.01 }
216
- optional :discount_percent, numeric: { max: 50 }
217
- required :tax_rate, numeric: { min: 0, max: 0.15 }
218
- 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 }
219
249
 
220
250
  def call
221
251
  calculate_order_total
222
252
  end
223
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"
224
262
  ```
225
263
 
226
- **Options:**
264
+ **Constraint Options:**
227
265
 
228
- | Option | Description |
229
- | ------------- | ----------- |
230
- | `:within` | Range specifying min and max value |
231
- | `:not_within` | Range specifying forbidden value range |
232
- | `:in` | Alias for `:within` |
233
- | `:not_in` | Alias for `:not_within` |
234
- | `:min` | Minimum value required |
235
- | `:max` | Maximum value allowed |
236
- | `:is` | Exact value required |
237
- | `: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 |
238
274
 
239
- **Error Messages:**
275
+ ## Error Handling
240
276
 
241
- | Option | Description |
242
- | --------------------- | ----------- |
243
- | `:within_message` | "must be within %{min} and %{max}" |
244
- | `:not_within_message` | "must not be within %{min} and %{max}" |
245
- | `:min_message` | "must be at least %{min}" |
246
- | `:max_message` | "must be at most %{max}" |
247
- | `:is_message` | "must be %{is}" |
248
- | `: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.
249
279
 
250
- ## Custom
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 }
251
285
 
252
- Validates using custom logic. Accepts any callable object (class, proc, lambda) implementing a `call` method that returns truthy for valid values.
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
253
318
 
254
319
  ```ruby
255
- class EmailDomainValidator
256
- def self.call(value, options)
257
- allowed_domains = options.dig(:custom, :allowed_domains) || ['example.com']
258
- domain = value.split('@').last
259
- allowed_domains.include?(domain)
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
260
333
  end
261
334
  end
262
335
 
263
- class CreateAccountTask < CMDx::Task
264
- required :work_email, custom: {
265
- validator: EmailDomainValidator,
266
- allowed_domains: ['company.com', 'partner.org'],
267
- message: "must be from an approved domain"
336
+ # Nested validation errors
337
+ result = ProcessOrderTask.call(
338
+ order: {
339
+ customer_email: "invalid",
340
+ items: [],
341
+ shipping: {
342
+ method: "invalid",
343
+ address: ""
344
+ }
268
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
269
358
 
270
- required :age, custom: {
271
- validator: ->(value, options) { value.between?(18, 120) },
272
- message: "must be a valid age"
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? }
273
380
  }
274
381
 
275
382
  def call
276
383
  create_user_account
277
384
  end
278
- end
279
- ```
280
-
281
- **Options:**
282
385
 
283
- | Option | Description |
284
- | ------------ | ----------- |
285
- | `:validator` | Callable object returning true/false. Receives value and options as parameters |
386
+ private
286
387
 
287
- ## Validation Results
388
+ def business_user?
389
+ user_type == "business"
390
+ end
288
391
 
289
- When validation fails, tasks enter a failed state with detailed error information:
392
+ def minor_user?
393
+ context.user_age < 18
394
+ end
290
395
 
291
- ```ruby
292
- class CreateUserTask < CMDx::Task
293
- required :email, format: { with: /@/, message: "format is invalid" }
294
- required :username, presence: { message: "cannot be empty" }
396
+ def parent_present?
397
+ context.parent_guardian_present?
398
+ end
295
399
  end
296
-
297
- result = CreateUserTask.call(email: "invalid", username: "")
298
-
299
- result.state #=> "interrupted"
300
- result.status #=> "failed"
301
- result.metadata #=> {
302
- #=> reason: "email format is invalid. username cannot be empty.",
303
- #=> messages: {
304
- #=> email: ["format is invalid"],
305
- #=> username: ["cannot be empty"]
306
- #=> }
307
- #=> }
308
-
309
- # Accessing individual error messages
310
- result.metadata[:messages][:email] #=> ["format is invalid"]
311
- result.metadata[:messages][:username] #=> ["cannot be empty"]
312
400
  ```
313
401
 
314
402
  ---