cmdx 1.18.0 → 1.20.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.DS_Store +0 -0
  3. data/CHANGELOG.md +73 -9
  4. data/README.md +1 -1
  5. data/lib/cmdx/attribute.rb +88 -20
  6. data/lib/cmdx/attribute_registry.rb +79 -8
  7. data/lib/cmdx/attribute_value.rb +8 -3
  8. data/lib/cmdx/callback_registry.rb +60 -26
  9. data/lib/cmdx/chain.rb +47 -4
  10. data/lib/cmdx/coercion_registry.rb +42 -20
  11. data/lib/cmdx/coercions/array.rb +8 -3
  12. data/lib/cmdx/coercions/big_decimal.rb +1 -1
  13. data/lib/cmdx/coercions/boolean.rb +6 -2
  14. data/lib/cmdx/coercions/complex.rb +1 -1
  15. data/lib/cmdx/coercions/date.rb +2 -7
  16. data/lib/cmdx/coercions/date_time.rb +2 -7
  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 +4 -5
  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 -7
  24. data/lib/cmdx/configuration.rb +26 -0
  25. data/lib/cmdx/context.rb +9 -6
  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 +77 -54
  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/timeout.rb +11 -10
  35. data/lib/cmdx/parallelizer.rb +100 -0
  36. data/lib/cmdx/pipeline.rb +42 -23
  37. data/lib/cmdx/railtie.rb +1 -1
  38. data/lib/cmdx/result.rb +27 -11
  39. data/lib/cmdx/retry.rb +166 -0
  40. data/lib/cmdx/settings.rb +222 -0
  41. data/lib/cmdx/task.rb +53 -61
  42. data/lib/cmdx/utils/format.rb +17 -1
  43. data/lib/cmdx/utils/normalize.rb +52 -0
  44. data/lib/cmdx/utils/wrap.rb +38 -0
  45. data/lib/cmdx/validator_registry.rb +45 -20
  46. data/lib/cmdx/validators/absence.rb +1 -1
  47. data/lib/cmdx/validators/exclusion.rb +2 -2
  48. data/lib/cmdx/validators/format.rb +1 -1
  49. data/lib/cmdx/validators/inclusion.rb +2 -2
  50. data/lib/cmdx/validators/length.rb +1 -1
  51. data/lib/cmdx/validators/numeric.rb +1 -1
  52. data/lib/cmdx/validators/presence.rb +1 -1
  53. data/lib/cmdx/version.rb +1 -1
  54. data/lib/cmdx.rb +12 -0
  55. data/lib/generators/cmdx/templates/install.rb +11 -0
  56. data/mkdocs.yml +5 -1
  57. metadata +6 -15
data/lib/cmdx/pipeline.rb CHANGED
@@ -6,6 +6,14 @@ module CMDx
6
6
  # and handling breakpoints that can interrupt execution at specific task statuses.
7
7
  class Pipeline
8
8
 
9
+ # @rbs SEQUENTIAL_REGEXP: Regexp
10
+ SEQUENTIAL_REGEXP = /\Asequential\z/
11
+ private_constant :SEQUENTIAL_REGEXP
12
+
13
+ # @rbs PARALLEL_REGEXP: Regexp
14
+ PARALLEL_REGEXP = /\Aparallel\z/
15
+ private_constant :PARALLEL_REGEXP
16
+
9
17
  # Returns the workflow being executed by this pipeline.
10
18
  #
11
19
  # @return [Workflow] The workflow instance
@@ -54,13 +62,20 @@ module CMDx
54
62
  #
55
63
  # @rbs () -> void
56
64
  def execute
65
+ default_breakpoints = Utils::Normalize.statuses(
66
+ workflow.class.settings.breakpoints ||
67
+ workflow.class.settings.workflow_breakpoints
68
+ )
69
+
57
70
  workflow.class.pipeline.each do |group|
58
71
  next unless Utils::Condition.evaluate(workflow, group.options)
59
72
 
60
- breakpoints = group.options[:breakpoints] ||
61
- workflow.class.settings[:breakpoints] ||
62
- workflow.class.settings[:workflow_breakpoints]
63
- breakpoints = Array(breakpoints).map(&:to_s).uniq
73
+ breakpoints =
74
+ if group.options.key?(:breakpoints)
75
+ Utils::Normalize.statuses(group.options[:breakpoints])
76
+ else
77
+ default_breakpoints
78
+ end
64
79
 
65
80
  execute_group_tasks(group, breakpoints)
