cmdx 1.0.0 → 1.1.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 (169) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/prompts/rspec.md +20 -0
  3. data/.cursor/prompts/yardoc.md +8 -0
  4. data/.rubocop.yml +5 -0
  5. data/CHANGELOG.md +101 -49
  6. data/README.md +2 -1
  7. data/docs/ai_prompts.md +10 -0
  8. data/docs/basics/call.md +11 -2
  9. data/docs/basics/chain.md +10 -1
  10. data/docs/basics/context.md +9 -0
  11. data/docs/basics/setup.md +9 -0
  12. data/docs/callbacks.md +14 -37
  13. data/docs/configuration.md +68 -27
  14. data/docs/getting_started.md +11 -0
  15. data/docs/internationalization.md +148 -0
  16. data/docs/interruptions/exceptions.md +10 -1
  17. data/docs/interruptions/faults.md +11 -2
  18. data/docs/interruptions/halt.md +9 -0
  19. data/docs/logging.md +14 -4
  20. data/docs/middlewares.md +53 -43
  21. data/docs/outcomes/result.md +9 -0
  22. data/docs/outcomes/states.md +9 -0
  23. data/docs/outcomes/statuses.md +9 -0
  24. data/docs/parameters/coercions.md +58 -38
  25. data/docs/parameters/defaults.md +10 -1
  26. data/docs/parameters/definitions.md +9 -0
  27. data/docs/parameters/namespacing.md +9 -0
  28. data/docs/parameters/validations.md +8 -67
  29. data/docs/testing.md +22 -13
  30. data/docs/tips_and_tricks.md +9 -0
  31. data/docs/workflows.md +14 -4
  32. data/lib/cmdx/.DS_Store +0 -0
  33. data/lib/cmdx/callback.rb +36 -56
  34. data/lib/cmdx/callback_registry.rb +82 -73
  35. data/lib/cmdx/chain.rb +65 -122
  36. data/lib/cmdx/chain_inspector.rb +22 -115
  37. data/lib/cmdx/chain_serializer.rb +17 -148
  38. data/lib/cmdx/coercion.rb +49 -0
  39. data/lib/cmdx/coercion_registry.rb +94 -0
  40. data/lib/cmdx/coercions/array.rb +18 -36
  41. data/lib/cmdx/coercions/big_decimal.rb +21 -33
  42. data/lib/cmdx/coercions/boolean.rb +21 -40
  43. data/lib/cmdx/coercions/complex.rb +18 -31
  44. data/lib/cmdx/coercions/date.rb +20 -39
  45. data/lib/cmdx/coercions/date_time.rb +22 -39
  46. data/lib/cmdx/coercions/float.rb +19 -32
  47. data/lib/cmdx/coercions/hash.rb +22 -41
  48. data/lib/cmdx/coercions/integer.rb +20 -33
  49. data/lib/cmdx/coercions/rational.rb +20 -32
  50. data/lib/cmdx/coercions/string.rb +23 -31
  51. data/lib/cmdx/coercions/time.rb +24 -40
  52. data/lib/cmdx/coercions/virtual.rb +14 -31
  53. data/lib/cmdx/configuration.rb +57 -171
  54. data/lib/cmdx/context.rb +22 -165
  55. data/lib/cmdx/core_ext/hash.rb +42 -67
  56. data/lib/cmdx/core_ext/module.rb +35 -79
  57. data/lib/cmdx/core_ext/object.rb +63 -98
  58. data/lib/cmdx/correlator.rb +40 -156
  59. data/lib/cmdx/error.rb +37 -202
  60. data/lib/cmdx/errors.rb +165 -202
  61. data/lib/cmdx/fault.rb +55 -158
  62. data/lib/cmdx/faults.rb +26 -137
  63. data/lib/cmdx/immutator.rb +22 -109
  64. data/lib/cmdx/lazy_struct.rb +103 -187
  65. data/lib/cmdx/log_formatters/json.rb +14 -40
  66. data/lib/cmdx/log_formatters/key_value.rb +14 -40
  67. data/lib/cmdx/log_formatters/line.rb +14 -48
  68. data/lib/cmdx/log_formatters/logstash.rb +14 -57
  69. data/lib/cmdx/log_formatters/pretty_json.rb +14 -50
  70. data/lib/cmdx/log_formatters/pretty_key_value.rb +13 -46
  71. data/lib/cmdx/log_formatters/pretty_line.rb +16 -54
  72. data/lib/cmdx/log_formatters/raw.rb +19 -49
  73. data/lib/cmdx/logger.rb +20 -82
  74. data/lib/cmdx/logger_ansi.rb +18 -75
  75. data/lib/cmdx/logger_serializer.rb +24 -114
  76. data/lib/cmdx/middleware.rb +38 -60
  77. data/lib/cmdx/middleware_registry.rb +81 -77
  78. data/lib/cmdx/middlewares/correlate.rb +41 -226
  79. data/lib/cmdx/middlewares/timeout.rb +46 -185
  80. data/lib/cmdx/parameter.rb +120 -198
  81. data/lib/cmdx/parameter_evaluator.rb +231 -0
  82. data/lib/cmdx/parameter_inspector.rb +25 -56
  83. data/lib/cmdx/parameter_registry.rb +59 -84
  84. data/lib/cmdx/parameter_serializer.rb +23 -74
  85. data/lib/cmdx/railtie.rb +24 -107
  86. data/lib/cmdx/result.rb +254 -260
  87. data/lib/cmdx/result_ansi.rb +19 -85
  88. data/lib/cmdx/result_inspector.rb +27 -68
  89. data/lib/cmdx/result_logger.rb +18 -81
  90. data/lib/cmdx/result_serializer.rb +28 -132
  91. data/lib/cmdx/rspec/matchers.rb +28 -0
  92. data/lib/cmdx/rspec/result_matchers/be_executed.rb +42 -0
  93. data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +94 -0
  94. data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +94 -0
  95. data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +59 -0
  96. data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +57 -0
  97. data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +87 -0
  98. data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +51 -0
  99. data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +58 -0
  100. data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +59 -0
  101. data/lib/cmdx/rspec/result_matchers/have_context.rb +86 -0
  102. data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +54 -0
  103. data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +52 -0
  104. data/lib/cmdx/rspec/result_matchers/have_metadata.rb +114 -0
  105. data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +66 -0
  106. data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +64 -0
  107. data/lib/cmdx/rspec/result_matchers/have_runtime.rb +78 -0
  108. data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +76 -0
  109. data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +62 -0
  110. data/lib/cmdx/rspec/task_matchers/have_callback.rb +85 -0
  111. data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +68 -0
  112. data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +92 -0
  113. data/lib/cmdx/rspec/task_matchers/have_middleware.rb +46 -0
  114. data/lib/cmdx/rspec/task_matchers/have_parameter.rb +181 -0
  115. data/lib/cmdx/task.rb +213 -425
  116. data/lib/cmdx/task_deprecator.rb +55 -0
  117. data/lib/cmdx/task_processor.rb +245 -0
  118. data/lib/cmdx/task_serializer.rb +22 -70
  119. data/lib/cmdx/utils/ansi_color.rb +13 -89
  120. data/lib/cmdx/utils/log_timestamp.rb +13 -42
  121. data/lib/cmdx/utils/monotonic_runtime.rb +13 -63
  122. data/lib/cmdx/utils/name_affix.rb +21 -71
  123. data/lib/cmdx/validator.rb +48 -0
  124. data/lib/cmdx/validator_registry.rb +86 -0
  125. data/lib/cmdx/validators/exclusion.rb +55 -94
  126. data/lib/cmdx/validators/format.rb +31 -85
  127. data/lib/cmdx/validators/inclusion.rb +65 -110
  128. data/lib/cmdx/validators/length.rb +117 -133
  129. data/lib/cmdx/validators/numeric.rb +123 -130
  130. data/lib/cmdx/validators/presence.rb +38 -79
  131. data/lib/cmdx/version.rb +1 -7
  132. data/lib/cmdx/workflow.rb +46 -339
  133. data/lib/cmdx.rb +1 -1
  134. data/lib/generators/cmdx/install_generator.rb +14 -31
  135. data/lib/generators/cmdx/task_generator.rb +39 -55
  136. data/lib/generators/cmdx/templates/install.rb +61 -11
  137. data/lib/generators/cmdx/workflow_generator.rb +41 -66
  138. data/lib/locales/ar.yml +35 -0
  139. data/lib/locales/cs.yml +35 -0
  140. data/lib/locales/da.yml +35 -0
  141. data/lib/locales/de.yml +35 -0
  142. data/lib/locales/el.yml +35 -0
  143. data/lib/locales/en.yml +19 -20
  144. data/lib/locales/es.yml +19 -20
  145. data/lib/locales/fi.yml +35 -0
  146. data/lib/locales/fr.yml +35 -0
  147. data/lib/locales/he.yml +35 -0
  148. data/lib/locales/hi.yml +35 -0
  149. data/lib/locales/it.yml +35 -0
  150. data/lib/locales/ja.yml +35 -0
  151. data/lib/locales/ko.yml +35 -0
  152. data/lib/locales/nl.yml +35 -0
  153. data/lib/locales/no.yml +35 -0
  154. data/lib/locales/pl.yml +35 -0
  155. data/lib/locales/pt.yml +35 -0
  156. data/lib/locales/ru.yml +35 -0
  157. data/lib/locales/sv.yml +35 -0
  158. data/lib/locales/th.yml +35 -0
  159. data/lib/locales/tr.yml +35 -0
  160. data/lib/locales/vi.yml +35 -0
  161. data/lib/locales/zh.yml +35 -0
  162. metadata +57 -8
  163. data/lib/cmdx/parameter_validator.rb +0 -81
  164. data/lib/cmdx/parameter_value.rb +0 -244
  165. data/lib/cmdx/parameters_inspector.rb +0 -72
  166. data/lib/cmdx/parameters_serializer.rb +0 -115
  167. data/lib/cmdx/rspec/result_matchers.rb +0 -917
  168. data/lib/cmdx/rspec/task_matchers.rb +0 -570
  169. data/lib/cmdx/validators/custom.rb +0 -102
