cmdx 1.19.0 → 1.21.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 (145) hide show
  1. checksums.yaml +4 -4
  2. data/.DS_Store +0 -0
  3. data/CHANGELOG.md +82 -16
  4. data/README.md +1 -1
  5. data/lib/cmdx/attribute.rb +82 -19
  6. data/lib/cmdx/attribute_registry.rb +79 -8
  7. data/lib/cmdx/attribute_value.rb +2 -2
  8. data/lib/cmdx/callback_registry.rb +60 -26
  9. data/lib/cmdx/chain.rb +34 -5
  10. data/lib/cmdx/coercion_registry.rb +42 -20
  11. data/lib/cmdx/coercions/array.rb +2 -2
  12. data/lib/cmdx/coercions/big_decimal.rb +1 -1
  13. data/lib/cmdx/coercions/boolean.rb +2 -2
  14. data/lib/cmdx/coercions/complex.rb +1 -1
  15. data/lib/cmdx/coercions/date.rb +1 -1
  16. data/lib/cmdx/coercions/date_time.rb +1 -1
  17. data/lib/cmdx/coercions/float.rb +1 -1
  18. data/lib/cmdx/coercions/hash.rb +1 -1
  19. data/lib/cmdx/coercions/integer.rb +1 -1
  20. data/lib/cmdx/coercions/rational.rb +1 -1
  21. data/lib/cmdx/coercions/string.rb +1 -1
  22. data/lib/cmdx/coercions/symbol.rb +1 -1
  23. data/lib/cmdx/coercions/time.rb +1 -1
  24. data/lib/cmdx/configuration.rb +38 -0
  25. data/lib/cmdx/context.rb +11 -8
  26. data/lib/cmdx/deprecator.rb +27 -14
  27. data/lib/cmdx/errors.rb +3 -4
  28. data/lib/cmdx/exception.rb +7 -0
  29. data/lib/cmdx/executor.rb +80 -53
  30. data/lib/cmdx/identifier.rb +4 -6
  31. data/lib/cmdx/locale.rb +32 -9
  32. data/lib/cmdx/middleware_registry.rb +43 -23
  33. data/lib/cmdx/middlewares/correlate.rb +4 -2
  34. data/lib/cmdx/middlewares/runtime.rb +18 -3
  35. data/lib/cmdx/middlewares/timeout.rb +11 -10
  36. data/lib/cmdx/parallelizer.rb +100 -0
  37. data/lib/cmdx/pipeline.rb +42 -23
  38. data/lib/cmdx/railtie.rb +1 -1
  39. data/lib/cmdx/result.rb +91 -19
  40. data/lib/cmdx/retry.rb +166 -0
  41. data/lib/cmdx/settings.rb +226 -0
  42. data/lib/cmdx/task.rb +62 -65
  43. data/lib/cmdx/utils/format.rb +17 -1
  44. data/lib/cmdx/utils/normalize.rb +52 -0
  45. data/lib/cmdx/utils/wrap.rb +38 -0
  46. data/lib/cmdx/validator_registry.rb +44 -19
  47. data/lib/cmdx/validators/absence.rb +1 -1
  48. data/lib/cmdx/validators/exclusion.rb +2 -2
  49. data/lib/cmdx/validators/format.rb +1 -1
  50. data/lib/cmdx/validators/inclusion.rb +2 -2
  51. data/lib/cmdx/validators/length.rb +1 -1
  52. data/lib/cmdx/validators/numeric.rb +1 -1
  53. data/lib/cmdx/validators/presence.rb +1 -1
  54. data/lib/cmdx/version.rb +1 -1
  55. data/lib/cmdx/workflow.rb +17 -0
  56. data/lib/cmdx.rb +12 -0
  57. data/lib/generators/cmdx/templates/install.rb +20 -5
  58. data/lib/locales/af.yml +2 -0
  59. data/lib/locales/ar.yml +2 -0
  60. data/lib/locales/az.yml +2 -0
  61. data/lib/locales/be.yml +2 -0
  62. data/lib/locales/bg.yml +2 -0
  63. data/lib/locales/bn.yml +2 -0
  64. data/lib/locales/bs.yml +2 -0
  65. data/lib/locales/ca.yml +2 -0
  66. data/lib/locales/cnr.yml +2 -0
  67. data/lib/locales/cs.yml +2 -0
  68. data/lib/locales/cy.yml +2 -0
  69. data/lib/locales/da.yml +2 -0
  70. data/lib/locales/de.yml +2 -0
  71. data/lib/locales/dz.yml +2 -0
  72. data/lib/locales/el.yml +2 -0
  73. data/lib/locales/en.yml +2 -0
  74. data/lib/locales/eo.yml +2 -0
  75. data/lib/locales/es.yml +2 -0
  76. data/lib/locales/et.yml +2 -0
  77. data/lib/locales/eu.yml +2 -0
  78. data/lib/locales/fa.yml +2 -0
  79. data/lib/locales/fi.yml +2 -0
  80. data/lib/locales/fr.yml +2 -0
  81. data/lib/locales/fy.yml +2 -0
  82. data/lib/locales/gd.yml +2 -0
  83. data/lib/locales/gl.yml +2 -0
  84. data/lib/locales/he.yml +2 -0
  85. data/lib/locales/hi.yml +2 -0
  86. data/lib/locales/hr.yml +2 -0
  87. data/lib/locales/hu.yml +2 -0
  88. data/lib/locales/hy.yml +2 -0
  89. data/lib/locales/id.yml +2 -0
  90. data/lib/locales/is.yml +2 -0
  91. data/lib/locales/it.yml +2 -0
  92. data/lib/locales/ja.yml +2 -0
  93. data/lib/locales/ka.yml +2 -0
  94. data/lib/locales/kk.yml +2 -0
  95. data/lib/locales/km.yml +2 -0
  96. data/lib/locales/kn.yml +2 -0
  97. data/lib/locales/ko.yml +2 -0
  98. data/lib/locales/lb.yml +2 -0
  99. data/lib/locales/lo.yml +2 -0
  100. data/lib/locales/lt.yml +2 -0
  101. data/lib/locales/lv.yml +2 -0
  102. data/lib/locales/mg.yml +2 -0
  103. data/lib/locales/mk.yml +2 -0
  104. data/lib/locales/ml.yml +2 -0
  105. data/lib/locales/mn.yml +2 -0
  106. data/lib/locales/mr-IN.yml +2 -0
  107. data/lib/locales/ms.yml +2 -0
  108. data/lib/locales/nb.yml +2 -0
  109. data/lib/locales/ne.yml +2 -0
  110. data/lib/locales/nl.yml +2 -0
  111. data/lib/locales/nn.yml +2 -0
  112. data/lib/locales/oc.yml +2 -0
  113. data/lib/locales/or.yml +2 -0
  114. data/lib/locales/pa.yml +2 -0
  115. data/lib/locales/pl.yml +2 -0
  116. data/lib/locales/pt.yml +2 -0
  117. data/lib/locales/rm.yml +2 -0
  118. data/lib/locales/ro.yml +2 -0
  119. data/lib/locales/ru.yml +2 -0
  120. data/lib/locales/sc.yml +2 -0
  121. data/lib/locales/sk.yml +2 -0
  122. data/lib/locales/sl.yml +2 -0
  123. data/lib/locales/sq.yml +2 -0
  124. data/lib/locales/sr.yml +2 -0
  125. data/lib/locales/st.yml +2 -0
  126. data/lib/locales/sv.yml +2 -0
  127. data/lib/locales/sw.yml +2 -0
  128. data/lib/locales/ta.yml +2 -0
  129. data/lib/locales/te.yml +2 -0
  130. data/lib/locales/th.yml +2 -0
  131. data/lib/locales/tl.yml +2 -0
  132. data/lib/locales/tr.yml +2 -0
  133. data/lib/locales/tt.yml +2 -0
  134. data/lib/locales/ug.yml +2 -0
  135. data/lib/locales/uk.yml +2 -0
  136. data/lib/locales/ur.yml +2 -0
  137. data/lib/locales/uz.yml +2 -0
  138. data/lib/locales/vi.yml +2 -0
  139. data/lib/locales/wo.yml +2 -0
  140. data/lib/locales/zh-CN.yml +2 -0
  141. data/lib/locales/zh-HK.yml +2 -0
  142. data/lib/locales/zh-TW.yml +2 -0
  143. data/lib/locales/zh-YUE.yml +2 -0
  144. data/mkdocs.yml +5 -1
  145. metadata +6 -15
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ # Value object encapsulating all per-task configuration. Registries are
5
+ # deep-duped on inheritance; scalar settings delegate to a parent Settings
6
+ # or to the global Configuration rather than eagerly copying values.
7
+ class Settings
8
+
9
+ class << self
10
+
11
+ private
12
+
13
+ # Defines a reader that delegates to the parent Settings chain,
14
+ # falling through to Configuration when no parent exists.
15
+ #
16
+ # @param names [Array<Symbol>] Setting names to define
17
+ #
18
+ # @rbs (*Symbol names) -> void
19
+ def delegate_to_configuration(*names)
20
+ names.each do |name|
21
+ ivar = :"@#{name}"
22
+
23
+ attr_writer(name)
24
+
25
+ define_method(name) do
26
+ return instance_variable_get(ivar) if instance_variable_defined?(ivar)
27
+
28
+ value = @parent ? @parent.public_send(name) : CMDx.configuration.public_send(name)
29
+ instance_variable_set(ivar, value)
30
+
31
+ value
32
+ end
33
+ end
34
+ end
35
+
36
+ # Defines a reader that delegates to the parent Settings only.
37
+ # Returns nil when the chain is exhausted.
38
+ #
39
+ # @param names [Array<Symbol>] Setting names to define
40
+ # @param with_fallback [Boolean] Whether to fall back to Configuration
41
+ #
42
+ # @rbs (*Symbol names, with_fallback: bool) -> void
43
+ def delegate_to_parent(*names, with_fallback: false)
44
+ names.each do |name|
45
+ ivar = :"@#{name}"
46
+
47
+ attr_writer(name)
48
+
49
+ define_method(name) do
50
+ return instance_variable_get(ivar) if instance_variable_defined?(ivar)
51
+
52
+ value = @parent&.public_send(name)
53
+ value ||= CMDx.configuration.public_send(name) if with_fallback
54
+ instance_variable_set(ivar, value)
55
+
56
+ value
57
+ end
58
+ end
59
+ end
60
+
61
+ end
62
+
63
+ # Returns the attribute registry for task parameters.
64
+ #
65
+ # @return [AttributeRegistry] The attribute registry
66
+ #
67
+ # @rbs @attributes: AttributeRegistry
68
+ attr_accessor :attributes
69
+
70
+ # Returns the callback registry for task lifecycle hooks.
71
+ #
72
+ # @return [CallbackRegistry] The callback registry
73
+ #
74
+ # @rbs @callbacks: CallbackRegistry
75
+ attr_accessor :callbacks
76
+
77
+ # Returns the coercion registry for type conversions.
78
+ #
79
+ # @return [CoercionRegistry] The coercion registry
80
+ #
81
+ # @rbs @coercions: CoercionRegistry
82
+ attr_accessor :coercions
83
+
84
+ # Returns the middleware registry for task execution.
85
+ #
86
+ # @return [MiddlewareRegistry] The middleware registry
87
+ #
88
+ # @rbs @middlewares: MiddlewareRegistry
89
+ attr_accessor :middlewares
90
+
91
+ # Returns the validator registry for attribute validation.
92
+ #
93
+ # @return [ValidatorRegistry] The validator registry
94
+ #
95
+ # @rbs @validators: ValidatorRegistry
96
+ attr_accessor :validators
97
+
98
+ # Returns the expected return keys after execution.
99
+ #
100
+ # @return [Array<Symbol>] Expected return keys after execution
101
+ #
102
+ # @rbs @returns: Array[Symbol]
103
+ attr_accessor :returns
104
+
105
+ # Returns the tags for task categorization.
106
+ #
107
+ # @return [Array<Symbol>] Tags for categorization
108
+ #
109
+ # @rbs @tags: Array[Symbol]
110
+ attr_accessor :tags
111
+
112
+ # @!attribute [rw] backtrace
113
+ # @return [Boolean] true if backtraces should be logged
114
+ delegate_to_configuration :backtrace
115
+
116
+ # @!attribute [rw] dump_context
117
+ # @return [Boolean] true if context should be included in Task#to_h
118
+ delegate_to_configuration :dump_context
119
+
120
+ # @!attribute [rw] rollback_on
121
+ # @return [Array<String>] Statuses that trigger rollback
122
+ delegate_to_configuration :rollback_on
123
+
124
+ # @!attribute [rw] task_breakpoints
125
+ # @return [Array<String>] Default task breakpoint statuses
126
+ delegate_to_configuration :task_breakpoints
127
+
128
+ # @!attribute [rw] workflow_breakpoints
129
+ # @return [Array<String>] Default workflow breakpoint statuses
130
+ delegate_to_configuration :workflow_breakpoints
131
+
132
+ # @!attribute [rw] backtrace_cleaner
133
+ # @return [Proc, nil] The backtrace cleaner proc
134
+ delegate_to_parent :backtrace_cleaner, with_fallback: true
135
+
136
+ # @!attribute [rw] breakpoints
137
+ # @return [Array<String>, nil] Per-task breakpoints override
138
+ delegate_to_parent :breakpoints
139
+
140
+ # @!attribute [rw] deprecate
141
+ # @return [Symbol, Proc, Boolean, nil] Deprecation behavior
142
+ delegate_to_parent :deprecate
143
+
144
+ # @!attribute [rw] exception_handler
145
+ # @return [Proc, nil] The exception handler proc
146
+ delegate_to_parent :exception_handler, with_fallback: true
147
+
148
+ # @!attribute [rw] logger
149
+ # @return [Logger] The logger instance
150
+ delegate_to_parent :logger, with_fallback: true
151
+
152
+ # @!attribute [rw] log_formatter
153
+ # @return [Proc, nil] Per-task log formatter override
154
+ delegate_to_parent :log_formatter
155
+
156
+ # @!attribute [rw] log_level
157
+ # @return [Integer, nil] Per-task log level override
158
+ delegate_to_parent :log_level
159
+
160
+ # @!attribute [rw] retries
161
+ # @return [Integer, nil] Number of retries on failure
162
+ delegate_to_parent :retries
163
+
164
+ # @!attribute [rw] retry_jitter
165
+ # @return [Numeric, Symbol, Proc, nil] Jitter between retries
166
+ delegate_to_parent :retry_jitter
167
+
168
+ # @!attribute [rw] retry_on
169
+ # @return [Array<Class>, Class, nil] Exception classes to retry on
170
+ delegate_to_parent :retry_on
171
+
172
+ # Creates a new Settings instance, inheriting registries from a parent
173
+ # Settings or the global Configuration. Scalar settings are resolved
174
+ # lazily via delegation rather than eagerly copied.
175
+ #
176
+ # @param parent [Settings, nil] Parent settings to inherit from
177
+ # @param overrides [Hash] Field values to override after inheritance
178
+ #
179
+ # @example
180
+ # Settings.new(parent: ParentTask.settings, deprecate: true)
181
+ #
182
+ # @rbs (?parent: Settings?, **untyped overrides) -> void
183
+ def initialize(parent: nil, **overrides)
184
+ @parent = parent
185
+
186
+ init_registries
187
+ init_collections
188
+
189
+ overrides.each { |key, value| public_send(:"#{key}=", value) }
190
+ end
191
+
192
+ private
193
+
194
+ # Dups registries from the parent Settings or global Configuration
195
+ # so each task class gets its own mutable copy.
196
+ #
197
+ # @rbs () -> void
198
+ def init_registries
199
+ if @parent
200
+ @middlewares = @parent.middlewares.dup
201
+ @callbacks = @parent.callbacks.dup
202
+ @coercions = @parent.coercions.dup
203
+ @validators = @parent.validators.dup
204
+ @attributes = @parent.attributes.dup
205
+ else
206
+ config = CMDx.configuration
207
+
208
+ @middlewares = config.middlewares.dup
209
+ @callbacks = config.callbacks.dup
210
+ @coercions = config.coercions.dup
211
+ @validators = config.validators.dup
212
+ @attributes = AttributeRegistry.new
213
+ end
214
+ end
215
+
216
+ # Initializes array-valued settings that need their own copy
217
+ # to avoid cross-class mutation.
218
+ #
219
+ # @rbs () -> void
220
+ def init_collections
221
+ @returns = @parent&.returns&.dup || EMPTY_ARRAY
222
+ @tags = @parent&.tags&.dup || EMPTY_ARRAY
223
+ end
224
+
225
+ end
226
+ end
data/lib/cmdx/task.rb CHANGED
@@ -70,72 +70,63 @@ module CMDx
70
70
  # @rbs @chain: Chain