66
81
  end
@@ -82,8 +97,8 @@ module CMDx
82
97
  # @rbs (untyped group, Array[String] breakpoints) -> void
83
98
  def execute_group_tasks(group, breakpoints)
84
99
  case strategy = group.options[:strategy]
85
- when NilClass, /sequential/ then execute_tasks_in_sequence(group, breakpoints)
86
- when /parallel/ then execute_tasks_in_parallel(group, breakpoints)
100
+ when NilClass, SEQUENTIAL_REGEXP then execute_tasks_in_sequence(group, breakpoints)
101
+ when PARALLEL_REGEXP then execute_tasks_in_parallel(group, breakpoints)
87
102
  else raise "unknown execution strategy #{strategy.inspect}"
88
103
  end
89
104
  end
@@ -111,39 +126,43 @@ module CMDx
111
126
  end
112
127
  end
113
128
 
114
- # Executes tasks in parallel using the parallel gem.
129
+ # Each task receives a snapshot of the workflow context to prevent
130
+ # unsynchronized concurrent writes to a shared Hash. Snapshots are
131
+ # merged back into the workflow context after all tasks complete.
115
132
  #
116
133
  # @param group [CMDx::Group] The task group to execute in parallel
117
- # @param breakpoints [Array<Symbol>] Status values that trigger execution breaks
118
- # @option group.options [Integer] :in_threads Number of threads to use
119
- # @option group.options [Integer] :in_processes Number of processes to use
134
+ # @param breakpoints [Array<String>] Status values that trigger execution breaks
135
+ # @option group.options [Integer] :pool_size Number of concurrent threads (defaults to task count)
120
136
  #
121
137
  # @return [void]
122
138
  #
123
- # @raise [HaltError] When a task result status matches a breakpoint
139
+ # @raise [Fault] When a task result status matches a breakpoint
124
140
  #
125
141
  # @example
126
142
  # execute_tasks_in_parallel(group, ["failed"])
127
143
  #
128
144
  # @rbs (untyped group, Array[String] breakpoints) -> void
129
145
  def execute_tasks_in_parallel(group, breakpoints)
130
- raise "install the `parallel` gem to use this feature" unless defined?(Parallel)
131
-
132
- parallel_options = group.options.slice(:in_threads, :in_processes)
133
- throwable_result = nil
146
+ contexts = group.tasks.map { Context.new(workflow.context.to_h) }
147
+ ctx_pairs = group.tasks.zip(contexts)
148
+ pool_size = group.options.fetch(:pool_size, ctx_pairs.size)
134
149
 
135
- Parallel.each(group.tasks, **parallel_options) do |task|
150
+ results = Parallelizer.call(ctx_pairs, pool_size:) do |task, context|
136
151
  Chain.current = workflow.chain
137
-
138
- task_result = task.execute(workflow.context)
139
- next unless breakpoints.include?(task_result.status)
140
-
141
- raise Parallel::Break, throwable_result = task_result
152
+ task.execute(context)
142
153
  end
143
154
 
144
- return if throwable_result.nil?
155
+ contexts.each { |ctx| workflow.context.merge!(ctx) }
156
+
157
+ faulted = results.select { |r| breakpoints.include?(r.status) }
158
+ return if faulted.empty?
145
159
 
146
- workflow.throw!(throwable_result)
160
+ workflow.public_send(
161
+ :"#{faulted.last.status}!",
162
+ Locale.t("cmdx.faults.unspecified"),
163
+ source: :parallel,
164
+ faults: faulted.map(&:to_h)
165
+ )
147
166
  end
148
167
 
149
168
  end
data/lib/cmdx/railtie.rb CHANGED
@@ -24,7 +24,7 @@ module CMDx
24
24
  #
25
25
  # @rbs (untyped app) -> void
26
26
  initializer("cmdx.configure_locales") do |app|
27
- Array(app.config.i18n.available_locales).each do |locale|
27
+ Utils::Wrap.array(app.config.i18n.available_locales).each do |locale|
28
28
  path = CMDx.gem_path.join("lib/locales/#{locale}.yml")
29
29
  next unless File.file?(path)
30
30
 
data/lib/cmdx/result.rb CHANGED
@@ -28,13 +28,17 @@ module CMDx
28
28
 
29
29
  # @rbs STRIP_FAILURE: Proc
30
30
  STRIP_FAILURE = proc do |hash, result, key|