@@ -1,95 +1,60 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CMDx
4
- # Parameter definition class for CMDx task parameter management.
4
+ # Parameter definition system for CMDx tasks.
5
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
6
+ # This class manages parameter definitions including type coercion, validation,
7
+ # and nested parameter structures. It handles the creation of accessor methods
8
+ # on task classes and provides a flexible system for defining required and
9
+ # optional parameters with various data types and validation rules.
45
10
  class Parameter
46
11
 
47
- __cmdx_attr_delegator :invalid?, :valid?,
48
- to: :errors
12
+ cmdx_attr_delegator :invalid?, :valid?,
13
+ to: :errors
49
14
 
50
15
  # @return [CMDx::Task] The task class this parameter belongs to
51
16
  attr_accessor :task
52
17
 
53
18
  # @return [Class] The task class this parameter is defined in
19
+ attr_reader :klass
20
+
54
21
  # @return [Parameter, nil] The parent parameter for nested parameters
22
+ attr_reader :parent
23
+
55
24
  # @return [Symbol] The parameter name
25
+ attr_reader :name
26
+
56
27
  # @return [Symbol, Array<Symbol>] The parameter type(s) for coercion
28
+ attr_reader :type
29
+
57
30
  # @return [Hash] The parameter configuration options
31
+ attr_reader :options
32
+
58
33
  # @return [Array<Parameter>] Child parameters for nested parameter definitions
