cmdx 1.1.2 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.DS_Store +0 -0
- data/.cursor/prompts/docs.md +4 -1
- data/.cursor/prompts/llms.md +20 -0
- data/.cursor/prompts/rspec.md +4 -1
- data/.cursor/prompts/yardoc.md +3 -2
- data/.cursor/rules/cursor-instructions.mdc +55 -1
- data/.irbrc +6 -0
- data/.rubocop.yml +29 -18
- data/CHANGELOG.md +11 -132
- data/LLM.md +3317 -0
- data/README.md +68 -44
- data/docs/attributes/coercions.md +162 -0
- data/docs/attributes/defaults.md +90 -0
- data/docs/attributes/definitions.md +281 -0
- data/docs/attributes/naming.md +78 -0
- data/docs/attributes/validations.md +309 -0
- data/docs/basics/chain.md +56 -249
- data/docs/basics/context.md +56 -289
- data/docs/basics/execution.md +114 -0
- data/docs/basics/setup.md +37 -334
- data/docs/callbacks.md +89 -467
- data/docs/deprecation.md +91 -174
- data/docs/getting_started.md +212 -202
- data/docs/internationalization.md +11 -647
- data/docs/interruptions/exceptions.md +23 -198
- data/docs/interruptions/faults.md +71 -151
- data/docs/interruptions/halt.md +109 -186
- data/docs/logging.md +44 -256
- data/docs/middlewares.md +113 -426
- data/docs/outcomes/result.md +81 -228
- data/docs/outcomes/states.md +33 -221
- data/docs/outcomes/statuses.md +21 -311
- data/docs/tips_and_tricks.md +120 -70
- data/docs/workflows.md +99 -283
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/attribute.rb +229 -0
- data/lib/cmdx/attribute_registry.rb +94 -0
- data/lib/cmdx/attribute_value.rb +193 -0
- data/lib/cmdx/callback_registry.rb +69 -77
- data/lib/cmdx/chain.rb +56 -73
- data/lib/cmdx/coercion_registry.rb +52 -68
- data/lib/cmdx/coercions/array.rb +19 -18
- data/lib/cmdx/coercions/big_decimal.rb +20 -24
- data/lib/cmdx/coercions/boolean.rb +26 -25
- data/lib/cmdx/coercions/complex.rb +21 -22
- data/lib/cmdx/coercions/date.rb +25 -23
- data/lib/cmdx/coercions/date_time.rb +24 -25
- data/lib/cmdx/coercions/float.rb +25 -22
- data/lib/cmdx/coercions/hash.rb +31 -32
- data/lib/cmdx/coercions/integer.rb +30 -24
- data/lib/cmdx/coercions/rational.rb +29 -24
- data/lib/cmdx/coercions/string.rb +19 -22
- data/lib/cmdx/coercions/symbol.rb +37 -0
- data/lib/cmdx/coercions/time.rb +26 -25
- data/lib/cmdx/configuration.rb +49 -108
- data/lib/cmdx/context.rb +222 -44
- data/lib/cmdx/deprecator.rb +61 -0
- data/lib/cmdx/errors.rb +42 -252
- data/lib/cmdx/exceptions.rb +39 -0
- data/lib/cmdx/faults.rb +78 -39
- data/lib/cmdx/freezer.rb +51 -0
- data/lib/cmdx/identifier.rb +30 -0
- data/lib/cmdx/locale.rb +52 -0
- data/lib/cmdx/log_formatters/json.rb +21 -22
- data/lib/cmdx/log_formatters/key_value.rb +20 -22
- data/lib/cmdx/log_formatters/line.rb +15 -22
- data/lib/cmdx/log_formatters/logstash.rb +22 -23
- data/lib/cmdx/log_formatters/raw.rb +16 -22
- data/lib/cmdx/middleware_registry.rb +70 -74
- data/lib/cmdx/middlewares/correlate.rb +90 -54
- data/lib/cmdx/middlewares/runtime.rb +58 -0
- data/lib/cmdx/middlewares/timeout.rb +48 -68
- data/lib/cmdx/railtie.rb +12 -45
- data/lib/cmdx/result.rb +229 -314
- data/lib/cmdx/task.rb +194 -366
- data/lib/cmdx/utils/call.rb +49 -0
- data/lib/cmdx/utils/condition.rb +71 -0
- data/lib/cmdx/utils/format.rb +61 -0
- data/lib/cmdx/validator_registry.rb +63 -72
- data/lib/cmdx/validators/exclusion.rb +38 -67
- data/lib/cmdx/validators/format.rb +48 -49
- data/lib/cmdx/validators/inclusion.rb +43 -74
- data/lib/cmdx/validators/length.rb +101 -162
- data/lib/cmdx/validators/numeric.rb +95 -170
- data/lib/cmdx/validators/presence.rb +37 -50
- data/lib/cmdx/version.rb +1 -1
- data/lib/cmdx/worker.rb +178 -0
- data/lib/cmdx/workflow.rb +85 -81
- data/lib/cmdx.rb +19 -13
- data/lib/generators/cmdx/install_generator.rb +14 -13
- data/lib/generators/cmdx/task_generator.rb +25 -50
- data/lib/generators/cmdx/templates/install.rb +11 -46
- data/lib/generators/cmdx/templates/task.rb.tt +3 -2
- data/lib/locales/en.yml +18 -4
- data/src/cmdx-logo.png +0 -0
- metadata +32 -116
- data/docs/ai_prompts.md +0 -393
- data/docs/basics/call.md +0 -317
- data/docs/configuration.md +0 -344
- data/docs/parameters/coercions.md +0 -396
- data/docs/parameters/defaults.md +0 -335
- data/docs/parameters/definitions.md +0 -446
- data/docs/parameters/namespacing.md +0 -378
- data/docs/parameters/validations.md +0 -405
- data/docs/testing.md +0 -553
- data/lib/cmdx/callback.rb +0 -53
- data/lib/cmdx/chain_inspector.rb +0 -56
- data/lib/cmdx/chain_serializer.rb +0 -63
- data/lib/cmdx/coercion.rb +0 -57
- data/lib/cmdx/coercions/virtual.rb +0 -29
- data/lib/cmdx/core_ext/hash.rb +0 -83
- data/lib/cmdx/core_ext/module.rb +0 -98
- data/lib/cmdx/core_ext/object.rb +0 -125
- data/lib/cmdx/correlator.rb +0 -122
- data/lib/cmdx/error.rb +0 -67
- data/lib/cmdx/fault.rb +0 -140
- data/lib/cmdx/immutator.rb +0 -52
- data/lib/cmdx/lazy_struct.rb +0 -246
- data/lib/cmdx/log_formatters/pretty_json.rb +0 -40
- data/lib/cmdx/log_formatters/pretty_key_value.rb +0 -38
- data/lib/cmdx/log_formatters/pretty_line.rb +0 -41
- data/lib/cmdx/logger.rb +0 -49
- data/lib/cmdx/logger_ansi.rb +0 -68
- data/lib/cmdx/logger_serializer.rb +0 -116
- data/lib/cmdx/middleware.rb +0 -70
- data/lib/cmdx/parameter.rb +0 -312
- data/lib/cmdx/parameter_evaluator.rb +0 -231
- data/lib/cmdx/parameter_inspector.rb +0 -66
- data/lib/cmdx/parameter_registry.rb +0 -106
- data/lib/cmdx/parameter_serializer.rb +0 -59
- data/lib/cmdx/result_ansi.rb +0 -71
- data/lib/cmdx/result_inspector.rb +0 -71
- data/lib/cmdx/result_logger.rb +0 -59
- data/lib/cmdx/result_serializer.rb +0 -104
- data/lib/cmdx/rspec/matchers.rb +0 -28
- data/lib/cmdx/rspec/result_matchers/be_executed.rb +0 -42
- data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +0 -94
- data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +0 -94
- data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +0 -59
- data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +0 -57
- data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +0 -87
- data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +0 -51
- data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +0 -58
- data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +0 -59
- data/lib/cmdx/rspec/result_matchers/have_context.rb +0 -86
- data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +0 -54
- data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +0 -52
- data/lib/cmdx/rspec/result_matchers/have_metadata.rb +0 -114
- data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +0 -66
- data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +0 -64
- data/lib/cmdx/rspec/result_matchers/have_runtime.rb +0 -78
- data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +0 -76
- data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +0 -62
- data/lib/cmdx/rspec/task_matchers/have_callback.rb +0 -85
- data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +0 -68
- data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +0 -92
- data/lib/cmdx/rspec/task_matchers/have_middleware.rb +0 -46
- data/lib/cmdx/rspec/task_matchers/have_parameter.rb +0 -181
- data/lib/cmdx/task_deprecator.rb +0 -58
- data/lib/cmdx/task_processor.rb +0 -246
- data/lib/cmdx/task_serializer.rb +0 -57
- data/lib/cmdx/utils/ansi_color.rb +0 -73
- data/lib/cmdx/utils/log_timestamp.rb +0 -36
- data/lib/cmdx/utils/monotonic_runtime.rb +0 -34
- data/lib/cmdx/utils/name_affix.rb +0 -52
- data/lib/cmdx/validator.rb +0 -57
- data/lib/generators/cmdx/templates/workflow.rb.tt +0 -7
- data/lib/generators/cmdx/workflow_generator.rb +0 -84
- data/lib/locales/ar.yml +0 -35
- data/lib/locales/cs.yml +0 -35
- data/lib/locales/da.yml +0 -35
- data/lib/locales/de.yml +0 -35
- data/lib/locales/el.yml +0 -35
- data/lib/locales/es.yml +0 -35
- data/lib/locales/fi.yml +0 -35
- data/lib/locales/fr.yml +0 -35
- data/lib/locales/he.yml +0 -35
- data/lib/locales/hi.yml +0 -35
- data/lib/locales/it.yml +0 -35
- data/lib/locales/ja.yml +0 -35
- data/lib/locales/ko.yml +0 -35
- data/lib/locales/nl.yml +0 -35
- data/lib/locales/no.yml +0 -35
- data/lib/locales/pl.yml +0 -35
- data/lib/locales/pt.yml +0 -35
- data/lib/locales/ru.yml +0 -35
- data/lib/locales/sv.yml +0 -35
- data/lib/locales/th.yml +0 -35
- data/lib/locales/tr.yml +0 -35
- data/lib/locales/vi.yml +0 -35
- data/lib/locales/zh.yml +0 -35
@@ -1,668 +1,32 @@
|
|
1
1
|
# Internationalization (i18n)
|
2
2
|
|
3
|
-
CMDx provides comprehensive internationalization support for all error messages,
|
3
|
+
CMDx provides comprehensive internationalization support for all error messages, attribute validation failures, coercion errors, and fault messages. All user-facing text is automatically localized based on the current `I18n.locale`, ensuring your applications can serve global audiences with native-language error reporting.
|
4
4
|
|
5
5
|
## Table of Contents
|
6
6
|
|
7
|
-
- [
|
8
|
-
- [Available Locales](#available-locales)
|
9
|
-
- [Configuration](#configuration)
|
10
|
-
- [Fault Messages](#fault-messages)
|
11
|
-
- [Parameter Messages](#parameter-messages)
|
12
|
-
- [Coercion Messages](#coercion-messages)
|
13
|
-
- [Validation Messages](#validation-messages)
|
14
|
-
- [Custom Message Overrides](#custom-message-overrides)
|
15
|
-
- [Error Handling and Debugging](#error-handling-and-debugging)
|
7
|
+
- [Localization](#localization)
|
16
8
|
|
17
|
-
##
|
18
|
-
|
19
|
-
```ruby
|
20
|
-
# Automatic localization based on I18n.locale
|
21
|
-
I18n.locale = :es
|
22
|
-
result = CreateUserTask.call(email: "invalid", age: "too-young")
|
23
|
-
result.metadata[:messages][:email] #=> ["formato inválido"]
|
24
|
-
|
25
|
-
# 24 built-in languages with complete coverage
|
26
|
-
# Parameter-specific overrides available
|
27
|
-
# Covers coercion, validation, and fault messages
|
28
|
-
```
|
9
|
+
## Localization
|
29
10
|
|
30
11
|
> [!NOTE]
|
31
|
-
> CMDx automatically localizes all error messages based on
|
32
|
-
|
33
|
-
## Available Locales
|
34
|
-
|
35
|
-
CMDx includes built-in translations for 24 major world languages, covering both Western and Eastern language families:
|
36
|
-
|
37
|
-
| Language | Locale | Language | Locale | Language | Locale |
|
38
|
-
|----------|--------|----------|--------|----------|--------|
|
39
|
-
| English | `:en` | Russian | `:ru` | Arabic | `:ar` |
|
40
|
-
| Spanish | `:es` | Korean | `:ko` | Dutch | `:nl` |
|
41
|
-
| French | `:fr` | Hindi | `:hi` | Swedish | `:sv` |
|
42
|
-
| German | `:de` | Polish | `:pl` | Norwegian | `:no` |
|
43
|
-
| Portuguese | `:pt` | Turkish | `:tr` | Finnish | `:fi` |
|
44
|
-
| Italian | `:it` | Danish | `:da` | Greek | `:el` |
|
45
|
-
| Japanese | `:ja` | Czech | `:cs` | Hebrew | `:he` |
|
46
|
-
| Chinese | `:zh` | Thai | `:th` | Vietnamese | `:vi` |
|
47
|
-
|
48
|
-
> [!TIP]
|
49
|
-
> All locales provide complete coverage for every error message type, including complex nested parameter validation errors and multi-type coercion failures.
|
50
|
-
|
51
|
-
## Configuration
|
52
|
-
|
53
|
-
### Basic Setup
|
12
|
+
> CMDx automatically localizes all error messages based on the `I18n.locale` setting.
|
54
13
|
|
55
14
|
```ruby
|
56
|
-
|
57
|
-
|
58
|
-
config.i18n.available_locales = [:en, :es, :fr, :de]
|
15
|
+
class ProcessQuote < CMDx::Task
|
16
|
+
attribute :price, type: :float
|
59
17
|
|
60
|
-
|
61
|
-
|
62
|
-
before_action :set_locale
|
63
|
-
|
64
|
-
private
|
65
|
-
|
66
|
-
def set_locale
|
67
|
-
I18n.locale = params[:locale] || request.headers['Accept-Language']&.scan(/^[a-z]{2}/)&.first || :en
|
18
|
+
def work
|
19
|
+
# Your logic here...
|
68
20
|
end
|
69
21
|
end
|
70
|
-
```
|
71
22
|
|
72
|
-
### Per-Request Localization
|
73
|
-
|
74
|
-
```ruby
|
75
|
-
class ProcessOrderTask < CMDx::Task
|
76
|
-
required :amount, type: :float
|
77
|
-
required :customer_email, format: { with: /@/ }
|
78
|
-
|
79
|
-
def call
|
80
|
-
# Task logic runs with current I18n.locale
|
81
|
-
ChargeCustomer.call(amount: amount, email: customer_email)
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
# Different locales produce localized errors
|
86
23
|
I18n.with_locale(:fr) do
|
87
|
-
result =
|
88
|
-
result.metadata[:messages][:
|
89
|
-
end
|
90
|
-
```
|
91
|
-
|
92
|
-
## Fault Messages
|
93
|
-
|
94
|
-
> [!IMPORTANT]
|
95
|
-
> Fault messages from `fail!` and `skip!` methods are automatically localized when no explicit reason is provided.
|
96
|
-
|
97
|
-
### Default Fault Localization
|
98
|
-
|
99
|
-
```ruby
|
100
|
-
class ProcessPaymentTask < CMDx::Task
|
101
|
-
required :payment_method, inclusion: { in: %w[card paypal bank] }
|
102
|
-
required :amount, type: :float
|
103
|
-
|
104
|
-
def call
|
105
|
-
if payment_declined?
|
106
|
-
fail! # Uses localized default message
|
107
|
-
end
|
108
|
-
|
109
|
-
if amount < minimum_charge
|
110
|
-
skip! # Uses localized default message
|
111
|
-
end
|
112
|
-
|
113
|
-
charge_payment
|
114
|
-
end
|
115
|
-
|
116
|
-
private
|
117
|
-
|
118
|
-
def payment_declined?
|
119
|
-
# Payment gateway logic
|
120
|
-
rand > 0.8
|
121
|
-
end
|
122
|
-
|
123
|
-
def minimum_charge
|
124
|
-
5.00
|
125
|
-
end
|
126
|
-
end
|
127
|
-
|
128
|
-
# English
|
129
|
-
I18n.locale = :en
|
130
|
-
result = ProcessPaymentTask.call(payment_method: "card", amount: 99.99)
|
131
|
-
result.metadata[:reason] #=> "no reason given"
|
132
|
-
|
133
|
-
# Spanish
|
134
|
-
I18n.locale = :es
|
135
|
-
result = ProcessPaymentTask.call(payment_method: "card", amount: 99.99)
|
136
|
-
result.metadata[:reason] #=> "no se proporcionó razón"
|
137
|
-
|
138
|
-
# Japanese
|
139
|
-
I18n.locale = :ja
|
140
|
-
result = ProcessPaymentTask.call(payment_method: "card", amount: 99.99)
|
141
|
-
result.metadata[:reason] #=> "理由が提供されませんでした"
|
142
|
-
```
|
143
|
-
|
144
|
-
### Custom Fault Messages
|
145
|
-
|
146
|
-
```ruby
|
147
|
-
class ProcessRefundTask < CMDx::Task
|
148
|
-
required :order_id, type: :integer
|
149
|
-
required :reason, presence: true
|
150
|
-
|
151
|
-
def call
|
152
|
-
order = find_order(order_id)
|
153
|
-
|
154
|
-
# Custom messages override locale defaults
|
155
|
-
fail!("Payment gateway unavailable") if gateway_down?
|
156
|
-
skip!("Refund already processed") if order.refunded?
|
157
|
-
|
158
|
-
process_refund(order)
|
159
|
-
end
|
160
|
-
end
|
161
|
-
```
|
162
|
-
|
163
|
-
## Parameter Messages
|
164
|
-
|
165
|
-
> [!WARNING]
|
166
|
-
> Parameter errors include both missing required parameters and undefined source method delegates, with full internationalization support.
|
167
|
-
|
168
|
-
### Required Parameter Errors
|
169
|
-
|
170
|
-
```ruby
|
171
|
-
class CreateUserAccountTask < CMDx::Task
|
172
|
-
required :email, format: { with: /@/ }
|
173
|
-
required :password, length: { min: 8 }
|
174
|
-
required :age, type: :integer, numeric: { min: 18 }
|
175
|
-
|
176
|
-
optional :profile_image, source: :nonexistent_upload_method
|
177
|
-
optional :referral_code, source: :missing_referral_source
|
178
|
-
|
179
|
-
def call
|
180
|
-
User.create!(email: email, password: password, age: age)
|
181
|
-
end
|
182
|
-
end
|
183
|
-
|
184
|
-
# Missing required parameters
|
185
|
-
I18n.locale = :en
|
186
|
-
result = CreateUserAccountTask.call({})
|
187
|
-
result.metadata[:messages]
|
188
|
-
# {
|
189
|
-
# email: ["is a required parameter"],
|
190
|
-
# password: ["is a required parameter"],
|
191
|
-
# age: ["is a required parameter"]
|
192
|
-
# }
|
193
|
-
|
194
|
-
# German localization
|
195
|
-
I18n.locale = :de
|
196
|
-
result = CreateUserAccountTask.call({})
|
197
|
-
result.metadata[:messages]
|
198
|
-
# {
|
199
|
-
# email: ["ist ein erforderlicher Parameter"],
|
200
|
-
# password: ["ist ein erforderlicher Parameter"],
|
201
|
-
# age: ["ist ein erforderlicher Parameter"]
|
202
|
-
# }
|
203
|
-
```
|
204
|
-
|
205
|
-
### Source Method Errors
|
206
|
-
|
207
|
-
```ruby
|
208
|
-
# Undefined source method delegation
|
209
|
-
I18n.locale = :en
|
210
|
-
result = CreateUserAccountTask.call(
|
211
|
-
email: "user@example.com",
|
212
|
-
password: "securepass",
|
213
|
-
age: 25
|
214
|
-
)
|
215
|
-
result.metadata[:messages]
|
216
|
-
# {
|
217
|
-
# profile_image: ["delegates to undefined method nonexistent_upload_method"],
|
218
|
-
# referral_code: ["delegates to undefined method missing_referral_source"]
|
219
|
-
# }
|
220
|
-
|
221
|
-
# French localization
|
222
|
-
I18n.locale = :fr
|
223
|
-
result = CreateUserAccountTask.call(
|
224
|
-
email: "user@example.com",
|
225
|
-
password: "securepass",
|
226
|
-
age: 25
|
227
|
-
)
|
228
|
-
result.metadata[:messages]
|
229
|
-
# {
|
230
|
-
# profile_image: ["délègue à la méthode non définie nonexistent_upload_method"],
|
231
|
-
# referral_code: ["délègue à la méthode non définie missing_referral_source"]
|
232
|
-
# }
|
233
|
-
```
|
234
|
-
|
235
|
-
## Coercion Messages
|
236
|
-
|
237
|
-
> [!NOTE]
|
238
|
-
> Type conversion failures provide detailed, localized error messages that specify the attempted type(s) and input value context.
|
239
|
-
|
240
|
-
### Single Type Coercion Errors
|
241
|
-
|
242
|
-
```ruby
|
243
|
-
class ProcessInventoryTask < CMDx::Task
|
244
|
-
required :product_id, type: :integer
|
245
|
-
required :price, type: :float
|
246
|
-
required :in_stock, type: :boolean
|
247
|
-
required :categories, type: :array
|
248
|
-
required :metadata, type: :hash
|
249
|
-
|
250
|
-
def call
|
251
|
-
# Task implementation
|
252
|
-
end
|
253
|
-
end
|
254
|
-
|
255
|
-
# English coercion errors
|
256
|
-
I18n.locale = :en
|
257
|
-
result = ProcessInventoryTask.call(
|
258
|
-
product_id: "not-a-number",
|
259
|
-
price: "invalid-price",
|
260
|
-
in_stock: "maybe",
|
261
|
-
categories: "[invalid json",
|
262
|
-
metadata: "not-a-hash"
|
263
|
-
)
|
264
|
-
|
265
|
-
result.metadata[:messages]
|
266
|
-
# {
|
267
|
-
# product_id: ["could not coerce into an integer"],
|
268
|
-
# price: ["could not coerce into a float"],
|
269
|
-
# in_stock: ["could not coerce into a boolean"],
|
270
|
-
# categories: ["could not coerce into an array"],
|
271
|
-
# metadata: ["could not coerce into a hash"]
|
272
|
-
# }
|
273
|
-
|
274
|
-
# Spanish coercion errors
|
275
|
-
I18n.locale = :es
|
276
|
-
result = ProcessInventoryTask.call(
|
277
|
-
product_id: "not-a-number",
|
278
|
-
price: "invalid-price"
|
279
|
-
)
|
280
|
-
|
281
|
-
result.metadata[:messages]
|
282
|
-
# {
|
283
|
-
# product_id: ["no se pudo coaccionar a un integer"],
|
284
|
-
# price: ["no se pudo coaccionar a un float"]
|
285
|
-
# }
|
286
|
-
```
|
287
|
-
|
288
|
-
### Multiple Type Coercion Errors
|
289
|
-
|
290
|
-
```ruby
|
291
|
-
class ProcessFlexibleDataTask < CMDx::Task
|
292
|
-
required :amount, type: [:float, :big_decimal, :integer]
|
293
|
-
required :identifier, type: [:integer, :string]
|
294
|
-
required :timestamp, type: [:datetime, :date, :time]
|
295
|
-
|
296
|
-
def call
|
297
|
-
# Task implementation
|
298
|
-
end
|
299
|
-
end
|
300
|
-
|
301
|
-
# Multiple type failure messages
|
302
|
-
I18n.locale = :en
|
303
|
-
result = ProcessFlexibleDataTask.call(
|
304
|
-
amount: "definitely-not-numeric",
|
305
|
-
identifier: nil,
|
306
|
-
timestamp: "not-a-date"
|
307
|
-
)
|
308
|
-
|
309
|
-
result.metadata[:messages]
|
310
|
-
# {
|
311
|
-
# amount: ["could not coerce into one of: float, big_decimal, integer"],
|
312
|
-
# identifier: ["could not coerce into one of: integer, string"],
|
313
|
-
# timestamp: ["could not coerce into one of: datetime, date, time"]
|
314
|
-
# }
|
315
|
-
|
316
|
-
# Chinese localization
|
317
|
-
I18n.locale = :zh
|
318
|
-
result = ProcessFlexibleDataTask.call(amount: "invalid")
|
319
|
-
result.metadata[:messages][:amount] #=> ["无法强制转换为以下类型之一:float、big_decimal、integer"]
|
320
|
-
```
|
321
|
-
|
322
|
-
### Nested Parameter Coercion
|
323
|
-
|
324
|
-
```ruby
|
325
|
-
class ProcessOrderTask < CMDx::Task
|
326
|
-
required :order, type: :hash do
|
327
|
-
required :id, type: :integer
|
328
|
-
required :total, type: :float
|
329
|
-
|
330
|
-
required :customer, type: :hash do
|
331
|
-
required :id, type: :integer
|
332
|
-
required :active, type: :boolean
|
333
|
-
end
|
334
|
-
end
|
335
|
-
|
336
|
-
def call
|
337
|
-
# Task implementation
|
338
|
-
end
|
339
|
-
end
|
340
|
-
|
341
|
-
# Nested coercion errors with full path context
|
342
|
-
result = ProcessOrderTask.call(
|
343
|
-
order: {
|
344
|
-
id: "not-a-number",
|
345
|
-
total: "invalid-amount",
|
346
|
-
customer: {
|
347
|
-
id: "bad-id",
|
348
|
-
active: "maybe"
|
349
|
-
}
|
350
|
-
}
|
351
|
-
)
|
352
|
-
|
353
|
-
result.metadata[:messages]
|
354
|
-
# {
|
355
|
-
# "order.id": ["could not coerce into an integer"],
|
356
|
-
# "order.total": ["could not coerce into a float"],
|
357
|
-
# "order.customer.id": ["could not coerce into an integer"],
|
358
|
-
# "order.customer.active": ["could not coerce into a boolean"]
|
359
|
-
# }
|
360
|
-
```
|
361
|
-
|
362
|
-
## Validation Messages
|
363
|
-
|
364
|
-
> [!TIP]
|
365
|
-
> All built-in validators provide comprehensive internationalization support, including contextual information for complex validation rules.
|
366
|
-
|
367
|
-
### Format Validation
|
368
|
-
|
369
|
-
```ruby
|
370
|
-
class CreateUserTask < CMDx::Task
|
371
|
-
required :email, format: { with: /@/, message: nil } # Use default i18n
|
372
|
-
required :phone, format: { with: /\A\+?[\d\s-()]+\z/ }
|
373
|
-
required :username, format: { with: /\A[a-zA-Z0-9_]+\z/ }
|
374
|
-
|
375
|
-
def call
|
376
|
-
User.create!(email: email, phone: phone, username: username)
|
377
|
-
end
|
378
|
-
end
|
379
|
-
|
380
|
-
# English format errors
|
381
|
-
I18n.locale = :en
|
382
|
-
result = CreateUserTask.call(
|
383
|
-
email: "not-an-email",
|
384
|
-
phone: "invalid!phone",
|
385
|
-
username: "bad@username"
|
386
|
-
)
|
387
|
-
|
388
|
-
result.metadata[:messages]
|
389
|
-
# {
|
390
|
-
# email: ["is an invalid format"],
|
391
|
-
# phone: ["is an invalid format"],
|
392
|
-
# username: ["is an invalid format"]
|
393
|
-
# }
|
394
|
-
|
395
|
-
# Japanese format errors
|
396
|
-
I18n.locale = :ja
|
397
|
-
result = CreateUserTask.call(email: "invalid", phone: "bad")
|
398
|
-
result.metadata[:messages]
|
399
|
-
# {
|
400
|
-
# email: ["無効な形式です"],
|
401
|
-
# phone: ["無効な形式です"]
|
402
|
-
# }
|
403
|
-
```
|
404
|
-
|
405
|
-
### Numeric Validation
|
406
|
-
|
407
|
-
```ruby
|
408
|
-
class ConfigureServiceTask < CMDx::Task
|
409
|
-
required :port, numeric: { min: 1024, max: 65535 }
|
410
|
-
required :timeout, numeric: { greater_than: 0, less_than: 300 }
|
411
|
-
required :retry_count, numeric: { min: 1, max: 10 }
|
412
|
-
|
413
|
-
def call
|
414
|
-
# Service configuration
|
415
|
-
end
|
416
|
-
end
|
417
|
-
|
418
|
-
# English numeric errors
|
419
|
-
I18n.locale = :en
|
420
|
-
result = ConfigureServiceTask.call(
|
421
|
-
port: 80, # Below minimum
|
422
|
-
timeout: 500, # Above maximum
|
423
|
-
retry_count: 0 # Below minimum
|
424
|
-
)
|
425
|
-
|
426
|
-
result.metadata[:messages]
|
427
|
-
# {
|
428
|
-
# port: ["must be greater than or equal to 1024"],
|
429
|
-
# timeout: ["must be less than 300"],
|
430
|
-
# retry_count: ["must be greater than or equal to 1"]
|
431
|
-
# }
|
432
|
-
|
433
|
-
# German numeric errors
|
434
|
-
I18n.locale = :de
|
435
|
-
result = ConfigureServiceTask.call(port: 80, timeout: 500)
|
436
|
-
result.metadata[:messages]
|
437
|
-
# {
|
438
|
-
# port: ["muss größer oder gleich 1024 sein"],
|
439
|
-
# timeout: ["muss kleiner als 300 sein"]
|
440
|
-
# }
|
441
|
-
```
|
442
|
-
|
443
|
-
### Inclusion and Exclusion
|
444
|
-
|
445
|
-
```ruby
|
446
|
-
class ProcessSubscriptionTask < CMDx::Task
|
447
|
-
required :plan, inclusion: { in: %w[basic premium enterprise] }
|
448
|
-
required :billing_cycle, inclusion: { in: %w[monthly yearly] }
|
449
|
-
required :username, exclusion: { from: %w[admin root system] }
|
450
|
-
|
451
|
-
def call
|
452
|
-
# Subscription processing
|
453
|
-
end
|
454
|
-
end
|
455
|
-
|
456
|
-
# English inclusion/exclusion errors
|
457
|
-
I18n.locale = :en
|
458
|
-
result = ProcessSubscriptionTask.call(
|
459
|
-
plan: "invalid-plan",
|
460
|
-
billing_cycle: "weekly",
|
461
|
-
username: "admin"
|
462
|
-
)
|
463
|
-
|
464
|
-
result.metadata[:messages]
|
465
|
-
# {
|
466
|
-
# plan: ["is not included in the list"],
|
467
|
-
# billing_cycle: ["is not included in the list"],
|
468
|
-
# username: ["is reserved"]
|
469
|
-
# }
|
470
|
-
|
471
|
-
# French inclusion/exclusion errors
|
472
|
-
I18n.locale = :fr
|
473
|
-
result = ProcessSubscriptionTask.call(plan: "invalid", username: "root")
|
474
|
-
result.metadata[:messages]
|
475
|
-
# {
|
476
|
-
# plan: ["n'est pas inclus dans la liste"],
|
477
|
-
# username: ["est réservé"]
|
478
|
-
# }
|
479
|
-
```
|
480
|
-
|
481
|
-
### Length Validation
|
482
|
-
|
483
|
-
```ruby
|
484
|
-
class CreatePostTask < CMDx::Task
|
485
|
-
required :title, length: { min: 5, max: 100 }
|
486
|
-
required :content, length: { min: 50 }
|
487
|
-
required :tags, length: { max: 10 }
|
488
|
-
|
489
|
-
def call
|
490
|
-
Post.create!(title: title, content: content, tags: tags)
|
491
|
-
end
|
492
|
-
end
|
493
|
-
|
494
|
-
# English length errors
|
495
|
-
I18n.locale = :en
|
496
|
-
result = CreatePostTask.call(
|
497
|
-
title: "Hi", # Too short
|
498
|
-
content: "Brief content", # Too short
|
499
|
-
tags: (1..15).to_a # Too many
|
500
|
-
)
|
501
|
-
|
502
|
-
result.metadata[:messages]
|
503
|
-
# {
|
504
|
-
# title: ["is too short (minimum is 5 characters)"],
|
505
|
-
# content: ["is too short (minimum is 50 characters)"],
|
506
|
-
# tags: ["is too long (maximum is 10 characters)"]
|
507
|
-
# }
|
508
|
-
|
509
|
-
# Russian length errors
|
510
|
-
I18n.locale = :ru
|
511
|
-
result = CreatePostTask.call(title: "Hi", content: "Short")
|
512
|
-
result.metadata[:messages]
|
513
|
-
# {
|
514
|
-
# title: ["слишком короткий (минимум 5 символов)"],
|
515
|
-
# content: ["слишком короткий (минимум 50 символов)"]
|
516
|
-
# }
|
517
|
-
```
|
518
|
-
|
519
|
-
## Custom Message Overrides
|
520
|
-
|
521
|
-
> [!IMPORTANT]
|
522
|
-
> Parameter-specific custom messages always take precedence over locale defaults, allowing fine-grained control while maintaining i18n support.
|
523
|
-
|
524
|
-
### Override Examples
|
525
|
-
|
526
|
-
```ruby
|
527
|
-
class RegisterAccountTask < CMDx::Task
|
528
|
-
required :email,
|
529
|
-
format: { with: /@/, message: "Please provide a valid email address" }
|
530
|
-
|
531
|
-
required :password,
|
532
|
-
length: { min: 8, message: "Password must be at least 8 characters" }
|
533
|
-
|
534
|
-
required :age,
|
535
|
-
numeric: { min: 18, message: "You must be 18 or older to register" }
|
536
|
-
|
537
|
-
def call
|
538
|
-
# Custom messages override i18n, regardless of locale
|
539
|
-
end
|
540
|
-
end
|
541
|
-
|
542
|
-
# Custom messages ignore locale settings
|
543
|
-
I18n.locale = :es
|
544
|
-
result = RegisterAccountTask.call(
|
545
|
-
email: "invalid",
|
546
|
-
password: "short",
|
547
|
-
age: 16
|
548
|
-
)
|
549
|
-
|
550
|
-
result.metadata[:messages]
|
551
|
-
# {
|
552
|
-
# email: ["Please provide a valid email address"], # Custom override
|
553
|
-
# password: ["Password must be at least 8 characters"], # Custom override
|
554
|
-
# age: ["You must be 18 or older to register"] # Custom override
|
555
|
-
# }
|
556
|
-
```
|
557
|
-
|
558
|
-
### Conditional Overrides
|
559
|
-
|
560
|
-
```ruby
|
561
|
-
class ProcessPaymentTask < CMDx::Task
|
562
|
-
required :amount, type: :float, numeric: { min: 0.01 }
|
563
|
-
|
564
|
-
# Conditional message based on context
|
565
|
-
def validate_amount
|
566
|
-
if context[:currency] == "USD" && amount < 0.50
|
567
|
-
add_error(:amount, "USD payments must be at least $0.50")
|
568
|
-
elsif context[:currency] == "EUR" && amount < 0.01
|
569
|
-
add_error(:amount, "EUR payments must be at least €0.01")
|
570
|
-
end
|
571
|
-
end
|
572
|
-
|
573
|
-
def call
|
574
|
-
validate_amount
|
575
|
-
# Payment processing
|
576
|
-
end
|
577
|
-
end
|
578
|
-
```
|
579
|
-
|
580
|
-
## Error Handling and Debugging
|
581
|
-
|
582
|
-
> [!WARNING]
|
583
|
-
> When debugging i18n issues, check locale availability, fallback behavior, and message key resolution to identify configuration problems.
|
584
|
-
|
585
|
-
### Debugging Missing Translations
|
586
|
-
|
587
|
-
```ruby
|
588
|
-
# Enable translation debugging
|
589
|
-
I18n.exception_handler = lambda do |exception, locale, key, options|
|
590
|
-
Rails.logger.warn "Missing translation: #{locale}.#{key}"
|
591
|
-
"translation missing: #{locale}.#{key}"
|
592
|
-
end
|
593
|
-
|
594
|
-
class DebuggingTask < CMDx::Task
|
595
|
-
required :test_param, type: :integer
|
596
|
-
|
597
|
-
def call
|
598
|
-
# Intentionally trigger coercion error for debugging
|
599
|
-
end
|
600
|
-
end
|
601
|
-
|
602
|
-
# Test with unsupported locale
|
603
|
-
I18n.locale = :unsupported_locale
|
604
|
-
result = DebuggingTask.call(test_param: "invalid")
|
605
|
-
# Logs: "Missing translation: unsupported_locale.cmdx.errors.coercion.integer"
|
606
|
-
```
|
607
|
-
|
608
|
-
### Fallback Configuration
|
609
|
-
|
610
|
-
```ruby
|
611
|
-
# Configure fallback behavior in Rails
|
612
|
-
I18n.fallbacks = { es: [:es, :en], fr: [:fr, :en] }
|
613
|
-
|
614
|
-
class TestLocalizationTask < CMDx::Task
|
615
|
-
required :value, type: :integer
|
616
|
-
|
617
|
-
def call
|
618
|
-
# Task logic
|
619
|
-
end
|
620
|
-
end
|
621
|
-
|
622
|
-
# Test fallback behavior
|
623
|
-
I18n.locale = :es # Falls back to :en if Spanish translation missing
|
624
|
-
result = TestLocalizationTask.call(value: "invalid")
|
625
|
-
# Uses English if Spanish translation unavailable
|
626
|
-
```
|
627
|
-
|
628
|
-
### Error Message Analysis
|
629
|
-
|
630
|
-
```ruby
|
631
|
-
class AnalyzeErrorsTask < CMDx::Task
|
632
|
-
required :data, type: :hash do
|
633
|
-
required :id, type: :integer
|
634
|
-
required :nested, type: :hash do
|
635
|
-
required :value, type: :float
|
636
|
-
end
|
637
|
-
end
|
638
|
-
|
639
|
-
def call
|
640
|
-
# Complex nested structure for testing
|
641
|
-
end
|
642
|
-
end
|
643
|
-
|
644
|
-
# Comprehensive error analysis
|
645
|
-
result = AnalyzeErrorsTask.call(
|
646
|
-
data: {
|
647
|
-
id: "not-integer",
|
648
|
-
nested: {
|
649
|
-
value: "not-float"
|
650
|
-
}
|
651
|
-
}
|
652
|
-
)
|
653
|
-
|
654
|
-
# Analyze error structure
|
655
|
-
puts "Failed: #{result.failed?}"
|
656
|
-
puts "Error count: #{result.metadata[:messages].count}"
|
657
|
-
puts "Nested errors present: #{result.metadata[:messages].keys.any? { |k| k.include?('.') }}"
|
658
|
-
|
659
|
-
result.metadata[:messages].each do |param, errors|
|
660
|
-
puts "#{param}: #{errors.join(', ')}"
|
24
|
+
result = ProcessQuote.execute(price: "invalid")
|
25
|
+
result.metadata[:messages][:price] #=> ["impossible de contraindre en float"]
|
661
26
|
end
|
662
|
-
# Output shows full parameter path context for nested errors
|
663
27
|
```
|
664
28
|
|
665
29
|
---
|
666
30
|
|
667
31
|
- **Prev:** [Logging](logging.md)
|
668
|
-
- **Next:** [
|
32
|
+
- **Next:** [Deprecation](deprecation.md)
|