cmdx 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/prompts/docs.md +9 -0
  3. data/.cursor/prompts/rspec.md +13 -12
  4. data/.cursor/prompts/yardoc.md +11 -6
  5. data/CHANGELOG.md +13 -2
  6. data/README.md +1 -0
  7. data/docs/ai_prompts.md +269 -195
  8. data/docs/basics/call.md +124 -58
  9. data/docs/basics/chain.md +190 -160
  10. data/docs/basics/context.md +242 -154
  11. data/docs/basics/setup.md +302 -32
  12. data/docs/callbacks.md +390 -94
  13. data/docs/configuration.md +181 -65
  14. data/docs/deprecation.md +245 -0
  15. data/docs/getting_started.md +161 -39
  16. data/docs/internationalization.md +590 -70
  17. data/docs/interruptions/exceptions.md +135 -118
  18. data/docs/interruptions/faults.md +150 -125
  19. data/docs/interruptions/halt.md +134 -80
  20. data/docs/logging.md +181 -118
  21. data/docs/middlewares.md +150 -377
  22. data/docs/outcomes/result.md +140 -112
  23. data/docs/outcomes/states.md +134 -99
  24. data/docs/outcomes/statuses.md +204 -146
  25. data/docs/parameters/coercions.md +232 -281
  26. data/docs/parameters/defaults.md +224 -169
  27. data/docs/parameters/definitions.md +289 -141
  28. data/docs/parameters/namespacing.md +250 -161
  29. data/docs/parameters/validations.md +260 -133
  30. data/docs/testing.md +191 -197
  31. data/docs/workflows.md +143 -98
  32. data/lib/cmdx/callback.rb +23 -19
  33. data/lib/cmdx/callback_registry.rb +1 -3
  34. data/lib/cmdx/chain_inspector.rb +23 -23
  35. data/lib/cmdx/chain_serializer.rb +38 -19
  36. data/lib/cmdx/coercion.rb +20 -12
  37. data/lib/cmdx/coercion_registry.rb +51 -32
  38. data/lib/cmdx/configuration.rb +84 -31
  39. data/lib/cmdx/context.rb +32 -21
  40. data/lib/cmdx/core_ext/hash.rb +13 -13
  41. data/lib/cmdx/core_ext/module.rb +1 -1
  42. data/lib/cmdx/core_ext/object.rb +12 -12
  43. data/lib/cmdx/correlator.rb +60 -39
  44. data/lib/cmdx/errors.rb +105 -131
  45. data/lib/cmdx/fault.rb +66 -45
  46. data/lib/cmdx/immutator.rb +20 -21
  47. data/lib/cmdx/lazy_struct.rb +78 -70
  48. data/lib/cmdx/log_formatters/json.rb +1 -1
  49. data/lib/cmdx/log_formatters/key_value.rb +1 -1
  50. data/lib/cmdx/log_formatters/line.rb +1 -1
  51. data/lib/cmdx/log_formatters/logstash.rb +1 -1
  52. data/lib/cmdx/log_formatters/pretty_json.rb +1 -1
  53. data/lib/cmdx/log_formatters/pretty_key_value.rb +1 -1
  54. data/lib/cmdx/log_formatters/pretty_line.rb +1 -1
  55. data/lib/cmdx/log_formatters/raw.rb +2 -2
  56. data/lib/cmdx/logger.rb +19 -14
  57. data/lib/cmdx/logger_ansi.rb +33 -17
  58. data/lib/cmdx/logger_serializer.rb +85 -24
  59. data/lib/cmdx/middleware.rb +39 -21
  60. data/lib/cmdx/middleware_registry.rb +4 -3
  61. data/lib/cmdx/parameter.rb +151 -89
  62. data/lib/cmdx/parameter_inspector.rb +34 -21
  63. data/lib/cmdx/parameter_registry.rb +36 -30
  64. data/lib/cmdx/parameter_serializer.rb +21 -14
  65. data/lib/cmdx/result.rb +136 -135
  66. data/lib/cmdx/result_ansi.rb +31 -17
  67. data/lib/cmdx/result_inspector.rb +32 -27
  68. data/lib/cmdx/result_logger.rb +23 -14
  69. data/lib/cmdx/result_serializer.rb +65 -27
  70. data/lib/cmdx/task.rb +234 -113
  71. data/lib/cmdx/task_deprecator.rb +22 -25
  72. data/lib/cmdx/task_processor.rb +89 -88
  73. data/lib/cmdx/task_serializer.rb +27 -14
  74. data/lib/cmdx/utils/monotonic_runtime.rb +2 -4
  75. data/lib/cmdx/validator.rb +25 -16
  76. data/lib/cmdx/validator_registry.rb +53 -31
  77. data/lib/cmdx/validators/exclusion.rb +1 -1
  78. data/lib/cmdx/validators/format.rb +2 -2
  79. data/lib/cmdx/validators/inclusion.rb +2 -2
  80. data/lib/cmdx/validators/length.rb +2 -2
  81. data/lib/cmdx/validators/numeric.rb +3 -3
  82. data/lib/cmdx/validators/presence.rb +2 -2
  83. data/lib/cmdx/version.rb +1 -1
  84. data/lib/cmdx/workflow.rb +54 -33
  85. data/lib/generators/cmdx/task_generator.rb +6 -6
  86. data/lib/generators/cmdx/workflow_generator.rb +6 -6
  87. metadata +3 -1
