cmdx 1.1.2 → 1.5.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/.DS_Store +0 -0
- data/.cursor/prompts/docs.md +4 -1
- data/.cursor/prompts/llms.md +20 -0
- data/.cursor/prompts/rspec.md +4 -1
- data/.cursor/prompts/yardoc.md +3 -2
- data/.cursor/rules/cursor-instructions.mdc +56 -1
- data/.irbrc +6 -0
- data/.rubocop.yml +29 -18
- data/CHANGELOG.md +5 -133
- data/LLM.md +3317 -0
- data/README.md +68 -44
- data/docs/attributes/coercions.md +162 -0
- data/docs/attributes/defaults.md +90 -0
- data/docs/attributes/definitions.md +281 -0
- data/docs/attributes/naming.md +78 -0
- data/docs/attributes/validations.md +309 -0
- data/docs/basics/chain.md +56 -249
- data/docs/basics/context.md +56 -289
- data/docs/basics/execution.md +114 -0
- data/docs/basics/setup.md +37 -334
- data/docs/callbacks.md +89 -467
- data/docs/deprecation.md +91 -174
- data/docs/getting_started.md +212 -202
- data/docs/internationalization.md +11 -647
- data/docs/interruptions/exceptions.md +23 -198
- data/docs/interruptions/faults.md +71 -151
- data/docs/interruptions/halt.md +109 -186
- data/docs/logging.md +44 -256
- data/docs/middlewares.md +113 -426
- data/docs/outcomes/result.md +81 -228
- data/docs/outcomes/states.md +33 -221
- data/docs/outcomes/statuses.md +21 -311
- data/docs/tips_and_tricks.md +120 -70
- data/docs/workflows.md +99 -283
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/attribute.rb +229 -0
- data/lib/cmdx/attribute_registry.rb +94 -0
- data/lib/cmdx/attribute_value.rb +193 -0
- data/lib/cmdx/callback_registry.rb +69 -77
- data/lib/cmdx/chain.rb +56 -73
- data/lib/cmdx/coercion_registry.rb +52 -68
- data/lib/cmdx/coercions/array.rb +19 -18
- data/lib/cmdx/coercions/big_decimal.rb +20 -24
- data/lib/cmdx/coercions/boolean.rb +26 -25
- data/lib/cmdx/coercions/complex.rb +21 -22
- data/lib/cmdx/coercions/date.rb +25 -23
- data/lib/cmdx/coercions/date_time.rb +24 -25
- data/lib/cmdx/coercions/float.rb +25 -22
- data/lib/cmdx/coercions/hash.rb +31 -32
- data/lib/cmdx/coercions/integer.rb +30 -24
- data/lib/cmdx/coercions/rational.rb +29 -24
- data/lib/cmdx/coercions/string.rb +19 -22
- data/lib/cmdx/coercions/symbol.rb +37 -0
- data/lib/cmdx/coercions/time.rb +26 -25
- data/lib/cmdx/configuration.rb +49 -108
- data/lib/cmdx/context.rb +222 -44
- data/lib/cmdx/deprecator.rb +61 -0
- data/lib/cmdx/errors.rb +42 -252
- data/lib/cmdx/exceptions.rb +39 -0
- data/lib/cmdx/faults.rb +78 -39
- data/lib/cmdx/freezer.rb +51 -0
- data/lib/cmdx/identifier.rb +30 -0
- data/lib/cmdx/locale.rb +52 -0
- data/lib/cmdx/log_formatters/json.rb +21 -22
- data/lib/cmdx/log_formatters/key_value.rb +20 -22
- data/lib/cmdx/log_formatters/line.rb +15 -22
- data/lib/cmdx/log_formatters/logstash.rb +22 -23
- data/lib/cmdx/log_formatters/raw.rb +16 -22
- data/lib/cmdx/middleware_registry.rb +70 -74
- data/lib/cmdx/middlewares/correlate.rb +90 -54
- data/lib/cmdx/middlewares/runtime.rb +58 -0
- data/lib/cmdx/middlewares/timeout.rb +48 -68
- data/lib/cmdx/railtie.rb +12 -45
- data/lib/cmdx/result.rb +229 -314
- data/lib/cmdx/task.rb +194 -366
- data/lib/cmdx/utils/call.rb +49 -0
- data/lib/cmdx/utils/condition.rb +71 -0
- data/lib/cmdx/utils/format.rb +61 -0
- data/lib/cmdx/validator_registry.rb +63 -72
- data/lib/cmdx/validators/exclusion.rb +38 -67
- data/lib/cmdx/validators/format.rb +48 -49
- data/lib/cmdx/validators/inclusion.rb +43 -74
- data/lib/cmdx/validators/length.rb +91 -154
- data/lib/cmdx/validators/numeric.rb +87 -162
- data/lib/cmdx/validators/presence.rb +37 -50
- data/lib/cmdx/version.rb +1 -1
- data/lib/cmdx/worker.rb +178 -0
- data/lib/cmdx/workflow.rb +85 -81
- data/lib/cmdx.rb +19 -13
- data/lib/generators/cmdx/install_generator.rb +14 -13
- data/lib/generators/cmdx/task_generator.rb +25 -50
- data/lib/generators/cmdx/templates/install.rb +11 -46
- data/lib/generators/cmdx/templates/task.rb.tt +3 -2
- data/lib/locales/en.yml +18 -4
- data/src/cmdx-logo.png +0 -0
- metadata +32 -116
- data/docs/ai_prompts.md +0 -393
- data/docs/basics/call.md +0 -317
- data/docs/configuration.md +0 -344
- data/docs/parameters/coercions.md +0 -396
- data/docs/parameters/defaults.md +0 -335
- data/docs/parameters/definitions.md +0 -446
- data/docs/parameters/namespacing.md +0 -378
- data/docs/parameters/validations.md +0 -405
- data/docs/testing.md +0 -553
- data/lib/cmdx/callback.rb +0 -53
- data/lib/cmdx/chain_inspector.rb +0 -56
- data/lib/cmdx/chain_serializer.rb +0 -63
- data/lib/cmdx/coercion.rb +0 -57
- data/lib/cmdx/coercions/virtual.rb +0 -29
- data/lib/cmdx/core_ext/hash.rb +0 -83
- data/lib/cmdx/core_ext/module.rb +0 -98
- data/lib/cmdx/core_ext/object.rb +0 -125
- data/lib/cmdx/correlator.rb +0 -122
- data/lib/cmdx/error.rb +0 -67
- data/lib/cmdx/fault.rb +0 -140
- data/lib/cmdx/immutator.rb +0 -52
- data/lib/cmdx/lazy_struct.rb +0 -246
- data/lib/cmdx/log_formatters/pretty_json.rb +0 -40
- data/lib/cmdx/log_formatters/pretty_key_value.rb +0 -38
- data/lib/cmdx/log_formatters/pretty_line.rb +0 -41
- data/lib/cmdx/logger.rb +0 -49
- data/lib/cmdx/logger_ansi.rb +0 -68
- data/lib/cmdx/logger_serializer.rb +0 -116
- data/lib/cmdx/middleware.rb +0 -70
- data/lib/cmdx/parameter.rb +0 -312
- data/lib/cmdx/parameter_evaluator.rb +0 -231
- data/lib/cmdx/parameter_inspector.rb +0 -66
- data/lib/cmdx/parameter_registry.rb +0 -106
- data/lib/cmdx/parameter_serializer.rb +0 -59
- data/lib/cmdx/result_ansi.rb +0 -71
- data/lib/cmdx/result_inspector.rb +0 -71
- data/lib/cmdx/result_logger.rb +0 -59
- data/lib/cmdx/result_serializer.rb +0 -104
- data/lib/cmdx/rspec/matchers.rb +0 -28
- data/lib/cmdx/rspec/result_matchers/be_executed.rb +0 -42
- data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +0 -94
- data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +0 -94
- data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +0 -59
- data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +0 -57
- data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +0 -87
- data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +0 -51
- data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +0 -58
- data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +0 -59
- data/lib/cmdx/rspec/result_matchers/have_context.rb +0 -86
- data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +0 -54
- data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +0 -52
- data/lib/cmdx/rspec/result_matchers/have_metadata.rb +0 -114
- data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +0 -66
- data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +0 -64
- data/lib/cmdx/rspec/result_matchers/have_runtime.rb +0 -78
- data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +0 -76
- data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +0 -62
- data/lib/cmdx/rspec/task_matchers/have_callback.rb +0 -85
- data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +0 -68
- data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +0 -92
- data/lib/cmdx/rspec/task_matchers/have_middleware.rb +0 -46
- data/lib/cmdx/rspec/task_matchers/have_parameter.rb +0 -181
- data/lib/cmdx/task_deprecator.rb +0 -58
- data/lib/cmdx/task_processor.rb +0 -246
- data/lib/cmdx/task_serializer.rb +0 -57
- data/lib/cmdx/utils/ansi_color.rb +0 -73
- data/lib/cmdx/utils/log_timestamp.rb +0 -36
- data/lib/cmdx/utils/monotonic_runtime.rb +0 -34
- data/lib/cmdx/utils/name_affix.rb +0 -52
- data/lib/cmdx/validator.rb +0 -57
- data/lib/generators/cmdx/templates/workflow.rb.tt +0 -7
- data/lib/generators/cmdx/workflow_generator.rb +0 -84
- data/lib/locales/ar.yml +0 -35
- data/lib/locales/cs.yml +0 -35
- data/lib/locales/da.yml +0 -35
- data/lib/locales/de.yml +0 -35
- data/lib/locales/el.yml +0 -35
- data/lib/locales/es.yml +0 -35
- data/lib/locales/fi.yml +0 -35
- data/lib/locales/fr.yml +0 -35
- data/lib/locales/he.yml +0 -35
- data/lib/locales/hi.yml +0 -35
- data/lib/locales/it.yml +0 -35
- data/lib/locales/ja.yml +0 -35
- data/lib/locales/ko.yml +0 -35
- data/lib/locales/nl.yml +0 -35
- data/lib/locales/no.yml +0 -35
- data/lib/locales/pl.yml +0 -35
- data/lib/locales/pt.yml +0 -35
- data/lib/locales/ru.yml +0 -35
- data/lib/locales/sv.yml +0 -35
- data/lib/locales/th.yml +0 -35
- data/lib/locales/tr.yml +0 -35
- data/lib/locales/vi.yml +0 -35
- data/lib/locales/zh.yml +0 -35
@@ -0,0 +1,229 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMDx
|
4
|
+
# Represents a configurable attribute within a CMDx task.
|
5
|
+
# Attributes define the data structure and validation rules for task parameters.
|
6
|
+
# They can be nested to create complex hierarchical data structures.
|
7
|
+
class Attribute
|
8
|
+
|
9
|
+
AFFIX = proc do |value, &block|
|
10
|
+
value == true ? block.call : value
|
11
|
+
end.freeze
|
12
|
+
private_constant :AFFIX
|
13
|
+
|
14
|
+
attr_accessor :task
|
15
|
+
|
16
|
+
attr_reader :name, :options, :children, :parent, :types
|
17
|
+
|
18
|
+
# Creates a new attribute with the specified name and configuration.
|
19
|
+
#
|
20
|
+
# @param name [Symbol, String] The name of the attribute
|
21
|
+
# @param options [Hash] Configuration options for the attribute
|
22
|
+
# @option options [Attribute] :parent The parent attribute for nested structures
|
23
|
+
# @option options [Boolean] :required Whether the attribute is required (default: false)
|
24
|
+
# @option options [Array<Class>, Class] :types The expected type(s) for the attribute value
|
25
|
+
# @option options [Symbol, String, Proc] :source The source of the attribute value
|
26
|
+
# @option options [Symbol, String] :as The method name to use for this attribute
|
27
|
+
# @option options [Symbol, String, Boolean] :prefix The prefix to add to the method name
|
28
|
+
# @option options [Symbol, String, Boolean] :suffix The suffix to add to the method name
|
29
|
+
# @option options [Object] :default The default value for the attribute
|
30
|
+
#
|
31
|
+
# @yield [self] Block to configure nested attributes
|
32
|
+
#
|
33
|
+
# @example
|
34
|
+
# Attribute.new(:user_id, required: true, types: [Integer, String]) do
|
35
|
+
# required :name, types: String
|
36
|
+
# optional :email, types: String
|
37
|
+
# end
|
38
|
+
def initialize(name, options = {}, &)
|
39
|
+
@parent = options.delete(:parent)
|
40
|
+
@required = options.delete(:required) || false
|
41
|
+
@types = Array(options.delete(:types) || options.delete(:type))
|
42
|
+
|
43
|
+
@name = name.to_sym
|
44
|
+
@options = options
|
45
|
+
@children = []
|
46
|
+
|
47
|
+
instance_eval(&) if block_given?
|
48
|
+
end
|
49
|
+
|
50
|
+
class << self
|
51
|
+
|
52
|
+
# Builds multiple attributes with the same configuration.
|
53
|
+
#
|
54
|
+
# @param names [Array<Symbol, String>] The names of the attributes to create
|
55
|
+
# @param options [Hash] Configuration options for the attributes
|
56
|
+
#
|
57
|
+
# @yield [self] Block to configure nested attributes
|
58
|
+
#
|
59
|
+
# @return [Array<Attribute>] Array of created attributes
|
60
|
+
#
|
61
|
+
# @raise [ArgumentError] When no names are provided or :as is used with multiple attributes
|
62
|
+
#
|
63
|
+
# @example
|
64
|
+
# Attribute.build(:first_name, :last_name, required: true, types: String)
|
65
|
+
def build(*names, **options, &)
|
66
|
+
if names.none?
|
67
|
+
raise ArgumentError, "no attributes given"
|
68
|
+
elsif (names.size > 1) && options.key?(:as)
|
69
|
+
raise ArgumentError, ":as option only supports one attribute per definition"
|
70
|
+
end
|
71
|
+
|
72
|
+
names.filter_map { |name| new(name, **options, &) }
|
73
|
+
end
|
74
|
+
|
75
|
+
# Creates optional attributes (not required).
|
76
|
+
#
|
77
|
+
# @param names [Array<Symbol, String>] The names of the attributes to create
|
78
|
+
# @param options [Hash] Configuration options for the attributes
|
79
|
+
#
|
80
|
+
# @yield [self] Block to configure nested attributes
|
81
|
+
#
|
82
|
+
# @return [Array<Attribute>] Array of created optional attributes
|
83
|
+
#
|
84
|
+
# @example
|
85
|
+
# Attribute.optional(:description, :tags, types: String)
|
86
|
+
def optional(*names, **options, &)
|
87
|
+
build(*names, **options.merge(required: false), &)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Creates required attributes.
|
91
|
+
#
|
92
|
+
# @param names [Array<Symbol, String>] The names of the attributes to create
|
93
|
+
# @param options [Hash] Configuration options for the attributes
|
94
|
+
#
|
95
|
+
# @yield [self] Block to configure nested attributes
|
96
|
+
#
|
97
|
+
# @return [Array<Attribute>] Array of created required attributes
|
98
|
+
#
|
99
|
+
# @example
|
100
|
+
# Attribute.required(:id, :name, types: [Integer, String])
|
101
|
+
def required(*names, **options, &)
|
102
|
+
build(*names, **options.merge(required: true), &)
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
# Checks if the attribute is required.
|
108
|
+
#
|
109
|
+
# @return [Boolean] true if the attribute is required, false otherwise
|
110
|
+
#
|
111
|
+
# @example
|
112
|
+
# attribute.required? # => true
|
113
|
+
def required?
|
114
|
+
!!@required
|
115
|
+
end
|
116
|
+
|
117
|
+
# Determines the source of the attribute value.
|
118
|
+
#
|
119
|
+
# @return [Symbol] The source identifier for the attribute value
|
120
|
+
#
|
121
|
+
# @example
|
122
|
+
# attribute.source # => :context
|
123
|
+
def source
|
124
|
+
@source ||= parent&.method_name || begin
|
125
|
+
value = options[:source]
|
126
|
+
|
127
|
+
if value.is_a?(Proc)
|
128
|
+
task.instance_eval(&value)
|
129
|
+
elsif value.respond_to?(:call)
|
130
|
+
value.call(task)
|
131
|
+
else
|
132
|
+
value || :context
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Generates the method name for accessing this attribute.
|
138
|
+
#
|
139
|
+
# @return [Symbol] The method name for the attribute
|
140
|
+
#
|
141
|
+
# @example
|
142
|
+
# attribute.method_name # => :user_name
|
143
|
+
def method_name
|
144
|
+
@method_name ||= options[:as] || begin
|
145
|
+
prefix = AFFIX.call(options[:prefix]) { "#{source}_" }
|
146
|
+
suffix = AFFIX.call(options[:suffix]) { "_#{source}" }
|
147
|
+
|
148
|
+
:"#{prefix}#{name}#{suffix}"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Defines and verifies the entire attribute tree including nested children.
|
153
|
+
def define_and_verify_tree
|
154
|
+
define_and_verify
|
155
|
+
|
156
|
+
children.each do |child|
|
157
|
+
child.task = task
|
158
|
+
child.define_and_verify_tree
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
private
|
163
|
+
|
164
|
+
# Creates nested attributes as children of this attribute.
|
165
|
+
#
|
166
|
+
# @param names [Array<Symbol, String>] The names of the child attributes
|
167
|
+
# @param options [Hash] Configuration options for the child attributes
|
168
|
+
#
|
169
|
+
# @yield [self] Block to configure the child attributes
|
170
|
+
#
|
171
|
+
# @return [Array<Attribute>] Array of created child attributes
|
172
|
+
#
|
173
|
+
# @example
|
174
|
+
# attributes :street, :city, :zip, types: String
|
175
|
+
def attributes(*names, **options, &)
|
176
|
+
attrs = self.class.build(*names, **options.merge(parent: self), &)
|
177
|
+
children.concat(attrs)
|
178
|
+
end
|
179
|
+
alias attribute attributes
|
180
|
+
|
181
|
+
# Creates optional nested attributes.
|
182
|
+
#
|
183
|
+
# @param names [Array<Symbol, String>] The names of the optional child attributes
|
184
|
+
# @param options [Hash] Configuration options for the child attributes
|
185
|
+
#
|
186
|
+
# @yield [self] Block to configure the child attributes
|
187
|
+
#
|
188
|
+
# @return [Array<Attribute>] Array of created optional child attributes
|
189
|
+
#
|
190
|
+
# @example
|
191
|
+
# optional :middle_name, :nickname, types: String
|
192
|
+
def optional(*names, **options, &)
|
193
|
+
attributes(*names, **options.merge(required: false), &)
|
194
|
+
end
|
195
|
+
|
196
|
+
# Creates required nested attributes.
|
197
|
+
#
|
198
|
+
# @param names [Array<Symbol, String>] The names of the required child attributes
|
199
|
+
# @param options [Hash] Configuration options for the child attributes
|
200
|
+
#
|
201
|
+
# @yield [self] Block to configure the child attributes
|
202
|
+
#
|
203
|
+
# @return [Array<Attribute>] Array of created required child attributes
|
204
|
+
#
|
205
|
+
# @example
|
206
|
+
# required :first_name, :last_name, types: String
|
207
|
+
def required(*names, **options, &)
|
208
|
+
attributes(*names, **options.merge(required: true), &)
|
209
|
+
end
|
210
|
+
|
211
|
+
# Defines the attribute method on the task and validates the configuration.
|
212
|
+
#
|
213
|
+
# @raise [RuntimeError] When the method name is already defined on the task
|
214
|
+
def define_and_verify
|
215
|
+
raise "#{task.class.name}##{method_name} already defined" if task.respond_to?(method_name, true)
|
216
|
+
|
217
|
+
attribute_value = AttributeValue.new(self)
|
218
|
+
attribute_value.generate
|
219
|
+
attribute_value.validate
|
220
|
+
|
221
|
+
task.instance_eval(<<~RUBY, __FILE__, __LINE__ + 1)
|
222
|
+
def #{method_name}
|
223
|
+
attributes[:#{method_name}]
|
224
|
+
end
|
225
|
+
RUBY
|
226
|
+
end
|
227
|
+
|
228
|
+
end
|
229
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMDx
|
4
|
+
# Manages a collection of attributes for task definition and verification.
|
5
|
+
# The registry provides methods to register, deregister, and process attributes
|
6
|
+
# in a hierarchical structure, supporting nested attribute definitions.
|
7
|
+
class AttributeRegistry
|
8
|
+
|
9
|
+
attr_reader :registry
|
10
|
+
alias to_a registry
|
11
|
+
|
12
|
+
# Creates a new attribute registry with an optional initial collection.
|
13
|
+
#
|
14
|
+
# @param registry [Array<Attribute>] Initial attributes to register
|
15
|
+
#
|
16
|
+
# @return [AttributeRegistry] A new registry instance
|
17
|
+
#
|
18
|
+
# @example
|
19
|
+
# registry = AttributeRegistry.new
|
20
|
+
# registry = AttributeRegistry.new([attr1, attr2])
|
21
|
+
def initialize(registry = [])
|
22
|
+
@registry = registry
|
23
|
+
end
|
24
|
+
|
25
|
+
# Creates a duplicate of this registry with copied attributes.
|
26
|
+
#
|
27
|
+
# @return [AttributeRegistry] A new registry with duplicated attributes
|
28
|
+
#
|
29
|
+
# @example
|
30
|
+
# new_registry = registry.dup
|
31
|
+
def dup
|
32
|
+
self.class.new(registry.dup)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Registers one or more attributes to the registry.
|
36
|
+
#
|
37
|
+
# @param attributes [Attribute, Array<Attribute>] Attribute(s) to register
|
38
|
+
#
|
39
|
+
# @return [AttributeRegistry] Self for method chaining
|
40
|
+
#
|
41
|
+
# @example
|
42
|
+
# registry.register(attribute)
|
43
|
+
# registry.register([attr1, attr2])
|
44
|
+
def register(attributes)
|
45
|
+
@registry.concat(Array(attributes))
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
# Removes attributes from the registry by name.
|
50
|
+
# Supports hierarchical attribute removal by matching the entire attribute tree.
|
51
|
+
#
|
52
|
+
# @param names [Symbol, String, Array<Symbol, String>] Name(s) of attributes to remove
|
53
|
+
#
|
54
|
+
# @return [AttributeRegistry] Self for method chaining
|
55
|
+
#
|
56
|
+
# @example
|
57
|
+
# registry.deregister(:name)
|
58
|
+
# registry.deregister(['name1', 'name2'])
|
59
|
+
def deregister(names)
|
60
|
+
Array(names).each do |name|
|
61
|
+
@registry.reject! { |attribute| matches_attribute_tree?(attribute, name.to_sym) }
|
62
|
+
end
|
63
|
+
|
64
|
+
self
|
65
|
+
end
|
66
|
+
|
67
|
+
# Associates all registered attributes with a task and verifies their definitions.
|
68
|
+
# This method is called during task setup to establish attribute-task relationships
|
69
|
+
# and validate the attribute hierarchy.
|
70
|
+
#
|
71
|
+
# @param task [Task] The task to associate with all attributes
|
72
|
+
def define_and_verify(task)
|
73
|
+
registry.each do |attribute|
|
74
|
+
attribute.task = task
|
75
|
+
attribute.define_and_verify_tree
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
# Recursively checks if an attribute or any of its children match the given name.
|
82
|
+
#
|
83
|
+
# @param attribute [Attribute] The attribute to check
|
84
|
+
# @param name [Symbol] The name to match against
|
85
|
+
#
|
86
|
+
# @return [Boolean] True if the attribute or any child matches the name
|
87
|
+
def matches_attribute_tree?(attribute, name)
|
88
|
+
return true if attribute.method_name == name
|
89
|
+
|
90
|
+
attribute.children.any? { |child| matches_attribute_tree?(child, name) }
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMDx
|
4
|
+
# Manages the value lifecycle for a single attribute within a task.
|
5
|
+
# Handles value sourcing, derivation, coercion, and validation through
|
6
|
+
# a coordinated pipeline that ensures data integrity and type safety.
|
7
|
+
class AttributeValue
|
8
|
+
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
attr_reader :attribute
|
12
|
+
|
13
|
+
def_delegators :attribute, :task, :parent, :name, :options, :types, :source, :method_name, :required?
|
14
|
+
def_delegators :task, :attributes, :errors
|
15
|
+
|
16
|
+
# Creates a new attribute value manager for the given attribute.
|
17
|
+
#
|
18
|
+
# @param attribute [Attribute] The attribute to manage values for
|
19
|
+
#
|
20
|
+
# @example
|
21
|
+
# attr = Attribute.new(:user_id, required: true)
|
22
|
+
# attr_value = AttributeValue.new(attr)
|
23
|
+
def initialize(attribute)
|
24
|
+
@attribute = attribute
|
25
|
+
end
|
26
|
+
|
27
|
+
# Retrieves the current value for this attribute from the task's attributes.
|
28
|
+
#
|
29
|
+
# @return [Object, nil] The current attribute value or nil if not set
|
30
|
+
#
|
31
|
+
# @example
|
32
|
+
# attr_value.value # => "john_doe"
|
33
|
+
def value
|
34
|
+
attributes[method_name]
|
35
|
+
end
|
36
|
+
|
37
|
+
# Generates the attribute value through the complete pipeline:
|
38
|
+
# sourcing, derivation, coercion, and storage.
|
39
|
+
#
|
40
|
+
# @return [Object, nil] The generated value or nil if generation failed
|
41
|
+
#
|
42
|
+
# @example
|
43
|
+
# attr_value.generate # => 42
|
44
|
+
def generate
|
45
|
+
return value if attributes.key?(method_name)
|
46
|
+
|
47
|
+
sourced_value = source_value
|
48
|
+
return if errors.for?(method_name)
|
49
|
+
|
50
|
+
derived_value = derive_value(sourced_value)
|
51
|
+
return if errors.for?(method_name)
|
52
|
+
|
53
|
+
coerced_value = coerce_value(derived_value)
|
54
|
+
return if errors.for?(method_name)
|
55
|
+
|
56
|
+
attributes[method_name] = coerced_value
|
57
|
+
end
|
58
|
+
|
59
|
+
# Validates the current attribute value against configured validators.
|
60
|
+
#
|
61
|
+
# @raise [ValidationError] When validation fails (handled internally)
|
62
|
+
#
|
63
|
+
# @example
|
64
|
+
# attr_value.validate
|
65
|
+
# # Validates value against :presence, :format, etc.
|
66
|
+
def validate
|
67
|
+
registry = task.class.settings[:validators]
|
68
|
+
|
69
|
+
options.slice(*registry.keys).each do |type, opts|
|
70
|
+
registry.validate(type, task, value, opts)
|
71
|
+
rescue ValidationError => e
|
72
|
+
errors.add(method_name, e.message)
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# Retrieves the source value for this attribute from various sources.
|
80
|
+
#
|
81
|
+
# @return [Object, nil] The sourced value or nil if unavailable
|
82
|
+
#
|
83
|
+
# @raise [NoMethodError] When the source method doesn't exist
|
84
|
+
#
|
85
|
+
# @example
|
86
|
+
# # Sources from task method, proc, or direct value
|
87
|
+
# source_value # => "raw_value"
|
88
|
+
def source_value
|
89
|
+
sourced_value =
|
90
|
+
case source
|
91
|
+
when Symbol then task.send(source)
|
92
|
+
when Proc then task.instance_eval(&source)
|
93
|
+
else source.respond_to?(:call) ? source.call(task) : source
|
94
|
+
end
|
95
|
+
|
96
|
+
if required? && (parent.nil? || parent&.required?)
|
97
|
+
case sourced_value
|
98
|
+
when Context, Hash then sourced_value.key?(name)
|
99
|
+
when Proc then true # Cannot be determined
|
100
|
+
else sourced_value.respond_to?(name, true)
|
101
|
+
end || errors.add(method_name, Locale.t("cmdx.attributes.required"))
|
102
|
+
end
|
103
|
+
|
104
|
+
sourced_value
|
105
|
+
rescue NoMethodError
|
106
|
+
errors.add(method_name, Locale.t("cmdx.attributes.undefined", method: source))
|
107
|
+
nil
|
108
|
+
end
|
109
|
+
|
110
|
+
# Retrieves the default value for this attribute if configured.
|
111
|
+
#
|
112
|
+
# @return [Object, nil] The default value or nil if not configured
|
113
|
+
#
|
114
|
+
# @example
|
115
|
+
# # Default can be symbol, proc, or direct value
|
116
|
+
# default_value # => "default_value"
|
117
|
+
def default_value
|
118
|
+
default = options[:default]
|
119
|
+
|
120
|
+
if default.is_a?(Symbol) && task.respond_to?(default, true)
|
121
|
+
task.send(default)
|
122
|
+
elsif default.is_a?(Proc)
|
123
|
+
task.instance_eval(&default)
|
124
|
+
elsif default.respond_to?(:call)
|
125
|
+
default.call(task)
|
126
|
+
else
|
127
|
+
default
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Derives the actual value from the source value using various strategies.
|
132
|
+
#
|
133
|
+
# @param source_value [Object] The source value to derive from
|
134
|
+
#
|
135
|
+
# @return [Object, nil] The derived value or nil if derivation failed
|
136
|
+
#
|
137
|
+
# @raise [NoMethodError] When the derivation method doesn't exist
|
138
|
+
#
|
139
|
+
# @example
|
140
|
+
# # Derives from hash key, method call, or proc execution
|
141
|
+
# derive_value({user_id: 42}) # => 42
|
142
|
+
def derive_value(source_value)
|
143
|
+
derived_value =
|
144
|
+
case source_value
|
145
|
+
when Context, Hash then source_value[name]
|
146
|
+
when Symbol then source_value.send(name)
|
147
|
+
when Proc then task.instance_exec(name, &source_value)
|
148
|
+
else source_value.call(task, name) if source_value.respond_to?(:call)
|
149
|
+
end
|
150
|
+
|
151
|
+
derived_value.nil? ? default_value : derived_value
|
152
|
+
rescue NoMethodError
|
153
|
+
errors.add(method_name, Locale.t("cmdx.attributes.undefined", method: name))
|
154
|
+
nil
|
155
|
+
end
|
156
|
+
|
157
|
+
# Coerces the derived value to the expected type(s) using the coercion registry.
|
158
|
+
#
|
159
|
+
# @param derived_value [Object] The value to coerce
|
160
|
+
#
|
161
|
+
# @return [Object, nil] The coerced value or nil if coercion failed
|
162
|
+
#
|
163
|
+
# @raise [CoercionError] When coercion fails (handled internally)
|
164
|
+
#
|
165
|
+
# @example
|
166
|
+
# # Coerces "42" to Integer, "true" to Boolean, etc.
|
167
|
+
# coerce_value("42") # => 42
|
168
|
+
def coerce_value(derived_value)
|
169
|
+
return derived_value if attribute.types.empty?
|
170
|
+
|
171
|
+
registry = task.class.settings[:coercions]
|
172
|
+
last_idx = attribute.types.size - 1
|
173
|
+
|
174
|
+
attribute.types.find.with_index do |type, i|
|
175
|
+
break registry.coerce(type, task, derived_value, options)
|
176
|
+
rescue CoercionError => e
|
177
|
+
next if i != last_idx
|
178
|
+
|
179
|
+
message =
|
180
|
+
if last_idx.zero?
|
181
|
+
e.message
|
182
|
+
else
|
183
|
+
tl = attribute.types.map { |t| Locale.t("cmdx.types.#{t}") }.join(", ")
|
184
|
+
Locale.t("cmdx.coercions.into_any", types: tl)
|
185
|
+
end
|
186
|
+
|
187
|
+
errors.add(method_name, message)
|
188
|
+
nil
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
end
|
193
|
+
end
|
@@ -1,113 +1,105 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module CMDx
|
4
|
-
# Registry for managing
|
4
|
+
# Registry for managing callbacks that can be executed at various points during task execution.
|
5
5
|
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
# handling phases.
|
6
|
+
# Callbacks are organized by type and can be registered with optional conditions and options.
|
7
|
+
# Each callback type represents a specific execution phase or outcome.
|
9
8
|
class CallbackRegistry
|
10
9
|
|
11
|
-
TYPES = [
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
10
|
+
TYPES = %i[
|
11
|
+
before_validation
|
12
|
+
before_execution
|
13
|
+
on_complete
|
14
|
+
on_interrupted
|
15
|
+
on_executed
|
16
|
+
on_success
|
17
|
+
on_skipped
|
18
|
+
on_failed
|
19
|
+
on_good
|
20
|
+
on_bad
|
21
21
|
].freeze
|
22
22
|
|
23
|
-
# @return [Hash] hash containing callback type keys and callback definition arrays
|
24
23
|
attr_reader :registry
|
24
|
+
alias to_h registry
|
25
25
|
|
26
|
-
#
|
27
|
-
#
|
28
|
-
# @param registry [Hash] initial registry hash with callback definitions
|
29
|
-
#
|
30
|
-
# @return [CallbackRegistry] a new callback registry instance
|
31
|
-
#
|
32
|
-
# @example Creating an empty registry
|
33
|
-
# CallbackRegistry.new
|
34
|
-
#
|
35
|
-
# @example Creating a registry with initial callbacks
|
36
|
-
# CallbackRegistry.new(before_execution: [[:my_callback, {}]])
|
26
|
+
# @param registry [Hash] Initial registry hash, defaults to empty
|
37
27
|
def initialize(registry = {})
|
38
|
-
@registry = registry
|
28
|
+
@registry = registry
|
39
29
|
end
|
40
30
|
|
41
|
-
#
|
42
|
-
#
|
43
|
-
# @param type [Symbol] the callback type to register
|
44
|
-
# @param callables [Array<Object>] callable objects to register
|
45
|
-
# @param options [Hash] options for conditional callback execution
|
46
|
-
# @param block [Proc] optional block to register as a callback
|
31
|
+
# Creates a deep copy of the registry with duplicated callable sets
|
47
32
|
#
|
48
|
-
# @return [CallbackRegistry]
|
49
|
-
|
50
|
-
|
51
|
-
|
33
|
+
# @return [CallbackRegistry] A new instance with duplicated registry contents
|
34
|
+
def dup
|
35
|
+
self.class.new(registry.transform_values(&:dup))
|
36
|
+
end
|
37
|
+
|
38
|
+
# Registers one or more callables for a specific callback type
|
52
39
|
#
|
53
|
-
# @
|
54
|
-
#
|
40
|
+
# @param type [Symbol] The callback type from TYPES
|
41
|
+
# @param callables [Array<#call>] Callable objects to register
|
42
|
+
# @param options [Hash] Options to pass to the callback
|
43
|
+
# @option options [Hash] :if Condition hash for conditional execution
|
44
|
+
# @option options [Hash] :unless Inverse condition hash for conditional execution
|
45
|
+
# @param block [Proc] Optional block to register as a callable
|
55
46
|
#
|
56
|
-
# @
|
57
|
-
# registry.register(:after_validation, NotificationCallback)
|
47
|
+
# @return [CallbackRegistry] self for method chaining
|
58
48
|
#
|
59
|
-
# @
|
60
|
-
# registry.register(:on_good, :send_notification, :log_success, if: -> { Rails.env.production? })
|
49
|
+
# @raise [ArgumentError] When type is not a valid callback type
|
61
50
|
#
|
62
|
-
# @example
|
63
|
-
# registry.register(:
|
51
|
+
# @example Register a method callback
|
52
|
+
# registry.register(:before_execution, :validate_inputs)
|
53
|
+
# @example Register a block with conditions
|
54
|
+
# registry.register(:on_success, if: { status: :completed }) do |task|
|
55
|
+
# task.log("Success callback executed")
|
56
|
+
# end
|
64
57
|
def register(type, *callables, **options, &block)
|
65
58
|
callables << block if block_given?
|
66
|
-
|
59
|
+
|
60
|
+
registry[type] ||= Set.new
|
61
|
+
registry[type] << [callables, options]
|
67
62
|
self
|
68
63
|
end
|
69
64
|
|
70
|
-
#
|
65
|
+
# Removes one or more callables for a specific callback type
|
71
66
|
#
|
72
|
-
# @param
|
73
|
-
# @param
|
67
|
+
# @param type [Symbol] The callback type from TYPES
|
68
|
+
# @param callables [Array<#call>] Callable objects to remove
|
69
|
+
# @param options [Hash] Options that were used during registration
|
70
|
+
# @param block [Proc] Optional block to remove
|
74
71
|
#
|
75
|
-
# @return [
|
72
|
+
# @return [CallbackRegistry] self for method chaining
|
76
73
|
#
|
77
|
-
# @
|
74
|
+
# @example Remove a specific callback
|
75
|
+
# registry.deregister(:before_execution, :validate_inputs)
|
76
|
+
def deregister(type, *callables, **options, &block)
|
77
|
+
callables << block if block_given?
|
78
|
+
return self unless registry[type]
|
79
|
+
|
80
|
+
registry[type].delete([callables, options])
|
81
|
+
registry.delete(type) if registry[type].empty?
|
82
|
+
self
|
83
|
+
end
|
84
|
+
|
85
|
+
# Invokes all registered callbacks for a given type
|
86
|
+
#
|
87
|
+
# @param type [Symbol] The callback type to invoke
|
88
|
+
# @param task [Task] The task instance to pass to callbacks
|
78
89
|
#
|
79
|
-
# @
|
80
|
-
# registry.call(task, :before_validation)
|
90
|
+
# @raise [TypeError] When type is not a valid callback type
|
81
91
|
#
|
82
|
-
# @example
|
83
|
-
# registry.
|
84
|
-
def
|
85
|
-
raise
|
92
|
+
# @example Invoke all before_execution callbacks
|
93
|
+
# registry.invoke(:before_execution, task)
|
94
|
+
def invoke(type, task)
|
95
|
+
raise TypeError, "unknown callback type #{type.inspect}" unless TYPES.include?(type)
|
86
96
|
|
87
97
|
Array(registry[type]).each do |callables, options|
|
88
|
-
next unless
|
98
|
+
next unless Utils::Condition.evaluate(task, options, task)
|
89
99
|
|
90
|
-
Array(callables).each
|
91
|
-
case callable
|
92
|
-
when Symbol, String, Proc
|
93
|
-
task.cmdx_try(callable)
|
94
|
-
else
|
95
|
-
callable.call(task)
|
96
|
-
end
|
97
|
-
end
|
100
|
+
Array(callables).each { |callable| Utils::Call.invoke(task, callable, task) }
|
98
101
|
end
|
99
102
|
end
|
100
103
|
|
101
|
-
# Returns a hash representation of the registry.
|
102
|
-
#
|
103
|
-
# @return [Hash] a deep copy of the registry hash
|
104
|
-
#
|
105
|
-
# @example Getting registry contents
|
106
|
-
# registry.to_h
|
107
|
-
# #=> { before_execution: [[:setup, {}]], on_good: [[:notify, { if: -> { true } }]] }
|
108
|
-
def to_h
|
109
|
-
registry.transform_values(&:dup)
|
110
|
-
end
|
111
|
-
|
112
104
|
end
|
113
105
|
end
|