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,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
|
-
- [
|
23
|
-
- [Single Type Coercion Errors](#single-type-coercion-errors)
|
24
|
-
- [Multiple Type Coercion Errors](#multiple-type-coercion-errors)
|
12
|
+
- [Error Handling](#error-handling)
|
25
13
|
- [Custom Coercion Options](#custom-coercion-options)
|
26
|
-
|
27
|
-
- [BigDecimal Precision Options](#bigdecimal-precision-options)
|
14
|
+
- [Custom Coercions](#custom-coercions)
|
28
15
|
|
29
16
|
## TLDR
|
30
17
|
|
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
|
163
|
-
|
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
|
-
```
|
121
|
+
## Advanced Examples
|
179
122
|
|
180
|
-
###
|
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,147 +217,179 @@ 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
|
-
>
|
318
|
-
|
319
|
-
### Single Type Coercion Errors
|
255
|
+
> Coercion failures provide detailed error information including parameter paths, attempted types, and specific failure reasons.
|
320
256
|
|
321
257
|
```ruby
|
322
|
-
class
|
323
|
-
|
324
|
-
required :
|
325
|
-
required :
|
326
|
-
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
|
327
262
|
|
328
263
|
def call
|
329
|
-
# Task logic
|
264
|
+
# Task logic
|
330
265
|
end
|
331
|
-
|
332
266
|
end
|
333
267
|
|
334
|
-
# Invalid
|
335
|
-
result =
|
336
|
-
|
337
|
-
|
338
|
-
|
268
|
+
# Invalid inputs
|
269
|
+
result = ProcessDataTask.call(
|
270
|
+
count: "not-a-number",
|
271
|
+
amount: "invalid-float",
|
272
|
+
active: "maybe"
|
339
273
|
)
|
340
274
|
|
341
|
-
result.failed?
|
275
|
+
result.failed? # → true
|
342
276
|
result.metadata
|
343
|
-
|
344
|
-
#
|
345
|
-
#
|
346
|
-
#
|
347
|
-
#
|
348
|
-
#
|
349
|
-
# }
|
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"]
|
350
283
|
# }
|
284
|
+
# }
|
351
285
|
```
|
352
286
|
|
353
|
-
###
|
287
|
+
### Common Error Scenarios
|
354
288
|
|
355
289
|
```ruby
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
required :customer_data, type: [:hash, :array, :string]
|
290
|
+
# Invalid array JSON
|
291
|
+
ProcessDataTask.call(items: "[invalid json")
|
292
|
+
# → "items could not coerce into an array"
|
360
293
|
|
361
|
-
|
362
|
-
|
363
|
-
|
294
|
+
# Invalid date format
|
295
|
+
ProcessDataTask.call(start_date: "not-a-date")
|
296
|
+
# → "start_date could not coerce into a date"
|
364
297
|
|
365
|
-
|
366
|
-
|
367
|
-
#
|
368
|
-
result = ProcessFlexibleDataTask.call(
|
369
|
-
order_value: "invalid-number",
|
370
|
-
customer_data: Object.new
|
371
|
-
)
|
372
|
-
|
373
|
-
result.failed? #=> true
|
374
|
-
result.metadata
|
375
|
-
#=> {
|
376
|
-
# reason: "order_value could not coerce into one of: float, integer. customer_data could not coerce into one of: hash, array, string.",
|
377
|
-
# messages: {
|
378
|
-
# order_value: ["could not coerce into one of: float, integer"],
|
379
|
-
# customer_data: ["could not coerce into one of: hash, array, string"]
|
380
|
-
# }
|
381
|
-
# }
|
298
|
+
# Multiple type failure
|
299
|
+
ProcessDataTask.call(value: "abc", type: [:integer, :float])
|
300
|
+
# → "value could not coerce into one of: integer, float"
|
382
301
|
```
|
383
302
|
|
384
303
|
## Custom Coercion Options
|
385
304
|
|
386
|
-
### Date/Time
|
305
|
+
### Date/Time Formats
|
387
306
|
|
388
307
|
```ruby
|
389
|
-
class
|
390
|
-
|
308
|
+
class ImportDataTask < CMDx::Task
|
391
309
|
# US date format
|
392
310
|
required :birth_date, type: :date, format: "%m/%d/%Y"
|
393
311
|
|
394
|
-
#
|
395
|
-
required :
|
312
|
+
# European datetime
|
313
|
+
required :timestamp, type: :datetime, format: "%d.%m.%Y %H:%M"
|
396
314
|
|
397
|
-
#
|
398
|
-
optional :
|
315
|
+
# 12-hour time
|
316
|
+
optional :appointment, type: :time, format: "%I:%M %p"
|
399
317
|
|
400
318
|
def call
|
401
|
-
|
402
|
-
event_timestamp #=> DateTime with timezone
|
403
|
-
meeting_time #=> Time with hour:minute format
|
319
|
+
# Dates parsed according to specified formats
|
404
320
|
end
|
405
|
-
|
406
321
|
end
|
407
|
-
|
408
|
-
ProcessCustomDateTask.call(
|
409
|
-
birth_date: "12/25/1990",
|
410
|
-
event_timestamp: "2023-12-25 10:30:00 UTC",
|
411
|
-
meeting_time: "14:30"
|
412
|
-
)
|
413
322
|
```
|
414
323
|
|
415
|
-
### BigDecimal Precision
|
324
|
+
### BigDecimal Precision
|
416
325
|
|
417
326
|
```ruby
|
418
|
-
class
|
419
|
-
|
327
|
+
class CalculatePriceTask < CMDx::Task
|
420
328
|
required :base_price, type: :big_decimal
|
421
|
-
required :tax_rate, type: :big_decimal, precision:
|
329
|
+
required :tax_rate, type: :big_decimal, precision: 8
|
422
330
|
|
423
331
|
def call
|
424
|
-
|
425
|
-
tax_rate #=> BigDecimal with 6-digit precision
|
332
|
+
tax_amount = base_price * tax_rate # High-precision calculation
|
426
333
|
end
|
334
|
+
end
|
335
|
+
```
|
336
|
+
|
337
|
+
## Custom Coercions
|
338
|
+
|
339
|
+
> [!NOTE]
|
340
|
+
> Register custom coercions for domain-specific types not covered by built-in coercions.
|
427
341
|
|
342
|
+
```ruby
|
343
|
+
# Custom coercion for currency handling
|
344
|
+
module CurrencyCoercion
|
345
|
+
module_function
|
346
|
+
|
347
|
+
def call(value, options = {})
|
348
|
+
return value if value.is_a?(BigDecimal)
|
349
|
+
|
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}"
|
355
|
+
end
|
428
356
|
end
|
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
|
368
|
+
CMDx.configure do |config|
|
369
|
+
config.coercions.register(:currency, CurrencyCoercion)
|
370
|
+
config.coercions.register(:slug, SlugCoercion)
|
371
|
+
end
|
372
|
+
|
373
|
+
# Use in tasks
|
374
|
+
class ProcessProductTask < CMDx::Task
|
375
|
+
required :price, type: :currency
|
376
|
+
required :url_slug, type: :slug
|
377
|
+
|
378
|
+
def call
|
379
|
+
price # → BigDecimal from "$99.99"
|
380
|
+
url_slug # → "my-product-name" from "My Product Name!"
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
ProcessProductTask.call(
|
385
|
+
price: "$149.99",
|
386
|
+
url_slug: "My Amazing Product!"
|
387
|
+
)
|
429
388
|
```
|
430
389
|
|
390
|
+
> [!TIP]
|
391
|
+
> Custom coercions should be idempotent and handle edge cases gracefully. Include proper error handling for invalid inputs.
|
392
|
+
|
431
393
|
---
|
432
394
|
|
433
395
|
- **Prev:** [Parameters - Namespacing](namespacing.md)
|