cmdx 0.5.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 +31 -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 +203 -31
  23. data/docs/outcomes/statuses.md +275 -30
  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 +367 -25
  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 +405 -37
  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 -62
  119. data/lib/cmdx/batch.rb +0 -43
  120. data/lib/cmdx/parameters.rb +0 -35
  121. data/lib/cmdx/run.rb +0 -39
  122. data/lib/cmdx/run_inspector.rb +0 -26
  123. data/lib/cmdx/run_serializer.rb +0 -20
  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
@@ -0,0 +1,232 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+
5
+ ##
6
+ # Timeout middleware that enforces execution time limits on tasks.
7
+ #
8
+ # This middleware wraps task execution with timeout protection, automatically
9
+ # failing tasks that exceed their configured timeout duration. The timeout
10
+ # value can be static, dynamic, or method-based. If no timeout is specified,
11
+ # it defaults to 3 seconds. Optionally supports conditional timeout application
12
+ # based on task or context state.
13
+ #
14
+ # ## Timeout Value Types
15
+ #
16
+ # The middleware supports multiple ways to specify timeout values:
17
+ # - **Static values** (Integer/Float): Fixed timeout duration
18
+ # - **Method symbols**: Calls the specified method on the task for dynamic calculation
19
+ # - **Procs/Lambdas**: Executed in task context for runtime timeout determination
20
+ #
21
+ # ## Conditional Execution
22
+ #
23
+ # The middleware supports conditional timeout application using `:if` and `:unless` options:
24
+ # - `:if` - Only applies timeout when the condition evaluates to true
25
+ # - `:unless` - Only applies timeout when the condition evaluates to false
26
+ # - Conditions can be Procs, method symbols, or boolean values
27
+ #
28
+ # @example Static timeout configuration
29
+ # class ProcessOrderTask < CMDx::Task
30
+ # use CMDx::Middlewares::Timeout, seconds: 30 # 30 seconds
31
+ #
32
+ # def call
33
+ # # Task logic that might take too long
34
+ # end
35
+ # end
36
+ #
37
+ # @example Dynamic timeout using proc
38
+ # class ProcessOrderTask < CMDx::Task
39
+ # use CMDx::Middlewares::Timeout, seconds: -> { complex_order? ? 60 : 30 }
40
+ #
41
+ # def call
42
+ # # Task logic with dynamic timeout based on order complexity
43
+ # end
44
+ #
45
+ # private
46
+ #
47
+ # def complex_order?
48
+ # context.order_items.count > 10
49
+ # end
50
+ # end
51
+ #
52
+ # @example Method-based timeout
53
+ # class ProcessOrderTask < CMDx::Task
54
+ # use CMDx::Middlewares::Timeout, seconds: :calculate_timeout
55
+ #
56
+ # def call
57
+ # # Task logic with method-calculated timeout
58
+ # end
59
+ #
60
+ # private
61
+ #
62
+ # def calculate_timeout
63
+ # base_timeout = 30
64
+ # base_timeout += (context.order_items.count * 2)
65
+ # base_timeout
66
+ # end
67
+ # end
68
+ #
69
+ # @example Using default timeout (3 seconds)
70
+ # class QuickTask < CMDx::Task
71
+ # use CMDx::Middlewares::Timeout # 3 seconds default
72
+ #
73
+ # def call
74
+ # # Task logic with default timeout
75
+ # end
76
+ # end
77
+ #
78
+ # @example Conditional timeout based on task context
79
+ # class ProcessOrderTask < CMDx::Task
80
+ # use CMDx::Middlewares::Timeout,
81
+ # seconds: 30,
82
+ # if: proc { context.enable_timeout? }
83
+ #
84
+ # def call
85
+ # # Task logic with conditional timeout
86
+ # end
87
+ # end
88
+ #
89
+ # @example Conditional timeout with method reference
90
+ # class ProcessOrderTask < CMDx::Task
91
+ # use CMDx::Middlewares::Timeout,
92
+ # seconds: 60,
93
+ # unless: :skip_timeout?
94
+ #
95
+ # def call
96
+ # # Task logic
97
+ # end
98
+ #
99
+ # private
100
+ #
101
+ # def skip_timeout?
102
+ # Rails.env.development?
103
+ # end
104
+ # end
105
+ #
106
+ # @example Global timeout middleware
107
+ # class ApplicationTask < CMDx::Task
108
+ # use CMDx::Middlewares::Timeout, seconds: 60 # Default 60 seconds
109
+ # end
110
+ #
111
+ # @see CMDx::Middleware Base middleware class
112
+ # @see CMDx::Task Task settings configuration
113
+ # @see CMDx::Workflow Workflow execution context
114
+
115
+ ##
116
+ # Custom timeout error class that inherits from Interrupt.
117
+ #
118
+ # This error is raised when task execution exceeds the configured timeout
119
+ # duration. It provides a clean way to distinguish timeout errors from
120
+ # other types of interruptions or exceptions.
121
+ #
122
+ # @example Catching timeout errors
123
+ # begin
124
+ # task.call
125
+ # rescue CMDx::TimeoutError => e
126
+ # puts "Task timed out: #{e.message}"
127
+ # end
128
+ #
129
+ # @see CMDx::Middlewares::Timeout The middleware that raises this error
130
+ TimeoutError = Class.new(Interrupt)
131
+
132
+ module Middlewares
133
+ class Timeout < CMDx::Middleware
134
+
135
+ # @return [Integer, Float, Symbol, Proc] The timeout value in seconds
136
+ # @return [Hash] The conditional options for timeout application
137
+ attr_reader :seconds, :conditional
138
+
139
+ ##
140
+ # Initializes the timeout middleware.
141
+ #
142
+ # @param options [Hash] Configuration options for the timeout middleware
143
+ # @option options [Integer, Float, Symbol, Proc] :seconds Timeout value in seconds.
144
+ # - Integer/Float: Used as-is for static timeout
145
+ # - Symbol: Called as method on task if it exists, otherwise used as numeric value
146
+ # - Proc/Lambda: Executed in task context for dynamic timeout calculation
147
+ # Defaults to 3 seconds if not provided.
148
+ # @option options [Symbol, Proc] :if Condition that must be truthy for timeout to be applied
149
+ # @option options [Symbol, Proc] :unless Condition that must be falsy for timeout to be applied
150
+ #
151
+ # @example Static timeout configuration
152
+ # CMDx::Middlewares::Timeout.new(seconds: 30)
153
+ #
154
+ # @example Dynamic timeout with proc
155
+ # CMDx::Middlewares::Timeout.new(seconds: -> { heavy_operation? ? 120 : 30 })
156
+ #
157
+ # @example Method-based timeout
158
+ # CMDx::Middlewares::Timeout.new(seconds: :calculate_timeout_limit)
159
+ #
160
+ # @example Using default timeout (3 seconds)
161
+ # CMDx::Middlewares::Timeout.new
162
+ #
163
+ # @example Conditional timeout
164
+ # CMDx::Middlewares::Timeout.new(seconds: 30, if: :production_mode?)
165
+ # CMDx::Middlewares::Timeout.new(seconds: 60, unless: proc { Rails.env.test? })
166
+ def initialize(options = {})
167
+ @seconds = options[:seconds] || 3
168
+ @conditional = options.slice(:if, :unless)
169
+ end
170
+
171
+ ##
172
+ # Executes the task with conditional timeout protection.
173
+ #
174
+ # Evaluates the conditional options to determine if timeout should be applied.
175
+ # If conditions are met, resolves the timeout value using and wraps the task
176
+ # execution with a timeout mechanism that will interrupt execution if it exceeds
177
+ # the configured time limit. If conditions are not met, executes the task
178
+ # without timeout protection.
179
+ #
180
+ # The timeout value determination follows this precedence:
181
+ # 1. Explicit timeout value (provided during middleware initialization)
182
+ # - Integer/Float: Used as-is for static timeout
183
+ # - Symbol: Called as method on task if it exists, otherwise used as numeric value
184
+ # - Proc/Lambda: Executed in task context for dynamic timeout calculation
185
+ # 2. Default value of 3 seconds if no timeout is specified
186
+ #
187
+ # @param task [CMDx::Task] The task instance to execute
188
+ # @param callable [#call] The next middleware or task execution callable
189
+ # @return [CMDx::Result] The task execution result
190
+ # @raise [TimeoutError] If execution exceeds the configured timeout and conditions are met
191
+ #
192
+ # @example Static timeout - successful execution
193
+ # # Task completes in 5 seconds, timeout is 30 seconds, condition is true
194
+ # result = task.call # => success
195
+ #
196
+ # @example Static timeout - timeout exceeded
197
+ # # Task would take 60 seconds, timeout is 30 seconds, condition is true
198
+ # result = task.call # => failed with timeout error
199
+ #
200
+ # @example Dynamic timeout with proc
201
+ # # Task uses proc to calculate 120 seconds for complex operation
202
+ # # Task completes in 90 seconds
203
+ # result = task.call # => success
204
+ #
205
+ # @example Method-based timeout
206
+ # # Task calls :timeout_limit method which returns 45 seconds
207
+ # # Task completes in 30 seconds
208
+ # result = task.call # => success
209
+ #
210
+ # @example Condition not met
211
+ # # Task takes 60 seconds, timeout is 30 seconds, but condition is false
212
+ # result = task.call # => success (no timeout applied)
213
+ def call(task, callable)
214
+ # Check if timeout should be applied based on conditions
215
+ return callable.call(task) unless task.__cmdx_eval(conditional)
216
+
217
+ # Get seconds using yield for dynamic generation
218
+ limit = task.__cmdx_yield(seconds) || 3
219
+
220
+ # Apply timeout protection
221
+ ::Timeout.timeout(limit, TimeoutError, "execution exceeded #{limit} seconds") do
222
+ callable.call(task)
223
+ end
224
+ rescue TimeoutError => e
225
+ task.fail!(reason: "[#{e.class}] #{e.message}", original_exception: e, seconds: limit)
226
+ task.result
227
+ end
228
+
229
+ end
230
+ end
231
+
232
+ end
@@ -1,13 +1,96 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CMDx
4
+ # Parameter definition class for CMDx task parameter management.
5
+ #
6
+ # The Parameter class represents individual parameter definitions within CMDx tasks.
7
+ # It handles parameter configuration, validation rules, type coercion, nested parameters,
8
+ # and method generation for accessing parameter values within task instances.
9
+ #
10
+ # @example Basic parameter definition
11
+ # class ProcessOrderTask < CMDx::Task
12
+ # required :order_id
13
+ # optional :priority
14
+ # end
15
+ #
16
+ # @example Parameter with type coercion and validation
17
+ # class ProcessUserTask < CMDx::Task
18
+ # required :age, type: :integer, numeric: { min: 18, max: 120 }
19
+ # required :email, type: :string, format: { with: /@/ }
20
+ # end
21
+ #
22
+ # @example Nested parameters
23
+ # class ProcessOrderTask < CMDx::Task
24
+ # required :shipping_address do
25
+ # required :street, :city, :state
26
+ # optional :apartment
27
+ # end
28
+ # end
29
+ #
30
+ # @example Parameter with custom source
31
+ # class ProcessUserTask < CMDx::Task
32
+ # required :name, source: :user
33
+ # required :company_name, source: -> { user.company }
34
+ # end
35
+ #
36
+ # @example Parameter with default values
37
+ # class ProcessOrderTask < CMDx::Task
38
+ # optional :priority, default: "normal"
39
+ # optional :notification, default: -> { user.preferences.notify? }
40
+ # end
41
+ #
42
+ # @see CMDx::Task Task parameter integration
43
+ # @see CMDx::ParameterValue Parameter value resolution and validation
44
+ # @see CMDx::Parameters Parameter collection management
4
45
  class Parameter
