cmdx 0.4.0 → 1.0.0

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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/.DS_Store +0 -0
  3. data/.cursor/rules/cursor-instructions.mdc +6 -0
  4. data/.rubocop.yml +16 -1
  5. data/.ruby-version +1 -1
  6. data/CHANGELOG.md +42 -1
  7. data/README.md +72 -25
  8. data/docs/ai_prompts.md +309 -0
  9. data/docs/basics/call.md +225 -14
  10. data/docs/basics/chain.md +271 -0
  11. data/docs/basics/context.md +232 -33
  12. data/docs/basics/setup.md +76 -12
  13. data/docs/callbacks.md +273 -0
  14. data/docs/configuration.md +158 -28
  15. data/docs/getting_started.md +134 -22
  16. data/docs/interruptions/exceptions.md +189 -11
  17. data/docs/interruptions/faults.md +187 -44
  18. data/docs/interruptions/halt.md +179 -35
  19. data/docs/logging.md +194 -53
  20. data/docs/middlewares.md +735 -0
  21. data/docs/outcomes/result.md +296 -10
  22. data/docs/outcomes/states.md +212 -19
  23. data/docs/outcomes/statuses.md +284 -18
  24. data/docs/parameters/coercions.md +402 -29
  25. data/docs/parameters/defaults.md +249 -25
  26. data/docs/parameters/definitions.md +238 -72
  27. data/docs/parameters/namespacing.md +250 -27
  28. data/docs/parameters/validations.md +193 -168
  29. data/docs/testing.md +550 -0
  30. data/docs/tips_and_tricks.md +95 -43
  31. data/docs/workflows.md +319 -0
  32. data/lib/cmdx/.DS_Store +0 -0
  33. data/lib/cmdx/callback.rb +69 -0
  34. data/lib/cmdx/callback_registry.rb +106 -0
  35. data/lib/cmdx/chain.rb +190 -0
  36. data/lib/cmdx/chain_inspector.rb +149 -0
  37. data/lib/cmdx/chain_serializer.rb +175 -0
  38. data/lib/cmdx/coercions/array.rb +37 -0
  39. data/lib/cmdx/coercions/big_decimal.rb +33 -0
  40. data/lib/cmdx/coercions/boolean.rb +41 -1
  41. data/lib/cmdx/coercions/complex.rb +31 -0
  42. data/lib/cmdx/coercions/date.rb +39 -0
  43. data/lib/cmdx/coercions/date_time.rb +39 -0
  44. data/lib/cmdx/coercions/float.rb +31 -0
  45. data/lib/cmdx/coercions/hash.rb +42 -0
  46. data/lib/cmdx/coercions/integer.rb +32 -0
  47. data/lib/cmdx/coercions/rational.rb +31 -0
  48. data/lib/cmdx/coercions/string.rb +31 -0
  49. data/lib/cmdx/coercions/time.rb +39 -0
  50. data/lib/cmdx/coercions/virtual.rb +31 -0
  51. data/lib/cmdx/configuration.rb +217 -9
  52. data/lib/cmdx/context.rb +173 -2
  53. data/lib/cmdx/core_ext/hash.rb +72 -0
  54. data/lib/cmdx/core_ext/module.rb +94 -0
  55. data/lib/cmdx/core_ext/object.rb +105 -0
  56. data/lib/cmdx/correlator.rb +217 -0
  57. data/lib/cmdx/error.rb +210 -8
  58. data/lib/cmdx/errors.rb +256 -1
  59. data/lib/cmdx/fault.rb +177 -2
  60. data/lib/cmdx/faults.rb +158 -2
  61. data/lib/cmdx/immutator.rb +121 -2
  62. data/lib/cmdx/lazy_struct.rb +261 -18
  63. data/lib/cmdx/log_formatters/json.rb +46 -0
  64. data/lib/cmdx/log_formatters/key_value.rb +46 -0
  65. data/lib/cmdx/log_formatters/line.rb +54 -0
  66. data/lib/cmdx/log_formatters/logstash.rb +64 -0
  67. data/lib/cmdx/log_formatters/pretty_json.rb +57 -0
  68. data/lib/cmdx/log_formatters/pretty_key_value.rb +51 -0
  69. data/lib/cmdx/log_formatters/pretty_line.rb +60 -0
  70. data/lib/cmdx/log_formatters/raw.rb +54 -0
  71. data/lib/cmdx/logger.rb +85 -0
  72. data/lib/cmdx/logger_ansi.rb +93 -7
  73. data/lib/cmdx/logger_serializer.rb +116 -0
  74. data/lib/cmdx/middleware.rb +74 -0
  75. data/lib/cmdx/middleware_registry.rb +106 -0
  76. data/lib/cmdx/middlewares/correlate.rb +266 -0
  77. data/lib/cmdx/middlewares/timeout.rb +232 -0
  78. data/lib/cmdx/parameter.rb +228 -1
  79. data/lib/cmdx/parameter_inspector.rb +61 -0
  80. data/lib/cmdx/parameter_registry.rb +125 -0
  81. data/lib/cmdx/parameter_serializer.rb +83 -0
  82. data/lib/cmdx/parameter_validator.rb +62 -0
  83. data/lib/cmdx/parameter_value.rb +109 -1
  84. data/lib/cmdx/parameters_inspector.rb +59 -0
  85. data/lib/cmdx/parameters_serializer.rb +102 -0
  86. data/lib/cmdx/railtie.rb +123 -3
  87. data/lib/cmdx/result.rb +399 -20
  88. data/lib/cmdx/result_ansi.rb +105 -9
  89. data/lib/cmdx/result_inspector.rb +76 -0
  90. data/lib/cmdx/result_logger.rb +90 -3
  91. data/lib/cmdx/result_serializer.rb +137 -0
  92. data/lib/cmdx/rspec/result_matchers.rb +917 -0
  93. data/lib/cmdx/rspec/task_matchers.rb +570 -0
  94. data/lib/cmdx/task.rb +409 -34
  95. data/lib/cmdx/task_serializer.rb +74 -2
  96. data/lib/cmdx/utils/ansi_color.rb +95 -0
  97. data/lib/cmdx/utils/log_timestamp.rb +48 -0
  98. data/lib/cmdx/utils/monotonic_runtime.rb +71 -4
  99. data/lib/cmdx/utils/name_affix.rb +78 -0
  100. data/lib/cmdx/validators/custom.rb +82 -0
  101. data/lib/cmdx/validators/exclusion.rb +94 -0
  102. data/lib/cmdx/validators/format.rb +102 -8
  103. data/lib/cmdx/validators/inclusion.rb +104 -0
  104. data/lib/cmdx/validators/length.rb +128 -0
  105. data/lib/cmdx/validators/numeric.rb +128 -0
  106. data/lib/cmdx/validators/presence.rb +93 -7
  107. data/lib/cmdx/version.rb +7 -1
  108. data/lib/cmdx/workflow.rb +394 -0
  109. data/lib/cmdx.rb +25 -64
  110. data/lib/generators/cmdx/install_generator.rb +37 -1
  111. data/lib/generators/cmdx/task_generator.rb +69 -1
  112. data/lib/generators/cmdx/templates/install.rb +8 -12
  113. data/lib/generators/cmdx/workflow_generator.rb +109 -0
  114. metadata +54 -15
  115. data/docs/basics/run.md +0 -34
  116. data/docs/batch.md +0 -53
  117. data/docs/example.md +0 -82
  118. data/docs/hooks.md +0 -59
  119. data/lib/cmdx/batch.rb +0 -43
  120. data/lib/cmdx/parameters.rb +0 -34
  121. data/lib/cmdx/run.rb +0 -38
  122. data/lib/cmdx/run_inspector.rb +0 -26
  123. data/lib/cmdx/run_serializer.rb +0 -16
  124. data/lib/cmdx/task_hook.rb +0 -18
  125. data/lib/generators/cmdx/batch_generator.rb +0 -30
  126. /data/lib/generators/cmdx/templates/{batch.rb.tt → workflow.rb.tt} +0 -0