34
+ attr_reader :children
35
+
59
36
  # @return [CMDx::Errors] Validation errors for this parameter
60
- attr_reader :klass, :parent, :name, :type, :options, :children, :errors
37
+ attr_reader :errors
61
38
 
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.
39
+ # Creates a new parameter definition with the given name and options.
67
40
  #
68
41
  # @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
42
+ # @param options [Hash] Configuration options for the parameter
43
+ # @option options [Class] :klass The task class this parameter belongs to (required)
44
+ # @option options [Parameter] :parent The parent parameter for nested parameters
45
+ # @option options [Symbol, Array<Symbol>] :type (:virtual) The parameter type(s) for coercion
46
+ # @option options [Boolean] :required (false) Whether the parameter is required
47
+ # @param block [Proc] Optional block for defining nested parameters
48
+ # @return [Parameter] The newly created parameter
49
+ # @raise [KeyError] If the :klass option is not provided
50
+ #
51
+ # @example Create a simple parameter
52
+ # Parameter.new(:name, klass: MyTask, type: :string, required: true)
53
+ #
54
+ # @example Create a parameter with nested children
55
+ # Parameter.new(:user, klass: MyTask, type: :hash) do
56
+ # required :name, type: :string
57
+ # optional :age, type: :integer
93
58
  # end
