cmdx 0.5.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.DS_Store +0 -0
- data/.cursor/rules/cursor-instructions.mdc +6 -0
- data/.rubocop.yml +19 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +95 -28
- data/README.md +73 -25
- data/docs/ai_prompts.md +319 -0
- data/docs/basics/call.md +234 -14
- data/docs/basics/chain.md +280 -0
- data/docs/basics/context.md +241 -33
- data/docs/basics/setup.md +85 -12
- data/docs/callbacks.md +283 -0
- data/docs/configuration.md +155 -30
- data/docs/getting_started.md +145 -22
- data/docs/internationalization.md +148 -0
- data/docs/interruptions/exceptions.md +198 -11
- data/docs/interruptions/faults.md +196 -44
- data/docs/interruptions/halt.md +188 -35
- data/docs/logging.md +204 -53
- data/docs/middlewares.md +745 -0
- data/docs/outcomes/result.md +305 -10
- data/docs/outcomes/states.md +212 -31
- data/docs/outcomes/statuses.md +284 -30
- data/docs/parameters/coercions.md +411 -29
- data/docs/parameters/defaults.md +258 -25
- data/docs/parameters/definitions.md +247 -72
- data/docs/parameters/namespacing.md +259 -27
- data/docs/parameters/validations.md +173 -168
- data/docs/testing.md +560 -0
- data/docs/tips_and_tricks.md +103 -42
- data/docs/workflows.md +329 -0
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/callback.rb +69 -0
- data/lib/cmdx/callback_registry.rb +106 -0
- data/lib/cmdx/chain.rb +190 -0
- data/lib/cmdx/chain_inspector.rb +149 -0
- data/lib/cmdx/chain_serializer.rb +175 -0
- data/lib/cmdx/coercions/array.rb +37 -0
- data/lib/cmdx/coercions/big_decimal.rb +33 -0
- data/lib/cmdx/coercions/boolean.rb +41 -1
- data/lib/cmdx/coercions/complex.rb +31 -0
- data/lib/cmdx/coercions/date.rb +39 -0
- data/lib/cmdx/coercions/date_time.rb +39 -0
- data/lib/cmdx/coercions/float.rb +31 -0
- data/lib/cmdx/coercions/hash.rb +42 -0
- data/lib/cmdx/coercions/integer.rb +32 -0
- data/lib/cmdx/coercions/rational.rb +31 -0
- data/lib/cmdx/coercions/string.rb +31 -0
- data/lib/cmdx/coercions/time.rb +39 -0
- data/lib/cmdx/coercions/virtual.rb +31 -0
- data/lib/cmdx/configuration.rb +217 -9
- data/lib/cmdx/context.rb +173 -2
- data/lib/cmdx/core_ext/hash.rb +72 -0
- data/lib/cmdx/core_ext/module.rb +94 -0
- data/lib/cmdx/core_ext/object.rb +105 -0
- data/lib/cmdx/correlator.rb +217 -0
- data/lib/cmdx/error.rb +210 -8
- data/lib/cmdx/errors.rb +256 -1
- data/lib/cmdx/fault.rb +177 -2
- data/lib/cmdx/faults.rb +158 -2
- data/lib/cmdx/immutator.rb +121 -2
- data/lib/cmdx/lazy_struct.rb +261 -18
- data/lib/cmdx/log_formatters/json.rb +46 -0
- data/lib/cmdx/log_formatters/key_value.rb +46 -0
- data/lib/cmdx/log_formatters/line.rb +54 -0
- data/lib/cmdx/log_formatters/logstash.rb +64 -0
- data/lib/cmdx/log_formatters/pretty_json.rb +57 -0
- data/lib/cmdx/log_formatters/pretty_key_value.rb +51 -0
- data/lib/cmdx/log_formatters/pretty_line.rb +60 -0
- data/lib/cmdx/log_formatters/raw.rb +54 -0
- data/lib/cmdx/logger.rb +85 -0
- data/lib/cmdx/logger_ansi.rb +93 -7
- data/lib/cmdx/logger_serializer.rb +116 -0
- data/lib/cmdx/middleware.rb +74 -0
- data/lib/cmdx/middleware_registry.rb +106 -0
- data/lib/cmdx/middlewares/correlate.rb +266 -0
- data/lib/cmdx/middlewares/timeout.rb +232 -0
- data/lib/cmdx/parameter.rb +228 -1
- data/lib/cmdx/parameter_inspector.rb +61 -0
- data/lib/cmdx/parameter_registry.rb +125 -0
- data/lib/cmdx/parameter_serializer.rb +83 -0
- data/lib/cmdx/parameter_validator.rb +62 -0
- data/lib/cmdx/parameter_value.rb +109 -1
- data/lib/cmdx/parameters_inspector.rb +59 -0
- data/lib/cmdx/parameters_serializer.rb +102 -0
- data/lib/cmdx/railtie.rb +123 -3
- data/lib/cmdx/result.rb +367 -25
- data/lib/cmdx/result_ansi.rb +105 -9
- data/lib/cmdx/result_inspector.rb +76 -0
- data/lib/cmdx/result_logger.rb +90 -3
- data/lib/cmdx/result_serializer.rb +137 -0
- data/lib/cmdx/rspec/result_matchers.rb +917 -0
- data/lib/cmdx/rspec/task_matchers.rb +570 -0
- data/lib/cmdx/task.rb +405 -37
- data/lib/cmdx/task_serializer.rb +74 -2
- data/lib/cmdx/utils/ansi_color.rb +95 -0
- data/lib/cmdx/utils/log_timestamp.rb +48 -0
- data/lib/cmdx/utils/monotonic_runtime.rb +71 -4
- data/lib/cmdx/utils/name_affix.rb +78 -0
- data/lib/cmdx/validators/custom.rb +82 -0
- data/lib/cmdx/validators/exclusion.rb +94 -0
- data/lib/cmdx/validators/format.rb +102 -8
- data/lib/cmdx/validators/inclusion.rb +104 -0
- data/lib/cmdx/validators/length.rb +128 -0
- data/lib/cmdx/validators/numeric.rb +128 -0
- data/lib/cmdx/validators/presence.rb +93 -7
- data/lib/cmdx/version.rb +7 -1
- data/lib/cmdx/workflow.rb +394 -0
- data/lib/cmdx.rb +25 -64
- data/lib/generators/cmdx/install_generator.rb +37 -1
- data/lib/generators/cmdx/task_generator.rb +69 -1
- data/lib/generators/cmdx/templates/install.rb +43 -15
- data/lib/generators/cmdx/workflow_generator.rb +109 -0
- data/lib/locales/ar.yml +36 -0
- data/lib/locales/cs.yml +36 -0
- data/lib/locales/da.yml +36 -0
- data/lib/locales/de.yml +36 -0
- data/lib/locales/el.yml +36 -0
- data/lib/locales/en.yml +20 -20
- data/lib/locales/es.yml +20 -20
- data/lib/locales/fi.yml +36 -0
- data/lib/locales/fr.yml +36 -0
- data/lib/locales/he.yml +36 -0
- data/lib/locales/hi.yml +36 -0
- data/lib/locales/it.yml +36 -0
- data/lib/locales/ja.yml +36 -0
- data/lib/locales/ko.yml +36 -0
- data/lib/locales/nl.yml +36 -0
- data/lib/locales/no.yml +36 -0
- data/lib/locales/pl.yml +36 -0
- data/lib/locales/pt.yml +36 -0
- data/lib/locales/ru.yml +36 -0
- data/lib/locales/sv.yml +36 -0
- data/lib/locales/th.yml +36 -0
- data/lib/locales/tr.yml +36 -0
- data/lib/locales/vi.yml +36 -0
- data/lib/locales/zh.yml +36 -0
- metadata +77 -15
- data/docs/basics/run.md +0 -34
- data/docs/batch.md +0 -53
- data/docs/example.md +0 -82
- data/docs/hooks.md +0 -62
- data/lib/cmdx/batch.rb +0 -43
- data/lib/cmdx/parameters.rb +0 -35
- data/lib/cmdx/run.rb +0 -39
- data/lib/cmdx/run_inspector.rb +0 -26
- data/lib/cmdx/run_serializer.rb +0 -20
- data/lib/cmdx/task_hook.rb +0 -18
- data/lib/generators/cmdx/batch_generator.rb +0 -30
- /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
|
data/lib/cmdx/parameter.rb
CHANGED
@@ -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?,
|
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]
|