cmdx 1.21.0 → 2.0.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 (195) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +118 -1
  3. data/README.md +37 -24
  4. data/lib/cmdx/.DS_Store +0 -0
  5. data/lib/cmdx/callbacks.rb +179 -0
  6. data/lib/cmdx/chain.rb +78 -175
  7. data/lib/cmdx/coercions/array.rb +19 -33
  8. data/lib/cmdx/coercions/big_decimal.rb +12 -29
  9. data/lib/cmdx/coercions/boolean.rb +25 -45
  10. data/lib/cmdx/coercions/coerce.rb +32 -0
  11. data/lib/cmdx/coercions/complex.rb +12 -27
  12. data/lib/cmdx/coercions/date.rb +29 -33
  13. data/lib/cmdx/coercions/date_time.rb +29 -33
  14. data/lib/cmdx/coercions/float.rb +8 -29
  15. data/lib/cmdx/coercions/hash.rb +17 -43
  16. data/lib/cmdx/coercions/integer.rb +8 -32
  17. data/lib/cmdx/coercions/rational.rb +12 -33
  18. data/lib/cmdx/coercions/string.rb +6 -24
  19. data/lib/cmdx/coercions/symbol.rb +12 -26
  20. data/lib/cmdx/coercions/time.rb +31 -35
  21. data/lib/cmdx/coercions.rb +174 -0
  22. data/lib/cmdx/configuration.rb +45 -237
  23. data/lib/cmdx/context.rb +264 -243
  24. data/lib/cmdx/deprecation.rb +67 -0
  25. data/lib/cmdx/deprecators/error.rb +22 -0
  26. data/lib/cmdx/deprecators/log.rb +22 -0
  27. data/lib/cmdx/deprecators/warn.rb +21 -0
  28. data/lib/cmdx/deprecators.rb +101 -0
  29. data/lib/cmdx/errors.rb +145 -79
  30. data/lib/cmdx/executors/fiber.rb +42 -0
  31. data/lib/cmdx/executors/thread.rb +36 -0
  32. data/lib/cmdx/executors.rb +95 -0
  33. data/lib/cmdx/fault.rb +85 -78
  34. data/lib/cmdx/i18n_proxy.rb +104 -0
  35. data/lib/cmdx/input.rb +294 -0
  36. data/lib/cmdx/inputs.rb +218 -0
  37. data/lib/cmdx/log_formatters/json.rb +9 -20
  38. data/lib/cmdx/log_formatters/key_value.rb +10 -21
  39. data/lib/cmdx/log_formatters/line.rb +7 -19
  40. data/lib/cmdx/log_formatters/logstash.rb +8 -21
  41. data/lib/cmdx/log_formatters/raw.rb +8 -20
  42. data/lib/cmdx/logger_proxy.rb +30 -0
  43. data/lib/cmdx/mergers/deep_merge.rb +23 -0
  44. data/lib/cmdx/mergers/last_write_wins.rb +23 -0
  45. data/lib/cmdx/mergers/no_merge.rb +20 -0
  46. data/lib/cmdx/mergers.rb +95 -0
  47. data/lib/cmdx/middlewares.rb +128 -0
  48. data/lib/cmdx/output.rb +115 -0
  49. data/lib/cmdx/outputs.rb +66 -0
  50. data/lib/cmdx/pipeline.rb +144 -131
  51. data/lib/cmdx/railtie.rb +10 -36
  52. data/lib/cmdx/result.rb +247 -524
  53. data/lib/cmdx/retriers/bounded_random.rb +24 -0
  54. data/lib/cmdx/retriers/decorrelated_jitter.rb +28 -0
  55. data/lib/cmdx/retriers/exponential.rb +23 -0
  56. data/lib/cmdx/retriers/fibonacci.rb +39 -0
  57. data/lib/cmdx/retriers/full_random.rb +23 -0
  58. data/lib/cmdx/retriers/half_random.rb +24 -0
  59. data/lib/cmdx/retriers/linear.rb +23 -0
  60. data/lib/cmdx/retriers.rb +106 -0
  61. data/lib/cmdx/retry.rb +117 -138
  62. data/lib/cmdx/runtime.rb +251 -0
  63. data/lib/cmdx/settings.rb +68 -200
  64. data/lib/cmdx/signal.rb +165 -0
  65. data/lib/cmdx/task.rb +443 -343
  66. data/lib/cmdx/telemetry.rb +108 -0
  67. data/lib/cmdx/util.rb +73 -0
  68. data/lib/cmdx/validators/absence.rb +10 -39
  69. data/lib/cmdx/validators/exclusion.rb +33 -52
  70. data/lib/cmdx/validators/format.rb +19 -49
  71. data/lib/cmdx/validators/inclusion.rb +33 -54
  72. data/lib/cmdx/validators/length.rb +125 -127
  73. data/lib/cmdx/validators/numeric.rb +123 -123
  74. data/lib/cmdx/validators/presence.rb +10 -39
  75. data/lib/cmdx/validators/validate.rb +31 -0
  76. data/lib/cmdx/validators.rb +161 -0
  77. data/lib/cmdx/version.rb +2 -4
  78. data/lib/cmdx/workflow.rb +71 -96
  79. data/lib/cmdx.rb +111 -42
  80. data/lib/generators/cmdx/install_generator.rb +7 -17
  81. data/lib/generators/cmdx/task_generator.rb +12 -29
  82. data/lib/generators/cmdx/templates/install.rb +120 -48
  83. data/lib/generators/cmdx/templates/task.rb.tt +1 -1
  84. data/lib/generators/cmdx/templates/workflow.rb.tt +1 -2
  85. data/lib/generators/cmdx/workflow_generator.rb +12 -29
  86. data/lib/locales/en.yml +8 -7
  87. data/mkdocs.yml +25 -23
  88. metadata +39 -138
  89. data/lib/cmdx/attribute.rb +0 -440
  90. data/lib/cmdx/attribute_registry.rb +0 -185
  91. data/lib/cmdx/attribute_value.rb +0 -252
  92. data/lib/cmdx/callback_registry.rb +0 -169
  93. data/lib/cmdx/coercion_registry.rb +0 -138
  94. data/lib/cmdx/deprecator.rb +0 -77
  95. data/lib/cmdx/exception.rb +0 -46
  96. data/lib/cmdx/executor.rb +0 -378
  97. data/lib/cmdx/identifier.rb +0 -30
  98. data/lib/cmdx/locale.rb +0 -78
  99. data/lib/cmdx/middleware_registry.rb +0 -148
  100. data/lib/cmdx/middlewares/correlate.rb +0 -140
  101. data/lib/cmdx/middlewares/runtime.rb +0 -77
  102. data/lib/cmdx/middlewares/timeout.rb +0 -78
  103. data/lib/cmdx/parallelizer.rb +0 -100
  104. data/lib/cmdx/utils/call.rb +0 -53
  105. data/lib/cmdx/utils/condition.rb +0 -71
  106. data/lib/cmdx/utils/format.rb +0 -82
  107. data/lib/cmdx/utils/normalize.rb +0 -52
  108. data/lib/cmdx/utils/wrap.rb +0 -38
  109. data/lib/cmdx/validator_registry.rb +0 -143
  110. data/lib/generators/cmdx/locale_generator.rb +0 -39
  111. data/lib/locales/af.yml +0 -55
  112. data/lib/locales/ar.yml +0 -55
  113. data/lib/locales/az.yml +0 -55
  114. data/lib/locales/be.yml +0 -55
  115. data/lib/locales/bg.yml +0 -55
  116. data/lib/locales/bn.yml +0 -55
  117. data/lib/locales/bs.yml +0 -55
  118. data/lib/locales/ca.yml +0 -55
  119. data/lib/locales/cnr.yml +0 -55
  120. data/lib/locales/cs.yml +0 -55
  121. data/lib/locales/cy.yml +0 -55
  122. data/lib/locales/da.yml +0 -55
  123. data/lib/locales/de.yml +0 -55
  124. data/lib/locales/dz.yml +0 -55
  125. data/lib/locales/el.yml +0 -55
  126. data/lib/locales/eo.yml +0 -55
  127. data/lib/locales/es.yml +0 -55
  128. data/lib/locales/et.yml +0 -55
  129. data/lib/locales/eu.yml +0 -55
  130. data/lib/locales/fa.yml +0 -55
  131. data/lib/locales/fi.yml +0 -55
  132. data/lib/locales/fr.yml +0 -55
  133. data/lib/locales/fy.yml +0 -55
  134. data/lib/locales/gd.yml +0 -55
  135. data/lib/locales/gl.yml +0 -55
  136. data/lib/locales/he.yml +0 -55
  137. data/lib/locales/hi.yml +0 -55
  138. data/lib/locales/hr.yml +0 -55
  139. data/lib/locales/hu.yml +0 -55
  140. data/lib/locales/hy.yml +0 -55
  141. data/lib/locales/id.yml +0 -55
  142. data/lib/locales/is.yml +0 -55
  143. data/lib/locales/it.yml +0 -55
  144. data/lib/locales/ja.yml +0 -55
  145. data/lib/locales/ka.yml +0 -55
  146. data/lib/locales/kk.yml +0 -55
  147. data/lib/locales/km.yml +0 -55
  148. data/lib/locales/kn.yml +0 -55
  149. data/lib/locales/ko.yml +0 -55
  150. data/lib/locales/lb.yml +0 -55
  151. data/lib/locales/lo.yml +0 -55
  152. data/lib/locales/lt.yml +0 -55
  153. data/lib/locales/lv.yml +0 -55
  154. data/lib/locales/mg.yml +0 -55
  155. data/lib/locales/mk.yml +0 -55
  156. data/lib/locales/ml.yml +0 -55
  157. data/lib/locales/mn.yml +0 -55
  158. data/lib/locales/mr-IN.yml +0 -55
  159. data/lib/locales/ms.yml +0 -55
  160. data/lib/locales/nb.yml +0 -55
  161. data/lib/locales/ne.yml +0 -55
  162. data/lib/locales/nl.yml +0 -55
  163. data/lib/locales/nn.yml +0 -55
  164. data/lib/locales/oc.yml +0 -55
  165. data/lib/locales/or.yml +0 -55
  166. data/lib/locales/pa.yml +0 -55
  167. data/lib/locales/pl.yml +0 -55
  168. data/lib/locales/pt.yml +0 -55
  169. data/lib/locales/rm.yml +0 -55
  170. data/lib/locales/ro.yml +0 -55
  171. data/lib/locales/ru.yml +0 -55
  172. data/lib/locales/sc.yml +0 -55
  173. data/lib/locales/sk.yml +0 -55
  174. data/lib/locales/sl.yml +0 -55
  175. data/lib/locales/sq.yml +0 -55
  176. data/lib/locales/sr.yml +0 -55
  177. data/lib/locales/st.yml +0 -55
  178. data/lib/locales/sv.yml +0 -55
  179. data/lib/locales/sw.yml +0 -55
  180. data/lib/locales/ta.yml +0 -55
  181. data/lib/locales/te.yml +0 -55
  182. data/lib/locales/th.yml +0 -55
  183. data/lib/locales/tl.yml +0 -55
  184. data/lib/locales/tr.yml +0 -55
  185. data/lib/locales/tt.yml +0 -55
  186. data/lib/locales/ug.yml +0 -55
  187. data/lib/locales/uk.yml +0 -55
  188. data/lib/locales/ur.yml +0 -55
  189. data/lib/locales/uz.yml +0 -55
  190. data/lib/locales/vi.yml +0 -55
  191. data/lib/locales/wo.yml +0 -55
  192. data/lib/locales/zh-CN.yml +0 -55
  193. data/lib/locales/zh-HK.yml +0 -55
  194. data/lib/locales/zh-TW.yml +0 -55
  195. data/lib/locales/zh-YUE.yml +0 -55