@@ -1,47 +1,271 @@
1
1
  # Parameters - Defaults
2
2
 
3
- Assign default values for parameters that return a `nil` value.
3
+ Parameter defaults provide fallback values when arguments are not provided or
4
+ resolve to `nil`. Defaults ensure tasks have sensible values for optional
5
+ parameters while maintaining flexibility for callers to override when needed.
6
+ Defaults work seamlessly with coercion, validation, and nested parameters.
7
+
8
+ ## Table of Contents
9
+
10
+ - [Default Value Fundamentals](#default-value-fundamentals)
11
+ - [Fixed Value Defaults](#fixed-value-defaults)
12
+ - [Callable Defaults](#callable-defaults)
13
+ - [Defaults with Type Coercion](#defaults-with-type-coercion)
14
+ - [Defaults with Validation](#defaults-with-validation)
15
+ - [Nested Parameter Defaults](#nested-parameter-defaults)
16
+
17
+ ## Default Value Fundamentals
18
+
19
+ > [!NOTE]
20
+ > Defaults are specified using the `:default` option and are applied when a parameter value resolves to `nil`. This includes cases where optional parameters are not provided in call arguments or when source objects return `nil` values.
21
+
22
+ ### Fixed Value Defaults
23
+
24
+ The simplest defaults use fixed values that are applied consistently:
4
25
 
5
26
  ```ruby
6
- class DetermineBoxSizeTask < CMDx::Task
27
+ class ProcessUserOrderTask < CMDx::Task
28
+
29
+ required :user_id, type: :integer
30
+ optional :priority, type: :string, default: "normal"
31
+ optional :send_confirmation, type: :boolean, default: true
32
+ optional :max_retries, type: :integer, default: 3
33
+
34
+ optional :notification_tags, type: :array, default: []
35
+ optional :order_metadata, type: :hash, default: {}
36
+ optional :created_at, type: :datetime, default: -> { Time.now }
7
37
 
8
- # Fixed value
9
- required :width, default: 12
38
+ def call
39
+ user_id #=> provided value (required)
40
+ priority #=> "normal" if not provided
41
+ send_confirmation #=> true if not provided
42
+ max_retries #=> 3 if not provided
43
+ notification_tags #=> [] if not provided
44
+ order_metadata #=> {} if not provided
45
+ created_at #=> current time if not provided
46
+ end
10
47
 
11
- # Proc or lambda
12
- optional :length, default: -> { Current.account.usa? ? 12 : 18 }
48
+ end
13
49
 
14
- # Symbol or string
15
- optional :depth, default: :depth_by_country
50
+ # Defaults applied for missing parameters
51
+ ProcessUserOrderTask.call(user_id: 12345)
52
+ # priority: "normal", send_confirmation: true, max_retries: 3, etc.
53
+
54
+ # Explicit values override defaults
55
+ ProcessUserOrderTask.call(
56
+ user_id: 12345,
57
+ priority: "urgent",
58
+ send_confirmation: false,
59
+ notification_tags: ["rush_order"]
60
+ )
61
+ ```
62
+
63
+ ### Callable Defaults
64
+
65
+ > [!TIP]
66
+ > Use procs, lambdas, or method symbols for dynamic defaults that are evaluated at parameter resolution time. This is especially useful for timestamps, UUIDs, and context-dependent values.
67
+
68
+ ```ruby
69
+ class SendOrderNotificationTask < CMDx::Task
70
+
71
+ required :order_id, type: :integer
72
+
73
+ # Dynamic defaults using procs
74
+ optional :sent_at, type: :datetime, default: -> { Time.now }
75
+ optional :tracking_id, type: :string, default: -> { SecureRandom.uuid }
76
+
77
+ # Environment-aware defaults
78
+ optional :notification_service, type: :string, default: -> { Rails.env.production? ? "sendgrid" : "mock" }
79
+ optional :sender_email, type: :string, default: -> { Rails.application.credentials.sender_email }
80
+
81
+ # Method symbol defaults
82
+ optional :template_name, type: :string, default: :determine_template
83
+ optional :delivery_time, type: :datetime, default: :calculate_delivery_window
16
84
 
17
85
  def call
18
- width #=> 12
19
- length #=> 18
20
- depth #=> 48
86
+ sent_at #=> current time when accessed
87
+ tracking_id #=> unique UUID when accessed
88
+ notification_service #=> production or test service
89
+ sender_email #=> configured sender email
90
+ template_name #=> result of determine_template method
91
+ delivery_time #=> result of calculate_delivery_window method
21
92
  end
22
93
 
23
94
  private
24
95
 
25
- def depth_by_country
26
- case Current.account.country
27
- when "usa" then 12
28
- when "can" then 18
29
- when "mex" then 24
30
- else 48
31
- end
96
+ def determine_template
97
+ order.priority == "urgent" ? "urgent_order" : "standard_order"
98
+ end
99
+
100
+ def calculate_delivery_window
101
+ order.priority == "urgent" ? 15.minutes.from_now : 1.hour.from_now
102
+ end
103
+
104
+ def order
105
+ @order ||= Order.find(order_id)
32
106
  end
33
107
 
34
108
  end
109
+ ```
110
+
111
+ ## Defaults with Type Coercion
112
+
113
+ > [!IMPORTANT]
114
+ > Defaults work seamlessly with type coercion, with the default value being subject to the same coercion rules as provided values.
115
+
116
+ ```ruby
117
+ class ConfigureOrderSettingsTask < CMDx::Task
118
+
119
+ # String defaults coerced to integers
120
+ optional :max_items, type: :integer, default: "50"
121
+
122
+ # JSON string defaults coerced to hash
123
+ optional :shipping_config, type: :hash, default: '{"carrier": "ups", "speed": "standard"}'
124
+
125
+ # String defaults coerced to arrays
126
+ optional :allowed_countries, type: :array, default: '["US", "CA", "UK"]'
35
127
 
36
- # Initializes with default values
37
- DetermineBoxSizeTask.call(width: nil, length: nil)
128
+ # String defaults coerced to booleans
129
+ optional :require_signature, type: :boolean, default: "true"
130
+
131
+ # String defaults coerced to dates
132
+ optional :embargo_date, type: :date, default: "2024-01-01"
133
+
134
+ # Dynamic defaults with coercion
135
+ optional :order_number, type: :string, default: -> { Time.now.to_i }
136
+
137
+ def call
138
+ max_items #=> 50 (integer)
139
+ shipping_config #=> {"carrier" => "ups", "speed" => "standard"} (hash)
140
+ allowed_countries #=> ["US", "CA", "UK"] (array)
141
+ require_signature #=> true (boolean)
142
+ embargo_date #=> Date object
143
+ order_number #=> "1640995200" (string from integer)
144
+ end
145
+
146
+ end
38
147
  ```
39
148
 
40
- > [!NOTE]
41
- > Defaults are subject to coercion and validations so take care setting
42
- > the fallback value to contain valid conditions.
149
+ ## Defaults with Validation
150
+
151
+ > [!WARNING]
152
+ > Default values are subject to the same validation rules as provided values, ensuring consistency and catching configuration errors early.
153
+
154
+ ```ruby
155
+ class ValidateOrderPriorityTask < CMDx::Task
156
+
157
+ required :order_id, type: :integer
158
+
159
+ # Default must pass inclusion validation
160
+ optional :priority, type: :string, default: "standard",
161
+ inclusion: { in: %w[low standard high urgent] }
162
+
163
+ # Numeric default with range validation
164
+ optional :processing_timeout, type: :integer, default: 300,
165
+ numeric: { min: 60, max: 3600 }
166
+
167
+ # Email default with format validation
168
+ optional :escalation_email, type: :string,
169
+ default: -> { "support@#{Rails.application.config.domain}" },
170
+ format: { with: /@/ }
171
+
172
+ # Custom validation with default
173
+ optional :approval_code, type: :string, default: :generate_approval_code,
174
+ custom: { validator: ApprovalCodeValidator }
175
+
176
+ def call
177
+ priority #=> "standard" (validated against inclusion list)
178
+ processing_timeout #=> 300 (validated within range)
179
+ escalation_email #=> support email (validated format)
180
+ approval_code #=> generated code (custom validated)
181
+ end
182
+
183
+ private
184
+
185
+ def generate_approval_code
186
+ "APV_#{SecureRandom.hex(8).upcase}"
187
+ end
188
+
189
+ end
190
+ ```
191
+
192
+ ## Nested Parameter Defaults
193
+
194
+ ```ruby
195
+ class ProcessOrderShippingTask < CMDx::Task
196
+
197
+ required :order_id, type: :integer
198
+
199
+ # Parent parameter with default
200
+ optional :shipping_details, type: :hash, default: {} do
201
+ optional :carrier, type: :string, default: "fedex"
202
+ optional :expedited, type: :boolean, default: false
203
+ optional :insurance_required, type: :boolean, default: -> { order_value > 500 }
204
+
205
+ optional :delivery_address, type: :hash, default: -> { customer_default_address } do
206
+ optional :country, type: :string, default: "US"
207
+ optional :state, type: :string, default: -> { determine_default_state }
208
+ optional :requires_appointment, type: :boolean, default: false
209
+ end
210
+ end
211
+
212
+ # Complex nested defaults
213
+ optional :notification_preferences, type: :hash, default: -> { customer_notification_defaults } do
214
+ optional :email_updates, type: :boolean, default: true
215
+ optional :sms_updates, type: :boolean, default: false
216
+
217
+ optional :delivery_window, type: :hash, default: {} do
218
+ optional :preferred_time, type: :string, default: "anytime"
219
+ optional :weekend_delivery, type: :boolean, default: false
220
+ end
221
+ end
222
+
223
+ def call
224
+ # Parent defaults applied when not provided
225
+ shipping_details #=> {} if not provided
226
+ notification_preferences #=> customer defaults if not provided
227
+
228
+ # Child defaults (when parent exists)
229
+ carrier #=> "fedex"
230
+ expedited #=> false
231
+ insurance_required #=> true if order > $500
232
+ country #=> "US"
233
+ state #=> determined by logic
234
+ email_updates #=> true
235
+ preferred_time #=> "anytime"
236
+ weekend_delivery #=> false
237
+ end
238
+
239
+ private
240
+
241
+ def order
242
+ @order ||= Order.find(order_id)
243
+ end
244
+
245
+ def order_value
246
+ order.total_amount
247
+ end
248
+
249
+ def customer_default_address
250
+ order.customer.default_shipping_address&.to_hash || {}
251
+ end
252
+
253
+ def determine_default_state
254
+ order.customer.billing_address&.state || "CA"
255
+ end
256
+
257
+ def customer_notification_defaults
258
+ prefs = order.customer.notification_preferences
259
+ {
260
+ email_updates: prefs.email_enabled?,
261
+ sms_updates: prefs.sms_enabled?
262
+ }
263
+ end
264
+
265
+ end
266
+ ```
43
267
 
44
268
  ---
45
269
 
46
- - **Prev:** [Validations](https://github.com/drexed/cmdx/blob/main/docs/parameters/validations.md)
47
- - **Next:** [Results](https://github.com/drexed/cmdx/blob/main/docs/outcomes.md)
270
+ - **Prev:** [Parameters - Validations](validations.md)
271
+ - **Next:** [Callbacks](../callbacks.md)
@@ -1,123 +1,289 @@
1
- ## Parameters - Definitions
1
+ # Parameters - Definitions
2
2
 
3
- Parameters provide a contract to verify that a task only executes if the arguments
4
- of a call match.
3
+ Parameters provide a contract to verify that task execution arguments match expected requirements and structure. They define the interface between task callers and task implementation, enabling automatic validation, type coercion, and method generation for clean parameter access within tasks.
5
4
 
6
- ## Basics
5
+ ## Table of Contents
7
6
 
8
- Parameters are defined based on methods that can be delegated to a source object
9
- (default `:context`) or keys on a hash. Parameters are automatically defined as
10
- attributes within the task instance. `optional` parameters that do not respond or
11
- missing the hash key will still delegate but return `nil` as a value.
7
+ - [Parameter Fundamentals](#parameter-fundamentals)
8
+ - [Parameter Sources](#parameter-sources)
9
+ - [Nested Parameters](#nested-parameters)
10
+ - [Parameter Method Generation](#parameter-method-generation)
11
+ - [Error Handling](#error-handling)
12
12
 
13
- ```ruby
14
- class DetermineBoxSizeTask < CMDx::Task
13
+ ## Parameter Fundamentals
14
+
15
+ Parameters are defined using `required` and `optional` class methods that automatically create accessor methods within task instances. Parameters are matched from call arguments and made available as instance methods.
16
+
17
+ > [!IMPORTANT]
18
+ > Required parameters must be provided in call arguments or task execution will fail.
15
19
 
16
- # Must be passed as call arguments
17
- required :material
20
+ ### Basic Parameter Definition
21
+
22
+ ```ruby
23
+ class CreateOrderTask < CMDx::Task
24
+ # Must be provided in call arguments
25
+ required :order_id
18
26
 
19
- # Returns value if passed as a call arguments, else returns nil
20
- optional :depth
27
+ # Optional - returns nil if not provided
28
+ optional :priority
21
29
 
22
- # Define multiple parameters one line
23
- optional :width, :height
30
+ # Multiple parameters in one declaration
31
+ required :customer_id, :product_id
32
+ optional :notes, :shipping_method
24
33
 
25
34
  def call
26
- material #=> "cardboard"
27
- depth #=> nil
28
- height #=> 12
29
- width #=> 24
35
+ order_id #=> 123 (from call arguments)
36
+ priority #=> "high" or nil
37
+ customer_id #=> 456 (from call arguments)
38
+ shipping_method #=> "express" or nil
30
39
  end
31
-
32
40
  end
33
41
 
34
- # Initializes local variables matching the parameter name
35
- DetermineBoxSizeTask.call(material: "cardboard", height: 12, width: 24)
42
+ # Parameters passed as keyword arguments
43
+ CreateOrderTask.call(
44
+ order_id: 123,
45
+ customer_id: 456,
46
+ product_id: 789,
47
+ priority: "high",
48
+ shipping_method: "express"
49
+ )
36
50
  ```
37
51
 
38
- ## Source
52
+ ## Parameter Sources
53
+
54
+ Parameters delegate to source objects within the task context. The default source is `:context`, but any accessible method or object can serve as a parameter source.
39
55
 
40
- Parameters will be delegated to the task context by default but any delegatable
41
- object within the task will do.
56
+ ### Default Context Source
42
57
 
43
58
  ```ruby
44
- class UpdateUserDetailsTask < CMDx::Task
59
+ class UpdateUserTask < CMDx::Task
60
+ # Delegates to context.user_id (default source)
61
+ required :user_id
62
+
63
+ # Explicitly specified context source
64
+ required :email, source: :context
65
+
66
+ def call
67
+ user_id #=> delegates to context.user_id
68
+ email #=> delegates to context.email
69
+ end
70
+ end
45
71
 
46
- # Default (:context)
47
- required :user
72
+ UpdateUserTask.call(user_id: 123, email: "user@example.com")
73
+ ```
48
74
 
49
- # Defined parameter
50
- required :email, source: :user
75
+ ### Custom Object Sources
51
76
 
52
- # Proc or lambda
53
- optional :address, source: -> { user.address }
77
+ ```ruby
78
+ class ProcessUserOrderTask < CMDx::Task
79
+ # Delegate to user object
80
+ required :name, :email, source: :user
54
81
 
55
- # Symbol or string
56
- optional :name, source: :company
82
+ # Delegate to order object
83
+ required :total, :status, source: :order
84
+ optional :discount, source: :order
57
85
 
58
86
  def call
59
- user #=> <User #a1b2c3d>
60
- email #=> "bill@bigcorp.com"
61
- address #=> "123 Maple St, Miami, Fl 33023"
62
- name #=> "Big Corp."
87
+ name #=> delegates to user.name
88
+ email #=> delegates to user.email
89
+ total #=> delegates to order.total
90
+ status #=> delegates to order.status
91
+ discount #=> delegates to order.discount
63
92
  end
64
93
 
65
94
  private
66
95
 
67
- def company
68
- user.account.company
96
+ def user
97
+ @user ||= User.find(context.user_id)
69
98
  end
70
99
 
100
+ def order
101
+ @order ||= user.orders.find(context.order_id)
102
+ end
71
103
  end
72
104
 
73
- # Hash or delegatable object
74
- user = User.new(email: "bill@bigcorp.com")
75
- UpdateUserDetailsTask.call(user: user)
105
+ ProcessUserOrderTask.call(user_id: 123, order_id: 456)
76
106
  ```
77
107
 
78
- ## Nesting
79
-
80
- Nesting builds upon parameter source option. Build complex parameter blocks that
81
- delegate to the parent parameter automatically.
108
+ ### Dynamic Sources
82
109
 
83
110
  ```ruby
84
- class UpdateUserDetailsTask < CMDx::Task
111
+ class ProcessDynamicParameterTask < CMDx::Task
112
+ # Lambda source for dynamic resolution
113
+ required :company_name, source: -> { user.company }
114
+
115
+ # Method name sources
116
+ required :account_type, source: :determine_account_type
117
+ optional :access_level, source: :calculate_access_level
118
+
119
+ def call
120
+ company_name #=> resolved via lambda
121
+ account_type #=> result of determine_account_type method
122
+ access_level #=> result of calculate_access_level method
123
+ end
124
+
125
+ private
126
+
127
+ def user
128
+ @user ||= User.find(context.user_id)
129
+ end
130
+
131
+ def determine_account_type
132
+ user.premium? ? "premium" : "standard"
133
+ end
134
+
135
+ def calculate_access_level
136
+ user.admin? ? "admin" : "user"
137
+ end
138
+ end
139
+ ```
140
+
141
+ ## Nested Parameters
142
+
143
+ Nested parameters allow complex parameter structures where child parameters automatically inherit their parent as the source. This enables validation and access of structured data.
85
144
 
86
- required :address do
87
- required :street1
88
- optional :street2
145
+ > [!NOTE]
146
+ > Child parameters are only required when their parent parameter is provided.
147
+
148
+ ### Basic Nesting
149
+
150
+ ```ruby
151
+ class CreateShippingLabelTask < CMDx::Task
152
+ # Parent parameter with nested children
153
+ required :shipping_address do
154
+ required :street, :city, :state, :zip_code
155
+ optional :apartment_number
89
156
  end
90
157
 
91
- optional :locality do
92
- required :city, :state # Required if locality argument is passed
93
- optional :zipcode
158
+ # Optional parent with required children
159
+ optional :billing_address do
160
+ required :street, :city # Only required if billing_address provided
161
+ optional :same_as_shipping
94
162
  end
95
163
 
96
164
  def call
97
- address #=> { city: "Miami", state: "Fl" }
98
- street1 #=> "123 Maple St."
99
- street2 #=> nil
165
+ # Parent parameter access
166
+ shipping_address #=> { street: "123 Main St", city: "Miami", ... }
100
167
 
101
- locality #=> { city: "Miami", state: "Fl" }
102
- city #=> "Miami"
103
- state #=> "Fl"
104
- zipcode #=> nil
168
+ # Child parameter access (delegates to parent)
169
+ street #=> "123 Main St" (from shipping_address.street)
170
+ city #=> "Miami" (from shipping_address.city)
171
+ apartment_number #=> nil (optional, not provided)
105
172
  end
173
+ end
174
+
175
+ CreateShippingLabelTask.call(
176
+ shipping_address: {
177
+ street: "123 Main St",
178
+ city: "Miami",
179
+ state: "FL",
180
+ zip_code: "33101"
181
+ }
182
+ )
183
+ ```
106
184
 
185
+ ### Multi-Level Nesting
186
+
187
+ ```ruby
188
+ class CreateUserProfileTask < CMDx::Task
189
+ required :user do
190
+ required :name, :email
191
+
192
+ required :profile do
193
+ required :age
194
+ optional :bio
195
+
196
+ optional :preferences do
197
+ optional :theme, :language
198
+ required :notifications # Required if preferences provided
199
+ end
200
+ end
201
+ end
202
+
203
+ def call
204
+ # Access at any nesting level
205
+ name #=> delegates to user.name
206
+ email #=> delegates to user.email
207
+ age #=> delegates to user.profile.age
208
+ theme #=> delegates to user.profile.preferences.theme
209
+ end
107
210
  end
211
+ ```
212
+
213
+ ## Parameter Method Generation
214
+
215
+ Parameters automatically generate accessor methods that delegate to their configured sources.
216
+
217
+ > [!TIP]
218
+ > Parameter names become instance methods accessible within the task.
219
+
220
+ ```ruby
221
+ class ProcessPaymentTask < CMDx::Task
222
+ # Standard method generation
223
+ required :payment_id # Generates: payment_id method
224
+
225
+ # Custom source with method name
226
+ required :account_name, source: :account # Generates: account_name method
227
+
228
+ # Nested parameter method generation
229
+ required :billing_info do
230
+ required :card_number # Generates: card_number method
231
+ required :expiry_date # Generates: expiry_date method
232
+ end
108
233
 
109
- # Hash or delegatable object
110
- address = Address.new(street1: "123 Maple St.")
111
- locality = { city: "Miami", state: "Fl" }
112
- UpdateUserDetailsTask.call(address: address, locality: locality)
234
+ def call
235
+ payment_id #=> accesses context.payment_id
236
+ account_name #=> accesses account.account_name
237
+ card_number #=> accesses billing_info.card_number
238
+ expiry_date #=> accesses billing_info.expiry_date
239
+ end
240
+
241
+ private
242
+
243
+ def account
244
+ @account ||= Account.find(context.account_id)
245
+ end
246
+ end
113
247
  ```
114
248
 
115
- > [!NOTE]
116
- > Optional parent parameters that have required child parameters will only have
117
- > the child parameters be required if the parent option is a delegatable source
118
- > or default value.
249
+ ## Error Handling
250
+
251
+ Parameter validation failures result in structured error information:
252
+
253
+ > [!WARNING]
254
+ > Invalid parameters will cause task execution to fail with detailed error messages.
255
+
256
+ ```ruby
257
+ class ValidateUserTask < CMDx::Task
258
+ required :age, type: :integer, numeric: { min: 18, max: 120 }
259
+ required :email, type: :string, format: { with: /@/ }
260
+ optional :phone, type: :string, format: { with: /\A\d{10}\z/ }
261
+
262
+ def call
263
+ # Task logic here
264
+ end
265
+ end
266
+
267
+ # Invalid parameters
268
+ result = ValidateUserTask.call(
269
+ age: "invalid",
270
+ email: "not-an-email",
271
+ phone: "123"
272
+ )
273
+
274
+ result.failed? #=> true
275
+ result.metadata
276
+ #=> {
277
+ # reason: "age could not coerce into an integer. email format is not valid. phone format is not valid.",
278
+ # messages: {
279
+ # age: ["could not coerce into an integer"],
280
+ # email: ["format is not valid"],
281
+ # phone: ["format is not valid"]
282
+ # }
283
+ # }
284
+ ```
119
285
 
120
286
  ---
121
287
 
122
- - **Prev:** [Parameters](https://github.com/drexed/cmdx/blob/main/docs/parameters.md)
123
- - **Next:** [Namespacing](https://github.com/drexed/cmdx/blob/main/docs/parameters/namespacing.md)
288
+ - **Prev:** [Configuration](../configuration.md)
289
+ - **Next:** [Parameters - Namespacing](namespacing.md)