71
71
  attr_reader :chain
72
72
 
73
- def_delegators :result, :skip!, :fail!, :throw!
73
+ def_delegators :result, :success!, :skip!, :fail!, :throw!
74
74
  def_delegators :chain, :dry_run?
75
75
 
76
- # @param context [Hash, Context] The initial context for the task
77
- #
78
- # @option context [Object] :* Any key-value pairs to initialize the context
76
+ # @param context [Hash, Context, nil] The initial context for the task
79
77
  #
80
78
  # @return [Task] A new task instance
81
79
  #
82
80
  # @raise [DeprecationError] If the task class is deprecated
83
81
  #
84
82
  # @example
83
+ # task = MyTask.new
85
84
  # task = MyTask.new(name: "example", priority: :high)
86
85
  # task = MyTask.new(Context.build(name: "example"))
87
86
  #
88
87
  # @rbs (untyped context) -> void
89
- def initialize(context = {})
88
+ def initialize(context = nil)
90
89
  Deprecator.restrict(self)
91
90
 
92
- @attributes = {}
93
- @errors = Errors.new
94
-
95
91
  @id = Identifier.generate
96
92
  @context = Context.build(context)
93
+ @errors = Errors.new
97
94
  @result = Result.new(self)
95
+ @chain = Chain.build(@result, dry_run: @context.delete(:dry_run))
98
96
 