5
46
 
6
- __cmdx_attr_delegator :invalid?, :valid?, to: :errors
47
+ __cmdx_attr_delegator :invalid?, :valid?,
48
+ to: :errors
7
49
 
50
+ # @return [CMDx::Task] The task class this parameter belongs to
8
51
  attr_accessor :task
52
+
53
+ # @return [Class] The task class this parameter is defined in
54
+ # @return [Parameter, nil] The parent parameter for nested parameters
55
+ # @return [Symbol] The parameter name
56
+ # @return [Symbol, Array<Symbol>] The parameter type(s) for coercion
57
+ # @return [Hash] The parameter configuration options
58
+ # @return [Array<Parameter>] Child parameters for nested parameter definitions
59
+ # @return [CMDx::Errors] Validation errors for this parameter
9
60
  attr_reader :klass, :parent, :name, :type, :options, :children, :errors
10
61
 
62
+ # Initializes a new Parameter instance.
63
+ #
64
+ # Creates a parameter definition with the specified configuration options.
65
+ # Automatically defines accessor methods on the task class and processes
66
+ # any nested parameter definitions provided via block.
67
+ #
68
+ # @param name [Symbol] The parameter name
69
+ # @param options [Hash] Parameter configuration options
70
+ # @option options [Class] :klass The task class (required)
71
+ # @option options [Parameter] :parent Parent parameter for nesting
72
+ # @option options [Symbol, Array<Symbol>] :type (:virtual) Type(s) for coercion
73
+ # @option options [Boolean] :required (false) Whether parameter is required
74
+ # @option options [Object, Proc] :default Default value or callable
75
+ # @option options [Symbol, Proc] :source (:context) Parameter value source
76
+ # @option options [Hash] :* Validation options (presence, format, etc.)
77
+ #
78
+ # @yield Optional block for defining nested parameters
79
+ #
80
+ # @raise [KeyError] If :klass option is not provided
81
+ #
82
+ # @example Basic parameter creation
83
+ # Parameter.new(:user_id, klass: MyTask, type: :integer, required: true)
84
+ #
85
+ # @example Parameter with validation
86
+ # Parameter.new(:email, klass: MyTask, type: :string,
87
+ # format: { with: /@/ }, presence: true)
88
+ #
89
+ # @example Nested parameter with block
90
+ # Parameter.new(:address, klass: MyTask) do
91
+ # required :street, :city
92
+ # optional :apartment
93
+ # end
11
94
  def initialize(name, **options, &)