94
59
  def initialize(name, **options, &)
95
60
  @klass = options.delete(:klass) || raise(KeyError, "klass option required")
@@ -108,26 +73,21 @@ module CMDx
108
73
 
109
74
  class << self
110
75
 
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.
76
+ # Creates one or more optional parameters with the given names and options.
115
77
  #
116
- # @param names [Array<Symbol>] Parameter names to define
117
- # @param options [Hash] Parameter configuration options
118
- # @yield Optional block for nested parameter definitions
78
+ # @param names [Array<Symbol>] Parameter names to create
79
+ # @param options [Hash] Configuration options for all parameters
80
+ # @param block [Proc] Optional block for defining nested parameters
81
+ # @return [Array<Parameter>] The created optional parameters
82
+ # @raise [ArgumentError] If no parameters are given or :as option is used with multiple names
119
83
  #
120
- # @return [Array<Parameter>] Created parameter instances
121
- # @raise [ArgumentError] If no parameter names provided or :as option used with multiple names
84
+ # @example Create multiple optional parameters
85
+ # Parameter.optional(:name, :email, type: :string, klass: MyTask)
122
86
  #
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: /@/ })
87
+ # @example Create optional parameter with nested structure
88
+ # Parameter.optional(:user, klass: MyTask, type: :hash) do
89
+ # required :name, type: :string
90
+ # end
131
91
  def optional(*names, **options, &)
132
92
  if names.none?
133
93
  raise ArgumentError, "no parameters given"
@@ -138,69 +98,56 @@ module CMDx
138
98
  names.filter_map { |n| new(n, **options, &) }
139
99
  end
140
100
 
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
101
+ # Creates one or more required parameters with the given names and options.
152
102
  #
153
- # @example Single required parameter
154
- # Parameter.required(:user_id, type: :integer)
103
+ # @param names [Array<Symbol>] Parameter names to create
104
+ # @param options [Hash] Configuration options for all parameters
105
+ # @param block [Proc] Optional block for defining nested parameters
106
+ # @return [Array<Parameter>] The created required parameters
107
+ # @raise [ArgumentError] If no parameters are given or :as option is used with multiple names
155
108
  #
156
- # @example Multiple required parameters
157
- # Parameter.required(:first_name, :last_name, type: :string, presence: true)
109
+ # @example Create multiple required parameters
110
+ # Parameter.required(:name, :email, type: :string, klass: MyTask)
158
111
  #
159
- # @example Required parameter with complex validation
160
- # Parameter.required(:age, type: :integer, numeric: { within: 18..120 })
112
+ # @example Create required parameter with validation
113
+ # Parameter.required(:age, type: :integer, validate: { numeric: { greater_than: 0 } }, klass: MyTask)
161
114
  def required(*names, **options, &)