99
- dry_run = @context.delete(:dry_run)
100
- @chain = Chain.build(@result, dry_run:)
97
+ @attributes = {}
101
98
  end
102
99
 
103
100
  class << self
104
101
 
105
- # @param options [Hash] Configuration options to merge with existing settings
106
- # @option options [Object] :* Any configuration option key-value pairs
102
+ # Returns the cached task type string for this class.
103
+ #
104
+ # @return [String] "Workflow" or "Task"
105
+ #
106
+ # @rbs () -> String
107
+ def type
108
+ @type ||= include?(Workflow) ? "Workflow" : "Task"
109
+ end
110
+
111
+ # Returns (and lazily creates) the task-level Settings object.
112
+ # On first access, inherits from the superclass settings or
113
+ # the global Configuration. Optional overrides are applied once.
114
+ #
115
+ # @param overrides [Hash] Configuration overrides applied on first access
116
+ # @option overrides [Object] :* Any configuration override key-value pairs
107
117
  #
108
- # @return [Hash] The merged settings hash
118
+ # @return [Settings] The settings instance for this task class
109
119
  #
110
120
  # @example
111
121
  # class MyTask < Task
112
122
  # settings deprecate: true, tags: [:experimental]
113
123
  # end
114
124
  #
115
- # @rbs (**untyped options) -> Hash[Symbol, untyped]
116
- def settings(**options)
125
+ # @rbs (**untyped overrides) -> Settings
126
+ def settings(**overrides)
117
127
  @settings ||= begin