12
95
  @klass = options.delete(:klass) || raise(KeyError, "klass option required")
13
96
  @parent = options.delete(:parent)
@@ -25,6 +108,26 @@ module CMDx
25
108
 
26
109
  class << self
27
110
 
111
+ # Defines one or more optional parameters.
112
+ #
113
+ # Creates parameter definitions that are not required for task execution.
114
+ # Optional parameters return nil if not provided in the call arguments.
115
+ #
116
+ # @param names [Array<Symbol>] Parameter names to define
117
+ # @param options [Hash] Parameter configuration options
118
+ # @yield Optional block for nested parameter definitions
119
+ #
120
+ # @return [Array<Parameter>] Created parameter instances
121
+ # @raise [ArgumentError] If no parameter names provided or :as option used with multiple names
122
+ #
123
+ # @example Single optional parameter
124
+ # Parameter.optional(:priority, type: :string, default: "normal")
125
+ #
126
+ # @example Multiple optional parameters
127
+ # Parameter.optional(:width, :height, type: :integer, numeric: { min: 0 })
128
+ #
129
+ # @example Optional parameter with validation
130
+ # Parameter.optional(:email, type: :string, format: { with: /@/ })
28
131
  def optional(*names, **options, &)
29
132
  if names.none?
30
133
  raise ArgumentError, "no parameters given"
