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.
Files changed (170) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/prompts/docs.md +9 -0
  3. data/.cursor/prompts/rspec.md +21 -0
  4. data/.cursor/prompts/yardoc.md +13 -0
  5. data/.rubocop.yml +2 -0
  6. data/CHANGELOG.md +29 -3
  7. data/README.md +2 -1
  8. data/docs/ai_prompts.md +269 -195
  9. data/docs/basics/call.md +126 -60
  10. data/docs/basics/chain.md +190 -160
  11. data/docs/basics/context.md +242 -154
  12. data/docs/basics/setup.md +302 -32
  13. data/docs/callbacks.md +382 -119
  14. data/docs/configuration.md +211 -49
  15. data/docs/deprecation.md +245 -0
  16. data/docs/getting_started.md +161 -39
  17. data/docs/internationalization.md +590 -70
  18. data/docs/interruptions/exceptions.md +135 -118
  19. data/docs/interruptions/faults.md +152 -127
  20. data/docs/interruptions/halt.md +134 -80
  21. data/docs/logging.md +183 -120
  22. data/docs/middlewares.md +165 -392
  23. data/docs/outcomes/result.md +140 -112
  24. data/docs/outcomes/states.md +134 -99
  25. data/docs/outcomes/statuses.md +204 -146
  26. data/docs/parameters/coercions.md +251 -289
  27. data/docs/parameters/defaults.md +224 -169
  28. data/docs/parameters/definitions.md +289 -141
  29. data/docs/parameters/namespacing.md +250 -161
  30. data/docs/parameters/validations.md +247 -159
  31. data/docs/testing.md +196 -203
  32. data/docs/workflows.md +146 -101
  33. data/lib/cmdx/.DS_Store +0 -0
  34. data/lib/cmdx/callback.rb +39 -55
  35. data/lib/cmdx/callback_registry.rb +80 -73
  36. data/lib/cmdx/chain.rb +65 -122
  37. data/lib/cmdx/chain_inspector.rb +23 -116
  38. data/lib/cmdx/chain_serializer.rb +34 -146
  39. data/lib/cmdx/coercion.rb +57 -0
  40. data/lib/cmdx/coercion_registry.rb +113 -0
  41. data/lib/cmdx/coercions/array.rb +18 -36
  42. data/lib/cmdx/coercions/big_decimal.rb +21 -33
  43. data/lib/cmdx/coercions/boolean.rb +21 -40
  44. data/lib/cmdx/coercions/complex.rb +18 -31
  45. data/lib/cmdx/coercions/date.rb +20 -39
  46. data/lib/cmdx/coercions/date_time.rb +22 -39
  47. data/lib/cmdx/coercions/float.rb +19 -32
  48. data/lib/cmdx/coercions/hash.rb +22 -41
  49. data/lib/cmdx/coercions/integer.rb +20 -33
  50. data/lib/cmdx/coercions/rational.rb +20 -32
  51. data/lib/cmdx/coercions/string.rb +23 -31
  52. data/lib/cmdx/coercions/time.rb +24 -40
  53. data/lib/cmdx/coercions/virtual.rb +14 -31
  54. data/lib/cmdx/configuration.rb +101 -162
  55. data/lib/cmdx/context.rb +34 -166
  56. data/lib/cmdx/core_ext/hash.rb +42 -67
  57. data/lib/cmdx/core_ext/module.rb +35 -79
  58. data/lib/cmdx/core_ext/object.rb +63 -98
  59. data/lib/cmdx/correlator.rb +59 -154
  60. data/lib/cmdx/error.rb +37 -202
  61. data/lib/cmdx/errors.rb +153 -216
  62. data/lib/cmdx/fault.rb +68 -150
  63. data/lib/cmdx/faults.rb +26 -137
  64. data/lib/cmdx/immutator.rb +22 -110
  65. data/lib/cmdx/lazy_struct.rb +110 -186
  66. data/lib/cmdx/log_formatters/json.rb +14 -40
  67. data/lib/cmdx/log_formatters/key_value.rb +14 -40
  68. data/lib/cmdx/log_formatters/line.rb +14 -48
  69. data/lib/cmdx/log_formatters/logstash.rb +14 -57
  70. data/lib/cmdx/log_formatters/pretty_json.rb +14 -50
  71. data/lib/cmdx/log_formatters/pretty_key_value.rb +13 -46
  72. data/lib/cmdx/log_formatters/pretty_line.rb +16 -54
  73. data/lib/cmdx/log_formatters/raw.rb +19 -49
  74. data/lib/cmdx/logger.rb +22 -79
  75. data/lib/cmdx/logger_ansi.rb +31 -72
  76. data/lib/cmdx/logger_serializer.rb +74 -103
  77. data/lib/cmdx/middleware.rb +56 -60
  78. data/lib/cmdx/middleware_registry.rb +82 -77
  79. data/lib/cmdx/middlewares/correlate.rb +41 -226
  80. data/lib/cmdx/middlewares/timeout.rb +46 -185
  81. data/lib/cmdx/parameter.rb +167 -183
  82. data/lib/cmdx/parameter_evaluator.rb +231 -0
  83. data/lib/cmdx/parameter_inspector.rb +37 -55
  84. data/lib/cmdx/parameter_registry.rb +65 -84
  85. data/lib/cmdx/parameter_serializer.rb +32 -76
  86. data/lib/cmdx/railtie.rb +24 -107
  87. data/lib/cmdx/result.rb +254 -259
  88. data/lib/cmdx/result_ansi.rb +28 -80
  89. data/lib/cmdx/result_inspector.rb +34 -70
  90. data/lib/cmdx/result_logger.rb +23 -77
  91. data/lib/cmdx/result_serializer.rb +59 -125
  92. data/lib/cmdx/rspec/matchers.rb +28 -0
  93. data/lib/cmdx/rspec/result_matchers/be_executed.rb +42 -0
  94. data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +94 -0
  95. data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +94 -0
  96. data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +59 -0
  97. data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +57 -0
  98. data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +87 -0
  99. data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +51 -0
  100. data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +58 -0
  101. data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +59 -0
  102. data/lib/cmdx/rspec/result_matchers/have_context.rb +86 -0
  103. data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +54 -0
  104. data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +52 -0
  105. data/lib/cmdx/rspec/result_matchers/have_metadata.rb +114 -0
  106. data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +66 -0
  107. data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +64 -0
  108. data/lib/cmdx/rspec/result_matchers/have_runtime.rb +78 -0
  109. data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +76 -0
  110. data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +62 -0
  111. data/lib/cmdx/rspec/task_matchers/have_callback.rb +85 -0
  112. data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +68 -0
  113. data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +92 -0
  114. data/lib/cmdx/rspec/task_matchers/have_middleware.rb +46 -0
  115. data/lib/cmdx/rspec/task_matchers/have_parameter.rb +181 -0
  116. data/lib/cmdx/task.rb +336 -427
  117. data/lib/cmdx/task_deprecator.rb +52 -0
  118. data/lib/cmdx/task_processor.rb +246 -0
  119. data/lib/cmdx/task_serializer.rb +34 -69
  120. data/lib/cmdx/utils/ansi_color.rb +13 -89
  121. data/lib/cmdx/utils/log_timestamp.rb +13 -42
  122. data/lib/cmdx/utils/monotonic_runtime.rb +11 -63
  123. data/lib/cmdx/utils/name_affix.rb +21 -71
  124. data/lib/cmdx/validator.rb +57 -0
  125. data/lib/cmdx/validator_registry.rb +108 -0
  126. data/lib/cmdx/validators/exclusion.rb +55 -94
  127. data/lib/cmdx/validators/format.rb +31 -85
  128. data/lib/cmdx/validators/inclusion.rb +65 -110
  129. data/lib/cmdx/validators/length.rb +117 -133
  130. data/lib/cmdx/validators/numeric.rb +123 -130
  131. data/lib/cmdx/validators/presence.rb +38 -79
  132. data/lib/cmdx/version.rb +1 -7
  133. data/lib/cmdx/workflow.rb +58 -330
  134. data/lib/cmdx.rb +1 -1
  135. data/lib/generators/cmdx/install_generator.rb +14 -31
  136. data/lib/generators/cmdx/task_generator.rb +39 -55
  137. data/lib/generators/cmdx/templates/install.rb +24 -6
  138. data/lib/generators/cmdx/workflow_generator.rb +41 -66
  139. data/lib/locales/ar.yml +0 -1
  140. data/lib/locales/cs.yml +0 -1
  141. data/lib/locales/da.yml +0 -1
  142. data/lib/locales/de.yml +0 -1
  143. data/lib/locales/el.yml +0 -1
  144. data/lib/locales/en.yml +0 -1
  145. data/lib/locales/es.yml +0 -1
  146. data/lib/locales/fi.yml +0 -1
  147. data/lib/locales/fr.yml +0 -1
  148. data/lib/locales/he.yml +0 -1
  149. data/lib/locales/hi.yml +0 -1
  150. data/lib/locales/it.yml +0 -1
  151. data/lib/locales/ja.yml +0 -1
  152. data/lib/locales/ko.yml +0 -1
  153. data/lib/locales/nl.yml +0 -1
  154. data/lib/locales/no.yml +0 -1
  155. data/lib/locales/pl.yml +0 -1
  156. data/lib/locales/pt.yml +0 -1
  157. data/lib/locales/ru.yml +0 -1
  158. data/lib/locales/sv.yml +0 -1
  159. data/lib/locales/th.yml +0 -1
  160. data/lib/locales/tr.yml +0 -1
  161. data/lib/locales/vi.yml +0 -1
  162. data/lib/locales/zh.yml +0 -1
  163. metadata +36 -8
  164. data/lib/cmdx/parameter_validator.rb +0 -81
  165. data/lib/cmdx/parameter_value.rb +0 -244
  166. data/lib/cmdx/parameters_inspector.rb +0 -72
  167. data/lib/cmdx/parameters_serializer.rb +0 -115
  168. data/lib/cmdx/rspec/result_matchers.rb +0 -917
  169. data/lib/cmdx/rspec/task_matchers.rb +0 -570
  170. 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 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)
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
- - [Date/Time Format Options](#datetime-format-options)
27
- - [BigDecimal Precision Options](#bigdecimal-precision-options)
14
+ - [Custom Coercions](#custom-coercions)
28
15
 
29
16
  ## TLDR
30
17
 
31
- - **Type coercion** - Automatic conversion using `type:` option (`:integer`, `:boolean`, `:array`, `:hash`, etc.)
32
- - **Multiple types** - Fallback with `type: [:float, :integer]` - tries each until one succeeds
33
- - **No conversion** - Default `:virtual` type returns values unchanged
34
- - **Before validation** - Coercion happens automatically before parameter validation
35
- - **Rich types** - Supports all Ruby built-ins plus JSON parsing for arrays/hashes
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
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
- ### 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,147 +217,179 @@ 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.
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 ValidateUserProfileTask < CMDx::Task
323
-
324
- required :age, type: :integer
325
- required :salary, type: :float
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 here
264
+ # Task logic
330
265
  end
331
-
332
266
  end
333
267
 
334
- # Invalid coercion inputs
335
- result = ValidateUserProfileTask.call(
336
- age: "not-a-number",
337
- salary: "invalid-amount",
338
- is_employed: "maybe"
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? #=> true
275
+ result.failed? # true
342
276
  result.metadata
343
- #=> {
344
- # reason: "age could not coerce into an integer. salary could not coerce into a float. is_employed could not coerce into a boolean.",
345
- # messages: {
346
- # age: ["could not coerce into an integer"],
347
- # salary: ["could not coerce into a float"],
348
- # is_employed: ["could not coerce into a boolean"]
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
- ### Multiple Type Coercion Errors
287
+ ### Common Error Scenarios
354
288
 
355
289
  ```ruby
356
- class ProcessFlexibleDataTask < CMDx::Task
357
-
358
- required :order_value, type: [:float, :integer]
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
- def call
362
- # Task logic here
363
- end
294
+ # Invalid date format
295
+ ProcessDataTask.call(start_date: "not-a-date")
296
+ # → "start_date could not coerce into a date"
364
297
 
365
- end
366
-
367
- # Failed coercion with multiple types
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 Format Options
305
+ ### Date/Time Formats
387
306
 
388
307
  ```ruby
389
- class ProcessCustomDateTask < CMDx::Task
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
- # ISO datetime with timezone
395
- 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"
396
314
 
397
- # 24-hour time format
398
- optional :meeting_time, type: :time, format: "%H:%M"
315
+ # 12-hour time
316
+ optional :appointment, type: :time, format: "%I:%M %p"
399
317
 
400
318
  def call
401
- birth_date #=> Date parsed with MM/DD/YYYY format
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 Options
324
+ ### BigDecimal Precision
416
325
 
417
326
  ```ruby
418
- class CalculatePricingTask < CMDx::Task
419
-
327
+ class CalculatePriceTask < CMDx::Task
420
328
  required :base_price, type: :big_decimal
421
- required :tax_rate, type: :big_decimal, precision: 6
329
+ required :tax_rate, type: :big_decimal, precision: 8
422
330
 
423
331
  def call
424
- base_price #=> BigDecimal with default precision
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)