162
115
  optional(*names, **options.merge(required: true), &)
163
116
  end
164
117
 
165
118
  end
166
119
 
167
- # Defines nested optional parameters within this parameter.
120
+ # Creates one or more optional child parameters under this parameter.
168
121
  #
169
- # Creates child parameter definitions that inherit this parameter as their source.
170
- # Child parameters are only validated if the parent parameter is provided.
122
+ # @param names [Array<Symbol>] Parameter names to create
123
+ # @param options [Hash] Configuration options for all parameters
124
+ # @param block [Proc] Optional block for defining nested parameters
125
+ # @return [Array<Parameter>] The created optional child parameters
171
126
  #
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
127
+ # @example Add optional child parameters
128
+ # user_param.optional(:nickname, :bio, type: :string)
175
129
  #
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 })
130
+ # @example Add optional child with further nesting
131
+ # user_param.optional(:preferences, type: :hash) do
132
+ # required :theme, type: :string
133
+ # end
183
134
  def optional(*names, **options, &)
184
135
  parameters = Parameter.optional(*names, **options.merge(klass: @klass, parent: self), &)
185
136
  children.concat(parameters)
186
137
  end
187
138
 
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.
139
+ # Creates one or more required child parameters under this parameter.
192
140
  #
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
141
+ # @param names [Array<Symbol>] Parameter names to create
142
+ # @param options [Hash] Configuration options for all parameters
143
+ # @param block [Proc] Optional block for defining nested parameters
144
+ # @return [Array<Parameter>] The created required child parameters
196
145
  #
197
- # @return [Array<Parameter>] Created child parameter instances
146
+ # @example Add required child parameters
147
+ # user_param.required(:first_name, :last_name, type: :string)
198
148
  #
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 })
149
+ # @example Add required child with validation
150
+ # user_param.required(:email, type: :string, validate: { format: { with: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i } })
204
151
  def required(*names, **options, &)
205
152
  parameters = Parameter.required(*names, **options.merge(klass: @klass, parent: self), &)
206
153
  children.concat(parameters)
@@ -208,117 +155,92 @@ module CMDx
208
155
 
209
156
  # Checks if this parameter is required.
210
157
  #
211
- # @return [Boolean] true if parameter is required, false otherwise
158
+ # @return [Boolean] True if the parameter is required, false otherwise
212
159
  #
213
- # @example
214
- # required_param.required? # => true
215
- # optional_param.required? # => false
160
+ # @example Check if parameter is required
161
+ # param.required? # => true
216
162
  def required?
217
163
  !!@required
218
164
  end
219
165
 
220
166
  # Checks if this parameter is optional.
221
167
  #
222
- # @return [Boolean] true if parameter is optional, false otherwise
168
+ # @return [Boolean] True if the parameter is optional, false otherwise
223
169
  #
224
- # @example
225
- # required_param.optional? # => false
226
- # optional_param.optional? # => true
170
+ # @example Check if parameter is optional
171
+ # param.optional? # => false
227
172
  def optional?
228
173
  !required?
229
174
  end
230
175
 
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
176
+ # Gets the method name that will be used to access this parameter's value.
237
177
  #
238
- # @example Default method name
239
- # Parameter.new(:user_id, klass: Task).method_name # => :user_id
178
+ # @return [Symbol] The method name for accessing this parameter
240
179
  #
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
180
+ # @example Get method name
181
+ # param.method_name # => :user_name
246
182
  def method_name
247
183
  @method_name ||= Utils::NameAffix.call(name, method_source, options)
248
184
  end
249
185
 
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.
186
+ # Gets the source object from which this parameter's value will be retrieved.
254
187
  #
255
- # @return [Symbol] The source method name
188
+ # @return [Symbol] The method source (:context by default, or parent's method_name)
256
189
  #
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
190
+ # @example Get method source
191
+ # param.method_source # => :context
265
192
  def method_source