@@ -35,48 +138,172 @@ module CMDx
35
138
  names.filter_map { |n| new(n, **options, &) }
36
139
  end
37
140
 
141
+ # Defines one or more required parameters.
142
+ #
143
+ # Creates parameter definitions that must be provided for task execution.
144
+ # Missing required parameters will cause task validation to fail.
145
+ #
146
+ # @param names [Array<Symbol>] Parameter names to define
147
+ # @param options [Hash] Parameter configuration options
148
+ # @yield Optional block for nested parameter definitions
149
+ #
150
+ # @return [Array<Parameter>] Created parameter instances
151
+ # @raise [ArgumentError] If no parameter names provided or :as option used with multiple names
152
+ #
153
+ # @example Single required parameter
154
+ # Parameter.required(:user_id, type: :integer)
155
+ #
156
+ # @example Multiple required parameters
157
+ # Parameter.required(:first_name, :last_name, type: :string, presence: true)
158
+ #
159
+ # @example Required parameter with complex validation
160
+ # Parameter.required(:age, type: :integer, numeric: { within: 18..120 })
38
161
  def required(*names, **options, &)
39
162
  optional(*names, **options.merge(required: true), &)
40
163
  end
41
164
 
42
165
  end
43
166
 
167
+ # Defines nested optional parameters within this parameter.
168
+ #
169
+ # Creates child parameter definitions that inherit this parameter as their source.
170
+ # Child parameters are only validated if the parent parameter is provided.
171
+ #
172
+ # @param names [Array<Symbol>] Child parameter names to define
173
+ # @param options [Hash] Parameter configuration options
174
+ # @yield Optional block for further nested parameter definitions
175
+ #
176
+ # @return [Array<Parameter>] Created child parameter instances
177
+ #
178
+ # @example Nested optional parameters
179
+ # address_param.optional(:apartment, :unit, type: :string)
180
+ #
181
+ # @example Nested parameter with validation
182
+ # user_param.optional(:age, type: :integer, numeric: { min: 0 })
44
183
  def optional(*names, **options, &)