118
- hash =
119
- if superclass.respond_to?(:settings)
120
- parent = superclass.settings
121
- parent
122
- .except(:backtrace_cleaner, :exception_handler, :logger, :deprecate)
123
- .transform_values!(&:dup)
124
- .merge!(
125
- backtrace_cleaner: parent[:backtrace_cleaner] || CMDx.configuration.backtrace_cleaner,
126
- exception_handler: parent[:exception_handler] || CMDx.configuration.exception_handler,
127
- logger: parent[:logger] || CMDx.configuration.logger,
128
- deprecate: parent[:deprecate]
129
- )
130
- else
131
- CMDx.configuration.to_h
132
- end
133
-
134
- hash[:attributes] ||= AttributeRegistry.new
135
- hash[:returns] ||= []
136
- hash[:tags] ||= []
137
-
138
- hash.merge!(options)
128
+ parent = superclass.settings if superclass.respond_to?(:settings)
129
+ Settings.new(parent:, **overrides)
139
130
  end
140
131
  end
141
132
 
@@ -151,11 +142,13 @@ module CMDx
151
142
  # @rbs (Symbol type, untyped object, *untyped) -> void
152
143
  def register(type, object, ...)
153
144
  case type
154
- when :attribute then settings[:attributes].register(object, ...)
155
- when :callback then settings[:callbacks].register(object, ...)
156
- when :coercion then settings[:coercions].register(object, ...)
157
- when :middleware then settings[:middlewares].register(object, ...)
158
- when :validator then settings[:validators].register(object, ...)
145
+ when :attribute
146
+ settings.attributes.register(object)
147
+ settings.attributes.define_readers_on!(self, Utils::Wrap.array(object))
148
+ when :callback then settings.callbacks.register(object, ...)
149
+ when :middleware then settings.middlewares.register(object, ...)
150
+ when :validator then settings.validators.register(object, ...)
151
+ when :coercion then settings.coercions.register(object, ...)
159
152
  else raise "unknown registry type #{type.inspect}"