31
- unless result.send(:"#{key}?")
31
+ unless result.public_send(:"#{key}?")
32
32
  # Strip caused/threw failures since its the same info as the log line
33
- hash[key] = result.send(key).to_h.except(:caused_failure, :threw_failure)
33
+ hash[key] = result.public_send(key).to_h.except(:caused_failure, :threw_failure)
34
34
  end
35
35
  end.freeze
36
36
  private_constant :STRIP_FAILURE
37
37
 
38
+ # @rbs FAILURE_KEY_REGEX: Regexp
39
+ FAILURE_KEY_REGEX = /_failure\z/
40
+ private_constant :FAILURE_KEY_REGEX
41
+
38
42
  # Returns the task instance associated with this result.
39
43
  #
40
44
  # @return [CMDx::Task] The task instance
@@ -261,7 +265,7 @@ module CMDx
261
265
  def on(*states_or_statuses, &)
262
266
  raise ArgumentError, "block required" unless block_given?
263
267
 
264
- yield(self) if states_or_statuses.any? { |s| send(:"#{s}?") }
268
+ yield(self) if states_or_statuses.any? { |s| public_send(:"#{s}?") }
265
269
  self
266
270
  end
267
271
 
@@ -338,7 +342,7 @@ module CMDx
338
342
  unless frames.empty?
339
343
  frames = frames.map(&:to_s)
340
344
 
341
- if (cleaner = task.class.settings[:backtrace_cleaner])
345
+ if (cleaner = task.class.settings.backtrace_cleaner)
342
346
  cleaner.call(frames)
343
347
  end
344
348
 
@@ -383,7 +387,7 @@ module CMDx
383
387
  def caused_failure
384
388
  return unless failed?
385
389
 
386
- chain.results.reverse.find(&:failed?)
390
+ chain.results.reverse_each.find(&:failed?)
387
391
  end
388
392
 
389
393
  # @return [Boolean] Whether this result caused the failure
@@ -411,8 +415,17 @@ module CMDx
411
415
  return unless failed?
412
416
 
413
417
  current = index
414
- results = chain.results.select(&:failed?)
415
- results.find { |r| r.index > current } || results.last
418
+ last_failed = nil
419
+
420
+ chain.results.each do |r|
421
+ next unless r.failed?
422
+
423
+ return r if r.index > current
424
+
425
+ last_failed = r
426
+ end
427
+
428
+ last_failed
416
429
  end
417
430
 
418
431
  # @return [Boolean] Whether this result threw the failure
@@ -469,7 +482,7 @@ module CMDx
469
482
  #
470
483
  # @rbs () -> Integer
471
484
  def index
472
- chain.index(self)
485
+ @chain_index || chain.index(self)
473
486
  end
474
487
 
475
488
  # @return [String] The outcome of the task execution
@@ -513,13 +526,16 @@ module CMDx
513
526
  #
514
527
  # @example
515
528
  # result.to_s # => "task_id=my_task state=complete status=success"
529
+ # @example With failure
530
+ # result.to_s # => "task_id=my_task state=complete status=failed threw_failure=<[1] MyTask: my_task>"
516
531
  #
517
532
  # @rbs () -> String
518
533
  def to_s
519
534
  Utils::Format.to_str(to_h) do |key, value|
520
- case key
521
- when /failure/ then "#{key}=<[#{value[:index]}] #{value[:class]}: #{value[:id]}>"
522
- else "#{key}=#{value.inspect}"
535
+ if FAILURE_KEY_REGEX.match?(key)
536
+ "#{key}=<[#{value[:index]}] #{value[:class]}: #{value[:id]}>"
537
+ else
538
+ "#{key}=#{value.inspect}"
523
539
  end
524
540
  end
525
541
  end