45
184
  parameters = Parameter.optional(*names, **options.merge(klass: @klass, parent: self), &)
46
185
  children.concat(parameters)
47
186
  end
48
187
 
188
+ # Defines nested required parameters within this parameter.
189
+ #
190
+ # Creates child parameter definitions that are required if the parent parameter
191
+ # is provided. Child parameters inherit this parameter as their source.
192
+ #
193
+ # @param names [Array<Symbol>] Child parameter names to define
194
+ # @param options [Hash] Parameter configuration options
195
+ # @yield Optional block for further nested parameter definitions
196
+ #
197
+ # @return [Array<Parameter>] Created child parameter instances
198
+ #
199
+ # @example Nested required parameters
200
+ # address_param.required(:street, :city, :state, type: :string)
201
+ #
202
+ # @example Nested parameter with validation
203
+ # payment_param.required(:amount, type: :float, numeric: { min: 0.01 })
49
204
  def required(*names, **options, &)
50
205
  parameters = Parameter.required(*names, **options.merge(klass: @klass, parent: self), &)
51
206
  children.concat(parameters)
52
207
  end
53
208
 
209
+ # Checks if this parameter is required.
210
+ #
211
+ # @return [Boolean] true if parameter is required, false otherwise
212
+ #
213
+ # @example
214
+ # required_param.required? # => true
215
+ # optional_param.required? # => false
54
216
  def required?
55
217
  !!@required
56
218
  end
57
219
 
220
+ # Checks if this parameter is optional.
221
+ #
222
+ # @return [Boolean] true if parameter is optional, false otherwise
223
+ #
224
+ # @example
225
+ # required_param.optional? # => false
226
+ # optional_param.optional? # => true
58
227
  def optional?
59
228
  !required?
60
229
  end
61
230
 
231
+ # Gets the method name that will be defined on the task class.
232
+ #
233
+ # The method name is generated using NameAffix utility and can be customized
234
+ # with :as, :prefix, and :suffix options.
235
+ #
236
+ # @return [Symbol] The generated method name
237
+ #
238
+ # @example Default method name
239
+ # Parameter.new(:user_id, klass: Task).method_name # => :user_id
240
+ #
241
+ # @example Custom method name
242
+ # Parameter.new(:id, klass: Task, as: :user_id).method_name # => :user_id
243
+ #
244
+ # @example Method name with prefix
245
+ # Parameter.new(:name, klass: Task, prefix: "get_").method_name # => :get_name
62
246
  def method_name
63
247
  @method_name ||= Utils::NameAffix.call(name, method_source, options)
64
248
  end
65
249
 
250
+ # Gets the source object/method that provides the parameter value.
251
+ #
252
+ # Determines where the parameter value should be retrieved from, defaulting
253
+ # to :context or inheriting from parent parameter.
254
+ #
255
+ # @return [Symbol] The source method name
256
+ #
257
+ # @example Default source
258
+ # Parameter.new(:user_id, klass: Task).method_source # => :context
259
+ #
260
+ # @example Custom source
261
+ # Parameter.new(:name, klass: Task, source: :user).method_source # => :user
262
+ #
263
+ # @example Inherited source from parent
264
+ # child_param.method_source # => parent parameter's method_name
66
265
  def method_source
67
266
  @method_source ||= options[:source] || parent&.method_name || :context
68
267
  end
69
268
 
269
+ # Converts the parameter to a hash representation.
270
+ #
271
+ # @return [Hash] Serialized parameter data including configuration and children
272
+ #
273
+ # @example
274
+ # param.to_h
275
+ # # => {
276
+ # # source: :context,
277
+ # # name: :user_id,
278
+ # # type: :integer,
279
+ # # required: true,
280
+ # # options: { numeric: { min: 1 } },
281
+ # # children: []
282
+ # # }
70
283
  def to_h