@@ -1,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 Coercion Examples](#advanced-coercion-examples)
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
- - [Coercion Error Handling](#coercion-error-handling)
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
- - **Type coercion** - Automatic conversion using `type:` option (`:integer`, `:boolean`, `:array`, `:hash`, etc.)
31
- - **Multiple types** - Fallback with `type: [:float, :integer]` - tries each until one succeeds
32
- - **No conversion** - Default `:virtual` type returns values unchanged
33
- - **Before validation** - Coercion happens automatically before parameter validation
34
- - **Rich types** - Supports all Ruby built-ins plus JSON parsing for arrays/hashes
35
- - **Custom coercions** - Register custom coercion types
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
- > By default, parameters use the `:virtual` type which returns values unchanged. Type coercion is specified using the `:type` option and occurs automatically during parameter value resolution, before validation.
41
-
42
- ### Available Coercion Types
43
-
44
- CMDx supports comprehensive type coercion for Ruby's built-in types:
45
-
46
- | Type | Description | Example Input | Example Output |
47
- |------|-------------|---------------|----------------|
48
- | `:array` | Converts to Array, handles JSON strings | `"[1,2,3]"` | `[1, 2, 3]` |
49
- | `:big_decimal` | High-precision decimal arithmetic | `"123.456"` | `BigDecimal("123.456")` |
50
- | `:boolean` | True/false conversion with text patterns | `"true"`, `"yes"`, `"1"` | `true` |
51
- | `:complex` | Complex number conversion | `"1+2i"` | `Complex(1, 2)` |
52
- | `:date` | Date object conversion | `"2023-12-25"` | `Date.new(2023, 12, 25)` |
53
- | `:datetime` | DateTime object conversion | `"2023-12-25 10:30"` | `DateTime` object |
54
- | `:float` | Floating-point number conversion | `"123.45"` | `123.45` |
55
- | `:hash` | Hash conversion, handles JSON strings | `'{"a":1}'` | `{"a" => 1}` |
56
- | `:integer` | Integer conversion, handles various formats | `"123"`, `"0xFF"` | `123`, `255` |
57
- | `:rational` | Rational number conversion | `"1/2"`, `0.5` | `Rational(1, 2)` |
58
- | `:string` | String conversion for any object | `123`, `:symbol` | `"123"`, `"symbol"` |
59
- | `:time` | Time object conversion | `"2023-12-25 10:30"` | `Time` object |
60
- | `:virtual` | No conversion (default) | `anything` | `anything` |
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 ProcessUserDataTask < CMDx::Task
66
-
60
+ class ProcessPaymentTask < CMDx::Task
61
+ required :amount, type: :float
67
62
  required :user_id, type: :integer
68
- required :order_total, type: :float
69
- required :is_premium, type: :boolean
70
- required :notes, type: :string
63
+ required :send_email, type: :boolean
71
64
 
72
- optional :product_tags, type: :array, default: []
73
- optional :preferences, type: :hash, default: {}
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
- user_id #=> 12345 (integer from "12345")
79
- order_total #=> 299.99 (float from "299.99")
80
- is_premium #=> true (boolean from "true")
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
- # Coercion happens automatically
91
- ProcessUserDataTask.call(
92
- user_id: "12345",
93
- order_total: "299.99",
94
- is_premium: "yes",
95
- notes: 67890,
96
- product_tags: "[\"electronics\",\"phone\"]",
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
- > Parameters can specify multiple types for fallback coercion, attempting each type in order until one succeeds. This provides flexible input handling while maintaining type safety.
90
+ > Specify multiple types for fallback coercion. CMDx attempts each type in order until one succeeds.
107
91
 
108
92
  ```ruby
109
- class ProcessOrderDataTask < CMDx::Task
110
-
111
- # Try float first for precise calculations, fall back to integer
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
- # Try hash first for structured data, fall back to string for raw data
115
- optional :shipping_info, type: [:hash, :string]
97
+ # Data: try structured hash, fall back to raw string
98
+ optional :notes, type: [:hash, :string]
116
99
 
117
- # Complex fallback for timestamps
118
- optional :scheduled_at, type: [:datetime, :date, :string]
100
+ # Temporal: flexible date/time handling
101
+ optional :due_date, type: [:datetime, :date, :string]
119
102
 
120
103
  def call
121
- amount #=> 149.99 (float) or 150 (integer) depending on input
122
- shipping_info #=> {"address" => "123 Main St"} (hash) or "Express shipping" (string)
123
- scheduled_at #=> DateTime, Date, or String depending on input format
124
- end
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
- def call
146
- item_ids #=> Array of product IDs
147
- quantities #=> Array of quantities or empty array
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
- # Array coercion handles multiple input formats
153
- ProcessOrderItemsTask.call(item_ids: [101, 102, 103]) # => already array
154
- ProcessOrderItemsTask.call(item_ids: "[101,102,103]") # => from JSON string
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
- ### Hash Coercion
160
-
161
- ```ruby
162
- class ProcessOrderConfigTask < CMDx::Task
121
+ ## Advanced Examples
163
122
 
164
- required :shipping_config, type: :hash
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 ValidateUserSettingsTask < CMDx::Task
184
-
185
- required :email_notifications, type: :boolean
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
- email_notifications #=> true or false from various inputs
191
- is_active #=> true or false
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
- # Boolean coercion recognizes many text patterns
198
- ValidateUserSettingsTask.call(email_notifications: "true") # => true
199
- ValidateUserSettingsTask.call(email_notifications: "yes") # => true
200
- ValidateUserSettingsTask.call(email_notifications: "1") # => true
201
- ValidateUserSettingsTask.call(email_notifications: "false") # => false
202
- ValidateUserSettingsTask.call(email_notifications: "no") # => false
203
- ValidateUserSettingsTask.call(email_notifications: "0") # => false
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
- ### Date and Time Coercion
148
+ ### Boolean Patterns
207
149
 
208
150
  ```ruby
209
- class ProcessOrderScheduleTask < CMDx::Task
210
-
211
- required :order_date, type: :date
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
- order_date #=> Date object
221
- created_at #=> DateTime object
222
- updated_at #=> Time object
223
- delivery_date #=> Date parsed with custom format
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
- ProcessOrderScheduleTask.call(
230
- order_date: "2023-12-25",
231
- created_at: "2023-12-25 10:30:00",
232
- updated_at: "2023-12-25 10:30:00",
233
- delivery_date: "2023-12-28",
234
- pickup_time: "14:30:00"
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
- ### Numeric Coercion
177
+ ### Date and Time Handling
239
178
 
240
179
  ```ruby
241
- class CalculateOrderTotalsTask < CMDx::Task
242
-
243
- required :item_count, type: :integer
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
- # High-precision for financial calculations
248
- optional :discount_amount, type: :big_decimal, precision: 4
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
- item_count #=> Integer from various formats
256
- subtotal #=> Float for currency
257
- tax_rate #=> Float for percentage
258
- discount_amount #=> BigDecimal with specified precision
259
- shipping_ratio #=> Rational number
260
- complex_calculation #=> Complex number
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
- CalculateOrderTotalsTask.call(
266
- item_count: "5",
267
- subtotal: "249.99",
268
- tax_rate: "0.0875",
269
- discount_amount: "25.0000",
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 works seamlessly with nested parameter structures, applying type conversion at each level of the hierarchy.
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 ProcessOrderDetailsTask < CMDx::Task
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 :is_active, type: :boolean
291
- optional :created_at, type: :datetime
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 #=> Hash (coerced from JSON string if needed)
297
-
298
- # Nested coercions
299
- id #=> Integer (from order.id)
300
- total #=> Float (from order.total)
301
- items #=> Array (from order.items)
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
- ## Coercion Error Handling
252
+ ## Error Handling
315
253
 
316
254
  > [!WARNING]
317
- > When coercion fails, CMDx provides detailed error information including the parameter name, attempted types, and specific failure reasons.
255
+ > Coercion failures provide detailed error information including parameter paths, attempted types, and specific failure reasons.
318
256
 
319
257
  ```ruby
320
- class ValidateUserProfileTask < CMDx::Task
321
-
322
- required :age, type: :integer
323
- required :salary, type: [:float, :big_decimal]
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 here
264
+ # Task logic
328
265
  end
329
-
330
266
  end
331
267
 
332
- # Invalid coercion inputs
333
- result = ValidateUserProfileTask.call(
334
- age: "not-a-number",
335
- salary: "invalid-amount",
336
- is_employed: "maybe"
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? #=> true
275
+ result.failed? # true
340
276
  result.metadata
341
- #=> {
342
- # reason: "age could not coerce into an integer. could not coerce into one of: float, big_decimal. is_employed could not coerce into a boolean.",
343
- # messages: {
344
- # age: ["could not coerce into an integer"],
345
- # salary: ["could not coerce into one of: float, big_decimal"],
346
- # is_employed: ["could not coerce into a boolean"]
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 Format Options
305
+ ### Date/Time Formats
354
306
 
355
307
  ```ruby
356
- class ProcessCustomDateTask < CMDx::Task
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
- # ISO datetime with timezone
362
- required :event_timestamp, type: :datetime, format: "%Y-%m-%d %H:%M:%S %Z"
312
+ # European datetime
313
+ required :timestamp, type: :datetime, format: "%d.%m.%Y %H:%M"
363
314
 
364
- # 24-hour time format
365
- optional :meeting_time, type: :time, format: "%H:%M"
315
+ # 12-hour time
316
+ optional :appointment, type: :time, format: "%I:%M %p"
366
317
 
367
318
  def call
368
- birth_date #=> Date parsed with MM/DD/YYYY format
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 Options
324
+ ### BigDecimal Precision
383
325
 
384
326
  ```ruby
385
- class CalculatePricingTask < CMDx::Task
386
-
327
+ class CalculatePriceTask < CMDx::Task
387
328
  required :base_price, type: :big_decimal
388
- required :tax_rate, type: :big_decimal, precision: 6
329
+ required :tax_rate, type: :big_decimal, precision: 8
389
330
 
390
331
  def call
391
- base_price #=> BigDecimal with default precision
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
- > CMDx allows you to register custom coercions for domain-specific types that aren't covered by the built-in coercions. Custom coercions can be registered globally or per-task basis.
340
+ > Register custom coercions for domain-specific types not covered by built-in coercions.
402
341
 
403
342
  ```ruby
404
- module MoneyCoercion
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
- # Handle string amounts like "$123.45"
411
- if value.is_a?(String)
412
- clean_value = value.gsub(/[$,]/, '')
413
- BigDecimal(clean_value)
414
- else
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(:money, MoneyCoercion)
422
- config.coercions.register(:slug, proc do |value|
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
- # Now use in any task
373
+ # Use in tasks
428
374
  class ProcessProductTask < CMDx::Task
429
- required :cost, type: :money
430
- required :slug, type: :slug
375
+ required :price, type: :currency
376
+ required :url_slug, type: :slug
431
377
 
432
378
  def call
433
- cost #=> 123.45
434
- slug #=> "my-blog-post-title" (URL-friendly)
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 - calling them multiple times with the same input should produce the same result. This ensures predictable behavior when coercions are applied during parameter processing.
391
+ > Custom coercions should be idempotent and handle edge cases gracefully. Include proper error handling for invalid inputs.
441
392
 
442
393
  ---
443
394