266
193
  @method_source ||= options[:source] || parent&.method_name || :context
267
194
  end
268
195
 
269
196
  # Converts the parameter to a hash representation.
270
197
  #
271
- # @return [Hash] Serialized parameter data including configuration and children
198
+ # @return [Hash] A hash representation of the parameter
272
199
  #
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
- # # }
200
+ # @example Convert to hash
201
+ # param.to_h # => { name: :user_name, type: :string, required: true, ... }
283
202
  def to_h
284
203
  ParameterSerializer.call(self)
285
204
  end
286
205
 
287
- # Converts the parameter to a string representation for inspection.
206
+ # Converts the parameter to a string representation.
288
207
  #
289
- # @return [String] Human-readable parameter description
208
+ # @return [String] A string representation of the parameter
290
209
  #
291
- # @example
292
- # param.to_s
293
- # # => "Parameter: name=user_id type=integer source=context required=true options={numeric: {min: 1}}"
210
+ # @example Convert to string
211
+ # param.to_s # => "Parameter(name: user_name, type: string, required: true)"
294
212
  def to_s
295
213
  ParameterInspector.call(to_h)
296
214
  end
297
215
 
298
216
  private
299
217
 
300
- # Defines the accessor method on the task class for this parameter.
218
+ # Defines the attribute accessor method for this parameter on the task class.
219
+ # The method handles parameter value retrieval, coercion, and validation.
301
220
  #
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
221
+ # @param parameter [Parameter] The parameter to define the method for
306
222
  # @return [void]
223
+ # @raise [CoercionError] If parameter value cannot be coerced to the expected type
224
+ # @raise [ValidationError] If parameter value fails validation
225
+ #
226
+ # @example Define parameter method (internal use)
227
+ # define_attribute(param) # Defines a private method on the task class
307
228
  def define_attribute(parameter)
308
229
  klass.send(:define_method, parameter.method_name) do
309
- @parameters_cache ||= {}
310
- return @parameters_cache[parameter.method_name] if @parameters_cache.key?(parameter.method_name)
311
-
312
- begin
313
- parameter_value = ParameterValue.new(self, parameter).call
314
- rescue CoercionError, ValidationError => e
315
- parameter.errors.add(parameter.method_name, e.message)
316
- errors.merge!(parameter.errors.to_hash)
317
- ensure
318
- @parameters_cache[parameter.method_name] = parameter_value
230
+ @cmd_parameter_value_cache ||= {}
231
+
232
+ unless @cmd_parameter_value_cache.key?(parameter.method_name)
233
+ begin
234
+ parameter_value = ParameterEvaluator.call(self, parameter)
235
+ rescue CoercionError, ValidationError => e
236
+ parameter.errors.add(parameter.method_name, e.message)
237
+ errors.merge!(parameter.errors.to_hash)
238
+ ensure
239
+ @cmd_parameter_value_cache[parameter.method_name] = parameter_value
240
+ end
319
241
  end
320
242
 
321
- @parameters_cache[parameter.method_name]
243
+ @cmd_parameter_value_cache[parameter.method_name]
322
244
  end
323
245
 
324
246
  klass.send(:private, parameter.method_name)
