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.
- checksums.yaml +4 -4
- data/.cursor/prompts/docs.md +9 -0
- data/.cursor/prompts/rspec.md +13 -12
- data/.cursor/prompts/yardoc.md +11 -6
- data/CHANGELOG.md +13 -2
- data/README.md +1 -0
- data/docs/ai_prompts.md +269 -195
- data/docs/basics/call.md +124 -58
- 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 +390 -94
- data/docs/configuration.md +181 -65
- 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 +150 -125
- data/docs/interruptions/halt.md +134 -80
- data/docs/logging.md +181 -118
- data/docs/middlewares.md +150 -377
- 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 +232 -281
- 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 +260 -133
- data/docs/testing.md +191 -197
- data/docs/workflows.md +143 -98
- data/lib/cmdx/callback.rb +23 -19
- data/lib/cmdx/callback_registry.rb +1 -3
- data/lib/cmdx/chain_inspector.rb +23 -23
- data/lib/cmdx/chain_serializer.rb +38 -19
- data/lib/cmdx/coercion.rb +20 -12
- data/lib/cmdx/coercion_registry.rb +51 -32
- data/lib/cmdx/configuration.rb +84 -31
- data/lib/cmdx/context.rb +32 -21
- data/lib/cmdx/core_ext/hash.rb +13 -13
- data/lib/cmdx/core_ext/module.rb +1 -1
- data/lib/cmdx/core_ext/object.rb +12 -12
- data/lib/cmdx/correlator.rb +60 -39
- data/lib/cmdx/errors.rb +105 -131
- data/lib/cmdx/fault.rb +66 -45
- data/lib/cmdx/immutator.rb +20 -21
- data/lib/cmdx/lazy_struct.rb +78 -70
- data/lib/cmdx/log_formatters/json.rb +1 -1
- data/lib/cmdx/log_formatters/key_value.rb +1 -1
- data/lib/cmdx/log_formatters/line.rb +1 -1
- data/lib/cmdx/log_formatters/logstash.rb +1 -1
- data/lib/cmdx/log_formatters/pretty_json.rb +1 -1
- data/lib/cmdx/log_formatters/pretty_key_value.rb +1 -1
- data/lib/cmdx/log_formatters/pretty_line.rb +1 -1
- data/lib/cmdx/log_formatters/raw.rb +2 -2
- data/lib/cmdx/logger.rb +19 -14
- data/lib/cmdx/logger_ansi.rb +33 -17
- data/lib/cmdx/logger_serializer.rb +85 -24
- data/lib/cmdx/middleware.rb +39 -21
- data/lib/cmdx/middleware_registry.rb +4 -3
- data/lib/cmdx/parameter.rb +151 -89
- data/lib/cmdx/parameter_inspector.rb +34 -21
- data/lib/cmdx/parameter_registry.rb +36 -30
- data/lib/cmdx/parameter_serializer.rb +21 -14
- data/lib/cmdx/result.rb +136 -135
- data/lib/cmdx/result_ansi.rb +31 -17
- data/lib/cmdx/result_inspector.rb +32 -27
- data/lib/cmdx/result_logger.rb +23 -14
- data/lib/cmdx/result_serializer.rb +65 -27
- data/lib/cmdx/task.rb +234 -113
- data/lib/cmdx/task_deprecator.rb +22 -25
- data/lib/cmdx/task_processor.rb +89 -88
- data/lib/cmdx/task_serializer.rb +27 -14
- data/lib/cmdx/utils/monotonic_runtime.rb +2 -4
- data/lib/cmdx/validator.rb +25 -16
- data/lib/cmdx/validator_registry.rb +53 -31
- data/lib/cmdx/validators/exclusion.rb +1 -1
- data/lib/cmdx/validators/format.rb +2 -2
- data/lib/cmdx/validators/inclusion.rb +2 -2
- data/lib/cmdx/validators/length.rb +2 -2
- data/lib/cmdx/validators/numeric.rb +3 -3
- data/lib/cmdx/validators/presence.rb +2 -2
- data/lib/cmdx/version.rb +1 -1
- data/lib/cmdx/workflow.rb +54 -33
- data/lib/generators/cmdx/task_generator.rb +6 -6
- data/lib/generators/cmdx/workflow_generator.rb +6 -6
- metadata +3 -1
@@ -1,285 +1,215 @@
|
|
1
1
|
# Parameters - Coercions
|
2
2
|
|
3
|
-
Parameter coercions provide automatic type conversion for task arguments, enabling
|
4
|
-
flexible input handling while ensuring type safety within task execution. Coercions
|
5
|
-
transform raw input values into expected types, supporting everything from simple
|
6
|
-
string-to-integer conversion to complex JSON parsing and custom type handling.
|
3
|
+
Parameter coercions provide automatic type conversion for task arguments, enabling flexible input handling while ensuring type safety. Coercions transform raw input values into expected types, supporting everything from simple string-to-integer conversion to complex JSON parsing and custom type handling.
|
7
4
|
|
8
5
|
## Table of Contents
|
9
6
|
|
10
7
|
- [TLDR](#tldr)
|
11
8
|
- [Coercion Fundamentals](#coercion-fundamentals)
|
12
|
-
- [Available Coercion Types](#available-coercion-types)
|
13
|
-
- [Basic Type Coercion](#basic-type-coercion)
|
14
9
|
- [Multiple Type Coercion](#multiple-type-coercion)
|
15
|
-
- [Advanced
|
16
|
-
- [Array Coercion](#array-coercion)
|
17
|
-
- [Hash Coercion](#hash-coercion)
|
18
|
-
- [Boolean Coercion](#boolean-coercion)
|
19
|
-
- [Date and Time Coercion](#date-and-time-coercion)
|
20
|
-
- [Numeric Coercion](#numeric-coercion)
|
10
|
+
- [Advanced Examples](#advanced-examples)
|
21
11
|
- [Coercion with Nested Parameters](#coercion-with-nested-parameters)
|
22
|
-
- [
|
12
|
+
- [Error Handling](#error-handling)
|
23
13
|
- [Custom Coercion Options](#custom-coercion-options)
|
24
|
-
- [Date/Time Format Options](#datetime-format-options)
|
25
|
-
- [BigDecimal Precision Options](#bigdecimal-precision-options)
|
26
14
|
- [Custom Coercions](#custom-coercions)
|
27
15
|
|
28
16
|
## TLDR
|
29
17
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
18
|
+
```ruby
|
19
|
+
# Basic type coercion
|
20
|
+
required :user_id, type: :integer # "123" → 123
|
21
|
+
required :active, type: :boolean # "true" → true
|
22
|
+
required :tags, type: :array # "[1,2,3]" → [1, 2, 3]
|
23
|
+
|
24
|
+
# Multiple type fallback
|
25
|
+
required :amount, type: [:float, :integer] # Tries float, then integer
|
26
|
+
|
27
|
+
# Custom formats
|
28
|
+
required :created_at, type: :date, format: "%Y-%m-%d"
|
29
|
+
|
30
|
+
# No conversion (default)
|
31
|
+
required :raw_data, type: :virtual # Returns unchanged
|
32
|
+
```
|
36
33
|
|
37
34
|
## Coercion Fundamentals
|
38
35
|
|
39
36
|
> [!NOTE]
|
40
|
-
>
|
41
|
-
|
42
|
-
### Available
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
|
47
|
-
|
48
|
-
| `:
|
49
|
-
| `:
|
50
|
-
| `:
|
51
|
-
| `:
|
52
|
-
| `:
|
53
|
-
| `:
|
54
|
-
| `:
|
55
|
-
| `:
|
56
|
-
| `:
|
57
|
-
| `:
|
58
|
-
| `:
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
### Basic Type Coercion
|
37
|
+
> Parameters use `:virtual` type by default (no conversion). Coercion occurs automatically during parameter resolution, before validation.
|
38
|
+
|
39
|
+
### Available Types
|
40
|
+
|
41
|
+
| Type | Description | Example |
|
42
|
+
|------|-------------|---------|
|
43
|
+
| `:array` | Array conversion, handles JSON | `"[1,2,3]"` → `[1, 2, 3]` |
|
44
|
+
| `:big_decimal` | High-precision decimal | `"123.45"` → `BigDecimal("123.45")` |
|
45
|
+
| `:boolean` | True/false with text patterns | `"yes"` → `true` |
|
46
|
+
| `:complex` | Complex numbers | `"1+2i"` → `Complex(1, 2)` |
|
47
|
+
| `:date` | Date objects | `"2023-12-25"` → `Date` |
|
48
|
+
| `:datetime` | DateTime objects | `"2023-12-25 10:30"` → `DateTime` |
|
49
|
+
| `:float` | Floating-point | `"123.45"` → `123.45` |
|
50
|
+
| `:hash` | Hash conversion, handles JSON | `'{"a":1}'` → `{"a" => 1}` |
|
51
|
+
| `:integer` | Integer, handles hex/octal | `"0xFF"` → `255` |
|
52
|
+
| `:rational` | Rational numbers | `"1/2"` → `Rational(1, 2)` |
|
53
|
+
| `:string` | String conversion | `123` → `"123"` |
|
54
|
+
| `:time` | Time objects | `"10:30:00"` → `Time` |
|
55
|
+
| `:virtual` | No conversion (default) | Input unchanged |
|
56
|
+
|
57
|
+
### Basic Usage
|
63
58
|
|
64
59
|
```ruby
|
65
|
-
class
|
66
|
-
|
60
|
+
class ProcessPaymentTask < CMDx::Task
|
61
|
+
required :amount, type: :float
|
67
62
|
required :user_id, type: :integer
|
68
|
-
required :
|
69
|
-
required :is_premium, type: :boolean
|
70
|
-
required :notes, type: :string
|
63
|
+
required :send_email, type: :boolean
|
71
64
|
|
72
|
-
optional :
|
73
|
-
optional :
|
74
|
-
optional :created_at, type: :datetime
|
75
|
-
optional :delivery_date, type: :date
|
65
|
+
optional :metadata, type: :hash, default: {}
|
66
|
+
optional :tags, type: :array, default: []
|
76
67
|
|
77
68
|
def call
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
notes #=> "Rush delivery" (string)
|
82
|
-
product_tags #=> ["electronics", "phone"] (array from JSON)
|
83
|
-
preferences #=> {"notifications" => true} (hash from JSON)
|
84
|
-
created_at #=> DateTime object
|
85
|
-
delivery_date #=> Date object
|
86
|
-
end
|
69
|
+
# All parameters automatically coerced
|
70
|
+
charge_amount = amount * 100 # Float math
|
71
|
+
user = User.find(user_id) # Integer lookup
|
87
72
|
|
73
|
+
send_notification if send_email # Boolean logic
|
74
|
+
end
|
88
75
|
end
|
89
76
|
|
90
|
-
#
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
preferences: '{"notifications":true}',
|
98
|
-
created_at: "2023-12-25 14:30:00",
|
99
|
-
delivery_date: "2023-12-28"
|
77
|
+
# Usage with string inputs
|
78
|
+
ProcessPaymentTask.call(
|
79
|
+
amount: "99.99", # → 99.99 (Float)
|
80
|
+
user_id: "12345", # → 12345 (Integer)
|
81
|
+
send_email: "true", # → true (Boolean)
|
82
|
+
metadata: '{"source":"web"}', # → {"source" => "web"} (Hash)
|
83
|
+
tags: "[\"priority\"]" # → ["priority"] (Array)
|
100
84
|
)
|
101
85
|
```
|
102
86
|
|
103
87
|
## Multiple Type Coercion
|
104
88
|
|
105
89
|
> [!TIP]
|
106
|
-
>
|
90
|
+
> Specify multiple types for fallback coercion. CMDx attempts each type in order until one succeeds.
|
107
91
|
|
108
92
|
```ruby
|
109
|
-
class
|
110
|
-
|
111
|
-
|
112
|
-
required :amount, type: [:float, :integer]
|
93
|
+
class ProcessOrderTask < CMDx::Task
|
94
|
+
# Numeric: try precise float, fall back to integer
|
95
|
+
required :total, type: [:float, :integer]
|
113
96
|
|
114
|
-
#
|
115
|
-
optional :
|
97
|
+
# Data: try structured hash, fall back to raw string
|
98
|
+
optional :notes, type: [:hash, :string]
|
116
99
|
|
117
|
-
#
|
118
|
-
optional :
|
100
|
+
# Temporal: flexible date/time handling
|
101
|
+
optional :due_date, type: [:datetime, :date, :string]
|
119
102
|
|
120
103
|
def call
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
end
|
127
|
-
|
128
|
-
# Different inputs produce different coerced types
|
129
|
-
ProcessOrderDataTask.call(amount: "149.99") # => 149.99 (float)
|
130
|
-
ProcessOrderDataTask.call(amount: "150") # => 150 (integer)
|
131
|
-
ProcessOrderDataTask.call(shipping_info: '{"address":"123 Main St"}') # => hash
|
132
|
-
ProcessOrderDataTask.call(shipping_info: "Express shipping") # => string
|
133
|
-
```
|
134
|
-
|
135
|
-
## Advanced Coercion Examples
|
136
|
-
|
137
|
-
### Array Coercion
|
138
|
-
|
139
|
-
```ruby
|
140
|
-
class ProcessOrderItemsTask < CMDx::Task
|
141
|
-
|
142
|
-
required :item_ids, type: :array
|
143
|
-
optional :quantities, type: :array, default: []
|
104
|
+
case total
|
105
|
+
when Float then process_precise_amount(total)
|
106
|
+
when Integer then process_rounded_amount(total)
|
107
|
+
end
|
144
108
|
|
145
|
-
|
146
|
-
|
147
|
-
|
109
|
+
case notes
|
110
|
+
when Hash then structured_notes = notes
|
111
|
+
when String then fallback_notes = notes
|
112
|
+
end
|
148
113
|
end
|
149
|
-
|
150
114
|
end
|
151
115
|
|
152
|
-
#
|
153
|
-
|
154
|
-
|
155
|
-
ProcessOrderItemsTask.call(item_ids: "101") # => ["101"] (wrapped)
|
156
|
-
ProcessOrderItemsTask.call(item_ids: nil) # => [] (nil to empty)
|
116
|
+
# Different inputs produce different types
|
117
|
+
ProcessOrderTask.call(total: "99.99") # → 99.99 (Float)
|
118
|
+
ProcessOrderTask.call(total: "100") # → 100 (Integer)
|
157
119
|
```
|
158
120
|
|
159
|
-
|
160
|
-
|
161
|
-
```ruby
|
162
|
-
class ProcessOrderConfigTask < CMDx::Task
|
121
|
+
## Advanced Examples
|
163
122
|
|
164
|
-
|
165
|
-
optional :payment_options, type: :hash, default: {}
|
166
|
-
|
167
|
-
def call
|
168
|
-
shipping_config #=> Hash with shipping configuration
|
169
|
-
payment_options #=> Hash with payment options or empty hash
|
170
|
-
end
|
171
|
-
|
172
|
-
end
|
173
|
-
|
174
|
-
# Hash coercion supports multiple formats
|
175
|
-
ProcessOrderConfigTask.call(shipping_config: {carrier: "UPS", speed: "express"})
|
176
|
-
ProcessOrderConfigTask.call(shipping_config: '{"carrier":"UPS","speed":"express"}')
|
177
|
-
ProcessOrderConfigTask.call(shipping_config: [:carrier, "UPS", :speed, "express"])
|
178
|
-
```
|
179
|
-
|
180
|
-
### Boolean Coercion
|
123
|
+
### Array and Hash Coercion
|
181
124
|
|
182
125
|
```ruby
|
183
|
-
class
|
184
|
-
|
185
|
-
required :
|
186
|
-
required :is_active, type: :boolean
|
187
|
-
optional :marketing_consent, type: :boolean, default: false
|
126
|
+
class ProcessInventoryTask < CMDx::Task
|
127
|
+
required :product_ids, type: :array
|
128
|
+
required :config, type: :hash
|
188
129
|
|
189
130
|
def call
|
190
|
-
|
191
|
-
|
192
|
-
marketing_consent #=> true or false with default
|
131
|
+
products = Product.where(id: product_ids)
|
132
|
+
apply_configuration(config)
|
193
133
|
end
|
194
|
-
|
195
134
|
end
|
196
135
|
|
197
|
-
#
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
136
|
+
# Multiple input formats supported
|
137
|
+
ProcessInventoryTask.call(
|
138
|
+
product_ids: [1, 2, 3], # Already array
|
139
|
+
product_ids: "[1,2,3]", # JSON string
|
140
|
+
product_ids: "1", # Single value → ["1"]
|
141
|
+
|
142
|
+
config: {key: "value"}, # Already hash
|
143
|
+
config: '{"key":"value"}', # JSON string
|
144
|
+
config: [:key, "value"] # Array pairs → Hash
|
145
|
+
)
|
204
146
|
```
|
205
147
|
|
206
|
-
###
|
148
|
+
### Boolean Patterns
|
207
149
|
|
208
150
|
```ruby
|
209
|
-
class
|
210
|
-
|
211
|
-
required :
|
212
|
-
required :created_at, type: :datetime
|
213
|
-
optional :updated_at, type: :time
|
214
|
-
|
215
|
-
# Custom format options for specific date/time formats
|
216
|
-
optional :delivery_date, type: :date, format: "%Y-%m-%d"
|
217
|
-
optional :pickup_time, type: :time, format: "%H:%M:%S"
|
151
|
+
class UpdateUserSettingsTask < CMDx::Task
|
152
|
+
required :notifications, type: :boolean
|
153
|
+
required :active, type: :boolean
|
218
154
|
|
219
155
|
def call
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
pickup_time #=> Time parsed with custom format
|
156
|
+
user.update!(
|
157
|
+
email_notifications: notifications,
|
158
|
+
account_active: active
|
159
|
+
)
|
225
160
|
end
|
226
|
-
|
227
161
|
end
|
228
162
|
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
163
|
+
# Boolean coercion recognizes many patterns
|
164
|
+
UpdateUserSettingsTask.call(
|
165
|
+
notifications: "true", # → true
|
166
|
+
notifications: "yes", # → true
|
167
|
+
notifications: "1", # → true
|
168
|
+
notifications: "on", # → true
|
169
|
+
|
170
|
+
active: "false", # → false
|
171
|
+
active: "no", # → false
|
172
|
+
active: "0", # → false
|
173
|
+
active: "off" # → false
|
235
174
|
)
|
236
175
|
```
|
237
176
|
|
238
|
-
###
|
177
|
+
### Date and Time Handling
|
239
178
|
|
240
179
|
```ruby
|
241
|
-
class
|
242
|
-
|
243
|
-
required :
|
244
|
-
required :subtotal, type: :float
|
245
|
-
required :tax_rate, type: :float
|
180
|
+
class ScheduleEventTask < CMDx::Task
|
181
|
+
required :event_date, type: :date
|
182
|
+
required :start_time, type: :time
|
246
183
|
|
247
|
-
#
|
248
|
-
optional :
|
249
|
-
|
250
|
-
# For specialized calculations
|
251
|
-
optional :shipping_ratio, type: :rational
|
252
|
-
optional :complex_calculation, type: :complex
|
184
|
+
# Custom formats for specific inputs
|
185
|
+
optional :deadline, type: :date, format: "%m/%d/%Y"
|
186
|
+
optional :meeting_time, type: :time, format: "%I:%M %p"
|
253
187
|
|
254
188
|
def call
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
189
|
+
Event.create!(
|
190
|
+
scheduled_date: event_date,
|
191
|
+
start_time: start_time,
|
192
|
+
deadline: deadline,
|
193
|
+
meeting_time: meeting_time
|
194
|
+
)
|
261
195
|
end
|
262
|
-
|
263
196
|
end
|
264
197
|
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
shipping_ratio: "1/10",
|
271
|
-
complex_calculation: "1+2i"
|
198
|
+
ScheduleEventTask.call(
|
199
|
+
event_date: "2023-12-25", # Standard ISO format
|
200
|
+
start_time: "14:30:00", # 24-hour format
|
201
|
+
deadline: "12/31/2023", # Custom MM/DD/YYYY format
|
202
|
+
meeting_time: "2:30 PM" # 12-hour with AM/PM
|
272
203
|
)
|
273
204
|
```
|
274
205
|
|
275
206
|
## Coercion with Nested Parameters
|
276
207
|
|
277
208
|
> [!IMPORTANT]
|
278
|
-
> Coercion
|
209
|
+
> Coercion applies at every level of nested parameter structures, enabling complex data transformation while maintaining type safety.
|
279
210
|
|
280
211
|
```ruby
|
281
|
-
class
|
282
|
-
|
212
|
+
class ProcessOrderTask < CMDx::Task
|
283
213
|
required :order, type: :hash do
|
284
214
|
required :id, type: :integer
|
285
215
|
required :total, type: :float
|
@@ -287,157 +217,178 @@ class ProcessOrderDetailsTask < CMDx::Task
|
|
287
217
|
|
288
218
|
optional :customer, type: :hash do
|
289
219
|
required :id, type: :integer
|
290
|
-
required :
|
291
|
-
optional :
|
220
|
+
required :active, type: :boolean
|
221
|
+
optional :signup_date, type: :date
|
292
222
|
end
|
293
223
|
end
|
294
224
|
|
295
225
|
def call
|
296
|
-
order
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
# Deep nested coercions
|
304
|
-
if customer
|
305
|
-
customer_id = id # Integer (from order.customer.id)
|
306
|
-
active_status = is_active # Boolean (from order.customer.is_active)
|
307
|
-
created_time = created_at # DateTime (from order.customer.created_at)
|
226
|
+
order_id = order[:id] # Integer (coerced)
|
227
|
+
total_amount = order[:total] # Float (coerced)
|
228
|
+
|
229
|
+
if order[:customer]
|
230
|
+
customer_id = order[:customer][:id] # Integer (coerced)
|
231
|
+
is_active = order[:customer][:active] # Boolean (coerced)
|
232
|
+
signup = order[:customer][:signup_date] # Date (coerced)
|
308
233
|
end
|
309
234
|
end
|
310
|
-
|
311
235
|
end
|
236
|
+
|
237
|
+
# JSON input with automatic nested coercion
|
238
|
+
ProcessOrderTask.call(
|
239
|
+
order: '{
|
240
|
+
"id": "12345",
|
241
|
+
"total": "299.99",
|
242
|
+
"items": ["item1", "item2"],
|
243
|
+
"customer": {
|
244
|
+
"id": "67890",
|
245
|
+
"active": "true",
|
246
|
+
"signup_date": "2023-01-15"
|
247
|
+
}
|
248
|
+
}'
|
249
|
+
)
|
312
250
|
```
|
313
251
|
|
314
|
-
##
|
252
|
+
## Error Handling
|
315
253
|
|
316
254
|
> [!WARNING]
|
317
|
-
>
|
255
|
+
> Coercion failures provide detailed error information including parameter paths, attempted types, and specific failure reasons.
|
318
256
|
|
319
257
|
```ruby
|
320
|
-
class
|
321
|
-
|
322
|
-
required :
|
323
|
-
required :
|
324
|
-
required :is_employed, type: :boolean
|
258
|
+
class ProcessDataTask < CMDx::Task
|
259
|
+
required :count, type: :integer
|
260
|
+
required :amount, type: [:float, :big_decimal]
|
261
|
+
required :active, type: :boolean
|
325
262
|
|
326
263
|
def call
|
327
|
-
# Task logic
|
264
|
+
# Task logic
|
328
265
|
end
|
329
|
-
|
330
266
|
end
|
331
267
|
|
332
|
-
# Invalid
|
333
|
-
result =
|
334
|
-
|
335
|
-
|
336
|
-
|
268
|
+
# Invalid inputs
|
269
|
+
result = ProcessDataTask.call(
|
270
|
+
count: "not-a-number",
|
271
|
+
amount: "invalid-float",
|
272
|
+
active: "maybe"
|
337
273
|
)
|
338
274
|
|
339
|
-
result.failed?
|
275
|
+
result.failed? # → true
|
340
276
|
result.metadata
|
341
|
-
|
342
|
-
#
|
343
|
-
#
|
344
|
-
#
|
345
|
-
#
|
346
|
-
#
|
347
|
-
# }
|
277
|
+
# {
|
278
|
+
# reason: "count could not coerce into an integer. amount could not coerce into one of: float, big_decimal. active could not coerce into a boolean.",
|
279
|
+
# messages: {
|
280
|
+
# count: ["could not coerce into an integer"],
|
281
|
+
# amount: ["could not coerce into one of: float, big_decimal"],
|
282
|
+
# active: ["could not coerce into a boolean"]
|
348
283
|
# }
|
284
|
+
# }
|
285
|
+
```
|
286
|
+
|
287
|
+
### Common Error Scenarios
|
288
|
+
|
289
|
+
```ruby
|
290
|
+
# Invalid array JSON
|
291
|
+
ProcessDataTask.call(items: "[invalid json")
|
292
|
+
# → "items could not coerce into an array"
|
293
|
+
|
294
|
+
# Invalid date format
|
295
|
+
ProcessDataTask.call(start_date: "not-a-date")
|
296
|
+
# → "start_date could not coerce into a date"
|
297
|
+
|
298
|
+
# Multiple type failure
|
299
|
+
ProcessDataTask.call(value: "abc", type: [:integer, :float])
|
300
|
+
# → "value could not coerce into one of: integer, float"
|
349
301
|
```
|
350
302
|
|
351
303
|
## Custom Coercion Options
|
352
304
|
|
353
|
-
### Date/Time
|
305
|
+
### Date/Time Formats
|
354
306
|
|
355
307
|
```ruby
|
356
|
-
class
|
357
|
-
|
308
|
+
class ImportDataTask < CMDx::Task
|
358
309
|
# US date format
|
359
310
|
required :birth_date, type: :date, format: "%m/%d/%Y"
|
360
311
|
|
361
|
-
#
|
362
|
-
required :
|
312
|
+
# European datetime
|
313
|
+
required :timestamp, type: :datetime, format: "%d.%m.%Y %H:%M"
|
363
314
|
|
364
|
-
#
|
365
|
-
optional :
|
315
|
+
# 12-hour time
|
316
|
+
optional :appointment, type: :time, format: "%I:%M %p"
|
366
317
|
|
367
318
|
def call
|
368
|
-
|
369
|
-
event_timestamp #=> DateTime with timezone
|
370
|
-
meeting_time #=> Time with hour:minute format
|
319
|
+
# Dates parsed according to specified formats
|
371
320
|
end
|
372
|
-
|
373
321
|
end
|
374
|
-
|
375
|
-
ProcessCustomDateTask.call(
|
376
|
-
birth_date: "12/25/1990",
|
377
|
-
event_timestamp: "2023-12-25 10:30:00 UTC",
|
378
|
-
meeting_time: "14:30"
|
379
|
-
)
|
380
322
|
```
|
381
323
|
|
382
|
-
### BigDecimal Precision
|
324
|
+
### BigDecimal Precision
|
383
325
|
|
384
326
|
```ruby
|
385
|
-
class
|
386
|
-
|
327
|
+
class CalculatePriceTask < CMDx::Task
|
387
328
|
required :base_price, type: :big_decimal
|
388
|
-
required :tax_rate, type: :big_decimal, precision:
|
329
|
+
required :tax_rate, type: :big_decimal, precision: 8
|
389
330
|
|
390
331
|
def call
|
391
|
-
|
392
|
-
tax_rate #=> BigDecimal with 6-digit precision
|
332
|
+
tax_amount = base_price * tax_rate # High-precision calculation
|
393
333
|
end
|
394
|
-
|
395
334
|
end
|
396
335
|
```
|
397
336
|
|
398
337
|
## Custom Coercions
|
399
338
|
|
400
339
|
> [!NOTE]
|
401
|
-
>
|
340
|
+
> Register custom coercions for domain-specific types not covered by built-in coercions.
|
402
341
|
|
403
342
|
```ruby
|
404
|
-
|
343
|
+
# Custom coercion for currency handling
|
344
|
+
module CurrencyCoercion
|
405
345
|
module_function
|
406
346
|
|
407
347
|
def call(value, options = {})
|
408
348
|
return value if value.is_a?(BigDecimal)
|
409
349
|
|
410
|
-
#
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
BigDecimal(value.to_s)
|
416
|
-
end
|
350
|
+
# Remove currency symbols and formatting
|
351
|
+
clean_value = value.to_s.gsub(/[$,£€¥]/, '')
|
352
|
+
BigDecimal(clean_value)
|
353
|
+
rescue ArgumentError
|
354
|
+
raise CMDx::Coercion::Error, "Invalid currency format: #{value}"
|
417
355
|
end
|
418
356
|
end
|
419
357
|
|
358
|
+
# URL slug coercion
|
359
|
+
SlugCoercion = proc do |value|
|
360
|
+
value.to_s.downcase
|
361
|
+
.gsub(/[^a-z0-9\s-]/, '')
|
362
|
+
.gsub(/\s+/, '-')
|
363
|
+
.gsub(/-+/, '-')
|
364
|
+
.strip('-')
|
365
|
+
end
|
366
|
+
|
367
|
+
# Register coercions globally
|
420
368
|
CMDx.configure do |config|
|
421
|
-
config.coercions.register(:
|
422
|
-
config.coercions.register(:slug,
|
423
|
-
value.to_s.downcase.gsub(/[^a-z0-9]+/, '-').gsub(/-+/, '-').strip('-')
|
424
|
-
end)
|
369
|
+
config.coercions.register(:currency, CurrencyCoercion)
|
370
|
+
config.coercions.register(:slug, SlugCoercion)
|
425
371
|
end
|
426
372
|
|
427
|
-
#
|
373
|
+
# Use in tasks
|
428
374
|
class ProcessProductTask < CMDx::Task
|
429
|
-
required :
|
430
|
-
required :
|
375
|
+
required :price, type: :currency
|
376
|
+
required :url_slug, type: :slug
|
431
377
|
|
432
378
|
def call
|
433
|
-
|
434
|
-
|
379
|
+
price # → BigDecimal from "$99.99"
|
380
|
+
url_slug # → "my-product-name" from "My Product Name!"
|
435
381
|
end
|
436
382
|
end
|
383
|
+
|
384
|
+
ProcessProductTask.call(
|
385
|
+
price: "$149.99",
|
386
|
+
url_slug: "My Amazing Product!"
|
387
|
+
)
|
437
388
|
```
|
438
389
|
|
439
390
|
> [!TIP]
|
440
|
-
> Custom coercions should be idempotent
|
391
|
+
> Custom coercions should be idempotent and handle edge cases gracefully. Include proper error handling for invalid inputs.
|
441
392
|
|
442
393
|
---
|
443
394
|
|