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.
Files changed (192) hide show
  1. checksums.yaml +4 -4
  2. data/.DS_Store +0 -0
  3. data/.cursor/prompts/docs.md +4 -1
  4. data/.cursor/prompts/llms.md +20 -0
  5. data/.cursor/prompts/rspec.md +4 -1
  6. data/.cursor/prompts/yardoc.md +3 -2
  7. data/.cursor/rules/cursor-instructions.mdc +56 -1
  8. data/.irbrc +6 -0
  9. data/.rubocop.yml +29 -18
  10. data/CHANGELOG.md +5 -133
  11. data/LLM.md +3317 -0
  12. data/README.md +68 -44
  13. data/docs/attributes/coercions.md +162 -0
  14. data/docs/attributes/defaults.md +90 -0
  15. data/docs/attributes/definitions.md +281 -0
  16. data/docs/attributes/naming.md +78 -0
  17. data/docs/attributes/validations.md +309 -0
  18. data/docs/basics/chain.md +56 -249
  19. data/docs/basics/context.md +56 -289
  20. data/docs/basics/execution.md +114 -0
  21. data/docs/basics/setup.md +37 -334
  22. data/docs/callbacks.md +89 -467
  23. data/docs/deprecation.md +91 -174
  24. data/docs/getting_started.md +212 -202
  25. data/docs/internationalization.md +11 -647
  26. data/docs/interruptions/exceptions.md +23 -198
  27. data/docs/interruptions/faults.md +71 -151
  28. data/docs/interruptions/halt.md +109 -186
  29. data/docs/logging.md +44 -256
  30. data/docs/middlewares.md +113 -426
  31. data/docs/outcomes/result.md +81 -228
  32. data/docs/outcomes/states.md +33 -221
  33. data/docs/outcomes/statuses.md +21 -311
  34. data/docs/tips_and_tricks.md +120 -70
  35. data/docs/workflows.md +99 -283
  36. data/lib/cmdx/.DS_Store +0 -0
  37. data/lib/cmdx/attribute.rb +229 -0
  38. data/lib/cmdx/attribute_registry.rb +94 -0
  39. data/lib/cmdx/attribute_value.rb +193 -0
  40. data/lib/cmdx/callback_registry.rb +69 -77
  41. data/lib/cmdx/chain.rb +56 -73
  42. data/lib/cmdx/coercion_registry.rb +52 -68
  43. data/lib/cmdx/coercions/array.rb +19 -18
  44. data/lib/cmdx/coercions/big_decimal.rb +20 -24
  45. data/lib/cmdx/coercions/boolean.rb +26 -25
  46. data/lib/cmdx/coercions/complex.rb +21 -22
  47. data/lib/cmdx/coercions/date.rb +25 -23
  48. data/lib/cmdx/coercions/date_time.rb +24 -25
  49. data/lib/cmdx/coercions/float.rb +25 -22
  50. data/lib/cmdx/coercions/hash.rb +31 -32
  51. data/lib/cmdx/coercions/integer.rb +30 -24
  52. data/lib/cmdx/coercions/rational.rb +29 -24
  53. data/lib/cmdx/coercions/string.rb +19 -22
  54. data/lib/cmdx/coercions/symbol.rb +37 -0
  55. data/lib/cmdx/coercions/time.rb +26 -25
  56. data/lib/cmdx/configuration.rb +49 -108
  57. data/lib/cmdx/context.rb +222 -44
  58. data/lib/cmdx/deprecator.rb +61 -0
  59. data/lib/cmdx/errors.rb +42 -252
  60. data/lib/cmdx/exceptions.rb +39 -0
  61. data/lib/cmdx/faults.rb +78 -39
  62. data/lib/cmdx/freezer.rb +51 -0
  63. data/lib/cmdx/identifier.rb +30 -0
  64. data/lib/cmdx/locale.rb +52 -0
  65. data/lib/cmdx/log_formatters/json.rb +21 -22
  66. data/lib/cmdx/log_formatters/key_value.rb +20 -22
  67. data/lib/cmdx/log_formatters/line.rb +15 -22
  68. data/lib/cmdx/log_formatters/logstash.rb +22 -23
  69. data/lib/cmdx/log_formatters/raw.rb +16 -22
  70. data/lib/cmdx/middleware_registry.rb +70 -74
  71. data/lib/cmdx/middlewares/correlate.rb +90 -54
  72. data/lib/cmdx/middlewares/runtime.rb +58 -0
  73. data/lib/cmdx/middlewares/timeout.rb +48 -68
  74. data/lib/cmdx/railtie.rb +12 -45
  75. data/lib/cmdx/result.rb +229 -314
  76. data/lib/cmdx/task.rb +194 -366
  77. data/lib/cmdx/utils/call.rb +49 -0
  78. data/lib/cmdx/utils/condition.rb +71 -0
  79. data/lib/cmdx/utils/format.rb +61 -0
  80. data/lib/cmdx/validator_registry.rb +63 -72
  81. data/lib/cmdx/validators/exclusion.rb +38 -67
  82. data/lib/cmdx/validators/format.rb +48 -49
  83. data/lib/cmdx/validators/inclusion.rb +43 -74
  84. data/lib/cmdx/validators/length.rb +91 -154
  85. data/lib/cmdx/validators/numeric.rb +87 -162
  86. data/lib/cmdx/validators/presence.rb +37 -50
  87. data/lib/cmdx/version.rb +1 -1
  88. data/lib/cmdx/worker.rb +178 -0
  89. data/lib/cmdx/workflow.rb +85 -81
  90. data/lib/cmdx.rb +19 -13
  91. data/lib/generators/cmdx/install_generator.rb +14 -13
  92. data/lib/generators/cmdx/task_generator.rb +25 -50
  93. data/lib/generators/cmdx/templates/install.rb +11 -46
  94. data/lib/generators/cmdx/templates/task.rb.tt +3 -2
  95. data/lib/locales/en.yml +18 -4
  96. data/src/cmdx-logo.png +0 -0
  97. metadata +32 -116
  98. data/docs/ai_prompts.md +0 -393
  99. data/docs/basics/call.md +0 -317
  100. data/docs/configuration.md +0 -344
  101. data/docs/parameters/coercions.md +0 -396
  102. data/docs/parameters/defaults.md +0 -335
  103. data/docs/parameters/definitions.md +0 -446
  104. data/docs/parameters/namespacing.md +0 -378
  105. data/docs/parameters/validations.md +0 -405
  106. data/docs/testing.md +0 -553
  107. data/lib/cmdx/callback.rb +0 -53
  108. data/lib/cmdx/chain_inspector.rb +0 -56
  109. data/lib/cmdx/chain_serializer.rb +0 -63
  110. data/lib/cmdx/coercion.rb +0 -57
  111. data/lib/cmdx/coercions/virtual.rb +0 -29
  112. data/lib/cmdx/core_ext/hash.rb +0 -83
  113. data/lib/cmdx/core_ext/module.rb +0 -98
  114. data/lib/cmdx/core_ext/object.rb +0 -125
  115. data/lib/cmdx/correlator.rb +0 -122
  116. data/lib/cmdx/error.rb +0 -67
  117. data/lib/cmdx/fault.rb +0 -140
  118. data/lib/cmdx/immutator.rb +0 -52
  119. data/lib/cmdx/lazy_struct.rb +0 -246
  120. data/lib/cmdx/log_formatters/pretty_json.rb +0 -40
  121. data/lib/cmdx/log_formatters/pretty_key_value.rb +0 -38
  122. data/lib/cmdx/log_formatters/pretty_line.rb +0 -41
  123. data/lib/cmdx/logger.rb +0 -49
  124. data/lib/cmdx/logger_ansi.rb +0 -68
  125. data/lib/cmdx/logger_serializer.rb +0 -116
  126. data/lib/cmdx/middleware.rb +0 -70
  127. data/lib/cmdx/parameter.rb +0 -312
  128. data/lib/cmdx/parameter_evaluator.rb +0 -231
  129. data/lib/cmdx/parameter_inspector.rb +0 -66
  130. data/lib/cmdx/parameter_registry.rb +0 -106
  131. data/lib/cmdx/parameter_serializer.rb +0 -59
  132. data/lib/cmdx/result_ansi.rb +0 -71
  133. data/lib/cmdx/result_inspector.rb +0 -71
  134. data/lib/cmdx/result_logger.rb +0 -59
  135. data/lib/cmdx/result_serializer.rb +0 -104
  136. data/lib/cmdx/rspec/matchers.rb +0 -28
  137. data/lib/cmdx/rspec/result_matchers/be_executed.rb +0 -42
  138. data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +0 -94
  139. data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +0 -94
  140. data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +0 -59
  141. data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +0 -57
  142. data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +0 -87
  143. data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +0 -51
  144. data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +0 -58
  145. data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +0 -59
  146. data/lib/cmdx/rspec/result_matchers/have_context.rb +0 -86
  147. data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +0 -54
  148. data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +0 -52
  149. data/lib/cmdx/rspec/result_matchers/have_metadata.rb +0 -114
  150. data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +0 -66
  151. data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +0 -64
  152. data/lib/cmdx/rspec/result_matchers/have_runtime.rb +0 -78
  153. data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +0 -76
  154. data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +0 -62
  155. data/lib/cmdx/rspec/task_matchers/have_callback.rb +0 -85
  156. data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +0 -68
  157. data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +0 -92
  158. data/lib/cmdx/rspec/task_matchers/have_middleware.rb +0 -46
  159. data/lib/cmdx/rspec/task_matchers/have_parameter.rb +0 -181
  160. data/lib/cmdx/task_deprecator.rb +0 -58
  161. data/lib/cmdx/task_processor.rb +0 -246
  162. data/lib/cmdx/task_serializer.rb +0 -57
  163. data/lib/cmdx/utils/ansi_color.rb +0 -73
  164. data/lib/cmdx/utils/log_timestamp.rb +0 -36
  165. data/lib/cmdx/utils/monotonic_runtime.rb +0 -34
  166. data/lib/cmdx/utils/name_affix.rb +0 -52
  167. data/lib/cmdx/validator.rb +0 -57
  168. data/lib/generators/cmdx/templates/workflow.rb.tt +0 -7
  169. data/lib/generators/cmdx/workflow_generator.rb +0 -84
  170. data/lib/locales/ar.yml +0 -35
  171. data/lib/locales/cs.yml +0 -35
  172. data/lib/locales/da.yml +0 -35
  173. data/lib/locales/de.yml +0 -35
  174. data/lib/locales/el.yml +0 -35
  175. data/lib/locales/es.yml +0 -35
  176. data/lib/locales/fi.yml +0 -35
  177. data/lib/locales/fr.yml +0 -35
  178. data/lib/locales/he.yml +0 -35
  179. data/lib/locales/hi.yml +0 -35
  180. data/lib/locales/it.yml +0 -35
  181. data/lib/locales/ja.yml +0 -35
  182. data/lib/locales/ko.yml +0 -35
  183. data/lib/locales/nl.yml +0 -35
  184. data/lib/locales/no.yml +0 -35
  185. data/lib/locales/pl.yml +0 -35
  186. data/lib/locales/pt.yml +0 -35
  187. data/lib/locales/ru.yml +0 -35
  188. data/lib/locales/sv.yml +0 -35
  189. data/lib/locales/th.yml +0 -35
  190. data/lib/locales/tr.yml +0 -35
  191. data/lib/locales/vi.yml +0 -35
  192. 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 callback definitions and execution within tasks.
