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.
- checksums.yaml +4 -4
- data/.cursor/prompts/docs.md +9 -0
- data/.cursor/prompts/rspec.md +21 -0
- data/.cursor/prompts/yardoc.md +13 -0
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +29 -3
- data/README.md +2 -1
- data/docs/ai_prompts.md +269 -195
- data/docs/basics/call.md +126 -60
- data/docs/basics/chain.md +190 -160
- data/docs/basics/context.md +242 -154
- data/docs/basics/setup.md +302 -32
- data/docs/callbacks.md +382 -119
- data/docs/configuration.md +211 -49
- data/docs/deprecation.md +245 -0
- data/docs/getting_started.md +161 -39
- data/docs/internationalization.md +590 -70
- data/docs/interruptions/exceptions.md +135 -118
- data/docs/interruptions/faults.md +152 -127
- data/docs/interruptions/halt.md +134 -80
- data/docs/logging.md +183 -120
- data/docs/middlewares.md +165 -392
- data/docs/outcomes/result.md +140 -112
- data/docs/outcomes/states.md +134 -99
- data/docs/outcomes/statuses.md +204 -146
- data/docs/parameters/coercions.md +251 -289
- data/docs/parameters/defaults.md +224 -169
- data/docs/parameters/definitions.md +289 -141
- data/docs/parameters/namespacing.md +250 -161
- data/docs/parameters/validations.md +247 -159
- data/docs/testing.md +196 -203
- data/docs/workflows.md +146 -101
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/callback.rb +39 -55
- data/lib/cmdx/callback_registry.rb +80 -73
- data/lib/cmdx/chain.rb +65 -122
- data/lib/cmdx/chain_inspector.rb +23 -116
- data/lib/cmdx/chain_serializer.rb +34 -146
- data/lib/cmdx/coercion.rb +57 -0
- data/lib/cmdx/coercion_registry.rb +113 -0
- data/lib/cmdx/coercions/array.rb +18 -36
- data/lib/cmdx/coercions/big_decimal.rb +21 -33
- data/lib/cmdx/coercions/boolean.rb +21 -40
- data/lib/cmdx/coercions/complex.rb +18 -31
- data/lib/cmdx/coercions/date.rb +20 -39
- data/lib/cmdx/coercions/date_time.rb +22 -39
- data/lib/cmdx/coercions/float.rb +19 -32
- data/lib/cmdx/coercions/hash.rb +22 -41
- data/lib/cmdx/coercions/integer.rb +20 -33
- data/lib/cmdx/coercions/rational.rb +20 -32
- data/lib/cmdx/coercions/string.rb +23 -31
- data/lib/cmdx/coercions/time.rb +24 -40
- data/lib/cmdx/coercions/virtual.rb +14 -31
- data/lib/cmdx/configuration.rb +101 -162
- data/lib/cmdx/context.rb +34 -166
- data/lib/cmdx/core_ext/hash.rb +42 -67
- data/lib/cmdx/core_ext/module.rb +35 -79
- data/lib/cmdx/core_ext/object.rb +63 -98
- data/lib/cmdx/correlator.rb +59 -154
- data/lib/cmdx/error.rb +37 -202
- data/lib/cmdx/errors.rb +153 -216
- data/lib/cmdx/fault.rb +68 -150
- data/lib/cmdx/faults.rb +26 -137
- data/lib/cmdx/immutator.rb +22 -110
- data/lib/cmdx/lazy_struct.rb +110 -186
- data/lib/cmdx/log_formatters/json.rb +14 -40
- data/lib/cmdx/log_formatters/key_value.rb +14 -40
- data/lib/cmdx/log_formatters/line.rb +14 -48
- data/lib/cmdx/log_formatters/logstash.rb +14 -57
- data/lib/cmdx/log_formatters/pretty_json.rb +14 -50
- data/lib/cmdx/log_formatters/pretty_key_value.rb +13 -46
- data/lib/cmdx/log_formatters/pretty_line.rb +16 -54
- data/lib/cmdx/log_formatters/raw.rb +19 -49
- data/lib/cmdx/logger.rb +22 -79
- data/lib/cmdx/logger_ansi.rb +31 -72
- data/lib/cmdx/logger_serializer.rb +74 -103
- data/lib/cmdx/middleware.rb +56 -60
- data/lib/cmdx/middleware_registry.rb +82 -77
- data/lib/cmdx/middlewares/correlate.rb +41 -226
- data/lib/cmdx/middlewares/timeout.rb +46 -185
- data/lib/cmdx/parameter.rb +167 -183
- data/lib/cmdx/parameter_evaluator.rb +231 -0
- data/lib/cmdx/parameter_inspector.rb +37 -55
- data/lib/cmdx/parameter_registry.rb +65 -84
- data/lib/cmdx/parameter_serializer.rb +32 -76
- data/lib/cmdx/railtie.rb +24 -107
- data/lib/cmdx/result.rb +254 -259
- data/lib/cmdx/result_ansi.rb +28 -80
- data/lib/cmdx/result_inspector.rb +34 -70
- data/lib/cmdx/result_logger.rb +23 -77
- data/lib/cmdx/result_serializer.rb +59 -125
- data/lib/cmdx/rspec/matchers.rb +28 -0
- data/lib/cmdx/rspec/result_matchers/be_executed.rb +42 -0
- data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +94 -0
- data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +94 -0
- data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +59 -0
- data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +57 -0
- data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +87 -0
- data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +51 -0
- data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +58 -0
- data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +59 -0
- data/lib/cmdx/rspec/result_matchers/have_context.rb +86 -0
- data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +54 -0
- data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +52 -0
- data/lib/cmdx/rspec/result_matchers/have_metadata.rb +114 -0
- data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +66 -0
- data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +64 -0
- data/lib/cmdx/rspec/result_matchers/have_runtime.rb +78 -0
- data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +76 -0
- data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +62 -0
- data/lib/cmdx/rspec/task_matchers/have_callback.rb +85 -0
- data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +68 -0
- data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +92 -0
- data/lib/cmdx/rspec/task_matchers/have_middleware.rb +46 -0
- data/lib/cmdx/rspec/task_matchers/have_parameter.rb +181 -0
- data/lib/cmdx/task.rb +336 -427
- data/lib/cmdx/task_deprecator.rb +52 -0
- data/lib/cmdx/task_processor.rb +246 -0
- data/lib/cmdx/task_serializer.rb +34 -69
- data/lib/cmdx/utils/ansi_color.rb +13 -89
- data/lib/cmdx/utils/log_timestamp.rb +13 -42
- data/lib/cmdx/utils/monotonic_runtime.rb +11 -63
- data/lib/cmdx/utils/name_affix.rb +21 -71
- data/lib/cmdx/validator.rb +57 -0
- data/lib/cmdx/validator_registry.rb +108 -0
- data/lib/cmdx/validators/exclusion.rb +55 -94
- data/lib/cmdx/validators/format.rb +31 -85
- data/lib/cmdx/validators/inclusion.rb +65 -110
- data/lib/cmdx/validators/length.rb +117 -133
- data/lib/cmdx/validators/numeric.rb +123 -130
- data/lib/cmdx/validators/presence.rb +38 -79
- data/lib/cmdx/version.rb +1 -7
- data/lib/cmdx/workflow.rb +58 -330
- data/lib/cmdx.rb +1 -1
- data/lib/generators/cmdx/install_generator.rb +14 -31
- data/lib/generators/cmdx/task_generator.rb +39 -55
- data/lib/generators/cmdx/templates/install.rb +24 -6
- data/lib/generators/cmdx/workflow_generator.rb +41 -66
- data/lib/locales/ar.yml +0 -1
- data/lib/locales/cs.yml +0 -1
- data/lib/locales/da.yml +0 -1
- data/lib/locales/de.yml +0 -1
- data/lib/locales/el.yml +0 -1
- data/lib/locales/en.yml +0 -1
- data/lib/locales/es.yml +0 -1
- data/lib/locales/fi.yml +0 -1
- data/lib/locales/fr.yml +0 -1
- data/lib/locales/he.yml +0 -1
- data/lib/locales/hi.yml +0 -1
- data/lib/locales/it.yml +0 -1
- data/lib/locales/ja.yml +0 -1
- data/lib/locales/ko.yml +0 -1
- data/lib/locales/nl.yml +0 -1
- data/lib/locales/no.yml +0 -1
- data/lib/locales/pl.yml +0 -1
- data/lib/locales/pt.yml +0 -1
- data/lib/locales/ru.yml +0 -1
- data/lib/locales/sv.yml +0 -1
- data/lib/locales/th.yml +0 -1
- data/lib/locales/tr.yml +0 -1
- data/lib/locales/vi.yml +0 -1
- data/lib/locales/zh.yml +0 -1
- metadata +36 -8
- data/lib/cmdx/parameter_validator.rb +0 -81
- data/lib/cmdx/parameter_value.rb +0 -244
- data/lib/cmdx/parameters_inspector.rb +0 -72
- data/lib/cmdx/parameters_serializer.rb +0 -115
- data/lib/cmdx/rspec/result_matchers.rb +0 -917
- data/lib/cmdx/rspec/task_matchers.rb +0 -570
- data/lib/cmdx/validators/custom.rb +0 -102
@@ -1,6 +1,6 @@
|
|
1
1
|
# Parameters - Validations
|
2
2
|
|
3
|
-
Parameter
|
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
|
-
- [
|
16
|
-
- [Validation
|
15
|
+
- [Error Handling](#error-handling)
|
16
|
+
- [Conditional Validation](#conditional-validation)
|
17
17
|
|
18
18
|
## TLDR
|
19
19
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
-
|
26
|
+
# Conditional validation
|
27
|
+
optional :phone, presence: { if: :phone_required? }
|
28
|
+
required :age, numeric: { min: 18, unless: :minor_allowed? }
|
27
29
|
|
28
|
-
|
30
|
+
# Custom messages
|
31
|
+
required :username, exclusion: { in: %w[admin root], message: "reserved name" }
|
32
|
+
```
|
29
33
|
|
30
|
-
|
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
|
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,
|
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
|
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
|
-
|
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,
|
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: :
|
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
|
83
|
-
context.
|
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
|
91
|
-
|
92
|
-
| `:with`
|
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
|
-
##
|
112
|
+
## Inclusion
|
96
113
|
|
97
|
-
|
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
|
101
|
-
required :
|
102
|
-
required :
|
103
|
-
|
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
|
-
|
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
|
114
|
-
|
115
|
-
| `:in`
|
116
|
-
| `:within`
|
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
|
121
|
-
|
122
|
-
| `:of_message`
|
123
|
-
| `:in_message`
|
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
|
-
##
|
154
|
+
## Exclusion
|
127
155
|
|
128
|
-
Validates that parameter values are
|
156
|
+
Validates that parameter values are not within a specific set of forbidden values.
|
129
157
|
|
130
158
|
```ruby
|
131
|
-
class
|
132
|
-
required :
|
133
|
-
required :
|
134
|
-
|
135
|
-
|
136
|
-
|
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
|
-
|
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
|
154
|
-
|
155
|
-
| `:in`
|
156
|
-
| `:within`
|
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
|
161
|
-
|
162
|
-
| `:of_message`
|
163
|
-
| `:in_message`
|
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
|
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 :
|
174
|
-
optional :summary, length: { max: 200 }
|
203
|
+
required :content, length: { min: 50 }
|
175
204
|
required :slug, length: { min: 3, max: 50 }
|
176
|
-
|
205
|
+
|
206
|
+
optional :summary, length: { max: 200, allow_nil: true }
|
207
|
+
optional :category_code, length: { is: 3 }
|
177
208
|
|
178
209
|
def call
|
179
|
-
|
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
|
187
|
-
|
188
|
-
| `:within`
|
189
|
-
| `:not_within` | Range specifying forbidden
|
190
|
-
| `:
|
191
|
-
| `:
|
192
|
-
| `:
|
193
|
-
| `:
|
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
|
200
|
-
|
201
|
-
| `:within_message`
|
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`
|
204
|
-
| `:max_message`
|
205
|
-
| `:is_message`
|
206
|
-
| `:is_not_message`
|
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
|
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
|
-
|
217
|
-
|
218
|
-
|
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
|
229
|
-
|
230
|
-
| `:within`
|
231
|
-
| `:not_within` | Range specifying forbidden value range |
|
232
|
-
| `:
|
233
|
-
| `:
|
234
|
-
| `:
|
235
|
-
| `:
|
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
|
-
|
275
|
+
## Error Handling
|
240
276
|
|
241
|
-
|
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
|
-
|
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
|
-
|
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
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
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
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
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
|
-
|
271
|
-
|
272
|
-
|
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
|
-
|
284
|
-
| ------------ | ----------- |
|
285
|
-
| `:validator` | Callable object returning true/false. Receives value and options as parameters |
|
386
|
+
private
|
286
387
|
|
287
|
-
|
388
|
+
def business_user?
|
389
|
+
user_type == "business"
|
390
|
+
end
|
288
391
|
|
289
|
-
|
392
|
+
def minor_user?
|
393
|
+
context.user_age < 18
|
394
|
+
end
|
290
395
|
|
291
|
-
|
292
|
-
|
293
|
-
|
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
|
---
|