160
153
  end
161
154
  end
@@ -172,11 +165,13 @@ module CMDx
172
165
  # @rbs (Symbol type, untyped object, *untyped) -> void
173
166
  def deregister(type, object, ...)
174
167
  case type
175
- when :attribute then settings[:attributes].deregister(object, ...)
176
- when :callback then settings[:callbacks].deregister(object, ...)
177
- when :coercion then settings[:coercions].deregister(object, ...)
178
- when :middleware then settings[:middlewares].deregister(object, ...)
179
- when :validator then settings[:validators].deregister(object, ...)
168
+ when :attribute
169
+ settings.attributes.undefine_readers_on!(self, object)
170
+ settings.attributes.deregister(object)
171
+ when :callback then settings.callbacks.deregister(object, ...)
172
+ when :middleware then settings.middlewares.deregister(object, ...)
173
+ when :validator then settings.validators.deregister(object, ...)
174
+ when :coercion then settings.coercions.deregister(object, ...)
180
175
  else raise "unknown registry type #{type.inspect}"
181
176
  end
182
177
  end
@@ -231,7 +226,7 @@ module CMDx
231
226
  #
232
227
  # @rbs (*untyped names) -> void
233
228
  def returns(*names)
234
- settings[:returns] |= names.map(&:to_sym)
229
+ settings.returns |= names.map(&:to_sym)
235
230
  end