@@ -0,0 +1,231 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ # Parameter evaluation system for task execution context.
5
+ #
6
+ # ParameterEvaluator processes parameter definitions by extracting values from
7
+ # task context sources, applying type coercions, performing validations, and
8
+ # handling optional parameters with default values. It ensures parameter values
9
+ # meet the requirements defined in parameter specifications before task execution.
10
+ class ParameterEvaluator
11
+
12
+ cmdx_attr_delegator :parent, :method_source, :name, :options, :required?, :optional?, :type,
13
+ to: :parameter,
14
+ private: true
15
+
16
+ # @return [CMDx::Task] The task instance being processed
17
+ attr_reader :task
18
+
19
+ # @return [CMDx::Parameter] The parameter definition being processed
20
+ attr_reader :parameter
21
+
22
+ # Creates a new parameter evaluator instance.
23
+ #
24
+ # @param task [CMDx::Task] the task instance containing parameter context
25
+ # @param parameter [CMDx::Parameter] the parameter definition to evaluate
26
+ #
27
+ # @example Create evaluator for a task parameter
28
+ # evaluator = ParameterEvaluator.new(task, parameter)
29
+ def initialize(task, parameter)
30
+ @task = task
31
+ @parameter = parameter
32
+ end
33
+
34
+ # Evaluates a parameter by creating a new evaluator instance and calling it.
35
+ #
36
+ # @param task [CMDx::Task] the task instance containing parameter context
37
+ # @param parameter [CMDx::Parameter] the parameter definition to evaluate
38
+ #
39
+ # @return [Object] the coerced and validated parameter value
40
+ #
41
+ # @raise [ValidationError] when parameter source is undefined or required parameter is missing
42
+ # @raise [CoercionError] when parameter value cannot be coerced to expected type
43
+ #
44
+ # @example Evaluate a parameter value
45
+ # value = ParameterEvaluator.call(task, parameter)
46
+ def self.call(task, parameter)
47
+ new(task, parameter).call
48
+ end
49
+
50
+ # Evaluates the parameter by applying coercion and validation.
51
+ #
52
+ # @return [Object] the coerced and validated parameter value
53
+ #
54
+ # @raise [ValidationError] when parameter source is undefined or required parameter is missing
55
+ # @raise [CoercionError] when parameter value cannot be coerced to expected type
56
+ #
57
+ # @example Evaluate parameter with coercion and validation
58
+ # evaluator = ParameterEvaluator.new(task, parameter)
59
+ # value = evaluator.call
60
+ def call
61
+ coerce!.tap { validate! }
62
+ end
63
+
64
+ private
65
+
66
+ # Checks if the parameter source method is defined on the task.
67
+ #
68
+ # @return [Boolean] true if the source method exists, false otherwise
69
+ #
70
+ # @example Check if parameter source is defined
71
+ # evaluator.send(:source_defined?) #=> true
72
+ def source_defined?
73
+ task.respond_to?(method_source, true) || task.cmdx_try(method_source)
74
+ end
75
+
76
+ # Retrieves the parameter source object from the task.
77
+ #
78
+ # @return [Object] the source object containing parameter values
79
+ #
80
+ # @raise [ValidationError] when the source method is not defined on the task
81
+ #
82
+ # @example Get parameter source
83
+ # evaluator.send(:source) #=> #<Context:...>
84
+ def source
85
+ return @source if defined?(@source)
86
+
87
+ unless source_defined?
88
+ raise ValidationError, I18n.t(
89
+ "cmdx.parameters.undefined",
90
+ default: "delegates to undefined method #{method_source}",
91
+ source: method_source
92
+ )
93
+ end
94
+
95
+ @source = task.cmdx_try(method_source)
96
+ end
97
+
98
+ # Checks if the parameter value exists in the source object.
99
+ #
100
+ # @return [Boolean] true if the parameter value exists, false otherwise
101
+ #
102
+ # @example Check if parameter value exists
103
+ # evaluator.send(:source_value?) #=> true
104
+ def source_value?
105
+ return false if source.nil?
106
+
107
+ source.cmdx_respond_to?(name, true)
108
+ end
109
+
110
+ # Checks if a required parameter value is missing from the source.
111
+ #
112
+ # @return [Boolean] true if required parameter is missing, false otherwise
113
+ #
114
+ # @example Check if required parameter is missing
115
+ # evaluator.send(:source_value_required?) #=> false
116
+ def source_value_required?
117
+ return false if parent&.optional? && source.nil?
118
+
119
+ required? && !source_value?
120
+ end
121
+
122
+ # Extracts the parameter value from the source with default handling.
123
+ #
124
+ # @return [Object] the parameter value or default value
125
+ #
126
+ # @raise [ValidationError] when a required parameter is missing
127
+ #
128
+ # @example Get parameter value with default
129
+ # evaluator.send(:value) #=> "default_value"
130
+ def value
131
+ return @value if defined?(@value)
132
+
133
+ if source_value_required?
134
+ raise ValidationError, I18n.t(
135
+ "cmdx.parameters.required",
136
+ default: "is a required parameter"
137
+ )
138
+ end
139
+
140
+ @value = source.cmdx_try(name)
141
+ return @value unless @value.nil? && options.key?(:default)
142
+
143
+ @value = task.cmdx_yield(options[:default])
144
+ end
145
+
146
+ # Applies type coercion to the parameter value.
147
+ #
148
+ # @return [Object] the coerced parameter value
149
+ #
150
+ # @raise [CoercionError] when value cannot be coerced to expected type
151
+ #
152
+ # @example Coerce parameter value
153
+ # evaluator.send(:coerce!) #=> 42
154
+ def coerce!
155
+ types = Array(type)
156
+ tsize = types.size - 1
157
+
158
+ types.each_with_index do |key, i|
159
+ break CMDx.configuration.coercions.call(task, key, value, options)
160
+ rescue CoercionError => e
161
+ next if tsize != i
162
+
163
+ raise(e) if tsize.zero?
164
+
165
+ values = types.map(&:to_s).join(", ")
166
+ raise CoercionError, I18n.t(
167
+ "cmdx.coercions.into_any",
168
+ values:,
169
+ default: "could not coerce into one of: #{values}"
170
+ )
171
+ end
172
+ end
173
+
174
+ # Checks if validations should be skipped for optional missing arguments.
175
+ #
176
+ # @return [Boolean] true if validations should be skipped, false otherwise
177
+ #
178
+ # @example Check if validations should be skipped
179
+ # evaluator.send(:skip_validations_due_to_optional_missing_argument?) #=> false
180
+ def skip_validations_due_to_optional_missing_argument?
181
+ optional? && value.nil? && !source.nil? && !source.cmdx_respond_to?(name, true)
182
+ end
183
+
184
+ # Checks if validator should be skipped due to conditional options.
185
+ #
186
+ # @param opts [Hash] the validator options
187
+ #
188
+ # @return [Boolean] true if validator should be skipped, false otherwise
189
+ #
190
+ # @example Check if validator should be skipped
191
+ # evaluator.send(:skip_validator_due_to_conditional?, :presence) #=> false
192
+ def skip_validator_due_to_conditional?(opts)
193
+ opts.is_a?(Hash) && !task.cmdx_eval(opts)
194
+ end
195
+
196
+ # Checks if validator should be skipped due to allow_nil option.
197
+ #
198
+ # @param opts [Symbol] the validator options
199
+ #
200
+ # @return [Boolean] true if validator should be skipped, false otherwise
201
+ #
202
+ # @example Check if validator should be skipped for nil
203
+ # evaluator.send(:skip_validator_due_to_allow_nil?, :presence) #=> true
204
+ def skip_validator_due_to_allow_nil?(opts)
205
+ opts.is_a?(Hash) && opts[:allow_nil] && value.nil?
206
+ end
207
+
208
+ # Applies all configured validations to the parameter value.
209
+ #
210
+ # @return [void]
211
+ #
212
+ # @raise [ValidationError] when parameter value fails validation
213
+ #
214
+ # @example Validate parameter value
215
+ # evaluator.send(:validate!)
216
+ def validate!
217
+ return if skip_validations_due_to_optional_missing_argument?
218
+
219
+ types = CMDx.configuration.validators.registry.keys
220
+
221
+ options.slice(*types).each_key do |key|
222
+ opts = options[key]
223
+ next if skip_validator_due_to_allow_nil?(opts)
224
+ next if skip_validator_due_to_conditional?(opts)
225
+
226
+ CMDx.configuration.validators.call(task, key, value, opts)
227
+ end
228
+ end
229
+
230
+ end
231
+ end