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.
- checksums.yaml +4 -4
- data/.cursor/prompts/rspec.md +20 -0
- data/.cursor/prompts/yardoc.md +8 -0
- data/.rubocop.yml +5 -0
- data/CHANGELOG.md +101 -49
- data/README.md +2 -1
- data/docs/ai_prompts.md +10 -0
- data/docs/basics/call.md +11 -2
- data/docs/basics/chain.md +10 -1
- data/docs/basics/context.md +9 -0
- data/docs/basics/setup.md +9 -0
- data/docs/callbacks.md +14 -37
- data/docs/configuration.md +68 -27
- data/docs/getting_started.md +11 -0
- data/docs/internationalization.md +148 -0
- data/docs/interruptions/exceptions.md +10 -1
- data/docs/interruptions/faults.md +11 -2
- data/docs/interruptions/halt.md +9 -0
- data/docs/logging.md +14 -4
- data/docs/middlewares.md +53 -43
- data/docs/outcomes/result.md +9 -0
- data/docs/outcomes/states.md +9 -0
- data/docs/outcomes/statuses.md +9 -0
- data/docs/parameters/coercions.md +58 -38
- data/docs/parameters/defaults.md +10 -1
- data/docs/parameters/definitions.md +9 -0
- data/docs/parameters/namespacing.md +9 -0
- data/docs/parameters/validations.md +8 -67
- data/docs/testing.md +22 -13
- data/docs/tips_and_tricks.md +9 -0
- data/docs/workflows.md +14 -4
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/callback.rb +36 -56
- data/lib/cmdx/callback_registry.rb +82 -73
- data/lib/cmdx/chain.rb +65 -122
- data/lib/cmdx/chain_inspector.rb +22 -115
- data/lib/cmdx/chain_serializer.rb +17 -148
- data/lib/cmdx/coercion.rb +49 -0
- data/lib/cmdx/coercion_registry.rb +94 -0
- data/lib/cmdx/coercions/array.rb +18 -36
- data/lib/cmdx/coercions/big_decimal.rb +21 -33
- data/lib/cmdx/coercions/boolean.rb +21 -40
- data/lib/cmdx/coercions/complex.rb +18 -31
- data/lib/cmdx/coercions/date.rb +20 -39
- data/lib/cmdx/coercions/date_time.rb +22 -39
- data/lib/cmdx/coercions/float.rb +19 -32
- data/lib/cmdx/coercions/hash.rb +22 -41
- data/lib/cmdx/coercions/integer.rb +20 -33
- data/lib/cmdx/coercions/rational.rb +20 -32
- data/lib/cmdx/coercions/string.rb +23 -31
- data/lib/cmdx/coercions/time.rb +24 -40
- data/lib/cmdx/coercions/virtual.rb +14 -31
- data/lib/cmdx/configuration.rb +57 -171
- data/lib/cmdx/context.rb +22 -165
- data/lib/cmdx/core_ext/hash.rb +42 -67
- data/lib/cmdx/core_ext/module.rb +35 -79
- data/lib/cmdx/core_ext/object.rb +63 -98
- data/lib/cmdx/correlator.rb +40 -156
- data/lib/cmdx/error.rb +37 -202
- data/lib/cmdx/errors.rb +165 -202
- data/lib/cmdx/fault.rb +55 -158
- data/lib/cmdx/faults.rb +26 -137
- data/lib/cmdx/immutator.rb +22 -109
- data/lib/cmdx/lazy_struct.rb +103 -187
- data/lib/cmdx/log_formatters/json.rb +14 -40
- data/lib/cmdx/log_formatters/key_value.rb +14 -40
- data/lib/cmdx/log_formatters/line.rb +14 -48
- data/lib/cmdx/log_formatters/logstash.rb +14 -57
- data/lib/cmdx/log_formatters/pretty_json.rb +14 -50
- data/lib/cmdx/log_formatters/pretty_key_value.rb +13 -46
- data/lib/cmdx/log_formatters/pretty_line.rb +16 -54
- data/lib/cmdx/log_formatters/raw.rb +19 -49
- data/lib/cmdx/logger.rb +20 -82
- data/lib/cmdx/logger_ansi.rb +18 -75
- data/lib/cmdx/logger_serializer.rb +24 -114
- data/lib/cmdx/middleware.rb +38 -60
- data/lib/cmdx/middleware_registry.rb +81 -77
- data/lib/cmdx/middlewares/correlate.rb +41 -226
- data/lib/cmdx/middlewares/timeout.rb +46 -185
- data/lib/cmdx/parameter.rb +120 -198
- data/lib/cmdx/parameter_evaluator.rb +231 -0
- data/lib/cmdx/parameter_inspector.rb +25 -56
- data/lib/cmdx/parameter_registry.rb +59 -84
- data/lib/cmdx/parameter_serializer.rb +23 -74
- data/lib/cmdx/railtie.rb +24 -107
- data/lib/cmdx/result.rb +254 -260
- data/lib/cmdx/result_ansi.rb +19 -85
- data/lib/cmdx/result_inspector.rb +27 -68
- data/lib/cmdx/result_logger.rb +18 -81
- data/lib/cmdx/result_serializer.rb +28 -132
- data/lib/cmdx/rspec/matchers.rb +28 -0
- data/lib/cmdx/rspec/result_matchers/be_executed.rb +42 -0
- data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +94 -0
- data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +94 -0
- data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +59 -0
- data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +57 -0
- data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +87 -0
- data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +51 -0
- data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +58 -0
- data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +59 -0
- data/lib/cmdx/rspec/result_matchers/have_context.rb +86 -0
- data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +54 -0
- data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +52 -0
- data/lib/cmdx/rspec/result_matchers/have_metadata.rb +114 -0
- data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +66 -0
- data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +64 -0
- data/lib/cmdx/rspec/result_matchers/have_runtime.rb +78 -0
- data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +76 -0
- data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +62 -0
- data/lib/cmdx/rspec/task_matchers/have_callback.rb +85 -0
- data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +68 -0
- data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +92 -0
- data/lib/cmdx/rspec/task_matchers/have_middleware.rb +46 -0
- data/lib/cmdx/rspec/task_matchers/have_parameter.rb +181 -0
- data/lib/cmdx/task.rb +213 -425
- data/lib/cmdx/task_deprecator.rb +55 -0
- data/lib/cmdx/task_processor.rb +245 -0
- data/lib/cmdx/task_serializer.rb +22 -70
- data/lib/cmdx/utils/ansi_color.rb +13 -89
- data/lib/cmdx/utils/log_timestamp.rb +13 -42
- data/lib/cmdx/utils/monotonic_runtime.rb +13 -63
- data/lib/cmdx/utils/name_affix.rb +21 -71
- data/lib/cmdx/validator.rb +48 -0
- data/lib/cmdx/validator_registry.rb +86 -0
- data/lib/cmdx/validators/exclusion.rb +55 -94
- data/lib/cmdx/validators/format.rb +31 -85
- data/lib/cmdx/validators/inclusion.rb +65 -110
- data/lib/cmdx/validators/length.rb +117 -133
- data/lib/cmdx/validators/numeric.rb +123 -130
- data/lib/cmdx/validators/presence.rb +38 -79
- data/lib/cmdx/version.rb +1 -7
- data/lib/cmdx/workflow.rb +46 -339
- data/lib/cmdx.rb +1 -1
- data/lib/generators/cmdx/install_generator.rb +14 -31
- data/lib/generators/cmdx/task_generator.rb +39 -55
- data/lib/generators/cmdx/templates/install.rb +61 -11
- data/lib/generators/cmdx/workflow_generator.rb +41 -66
- data/lib/locales/ar.yml +35 -0
- data/lib/locales/cs.yml +35 -0
- data/lib/locales/da.yml +35 -0
- data/lib/locales/de.yml +35 -0
- data/lib/locales/el.yml +35 -0
- data/lib/locales/en.yml +19 -20
- data/lib/locales/es.yml +19 -20
- data/lib/locales/fi.yml +35 -0
- data/lib/locales/fr.yml +35 -0
- data/lib/locales/he.yml +35 -0
- data/lib/locales/hi.yml +35 -0
- data/lib/locales/it.yml +35 -0
- data/lib/locales/ja.yml +35 -0
- data/lib/locales/ko.yml +35 -0
- data/lib/locales/nl.yml +35 -0
- data/lib/locales/no.yml +35 -0
- data/lib/locales/pl.yml +35 -0
- data/lib/locales/pt.yml +35 -0
- data/lib/locales/ru.yml +35 -0
- data/lib/locales/sv.yml +35 -0
- data/lib/locales/th.yml +35 -0
- data/lib/locales/tr.yml +35 -0
- data/lib/locales/vi.yml +35 -0
- data/lib/locales/zh.yml +35 -0
- metadata +57 -8
- data/lib/cmdx/parameter_validator.rb +0 -81
- data/lib/cmdx/parameter_value.rb +0 -244
- data/lib/cmdx/parameters_inspector.rb +0 -72
- data/lib/cmdx/parameters_serializer.rb +0 -115
- data/lib/cmdx/rspec/result_matchers.rb +0 -917
- data/lib/cmdx/rspec/task_matchers.rb +0 -570
- data/lib/cmdx/validators/custom.rb +0 -102
data/lib/cmdx/parameter.rb
CHANGED
@@ -1,95 +1,60 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module CMDx
|
4
|
-
# Parameter definition
|
4
|
+
# Parameter definition system for CMDx tasks.
|
5
5
|
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
# and
|
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
|
-
|
48
|
-
|
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 :
|
37
|
+
attr_reader :errors
|
61
38
|
|
62
|
-
#
|
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]
|
70
|
-
# @option options [Class] :klass The task class (required)
|
71
|
-
# @option options [Parameter] :parent
|
72
|
-
# @option options [Symbol, Array<Symbol>] :type (:virtual)
|
73
|
-
# @option options [Boolean] :required (false) Whether parameter is required
|
74
|
-
# @
|
75
|
-
# @
|
76
|
-
# @
|
77
|
-
#
|
78
|
-
# @
|
79
|
-
#
|
80
|
-
#
|
81
|
-
#
|
82
|
-
#
|
83
|
-
#
|
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
|
-
#
|
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
|
117
|
-
# @param options [Hash]
|
118
|
-
# @
|
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
|
-
# @
|
121
|
-
#
|
84
|
+
# @example Create multiple optional parameters
|
85
|
+
# Parameter.optional(:name, :email, type: :string, klass: MyTask)
|
122
86
|
#
|
123
|
-
# @example
|
124
|
-
# Parameter.optional(:
|
125
|
-
#
|
126
|
-
#
|
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
|
-
#
|
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
|
-
# @
|
154
|
-
#
|
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
|
157
|
-
# Parameter.required(:
|
109
|
+
# @example Create multiple required parameters
|
110
|
+
# Parameter.required(:name, :email, type: :string, klass: MyTask)
|
158
111
|
#
|
159
|
-
# @example
|
160
|
-
# Parameter.required(:age, type: :integer, numeric: {
|
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
|
-
#
|
120
|
+
# Creates one or more optional child parameters under this parameter.
|
168
121
|
#
|
169
|
-
#
|
170
|
-
#
|
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
|
-
# @
|
173
|
-
#
|
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
|
-
# @
|
177
|
-
#
|
178
|
-
#
|
179
|
-
#
|
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
|
-
#
|
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>]
|
194
|
-
# @param options [Hash]
|
195
|
-
# @
|
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
|
-
# @
|
146
|
+
# @example Add required child parameters
|
147
|
+
# user_param.required(:first_name, :last_name, type: :string)
|
198
148
|
#
|
199
|
-
# @example
|
200
|
-
#
|
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]
|
158
|
+
# @return [Boolean] True if the parameter is required, false otherwise
|
212
159
|
#
|
213
|
-
# @example
|
214
|
-
#
|
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]
|
168
|
+
# @return [Boolean] True if the parameter is optional, false otherwise
|
223
169
|
#
|
224
|
-
# @example
|
225
|
-
#
|
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
|
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
|
-
# @
|
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
|
242
|
-
#
|
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
|
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
|
188
|
+
# @return [Symbol] The method source (:context by default, or parent's method_name)
|
256
189
|
#
|
257
|
-
# @example
|
258
|
-
#
|
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]
|
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
|
206
|
+
# Converts the parameter to a string representation.
|
288
207
|
#
|
289
|
-
# @return [String]
|
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
|
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
|
-
#
|
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
|
-
@
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
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
|
-
@
|
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
|