236
231
 
237
232
  # Removes declared returns from the task.
@@ -243,7 +238,7 @@ module CMDx
243
238
  #
244
239
  # @rbs (*Symbol names) -> void
245
240
  def remove_returns(*names)
246
- settings[:returns] -= names.map(&:to_sym)
241
+ settings.returns -= names.map(&:to_sym)
247
242
  end
248
243
  alias remove_return remove_returns
249
244
 
@@ -261,7 +256,7 @@ module CMDx
261
256
  #
262
257
  # @rbs () -> Hash[Symbol, Hash[Symbol, untyped]]
263
258
  def attributes_schema
264
- Array(settings[:attributes]).to_h do |attr|
259
+ Utils::Wrap.array(settings.attributes).to_h do |attr|
265
260
  [attr.method_name, attr.to_h]
266
261
  end
267
262
  end
@@ -292,11 +287,8 @@ module CMDx
292
287
  #
293
288
  # @example
294
289
  # result = MyTask.execute(name: "example")
295
- # if result.success?
296
- # puts "Task completed successfully"
297
- # end
298
290
  #
299
- # @rbs (*untyped args, **untyped kwargs) ?{ (Result) -> void } -> Result
291
+ # @rbs (*untyped args, dry_run: bool, **untyped kwargs) ?{ (Result) -> void } -> Result
300
292
  def execute(*args, **kwargs)
301
293
  task = new(*args, **kwargs)
302
294
  task.execute(raise: false)
@@ -313,9 +305,8 @@ module CMDx
313
305
  #
314
306
  # @example
315
307
  # result = MyTask.execute!(name: "example")
316
- # # Will raise an exception if execution fails
317
308
  #
318
- # @rbs (*untyped args, **untyped kwargs) ?{ (Result) -> void } -> Result
309
+ # @rbs (*untyped args, dry_run: bool, **untyped kwargs) ?{ (Result) -> void } -> Result
319
310
  def execute!(*args, **kwargs)
320
311
  task = new(*args, **kwargs)
321
312
  task.execute(raise: true)
@@ -366,14 +357,13 @@ module CMDx
366
357
  # @rbs () -> Logger
367
358
  def logger
368
359
  @logger ||= begin
369
- log_instance = self.class.settings[:logger] || CMDx.configuration.logger
370
- log_level = self.class.settings[:log_level]
371
- log_formatter = self.class.settings[:log_formatter]
360
+ settings = self.class.settings
361
+ log_instance = settings.logger || CMDx.configuration.logger
372
362
 
373
- if log_level || log_formatter
363
+ if settings.log_level || settings.log_formatter
374
364
  log_instance = log_instance.dup
375
- log_instance.level = log_level if log_level
376
- log_instance.formatter = log_formatter if log_formatter
365
+ log_instance.level = settings.log_level if settings.log_level
366
+ log_instance.formatter = settings.log_formatter if settings.log_formatter
377
367
  end
378
368
 
379
369
  log_instance
@@ -386,6 +376,7 @@ module CMDx
386
376
  # @option return [Array<Symbol>] :tags The task tags