data/lib/cmdx/retry.rb ADDED
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ # Manages retry logic and state for task execution.
5
+ #
6
+ # The Retry class tracks retry availability, attempt counts, and
7
+ # remaining retries for a given task. It also resolves exception
8
+ # matching and computes wait times using configurable jitter strategies.
9
+ class Retry
10
+
11
+ # Returns the task instance associated with this retry.
12
+ #
13
+ # @return [Task] the task being retried
14
+ #
15
+ # @example
16
+ # retry_instance.task # => #<CreateUser ...>
17
+ #
18
+ # @rbs @task: Task
19
+ attr_reader :task
20
+
21
+ # Creates a new Retry instance for the given task.
22
+ #
23
+ # @param task [Task] the task to manage retries for
24
+ #
25
+ # @return [Retry] a new Retry instance
26
+ #
27
+ # @example
28
+ # retry_instance = Retry.new(task)
29
+ #
30
+ # @rbs (Task task) -> void
31
+ def initialize(task)
32
+ @task = task
33
+ end
34
+
35
+ # Returns the total number of retries configured for the task.
36
+ #
37
+ # @return [Integer] the configured retry count
38
+ #
39
+ # @example
40
+ # retry_instance.available # => 3
41
+ #
42
+ # @rbs () -> Integer
43
+ def available
44
+ Integer(task.class.settings.retries || 0)
45
+ end
46
+
47
+ # Checks if the task has any retries configured.
48
+ #
49
+ # @return [Boolean] true if retries are configured
50
+ #
51
+ # @example
52
+ # retry_instance.available? # => true
53
+ #
54
+ # @rbs () -> bool
55
+ def available?
56
+ available.positive?
57
+ end
58
+
59
+ # Returns the number of retry attempts already made.
60
+ #
61
+ # @return [Integer] the current retry attempt count
62
+ #
63
+ # @example
64
+ # retry_instance.attempts # => 1
65
+ #
66
+ # @rbs () -> Integer
67
+ def attempts
68
+ Integer(task.result.retries || 0)
69
+ end
70
+
71
+ # Checks if the task has been retried at least once.
72
+ #
73
+ # @return [Boolean] true if at least one retry has occurred
74
+ #
75
+ # @example
76
+ # retry_instance.retried? # => true
77
+ #
78
+ # @rbs () -> bool
79
+ def retried?
80
+ attempts.positive?
81
+ end
82
+
83
+ # Returns the number of retries still available.
84
+ #
85
+ # @return [Integer] the remaining retry count
86
+ #
87
+ # @example
88
+ # retry_instance.remaining # => 2
89
+ #
90
+ # @rbs () -> Integer
91
+ def remaining
92
+ available - attempts
93
+ end
94
+
95
+ # Checks if there are retries still available.
96
+ #
97
+ # @return [Boolean] true if remaining retries exist
98
+ #
99
+ # @example
100
+ # retry_instance.remaining? # => true
101
+ #
102
+ # @rbs () -> bool
103
+ def remaining?
104
+ remaining.positive?
105
+ end
106
+
107
+ # Returns the list of exception classes eligible for retry.
108
+ #
109
+ # @return [Array<Class>] exception classes that trigger a retry
110
+ #
111
+ # @example
112
+ # retry_instance.exceptions # => [StandardError, CMDx::TimeoutError]
113
+ #
114
+ # @rbs () -> Array[Class]
115
+ def exceptions
116
+ @exceptions ||= Utils::Wrap.array(
117
+ task.class.settings.retry_on ||
118
+ [StandardError, CMDx::TimeoutError]
119
+ )
120
+ end
121
+
122
+ # Checks if the given exception matches any configured retry exception.
123
+ #
124
+ # @param exception [Exception] the exception to check
125
+ #
126
+ # @return [Boolean] true if the exception qualifies for retry
127
+ #
128
+ # @example
129
+ # retry_instance.exception?(RuntimeError.new("fail")) # => true
130
+ #
131
+ # @rbs (Exception exception) -> bool
132
+ def exception?(exception)
133
+ exceptions.any? { |e| exception.class <= e }
134
+ end
135
+
136
+ # Computes the wait time before the next retry attempt.
137
+ #
138
+ # Supports multiple jitter strategies: a Symbol calls a task method,
139
+ # a Proc is evaluated in the task instance context, a callable object
140
+ # receives the task and attempts, and a Numeric is multiplied by the
141
+ # attempt count.
142
+ #
143
+ # @return [Float] the wait duration in seconds
144
+ #
145
+ # @example With numeric jitter (0.5 * attempts)
146
+ # retry_instance.wait # => 1.0
147
+ # @example With symbol jitter referencing a task method
148
+ # retry_instance.wait # => 2.5
149
+ #
150
+ # @rbs () -> Float
151
+ def wait
152
+ jitter = task.class.settings.retry_jitter
153
+
154
+ if jitter.is_a?(Symbol)
155
+ task.send(jitter, attempts)
156
+ elsif jitter.is_a?(Proc)
157
+ task.instance_exec(attempts, &jitter)
158
+ elsif jitter.respond_to?(:call)
159
+ jitter.call(task, attempts)
160
+ else
161
+ jitter.to_f * attempts
162
+ end.to_f
163
+ end
164
+
165
+ end
166
+ end
@@ -0,0 +1,222 @@
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] rollback_on
117
+ # @return [Array<String>] Statuses that trigger rollback
118
+ delegate_to_configuration :rollback_on
119
+
120
+ # @!attribute [rw] task_breakpoints
121
+ # @return [Array<String>] Default task breakpoint statuses
122
+ delegate_to_configuration :task_breakpoints
123
+
124
+ # @!attribute [rw] workflow_breakpoints
125
+ # @return [Array<String>] Default workflow breakpoint statuses
126
+ delegate_to_configuration :workflow_breakpoints
127
+
128
+ # @!attribute [rw] backtrace_cleaner
129
+ # @return [Proc, nil] The backtrace cleaner proc
130
+ delegate_to_parent :backtrace_cleaner, with_fallback: true
131
+
132
+ # @!attribute [rw] breakpoints
133
+ # @return [Array<String>, nil] Per-task breakpoints override
134
+ delegate_to_parent :breakpoints
135
+
136
+ # @!attribute [rw] deprecate
137
+ # @return [Symbol, Proc, Boolean, nil] Deprecation behavior
138
+ delegate_to_parent :deprecate
139
+
140
+ # @!attribute [rw] exception_handler
141
+ # @return [Proc, nil] The exception handler proc
142
+ delegate_to_parent :exception_handler, with_fallback: true
143
+
144
+ # @!attribute [rw] logger
145
+ # @return [Logger] The logger instance
146
+ delegate_to_parent :logger, with_fallback: true
147
+
148
+ # @!attribute [rw] log_formatter
149
+ # @return [Proc, nil] Per-task log formatter override
150
+ delegate_to_parent :log_formatter
151
+
152
+ # @!attribute [rw] log_level
153
+ # @return [Integer, nil] Per-task log level override
154
+ delegate_to_parent :log_level
155
+
156
+ # @!attribute [rw] retries
157
+ # @return [Integer, nil] Number of retries on failure
158
+ delegate_to_parent :retries
159
+
160
+ # @!attribute [rw] retry_jitter
161
+ # @return [Numeric, Symbol, Proc, nil] Jitter between retries
162
+ delegate_to_parent :retry_jitter
163
+
164
+ # @!attribute [rw] retry_on
165
+ # @return [Array<Class>, Class, nil] Exception classes to retry on
166
+ delegate_to_parent :retry_on
167
+
168
+ # Creates a new Settings instance, inheriting registries from a parent
169
+ # Settings or the global Configuration. Scalar settings are resolved
170
+ # lazily via delegation rather than eagerly copied.
171
+ #
172
+ # @param parent [Settings, nil] Parent settings to inherit from
173
+ # @param overrides [Hash] Field values to override after inheritance
174
+ #
175
+ # @example
176
+ # Settings.new(parent: ParentTask.settings, deprecate: true)
177
+ #
178
+ # @rbs (?parent: Settings?, **untyped overrides) -> void
179
+ def initialize(parent: nil, **overrides)
180
+ @parent = parent
181
+
182
+ init_registries
183
+ init_collections
184
+
185
+ overrides.each { |key, value| public_send(:"#{key}=", value) }
186
+ end
187
+
188
+ private
189
+
190
+ # Dups registries from the parent Settings or global Configuration
191
+ # so each task class gets its own mutable copy.
192
+ #
193
+ # @rbs () -> void
194
+ def init_registries
195
+ if @parent
196
+ @middlewares = @parent.middlewares.dup
197
+ @callbacks = @parent.callbacks.dup
198
+ @coercions = @parent.coercions.dup
199
+ @validators = @parent.validators.dup
200
+ @attributes = @parent.attributes.dup
201
+ else
202
+ config = CMDx.configuration
203
+
204
+ @middlewares = config.middlewares.dup
205
+ @callbacks = config.callbacks.dup
206
+ @coercions = config.coercions.dup
207
+ @validators = config.validators.dup
208
+ @attributes = AttributeRegistry.new
209
+ end
210
+ end
211
+
212
+ # Initializes array-valued settings that need their own copy
213
+ # to avoid cross-class mutation.
214
+ #
215
+ # @rbs () -> void
216
+ def init_collections
217
+ @returns = @parent&.returns&.dup || EMPTY_ARRAY
218
+ @tags = @parent&.tags&.dup || EMPTY_ARRAY
219
+ end
220
+
221
+ end
222
+ end