71
284
  ParameterSerializer.call(self)
72
285
  end
73
286
 
287
+ # Converts the parameter to a string representation for inspection.
288
+ #
289
+ # @return [String] Human-readable parameter description
290
+ #
291
+ # @example
292
+ # param.to_s
293
+ # # => "Parameter: name=user_id type=integer source=context required=true options={numeric: {min: 1}}"
74
294
  def to_s
75
295
  ParameterInspector.call(to_h)
76
296
  end
77
297
 
78
298
  private
79
299
 
300
+ # Defines the accessor method on the task class for this parameter.
301
+ #
302
+ # Creates a private method that handles parameter value resolution,
303
+ # type coercion, validation, and error handling with caching.
304
+ #
305
+ # @param parameter [Parameter] The parameter to define method for
306
+ # @return [void]
80
307
  def define_attribute(parameter)
81
308
  klass.send(:define_method, parameter.method_name) do
82
309
  @parameters_cache ||= {}
@@ -1,14 +1,75 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CMDx
4
+ # Parameter inspection utility for generating human-readable parameter descriptions.
5
+ #
6
+ # The ParameterInspector module provides functionality to convert parameter
7
+ # hash representations into formatted, human-readable strings. It handles
8
+ # nested parameter structures with proper indentation and ordering.
9
+ #
10
+ # @example Basic parameter inspection
11
+ # parameter_hash = {
12
+ # name: :user_id,
13
+ # type: :integer,
14
+ # source: :context,
15
+ # required: true,
16
+ # options: { numeric: { min: 1 } },
17
+ # children: []
18
+ # }
19
+ # ParameterInspector.call(parameter_hash)
20
+ # # => "Parameter: name=user_id type=integer source=context required=true options={numeric: {min: 1}}"
21
+ #
22
+ # @example Nested parameter inspection
23
+ # nested_parameter = {
24
+ # name: :address,
25
+ # type: :virtual,
26
+ # source: :context,
27
+ # required: true,
28
+ # options: {},
29
+ # children: [
30
+ # { name: :street, type: :string, source: :address, required: true, options: {}, children: [] }
31
+ # ]
32
+ # }
33
+ # ParameterInspector.call(nested_parameter)
34
+ # # => "Parameter: name=address type=virtual source=context required=true options={}
35
+ # # ↳ Parameter: name=street type=string source=address required=true options={}"
36
+ #
37
+ # @see CMDx::Parameter Parameter hash serialization via to_h
38
+ # @see CMDx::ParameterSerializer Parameter-to-hash conversion
4
39
  module ParameterInspector
5
40
 
41
+ # Ordered keys for consistent parameter inspection output.
42
+ #
43
+ # Defines the order in which parameter attributes are displayed
44
+ # in the inspection string, with children handled specially.
6
45
  ORDERED_KEYS = %i[
7
46
  name type source required options children
8
47
  ].freeze
9
48
 
10
49
  module_function
11
50
 
51
+ # Converts a parameter hash to a human-readable string representation.
52
+ #
53
+ # Formats parameter data into a structured string with proper ordering
54
+ # and indentation for nested parameters. Child parameters are displayed
55
+ # with increased indentation and arrow prefixes.
56
+ #
57
+ # @param parameter [Hash] The parameter hash to inspect
58
+ # @param depth [Integer] The current nesting depth for indentation (default: 1)
59
+ # @return [String] Formatted parameter description
60
+ #
61
+ # @example Single parameter inspection
62
+ # ParameterInspector.call(param_hash)
63
+ # # => "Parameter: name=user_id type=integer source=context required=true"
64
+ #
65
+ # @example Nested parameter inspection with custom depth
66
+ # ParameterInspector.call(param_hash, 2)
67
+ # # => "Parameter: name=address type=virtual source=context required=true
68
+ # # ↳ Parameter: name=street type=string source=address required=true"
69
+ #
70
+ # @example Parameter with options
71
+ # ParameterInspector.call(param_with_validation)
72
+ # # => "Parameter: name=email type=string source=context required=true options={format: {with: /@/}}"
12
73
  def call(parameter, depth = 1)
13
74
  ORDERED_KEYS.filter_map do |key|
14
75
  value = parameter[key]