@@ -1,185 +0,0 @@
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
- # Returns the collection of registered attributes.
10
- #
11
- # @return [Array<Attribute>] Array of registered attributes
12
- #
13
- # @example
14
- # registry.registry # => [#<Attribute @name=:name>, #<Attribute @name=:email>]
15
- #
16
- # @rbs @registry: Array[Attribute]
17
- attr_reader :registry
18
- alias to_a registry
19
-
20
- # Creates a new attribute registry with an optional initial collection.
21
- #
22
- # @param registry [Array<Attribute>] Initial attributes to register
23
- #
24
- # @return [AttributeRegistry] A new registry instance
25
- #
26
- # @example
27
- # registry = AttributeRegistry.new
28
- # registry = AttributeRegistry.new([attr1, attr2])
29
- #
30
- # @rbs (?Array[Attribute] registry) -> void
31
- def initialize(registry = [])
32
- @registry = registry
33
- end
34
-
35
- # Creates a duplicate of this registry with copied attributes.
36
- #
37
- # @return [AttributeRegistry] A new registry with duplicated attributes
38
- #
39
- # @example
40
- # new_registry = registry.dup
41
- #
42
- # @rbs () -> AttributeRegistry
43
- def dup
44
- self.class.new(registry.dup)
45
- end
46
-
47
- # Registers one or more attributes to the registry.
48
- #
49
- # @param attributes [Attribute, Array<Attribute>] Attribute(s) to register
50
- #
51
- # @return [AttributeRegistry] Self for method chaining
52
- #
53
- # @example
54
- # registry.register(attribute)
55
- # registry.register([attr1, attr2])
56
- #
57
- # @rbs (Attribute | Array[Attribute] attributes) -> self
58
- def register(attributes)
59
- @registry.concat(Utils::Wrap.array(attributes))
60
- self
61
- end
62
-
63
- # Removes attributes from the registry by name.
64
- # Supports hierarchical attribute removal by matching the entire attribute tree.
65
- #
66
- # @param names [Symbol, String, Array<Symbol, String>] Name(s) of attributes to remove
67
- #
68
- # @return [AttributeRegistry] Self for method chaining
69
- #
70
- # @example
71
- # registry.deregister(:name)
72
- # registry.deregister(['name1', 'name2'])
73
- #
74
- # @rbs ((Symbol | String | Array[Symbol | String]) names) -> self
75
- def deregister(names)
76
- Utils::Wrap.array(names).each do |name|
77
- @registry.reject! { |attribute| matches_attribute_tree?(attribute, name.to_sym) }
78
- end
79
-
80
- self
81
- end
82
-
83
- # Eagerly defines attribute reader methods on the task class for all
84
- # attributes whose method names can be statically resolved. Called at
85
- # class definition time so readers are defined once, not per-execution.
86
- #
87
- # @param task_class [Class] The task class to define readers on
88
- # @param attrs [Array<Attribute>] Attributes to process (defaults to all)
89
- #
90
- # @rbs (Class task_class, ?Array[Attribute] attrs) -> void
91
- def define_readers_on!(task_class, attrs = registry)
92
- attrs.each { |attr| define_reader_tree!(task_class, attr) }
93
- end
94
-
95
- # Removes eagerly defined reader methods from the task class for
96
- # attributes about to be deregistered. Must be called before {#deregister}.
97
- #
98
- # @param task_class [Class] The task class to undefine readers on
99
- # @param names [Array<Symbol, String>] Attribute method names being removed
100
- #
101
- # @rbs (Class task_class, (Symbol | String | Array[Symbol | String]) names) -> void
102
- def undefine_readers_on!(task_class, names)
103
- Utils::Wrap.array(names).each do |name|
104
- sym = name.to_sym
105
-
106
- registry.each do |attribute|
107
- next unless matches_attribute_tree?(attribute, sym)
108
-
109
- undefine_reader_tree!(task_class, attribute)
110
- end
111
- end
112
- end
113
-
114
- # Verifies attribute definitions against a task instance.
115
- # Each attribute is duped before binding so the class-level originals
116
- # are never mutated — this eliminates the concurrency hazard of
117
- # shared mutable @task, @source, and @method_name state.
118
- #
119
- # @param task [Task] The task to verify attributes against
120
- #
121
- # @rbs (Task task) -> void
122
- def define_and_verify(task)
123
- registry.each do |attribute|
124
- duplicate = attribute.dup
125
- duplicate.task = task
126
- duplicate.define_and_verify_tree
127
- end
128
- end
129
-
130
- private
131
-
132
- # Recursively defines reader methods for an attribute and its children
133
- # when the method name is statically resolvable.
134
- #
135
- # @param task_class [Class] The task class to define readers on
136
- # @param attribute [Attribute] The attribute to define a reader for
137
- #
138
- # @rbs (Class task_class, Attribute attribute) -> void
139
- def define_reader_tree!(task_class, attribute)
140
- name = attribute.allocation_name
141
-
142
- if name && !task_class.method_defined?(name)
143
- if task_class.private_method_defined?(name)
144
- raise <<~MESSAGE
145
- The method #{name.inspect} is already defined on the #{task_class.name} task.
146
- This may be due conflicts with one of the task's user defined or internal methods/attributes.
147
-
148
- Use :as, :prefix, and/or :suffix attribute options to avoid conflicts with existing methods.
149
- MESSAGE
150
- end
151
-
152
- task_class.define_method(name) { attributes[name] }
153
- end
154
-
155
- attribute.children.each { |child| define_reader_tree!(task_class, child) }
156
- end
157
-
158
- # Recursively removes reader methods for an attribute and its children.
159
- #
160
- # @param task_class [Class] The task class to undefine readers on
161
- # @param attribute [Attribute] The attribute whose readers to remove
162
- #
163
- # @rbs (Class task_class, Attribute attribute) -> void
164
- def undefine_reader_tree!(task_class, attribute)
165
- name = attribute.allocation_name
166
- task_class.remove_method(name) if name && task_class.method_defined?(name, false)
167
- attribute.children.each { |child| undefine_reader_tree!(task_class, child) }
168
- end
169
-
170
- # Recursively checks if an attribute or any of its children match the given name.
171
- #
172
- # @param attribute [Attribute] The attribute to check
173
- # @param name [Symbol] The name to match against
174
- #
175
- # @return [Boolean] True if the attribute or any child matches the name
176
- #
177
- # @rbs (Attribute attribute, Symbol name) -> bool
178
- def matches_attribute_tree?(attribute, name)
179
- return true if attribute.method_name == name
180
-
181
- attribute.children.any? { |child| matches_attribute_tree?(child, name) }
182
- end
183
-
184
- end
185
- end
@@ -1,252 +0,0 @@
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
- # Returns the attribute managed by this value handler.
12
- #
13
- # @return [Attribute] The attribute instance
14
- #
15
- # @example
16
- # attr_value.attribute.name # => :user_id
17
- #
18
- # @rbs @attribute: Attribute
19
- attr_reader :attribute
20
-
21
- def_delegators :attribute, :task, :parent, :name, :options, :types, :source, :method_name, :required?, :optional?
22
- def_delegators :task, :attributes, :errors
23
-
24
- # Creates a new attribute value manager for the given attribute.
25
- #
26
- # @param attribute [Attribute] The attribute to manage values for
27
- #
28
- # @example
29
- # attr = Attribute.new(:user_id, required: true)
30
- # attr_value = AttributeValue.new(attr)
31
- #
32
- # @rbs (Attribute attribute) -> void
33
- def initialize(attribute)
34
- @attribute = attribute
35
- end
36
-
37
- # Retrieves the current value for this attribute from the task's attributes.
38
- #
39
- # @return [Object, nil] The current attribute value or nil if not set
40
- #
41
- # @example
42
- # attr_value.value # => "john_doe"
43
- #
44
- # @rbs () -> untyped
45
- def value
46
- attributes[method_name]
47
- end
48
-
49
- # Generates the attribute value through the complete pipeline:
50
- # sourcing, derivation, coercion, and storage.
51
- #
52
- # @return [Object, nil] The generated value or nil if generation failed
53
- #
54
- # @example
55
- # attr_value.generate # => 42
56
- #
57
- # @rbs () -> untyped
58
- def generate
59
- return value if attributes.key?(method_name)
60
-
61
- sourced_value = source_value
62
- return if errors.for?(method_name)
63
-
64
- derived_value = derive_value(sourced_value)
65
- return if errors.for?(method_name)
66
-
67
- coerced_value = coerce_value(derived_value)
68
- transformed_value = transform_value(coerced_value)
69
- return if errors.for?(method_name)
70
-
71
- attributes[method_name] = transformed_value
72
- end
73
-
74
- # Validates the current attribute value against configured validators.
75
- #
76
- # @raise [ValidationError] When validation fails (handled internally)
77
- #
78
- # @example
79
- # attr_value.validate
80
- # # Validates value against :presence, :format, etc.
81
- #
82
- # @rbs () -> void
83
- def validate
84
- registry = task.class.settings.validators
85
-
86
- options.slice(*registry.keys).each do |type, opts|
87
- registry.validate(type, task, value, opts)
88
- rescue ValidationError => e
89
- errors.add(method_name, e.message)
90
- nil
91
- end
92
- end
93
-
94
- private
95
-
96
- # Retrieves the source value for this attribute from various sources.
97
- #
98
- # @return [Object, nil] The sourced value or nil if unavailable
99
- #
100
- # @raise [NoMethodError] When the source method doesn't exist
101
- #
102
- # @example
103
- # # Sources from task method, proc, or direct value
104
- # source_value # => "raw_value"
105
- # @rbs () -> untyped
106
- def source_value
107
- sourced_value =
108
- case source
109
- when Symbol then task.send(source)
110
- when Proc then task.instance_eval(&source)
111
- else source.respond_to?(:call) ? source.call(task) : source
112
- end
113
-
114
- if required? && (parent.nil? || parent&.required?) && Utils::Condition.evaluate(task, options)
115
- case sourced_value
116
- when Context then sourced_value.key?(name)
117
- when Hash then sourced_value.key?(name.to_s) || sourced_value.key?(name.to_sym)
118
- when Proc then true # HACK: Cannot be determined so assume it will respond
119
- else sourced_value.respond_to?(name, true)
120
- end || errors.add(method_name, Locale.t("cmdx.attributes.required", method: source))
121
- end
122
-
123
- sourced_value
124
- rescue NoMethodError
125
- errors.add(method_name, Locale.t("cmdx.attributes.undefined", method: source))
126
- nil
127
- end
128
-
129
- # Retrieves the default value for this attribute if configured.
130
- #
131
- # @return [Object, nil] The default value or nil if not configured
132
- #
133
- # @example
134
- # # Default can be symbol, proc, or direct value
135
- # -> { rand(100) } # => 23
136
- #
137
- # @rbs () -> untyped
138
- def default_value
139
- default = options[:default]
140
-
141
- if default.is_a?(Symbol) && task.respond_to?(default, true)
142
- task.send(default)
143
- elsif default.is_a?(Proc)
144
- task.instance_eval(&default)
145
- elsif default.respond_to?(:call)
146
- default.call(task)
147
- else
148
- default
149
- end
150
- end
151
-
152
- # Derives the actual value from the source value using various strategies.
153
- #
154
- # @param source_value [Object] The source value to derive from
155
- #
156
- # @return [Object, nil] The derived value or nil if derivation failed
157
- #
158
- # @raise [NoMethodError] When the derivation method doesn't exist
159
- #
160
- # @example
161
- # # Derives from hash key, method call, or proc execution
162
- # context.user_id # => 42
163
- #
164
- # @rbs (untyped source_value) -> untyped
165
- def derive_value(source_value)
166
- derived_value =
167
- case source_value
168
- when Context then source_value[name]
169
- when Hash
170
- if source_value.key?(name.to_s)
171
- source_value[name.to_s]
172
- elsif source_value.key?(name.to_sym)
173
- source_value[name.to_sym]
174
- end
175
- when Symbol then source_value.send(name)
176
- when Proc then task.instance_exec(name, &source_value)
177
- else
178
- if source_value.respond_to?(:call)
179
- source_value.call(task, name)
180
- elsif source_value.respond_to?(name, true)
181
- source_value.send(name)
182
- end
183
- end
184
-
185
- derived_value.nil? ? default_value : derived_value
186
- rescue NoMethodError
187
- errors.add(method_name, Locale.t("cmdx.attributes.undefined", method: name))
188
- nil
189
- end
190
-
191
- # Transforms the derived value using the transform option.
192
- #
193
- # @param derived_value [Object] The value to transform
194
- #
195
- # @return [Object, nil] The transformed value or nil if transformation failed
196
- #
197
- # @example
198
- # :downcase # => "hello"
199
- #
200
- # @rbs (untyped derived_value) -> untyped
201
- def transform_value(derived_value)
202
- transform = options[:transform]
203
-
204
- if transform.is_a?(Symbol) && derived_value.respond_to?(transform, true)
205
- derived_value.send(transform)
206
- elsif transform.respond_to?(:call)
207
- transform.call(derived_value)
208
- else
209
- derived_value
210
- end
211
- end
212
-
213
- # Coerces the derived value to the expected type(s) using the coercion registry.
214
- #
215
- # @param transformed_value [Object] The value to coerce
216
- #
217
- # @return [Object, nil] The coerced value or nil if coercion failed
218
- #
219
- # @raise [CoercionError] When coercion fails (handled internally)
220
- #
221
- # @example
222
- # # Coerces "42" to Integer, "true" to Boolean, etc.
223
- # coerce_value("42") # => 42
224
- #
225
- # @rbs (untyped transformed_value) -> untyped
226
- def coerce_value(transformed_value)
227
- return transformed_value if types.empty?
228
-
229
- registry = task.class.settings.coercions
230
- last_idx = types.size - 1
231
-
232
- types.find.with_index do |type, i|
233
- break registry.coerce(type, task, transformed_value, options)
234
- rescue CoercionError => e
235
- next if i != last_idx
236
-
237
- unless optional? && transformed_value.nil?
238
- message =
239
- if last_idx.zero?
240
- e.message
241
- else
242
- tl = types.map { |t| Locale.t("cmdx.types.#{t}") }.join(", ")
243
- Locale.t("cmdx.coercions.into_any", types: tl)
244
- end
245
-
246
- errors.add(method_name, message)
247
- end
248
- end
249
- end
250
-
251
- end
252
- end
@@ -1,169 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CMDx
4
- # Registry for managing callbacks that can be executed at various points during task execution.
5
- #
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.
8
- #
9
- # Supports copy-on-write semantics: a duped registry shares the parent's
10
- # data until a write operation triggers materialization.
11
- class CallbackRegistry
12
-
13
- extend Forwardable
14
-
15
- # @rbs TYPES: Array[Symbol]
16
- TYPES = %i[
17
- before_validation
18
- before_execution
19
- on_complete
20
- on_interrupted
21
- on_executed
22
- on_success
23
- on_skipped
24
- on_failed
25
- on_good
26
- on_bad
27
- ].freeze
28
-
29
- # @rbs TYPES_SET: Set[Symbol]
30
- TYPES_SET = TYPES.to_set.freeze
31
- private_constant :TYPES_SET
32
-
33
- def_delegators :registry, :empty?
34
-
35
- # @param registry [Hash, nil] Initial registry hash, defaults to empty
36
- #
37
- # @rbs (?Hash[Symbol, Set[Array[untyped]]]? registry) -> void
38
- def initialize(registry = nil)
39
- @registry = registry || {}
40
- end
41
-
42
- # Sets up copy-on-write state when duplicated via dup.
43
- #
44
- # @param source [CallbackRegistry] The registry being duplicated
45
- #
46
- # @rbs (CallbackRegistry source) -> void
47
- def initialize_dup(source)
48
- @parent = source
49
- @registry = nil
50
- super
51
- end
52
-
53
- # Returns the internal registry of callbacks organized by type.
54
- # Delegates to the parent registry when not yet materialized.
55
- #
56
- # @return [Hash{Symbol => Set<Array>}] Hash mapping callback types to their registered callables
57
- #
58
- # @example
59
- # registry.registry # => { before_execution: #<Set: [[[:validate], {}]]> }
60
- #
61
- # @rbs () -> Hash[Symbol, Set[Array[untyped]]]
62
- def registry
63
- @registry || @parent.registry
64
- end
65
- alias to_h registry
66
-
67
- # Registers one or more callables for a specific callback type
68
- #
69
- # @param type [Symbol] The callback type from TYPES
70
- # @param callables [Array<#call>] Callable objects to register
71
- # @param options [Hash] Options to pass to the callback
72
- # @param block [Proc] Optional block to register as a callable
73
- # @option options [Hash] :if Condition hash for conditional execution
74
- # @option options [Hash] :unless Inverse condition hash for conditional execution
75
- #
76
- # @return [CallbackRegistry] self for method chaining
77
- #
78
- # @raise [ArgumentError] When type is not a valid callback type
79
- #
80
- # @example Register a method callback
81
- # registry.register(:before_execution, :validate_inputs)
82
- # @example Register a block with conditions
83
- # registry.register(:on_success, if: { status: :completed }) do |task|
84
- # task.log("Success callback executed")
85
- # end
86
- #
87
- # @rbs (Symbol type, *untyped callables, **untyped options) ?{ (Task) -> void } -> self
88
- def register(type, *callables, **options, &block)
89
- materialize!
90
-
91
- callables << block if block_given?
92
-
93
- @registry[type] ||= Set.new
94
- @registry[type] << [callables, options]
95
- self
96
- end
97
-
98
- # Removes one or more callables for a specific callback type
99
- #
100
- # @param type [Symbol] The callback type from TYPES
101
- # @param callables [Array<#call>] Callable objects to remove
102
- # @param options [Hash] Options that were used during registration
103
- # @param block [Proc] Optional block to remove
104
- # @option options [Object] :* Any option key-value pairs
105
- #
106
- # @return [CallbackRegistry] self for method chaining
107
- #
108
- # @example Remove a specific callback
109
- # registry.deregister(:before_execution, :validate_inputs)
110
- #
111
- # @rbs (Symbol type, *untyped callables, **untyped options) ?{ (Task) -> void } -> self
112
- def deregister(type, *callables, **options, &block)
113
- materialize!
114
-
115
- callables << block if block_given?
116
- return self unless @registry[type]
117
-
118
- @registry[type].delete([callables, options])
119
- @registry.delete(type) if @registry[type].empty?
120
- self
121
- end
122
-
123
- # Invokes all registered callbacks for a given type
124
- #
125
- # @param type [Symbol] The callback type to invoke
126
- # @param task [Task] The task instance to pass to callbacks
127
- #
128
- # @raise [TypeError] When type is not a valid callback type
129
- #
130
- # @example Invoke all before_execution callbacks
131
- # registry.invoke(:before_execution, task)
132
- #
133
- # @rbs (Symbol type, Task task) -> void
134
- def invoke(type, task)
135
- raise TypeError, "unknown callback type #{type.inspect}" unless TYPES_SET.include?(type)
136
- return unless registry[type]
137
-
138
- registry[type].each do |callables, options|
139
- next unless Utils::Condition.evaluate(task, options)
140
-
141
- Utils::Wrap.array(callables).each do |callable|
142
- if callable.is_a?(Symbol)
143
- task.send(callable)
144
- elsif callable.is_a?(Proc)
145
- task.instance_exec(&callable)
146
- elsif callable.respond_to?(:call)
147
- callable.call(task)
148
- else
149
- raise "cannot invoke #{callable}"
150
- end
151
- end
152
- end
153
- end
154
-
155
- private
156
-
157
- # Copies the parent's registry data into this instance,
158
- # severing the copy-on-write link.
159
- #
160
- # @rbs () -> void
161
- def materialize!
162
- return if @registry
163
-
164
- @registry = @parent.registry.transform_values(&:dup)
165
- @parent = nil
166
- end
167
-
168
- end
169
- end