4
+ # Registry for managing callbacks that can be executed at various points during task execution.
5
5
  #
6
- # This registry handles the registration and execution of callbacks at various
7
- # points in the task lifecycle, including validation, execution, and outcome
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
- :before_validation,
13
- :after_validation,
14
- :before_execution,
15
- :after_execution,
16
- :on_executed,
17
- :on_good,
18
- :on_bad,
19
- *Result::STATUSES.map { |s| :"on_#{s}" },
20
- *Result::STATES.map { |s| :"on_#{s}" }
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
- # Initializes a new callback registry.
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.to_h
28
+ @registry = registry
39
29
  end
40
30
 
41
- # Registers one or more callbacks for a specific type.
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] returns self for method chaining
49
- #
50
- # @example Registering a symbol callback
51
- # registry.register(:before_execution, :setup_database)
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
- # @example Registering a Proc callback
54
- # registry.register(:on_good, ->(task) { puts "Task completed: #{task.name}" })
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
- # @example Registering a Callback class
57
- # registry.register(:after_validation, NotificationCallback)
47
+ # @return [CallbackRegistry] self for method chaining
58
48
  #
59
- # @example Registering multiple callbacks with options
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 Registering a block callback
63
- # registry.register(:after_validation) { |task| puts "Validation complete" }
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
- (registry[type] ||= []).push([callables, options]).uniq!
59
+
60
+ registry[type] ||= Set.new
61
+ registry[type] << [callables, options]
67
62
  self
68
63
  end
69
64
 
70
- # Executes all registered callbacks for a specific type.
65
+ # Removes one or more callables for a specific callback type
71
66
  #
72
- # @param task [Task] the task instance to execute callbacks on
73
- # @param type [Symbol] the callback type to execute
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 [void]
72
+ # @return [CallbackRegistry] self for method chaining
76
73
  #
77
- # @raise [UnknownCallbackError] when the callback type is not recognized
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
- # @example Executing before_validation callbacks
80
- # registry.call(task, :before_validation)
90
+ # @raise [TypeError] When type is not a valid callback type
81
91
  #
82
- # @example Executing outcome callbacks
83
- # registry.call(task, :on_good)
84
- def call(task, type)
85
- raise UnknownCallbackError, "unknown callback #{type}" unless TYPES.include?(type)
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 task.cmdx_eval(options)
98
+ next unless Utils::Condition.evaluate(task, options, task)
89
99
 
90
- Array(callables).each do |callable|
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