387
377
  # @option return [String] :class The task class name
388
378
  # @option return [String] :id The task identifier
379
+ # @option return [Hash] :context The task context (when dump_context is true)
389
380
  #
390
381
  # @return [Hash] A hash representation of the task
391
382
  #
@@ -399,12 +390,18 @@ module CMDx
399
390
  {
400
391
  index: result.index,
401
392
  chain_id: chain.id,
402
- type: self.class.include?(Workflow) ? "Workflow" : "Task",
403
- tags: self.class.settings[:tags],
393
+ type: self.class.type,
404
394
  class: self.class.name,
395
+ id:,
405
396
  dry_run: dry_run?,
406
- id:
407
- }
397
+ tags: self.class.settings.tags
398
+ }.tap do |hash|
399
+ if self.class.settings.dump_context
400
+ # Large context can make dumps and logs very noisy,
401
+ # so only include it if explicitly enabled
402
+ hash[:context] = context.to_h
403
+ end
404
+ end
408
405
  end
409
406
 
410
407
  # @return [String] A string representation of the task
@@ -32,7 +32,7 @@ module CMDx
32
32
  #
33
33
  # @rbs (untyped message) -> untyped
34
34
  def to_log(message)
35
- if message.respond_to?(:to_h) && message.class.ancestors.any? { |a| a.name&.start_with?("CMDx::") }
35
+ if message.respond_to?(:to_h) && cmdx_based_object?(message.class)
36
36
  message.to_h
37
37
  else
38
38
  message
@@ -61,6 +61,22 @@ module CMDx
61
61
  hash.map(&block).join(" ")
62
62
  end
63
63
 
64
+ private
65
+
66
+ # Checks if a class belongs to the CMDx namespace, caching per class.
67
+ #
68
+ # @param klass [Class] The class to check
69
+ #
70
+ # @return [Boolean] true if the class is in the CMDx namespace
71
+ #
72
+ # @rbs (Class klass) -> bool
73
+ def cmdx_based_object?(klass)
74
+ @cmdx_classes ||= {}
75
+ return @cmdx_classes[klass] if @cmdx_classes.key?(klass)
76
+
77
+ @cmdx_classes[klass] = klass.ancestors.any? { |a| a.name&.start_with?("CMDx::") }
78
+ end
79
+
64
80
  end
65
81
  end
66
82
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ module Utils
5
+ # Provides normalization utilities for a variety of objects
6
+ # into consistent formats.
7
+ module Normalize
8
+
9
+ extend self
10
+
11
+ # Normalizes an exception into a string representation.
12
+ #
13
+ # @param exception [Exception] The exception to normalize
14
+ #
15
+ # @return [String] The normalized exception string
16
+ #
17
+ # @example From exception
18
+ # Normalize.exception(StandardError.new("test"))
19
+ # # => "[StandardError] test"
20
+ #
21
+ # @rbs (Exception exception) -> String
22
+ def exception(exception)
23
+ "[#{exception.class}] #{exception.message}"
24
+ end
25
+
26
+ # Normalizes an object into an array of unique status strings.
27
+ #
28
+ # @param object [Object] The object to normalize into status strings
29
+ #
30
+ # @return [Array<String>] Unique status strings
31
+ #
32
+ # @example From array of symbols
33
+ # Normalize.statuses([:success, :pending, :success])
34
+ # # => ["success", "pending"]
35
+ # @example From single value
36
+ # Normalize.statuses(:success)
37
+ # # => ["success"]
38
+ # @example From nil
39
+ # Normalize.statuses(nil)
40
+ # # => []
41
+ #
42
+ # @rbs (untyped object) -> Array[String]
43
+ def statuses(object)
44
+ ary = Wrap.array(object)
45
+ return EMPTY_ARRAY if ary.empty?
46
+
47
+ ary.map(&:to_s).uniq
48
+ end
49
+
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ module Utils
5
+ # Provides array wrapping utilities for normalizing input values
6
+ # into consistent array structures.
7
+ module Wrap
8
+
9
+ extend self
10
+
11
+ # Wraps an object in an array if it is not already an array.
12
+ #
13
+ # @param object [Object] The object to wrap in an array
14
+ #
15
+ # @return [Array] The wrapped array
16
+ #
17
+ # @example Already an array
18
+ # Wrap.array([1, 2, 3])
19
+ # # => [1, 2, 3]
20
+ # @example Single value
21
+ # Wrap.array(1)
22
+ # # => [1]
23
+ # @example Nil value
24
+ # Wrap.array(nil)
25
+ # # => []
26
+ #
27
+ # @rbs (untyped object) -> Array[untyped]
28
+ def array(object)
29
+ case object
30
+ when Array then object
31
+ when NilClass then EMPTY_ARRAY
32
+ else Array(object)
33
+ end
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -3,21 +3,13 @@
3
3
  module CMDx
4
4
  # Registry for managing validation rules and their corresponding validator classes.
5
5
  # Provides methods to register, deregister, and execute validators against task values.
6
+ #
7
+ # Supports copy-on-write semantics: a duped registry shares the parent's
8
+ # data until a write operation triggers materialization.
6
9
  class ValidatorRegistry
7
10
 
8
11
  extend Forwardable
9
12
 
10
- # Returns the internal registry mapping validator types to classes.
11
- #
12
- # @return [Hash{Symbol => Class}] Hash of validator type names to validator classes
13
- #
14
- # @example
15
- # registry.registry # => { presence: Validators::Presence, format: Validators::Format }
16
- #
17
- # @rbs @registry: Hash[Symbol, Class]
18
- attr_reader :registry
19
- alias to_h registry
20
-
21
13
  def_delegators :registry, :keys
22
14
 
23
15
  # Initialize a new validator registry with default validators.
@@ -39,14 +31,30 @@ module CMDx
39
31
  }
40
32
  end
41
33
 
42
- # Create a duplicate of the registry with copied internal state.
34
+ # Sets up copy-on-write state when duplicated via dup.
35
+ #
36
+ # @param source [ValidatorRegistry] The registry being duplicated
37
+ #
38
+ # @rbs (ValidatorRegistry source) -> void
39
+ def initialize_dup(source)
40
+ @parent = source
41
+ @registry = nil
42
+ super
43
+ end
44
+
45
+ # Returns the internal registry mapping validator types to classes.
46
+ # Delegates to the parent registry when not yet materialized.
43
47
  #
44
- # @return [ValidatorRegistry] A new validator registry with duplicated registry hash
48
+ # @return [Hash{Symbol => Class}] Hash of validator type names to validator classes
45
49
  #
46
- # @rbs () -> ValidatorRegistry
47
- def dup
48
- self.class.new(registry.dup)
50
+ # @example
51
+ # registry.registry # => { presence: Validators::Presence, format: Validators::Format }
52
+ #
53
+ # @rbs () -> Hash[Symbol, Class]
54
+ def registry
55
+ @registry || @parent.registry
49
56
  end
57
+ alias to_h registry
50
58
 
51
59
  # Register a new validator class with the given name.
52
60
  #
@@ -61,7 +69,9 @@ module CMDx
61
69
  #
62
70
  # @rbs ((String | Symbol) name, Class validator) -> self
63
71
  def register(name, validator)
64
- registry[name.to_sym] = validator
72
+ materialize!
73
+
74
+ @registry[name.to_sym] = validator
65
75
  self
66
76
  end
67
77
 
@@ -77,7 +87,9 @@ module CMDx
77
87
  #
78
88
  # @rbs ((String | Symbol) name) -> self
79
89
  def deregister(name)
80
- registry.delete(name.to_sym)
90
+ materialize!
91
+
92
+ @registry.delete(name.to_sym)
81
93
  self
82
94
  end
83
95
 
@@ -96,7 +108,7 @@ module CMDx
96
108
  # registry.validate(:length, task, password, { min: 8, allow_nil: false })
97
109
  #
98
110
  # @rbs (Symbol type, Task task, untyped value, untyped options) -> untyped
99
- def validate(type, task, value, options = {})
111
+ def validate(type, task, value, options = EMPTY_HASH)
100
112
  raise TypeError, "unknown validator type #{type.inspect}" unless registry.key?(type)
101
113
 
102
114
  match =
@@ -114,5 +126,18 @@ module CMDx
114
126
  Utils::Call.invoke(task, registry[type], value, options)
115
127
  end
116
128
 
129
+ private
130
+
131
+ # Copies the parent's registry data into this instance,
132
+ # severing the copy-on-write link.
133
+ #
134
+ # @rbs () -> void
135
+ def materialize!
136
+ return if @registry
137
+
138
+ @registry = @parent.registry.dup
139
+ @parent = nil
140
+ end
141
+
117
142
  end
118
143
  end