cmdx 1.20.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 +131 -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 -225
  23. data/lib/cmdx/context.rb +263 -242
  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 +252 -473
  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 -196
  64. data/lib/cmdx/signal.rb +165 -0
  65. data/lib/cmdx/task.rb +443 -336
  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 +74 -82
  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 +128 -52
  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 +9 -6
  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 -374
  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 -62
  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 -53
  112. data/lib/locales/ar.yml +0 -53
  113. data/lib/locales/az.yml +0 -53
  114. data/lib/locales/be.yml +0 -53
  115. data/lib/locales/bg.yml +0 -53
  116. data/lib/locales/bn.yml +0 -53
  117. data/lib/locales/bs.yml +0 -53
  118. data/lib/locales/ca.yml +0 -53
  119. data/lib/locales/cnr.yml +0 -53
  120. data/lib/locales/cs.yml +0 -53
  121. data/lib/locales/cy.yml +0 -53
  122. data/lib/locales/da.yml +0 -53
  123. data/lib/locales/de.yml +0 -53
  124. data/lib/locales/dz.yml +0 -53
  125. data/lib/locales/el.yml +0 -53
  126. data/lib/locales/eo.yml +0 -53
  127. data/lib/locales/es.yml +0 -53
  128. data/lib/locales/et.yml +0 -53
  129. data/lib/locales/eu.yml +0 -53
  130. data/lib/locales/fa.yml +0 -53
  131. data/lib/locales/fi.yml +0 -53
  132. data/lib/locales/fr.yml +0 -53
  133. data/lib/locales/fy.yml +0 -53
  134. data/lib/locales/gd.yml +0 -53
  135. data/lib/locales/gl.yml +0 -53
  136. data/lib/locales/he.yml +0 -53
  137. data/lib/locales/hi.yml +0 -53
  138. data/lib/locales/hr.yml +0 -53
  139. data/lib/locales/hu.yml +0 -53
  140. data/lib/locales/hy.yml +0 -53
  141. data/lib/locales/id.yml +0 -53
  142. data/lib/locales/is.yml +0 -53
  143. data/lib/locales/it.yml +0 -53
  144. data/lib/locales/ja.yml +0 -53
  145. data/lib/locales/ka.yml +0 -53
  146. data/lib/locales/kk.yml +0 -53
  147. data/lib/locales/km.yml +0 -53
  148. data/lib/locales/kn.yml +0 -53
  149. data/lib/locales/ko.yml +0 -53
  150. data/lib/locales/lb.yml +0 -53
  151. data/lib/locales/lo.yml +0 -53
  152. data/lib/locales/lt.yml +0 -53
  153. data/lib/locales/lv.yml +0 -53
  154. data/lib/locales/mg.yml +0 -53
  155. data/lib/locales/mk.yml +0 -53
  156. data/lib/locales/ml.yml +0 -53
  157. data/lib/locales/mn.yml +0 -53
  158. data/lib/locales/mr-IN.yml +0 -53
  159. data/lib/locales/ms.yml +0 -53
  160. data/lib/locales/nb.yml +0 -53
  161. data/lib/locales/ne.yml +0 -53
  162. data/lib/locales/nl.yml +0 -53
  163. data/lib/locales/nn.yml +0 -53
  164. data/lib/locales/oc.yml +0 -53
  165. data/lib/locales/or.yml +0 -53
  166. data/lib/locales/pa.yml +0 -53
  167. data/lib/locales/pl.yml +0 -53
  168. data/lib/locales/pt.yml +0 -53
  169. data/lib/locales/rm.yml +0 -53
  170. data/lib/locales/ro.yml +0 -53
  171. data/lib/locales/ru.yml +0 -53
  172. data/lib/locales/sc.yml +0 -53
  173. data/lib/locales/sk.yml +0 -53
  174. data/lib/locales/sl.yml +0 -53
  175. data/lib/locales/sq.yml +0 -53
  176. data/lib/locales/sr.yml +0 -53
  177. data/lib/locales/st.yml +0 -53
  178. data/lib/locales/sv.yml +0 -53
  179. data/lib/locales/sw.yml +0 -53
  180. data/lib/locales/ta.yml +0 -53
  181. data/lib/locales/te.yml +0 -53
  182. data/lib/locales/th.yml +0 -53
  183. data/lib/locales/tl.yml +0 -53
  184. data/lib/locales/tr.yml +0 -53
  185. data/lib/locales/tt.yml +0 -53
  186. data/lib/locales/ug.yml +0 -53
  187. data/lib/locales/uk.yml +0 -53
  188. data/lib/locales/ur.yml +0 -53
  189. data/lib/locales/uz.yml +0 -53
  190. data/lib/locales/vi.yml +0 -53
  191. data/lib/locales/wo.yml +0 -53
  192. data/lib/locales/zh-CN.yml +0 -53
  193. data/lib/locales/zh-HK.yml +0 -53
  194. data/lib/locales/zh-TW.yml +0 -53
  195. data/lib/locales/zh-YUE.yml +0 -53
@@ -1,47 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CMDx
4
- module Validators
5
- # Validates that a value is present and not empty
6
- #
7
- # This validator ensures that the given value exists and contains meaningful content.
8
- # It handles different value types appropriately:
9
- # - Strings: checks for non-whitespace characters
10
- # - Collections: checks for non-empty collections
11
- # - Other objects: checks for non-nil values
4
+ class Validators
5
+ # Validates that a value is present: non-`nil`, non-empty, and (for
6
+ # strings) not whitespace-only.
12
7
  module Presence
13
8
 
14
9
  extend self
15
10
 
16
- # Validates that a value is present and not empty
17
- #
18
- # @param value [Object] The value to validate for presence
19
- # @param options [Hash] Validation configuration options
20
- # @option options [String] :message Custom error message
21
- #
22
- # @return [nil] Returns nil if validation passes
23
- #
24
- # @raise [ValidationError] When the value is empty, nil, or contains only whitespace
25
- #
26
- # @example Validate string presence
27
- # Presence.call("hello world")
28
- # # => nil (validation passes)
29
- # @example Validate empty string
30
- # Presence.call(" ")
31
- # # => raises ValidationError
32
- # @example Validate array presence
33
- # Presence.call([1, 2, 3])
34
- # # => nil (validation passes)
35
- # @example Validate empty array
36
- # Presence.call([])
37
- # # => raises ValidationError
38
- # @example Validate with custom message
39
- # Presence.call(nil, message: "Value cannot be blank")
40
- # # => raises ValidationError with custom message
41
- #
42
- # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> nil
11
+ # @param value [Object]
12
+ # @param options [Hash{Symbol => Object}]
13
+ # @option options [String] :message override for the failure message
14
+ # @return [Validators::Failure, nil]
43
15
  def call(value, options = EMPTY_HASH)
44
- match =
16
+ present =
45
17
  if value.is_a?(String)
46
18
  /\S/.match?(value)
47
19
  elsif value.respond_to?(:empty?)
@@ -50,10 +22,9 @@ module CMDx
50
22
  !value.nil?
51
23
  end
52
24
 
53
- return if match
25
+ return if present
54
26
 
55
- message = options[:message] if options.is_a?(Hash)
56
- raise ValidationError, message || Locale.t("cmdx.validators.presence")
27
+ Failure.new(options[:message] || I18nProxy.t("cmdx.validators.presence"))
57
28
  end
58
29
 
59
30
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ class Validators
5
+ # Invokes an inline `:validate` handler. Used by {Validators#validate}
6
+ # for each handler passed under the `:validate` option.
7
+ module Validate
8
+
9
+ extend self
10
+
11
+ # @param task [Task] receiver for Symbol/Proc handlers, also passed to callable handlers
12
+ # @param value [Object]
13
+ # @param handler [Symbol, Proc, #call]
14
+ # @return [Validators::Failure, nil, Object] handler's return value
15
+ # @raise [ArgumentError] when `handler` isn't a supported type
16
+ def call(task, value, handler)
17
+ case handler
18
+ when Symbol
19
+ task.send(handler, value)
20
+ when Proc
21
+ task.instance_exec(value, &handler)
22
+ else
23
+ return handler.call(value, task) if handler.respond_to?(:call)
24
+
25
+ raise ArgumentError, "validate handler must be a Symbol, Proc, or respond to #call"
26
+ end
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ # Registry of named validators applied to resolved input/output values.
5
+ # Ships with built-ins for `:absence`, `:exclusion`, `:format`,
6
+ # `:inclusion`, `:length`, `:numeric`, `:presence`. Validators return a
7
+ # {Failure} on invalid input (recorded on `task.errors`) or `nil` on
8
+ # success. The `:validate` key supports inline callables.
9
+ class Validators
10
+
11
+ # Sentinel returned by a validator to signal invalid input. Runtime
12
+ # records its `message` on the task's errors.
13
+ Failure = Data.define(:message)
14
+
15
+ attr_reader :registry
16
+
17
+ def initialize
18
+ @registry = {
19
+ absence: Validators::Absence,
20
+ exclusion: Validators::Exclusion,
21
+ format: Validators::Format,
22
+ inclusion: Validators::Inclusion,
23
+ length: Validators::Length,
24
+ numeric: Validators::Numeric,
25
+ presence: Validators::Presence
26
+ }
27
+ end
28
+
29
+ # @param source [Validators] registry to duplicate
30
+ # @return [void]
31
+ def initialize_copy(source)
32
+ @registry = source.registry.dup
33
+ end
34
+
35
+ # Registers a named validator, overwriting any existing entry.
36
+ #
37
+ # @param name [Symbol]
38
+ # @param callable [#call, nil] pass either this or a block
39
+ # @param block [#call, nil] validator callable when `callable` is omitted
40
+ # @yield validator body — `call(value, options = {})`
41
+ # @return [Validators] self for chaining
42
+ # @raise [ArgumentError] when both `callable` and a block are given, or
43
+ # when the resolved validator isn't callable
44
+ def register(name, callable = nil, &block)
45
+ validator = callable || block
46
+
47
+ if callable && block
48
+ raise ArgumentError, "provide either a callable or a block, not both"
49
+ elsif !validator.respond_to?(:call)
50
+ raise ArgumentError, "validator must respond to #call"
51
+ end
52
+
53
+ registry[name.to_sym] = validator
54
+ self
55
+ end
56
+
57
+ # @param name [Symbol]
58
+ # @return [Validators] self for chaining
59
+ def deregister(name)
60
+ registry.delete(name.to_sym)
61
+ self
62
+ end
63
+
64
+ # @param name [Symbol]
65
+ # @return [#call]
66
+ # @raise [ArgumentError] when `name` isn't registered
67
+ def lookup(name)
68
+ registry[name] || begin
69
+ raise ArgumentError, "unknown validator: #{name}"
70
+ end
71
+ end
72
+
73
+ # Picks registered-validator keys out of a declaration's options and
74
+ # appends `:validate` (inline callable(s)) when present.
75
+ #
76
+ # @param options [Hash{Symbol => Object}] declaration options
77
+ # @option options [Object] :presence payload for the presence validator (`call`)
78
+ # @option options [Object] :absence payload for the absence validator (`call`)
79
+ # @option options [Object] :format payload for the format validator (`call`)
80
+ # @option options [Object] :inclusion payload for the inclusion validator (`call`)
81
+ # @option options [Object] :exclusion payload for the exclusion validator (`call`)
82
+ # @option options [Object] :length payload for the length validator (`call`)
83
+ # @option options [Object] :numeric payload for the numeric validator (`call`)
84
+ # @option options [Object, Array<Object>] :validate inline callable(s) (`Validators::Validate`)
85
+ # @return [Hash{Symbol => Object}] validator rules to run
86
+ def extract(options)
87
+ return EMPTY_HASH if options.empty?
88
+
89
+ rules = options.slice(*registry.keys)
90
+ rules = rules.merge(validate: options[:validate]) if options.key?(:validate)
91
+ rules
92
+ end
93
+
94
+ # @return [Boolean]
95
+ def empty?
96
+ registry.empty?
97
+ end
98
+
99
+ # @return [Integer]
100
+ def size
101
+ registry.size
102
+ end
103
+
104
+ # Runs every rule against `value`, recording a failure message on
105
+ # `task.errors` under `name` for each failure. Respects `:allow_nil`
106
+ # and `:if`/`:unless` per-rule.
107
+ #
108
+ # @param task [Task]
109
+ # @param name [Symbol] attribute name for error reporting
110
+ # @param value [Object] value being validated
111
+ # @param rules [Hash{Symbol => Object}] from {#extract}
112
+ # @return [void]
113
+ def validate(task, name, value, rules)
114
+ return if rules.empty?
115
+
116
+ rules.each do |type, raw_options|
117
+ if type == :validate
118
+ Array(raw_options).each do |handler|
119
+ result = Validators::Validate.call(task, value, handler)
120
+ task.errors.add(name, result.message) if result.is_a?(Failure)
121
+ end
122
+ next
123
+ end
124
+
125
+ options = normalize_options(raw_options)
126
+ next if options.nil?
127
+
128
+ next if options[:allow_nil] && value.nil?
129
+ next unless Util.satisfied?(options[:if], options[:unless], task, value)
130
+
131
+ result = lookup(type).call(value, options)
132
+ next unless result.is_a?(Failure)
133
+
134
+ task.errors.add(name, result.message)
135
+ end
136
+ end
137
+
138
+ private
139
+
140
+ # @param raw_options [Object] truthy flag, Hash, Array, Regexp, etc. from a declaration
141
+ # @return [Hash{Symbol => Object}, nil] normalized rule options, or nil when disabled
142
+ # @raise [ArgumentError] when `raw_options` has an unsupported shape
143
+ def normalize_options(raw_options)
144
+ case raw_options
145
+ when FalseClass, NilClass
146
+ nil
147
+ when TrueClass
148
+ EMPTY_HASH
149
+ when Hash
150
+ raw_options
151
+ when Array
152
+ { in: raw_options }
153
+ when Regexp
154
+ { with: raw_options }
155
+ else
156
+ raise ArgumentError, "unsupported validator option format: #{raw_options.inspect}"
157
+ end
158
+ end
159
+
160
+ end
161
+ end
data/lib/cmdx/version.rb CHANGED
@@ -2,9 +2,7 @@
2
2
 
3
3
  module CMDx
4
4
 
5
- # @return [String] the version of the CMDx gem
6
- #
7
- # @rbs return: String
8
- VERSION = "1.20.0"
5
+ # Gem version. Bumped on release; mirrored in the gemspec.
6
+ VERSION = "2.0.0"
9
7
 
10
8
  end
data/lib/cmdx/workflow.rb CHANGED
@@ -1,115 +1,107 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CMDx
4
- # Provides workflow execution capabilities by organizing tasks into execution groups.
5
- # Workflows allow you to define sequences of tasks that can be executed conditionally
6
- # with breakpoint handling and context management.
4
+ # Mixin that turns a {Task} subclass into a workflow: a pipeline of
5
+ # ordered task groups run sequentially or in parallel. Defining `#work`
6
+ # on a workflow is forbidden — `#work` is auto-generated to delegate to
7
+ # {Pipeline}. Subclasses inherit the parent's pipeline (via dup).
8
+ #
9
+ # @see Pipeline
7
10
  module Workflow
8
11
 
9
12
  module ClassMethods
10
13
 
11
- # Prevents redefinition of the work method to maintain workflow integrity.
12
- #
13
- # @param method_name [Symbol] The name of the method being added
14
- #
15
- # @raise [RuntimeError] If attempting to redefine the work method
16
- #
17
- # @example
18
- # class MyWorkflow
19
- # include CMDx::Workflow
20
- # # This would raise an error:
21
- # # def work; end
22
- # end
23
- #
24
- # @rbs (Symbol method_name) -> void
25
- def method_added(method_name)
26
- raise "cannot redefine #{name}##{method_name} method" if method_name == :work
27
-
14
+ # @api private
15
+ # @param subclass [Class] newly defined workflow subclass
16
+ # @return [void]
17
+ def inherited(subclass)
28
18
  super
19
+ subclass.instance_variable_set(:@pipeline, pipeline.dup)
29
20
  end
30
21
 
31
- # Returns the collection of execution groups for this workflow.
32
- #
33
- # @return [Array<ExecutionGroup>] Array of execution groups
34
- #
35
- # @example
36
- # class MyWorkflow
37
- # include CMDx::Workflow
38
- # task Task1
39
- # task Task2
40
- # puts pipeline.size # => 2
41
- # end
42
- #
43
- # @rbs () -> Array[ExecutionGroup]
22
+ # @return [Array<ExecutionGroup>] declared groups, in order
44
23
  def pipeline
45
24
  @pipeline ||= []
46
25
  end
47
26
 
48
- # Adds multiple tasks to the workflow with optional configuration.
49
- #
50
- # @param tasks [Array<Class>] Array of task classes to add
51
- # @param options [Hash] Configuration options for the task execution
52
- # @option options [Hash] :breakpoints Breakpoints that trigger workflow interruption
53
- # @option options [Hash] :conditions Conditional logic for task execution
54
- #
55
- # @raise [TypeError] If any task is not a CMDx::Task subclass
56
- #
57
- # @example
58
- # class MyWorkflow
59
- # include CMDx::Workflow
60
- # tasks ValidateTask, ProcessTask, NotifyTask, breakpoints: [:failure, :halt]
61
- # end
27
+ # Declares a task group. With no arguments, returns the pipeline.
28
+ # Tasks must be `Task` subclasses.
62
29
  #
63
- # @rbs (*untyped tasks, **untyped options) -> void
30
+ # @param tasks [Array<Class<Task>>]
31
+ # @param options [Hash{Symbol => Object}]
32
+ # @option options [:sequential, :parallel] :strategy (:sequential)
33
+ # @option options [Integer] :pool_size parallel worker/fiber count
34
+ # @option options [:threads, :fibers, #call] :executor (:threads) parallel
35
+ # dispatch backend. `:fibers` requires a `Fiber.scheduler` to be
36
+ # installed (e.g. `Async { ... }`). A custom callable accepting
37
+ # `jobs:, concurrency:, on_job:` may also be passed.
38
+ # @option options [:last_write_wins, :deep_merge, :no_merge, #call] :merger
39
+ # (:last_write_wins) how successful parallel contexts are folded back
40
+ # into the workflow context. Merging happens in declaration order. A
41
+ # callable `->(workflow_context, result) { ... }` may be passed to
42
+ # implement custom behavior (e.g. namespacing by task name).
43
+ # @option options [Boolean] :continue_on_failure (false) when `true`,
44
+ # run every task in the group to completion (even after a failure)
45
+ # and aggregate all failures into the workflow's `errors`. Each
46
+ # failed result's `errors` are merged in with keys namespaced as
47
+ # `"TaskClass.input"`; failures with no errors entries (bare
48
+ # `fail!("reason")`) record under `"TaskClass.<status>"` (e.g.
49
+ # `"MyTask.failed"`) with `result.reason` as the message (falling
50
+ # back to the localized `cmdx.reasons.unspecified` string when
51
+ # `reason` is nil). The pipeline still halts after the group with
52
+ # the first failure (declaration order) as the signal origin.
53
+ # Applies to both `:sequential` and `:parallel` strategies. When
54
+ # `false` (default), `:sequential` halts on the first failure and
55
+ # `:parallel` cancels pending tasks (in-flight tasks still finish).
56
+ # @option options [Symbol, Proc, #call] :if
57
+ # @option options [Symbol, Proc, #call] :unless
58
+ # @return [Array<ExecutionGroup>] the full pipeline
59
+ # @raise [DefinitionError] when called with options but no tasks
60
+ # @raise [TypeError] when any element isn't a `Task` subclass
64
61
  def tasks(*tasks, **options)
62
+ raise DefinitionError, "#{name}: cannot declare an empty task group" if tasks.empty?
63
+
65
64
  pipeline << ExecutionGroup.new(
66
- tasks.map do |task|
67
- next task if task.is_a?(Class) && (task <= Task)
65
+ tasks:
66
+ tasks.map do |task|
67
+ next task if task.is_a?(Class) && (task <= Task)
68
68
 
69
- raise TypeError, "must be a CMDx::Task"
70
- end,
71
- options
69
+ raise TypeError, "#{task.inspect} is not a Task"
70
+ end,
71
+ options:
72
72
  )
73
73
  end
74
74
  alias task tasks
75
75
 
76
+ private
77
+
78
+ # Forbids user-defined `work` on workflows; `Workflow#work` delegates
79
+ # to {Pipeline}.
80
+ #
81
+ # @param method_name [Symbol] hook name reported by Ruby
82
+ # @return [void]
83
+ # @raise [ImplementationError] when a workflow defines `work`
84
+ def method_added(method_name)
85
+ return super unless method_name == :work
86
+
87
+ raise ImplementationError, "cannot define #{name}##{method_name} in a workflow"
88
+ end
89
+
76
90
  end
77
91
 
78
- # Represents a group of tasks with shared execution options.
79
- # @attr tasks [Array<Class>] Array of task classes in this group
80
- # @attr options [Hash] Configuration options for the group
81
- ExecutionGroup = Struct.new(:tasks, :options)
92
+ # Immutable declaration of a task group.
93
+ ExecutionGroup = Data.define(:tasks, :options)
82
94
 
83
- # Extends the including class with workflow capabilities.
84
- #
85
- # @param base [Class] The class including this module
86
- #
87
- # @example
88
- # class MyWorkflow
89
- # include CMDx::Workflow
90
- # # Now has access to task, tasks, and work methods
91
- # end
92
- #
93
- # @rbs (Class base) -> void
95
+ # @api private
96
+ # @param base [Class] task class including this mixin
97
+ # @return [void]
94
98
  def self.included(base)
95
99
  base.extend(ClassMethods)
96
100
  end
97
101
 
98
- # Executes the workflow by processing all tasks in the pipeline.
99
- # This method delegates execution to the Pipeline class which handles
100
- # the processing of tasks with proper error handling and context management.
101
- #
102
- # @example
103
- # class MyWorkflow
104
- # include CMDx::Workflow
105
- # task ValidateTask
106
- # task ProcessTask
107
- # end
108
- #
109
- # workflow = MyWorkflow.new
110
- # result = workflow.work
102
+ # Runs the workflow's pipeline. Not meant to be overridden.
111
103
  #
112
- # @rbs () -> void
104
+ # @return [void]
113
105
  def work
114
106
  Pipeline.execute(self)
115
107
  end
data/lib/cmdx.rb CHANGED
@@ -2,78 +2,147 @@
2
2
 
3
3
  require "bigdecimal"
4
4
  require "date"
5
- require "forwardable"
6
5
  require "json"
7
6
  require "logger"
8
- require "pathname"
9
7
  require "securerandom"
10
- require "set"
11
8
  require "time"
12
- require "timeout"
13
9
  require "yaml"
14
- require "zeitwerk"
15
10
 
16
11
  module CMDx
17
12
 
18
- # @rbs EMPTY_ARRAY: Array[untyped]
13
+ # Frozen empty array reused as a sentinel return value to avoid per-call
14
+ # allocations on hot paths.
15
+ #
16
+ # @api private
19
17
  EMPTY_ARRAY = [].freeze
20
18
  private_constant :EMPTY_ARRAY
21
19
 
22
- # @rbs EMPTY_HASH: Hash[untyped, untyped]
20
+ # Frozen empty hash reused as a sentinel return value to avoid per-call
21
+ # allocations on hot paths.
22
+ #
23
+ # @api private
23
24
  EMPTY_HASH = {}.freeze
24
25
  private_constant :EMPTY_HASH
25
26
 
26
- # @rbs EMPTY_STRING: String
27
+ # Shared empty string constant used as a sentinel default. Intentionally
28
+ # not frozen so callers may treat it as a mutable seed when needed.
29
+ #
30
+ # @api private
27
31
  EMPTY_STRING = ""
28
32
  private_constant :EMPTY_STRING
29
33
 
30
- extend self
34
+ # Root exception type for the library. Every CMDx-raised exception inherits
35
+ # from this class, so `rescue CMDx::Error` (or its alias `CMDx::Exception`)
36
+ # catches anything thrown by the framework without trapping unrelated
37
+ # `StandardError` descendants. {Fault} is the notable subclass propagated
38
+ # by `execute!`.
39
+ Error = Exception = Class.new(StandardError)
31
40
 
32
- # Returns the path to the CMDx gem.
33
- #
34
- # @return [Pathname] the path to the CMDx gem
35
- #
36
- # @example
37
- # CMDx.gem_path # => Pathname.new("/path/to/cmdx")
38
- #
39
- # @rbs return: Pathname
40
- def gem_path
41
- @gem_path ||= Pathname.new(__dir__).parent
42
- end
41
+ # Raised when an `around_execution` callback fails to invoke its
42
+ # continuation, which would otherwise silently skip the task body.
43
+ CallbackError = Class.new(Error)
43
44
 
44
- end
45
+ # Raised when a task or workflow attempts to define an input where an
46
+ # accessor with the same name already exists.
47
+ DefinitionError = Class.new(Error)
45
48
 
46
- # Set up Zeitwerk loader for the CMDx gem
47
- loader = Zeitwerk::Loader.for_gem
48
- loader.inflector.inflect("cmdx" => "CMDx", "json" => "JSON")
49
- loader.ignore("#{__dir__}/cmdx/configuration")
50
- loader.ignore("#{__dir__}/cmdx/exception")
51
- loader.ignore("#{__dir__}/cmdx/fault")
52
- loader.ignore("#{__dir__}/cmdx/railtie")
53
- loader.ignore("#{__dir__}/generators")
54
- loader.ignore("#{__dir__}/locales")
55
- loader.setup
49
+ # Raised by {Deprecation} when a task configured with `deprecation(:error)`
50
+ # is executed. Signals that the caller must migrate off the deprecated task
51
+ # before continuing.
52
+ DeprecationError = Class.new(Error)
56
53
 
57
- # Pre-load configuration to make module methods available
58
- # This is acceptable since configuration is fundamental to the framework
59
- require_relative "cmdx/configuration"
54
+ # Raised when a subclass fails to fulfill an abstract contract — most
55
+ # commonly when {Task} is invoked without overriding `#work`, or when a
56
+ # {Workflow} attempts to define `#work` itself.
57
+ ImplementationError = Class.new(Error)
60
58
 
61
- # Pre-load exceptions to make them available at the top level
62
- # This ensures CMDx::Error and its descendants are always available
63
- require_relative "cmdx/exception"
59
+ # Raised by the middleware chain when a registered middleware fails to
60
+ # yield to `next_link`, which would otherwise silently skip the task body.
61
+ MiddlewareError = Class.new(Error)
64
62
 
65
- # Pre-load fault classes to make them available at the top level
66
- # This ensures CMDx::FailFault and CMDx::SkipFault are always available
63
+ end
64
+
65
+ require_relative "cmdx/version"
67
66
  require_relative "cmdx/fault"
67
+ require_relative "cmdx/util"
68
+ require_relative "cmdx/i18n_proxy"
69
+ require_relative "cmdx/logger_proxy"
70
+ require_relative "cmdx/log_formatters/json"
71
+ require_relative "cmdx/log_formatters/key_value"
72
+ require_relative "cmdx/log_formatters/line"
73
+ require_relative "cmdx/log_formatters/logstash"
74
+ require_relative "cmdx/log_formatters/raw"
75
+ require_relative "cmdx/coercions/array"
76
+ require_relative "cmdx/coercions/big_decimal"
77
+ require_relative "cmdx/coercions/boolean"
78
+ require_relative "cmdx/coercions/complex"
79
+ require_relative "cmdx/coercions/date"
80
+ require_relative "cmdx/coercions/date_time"
81
+ require_relative "cmdx/coercions/float"
82
+ require_relative "cmdx/coercions/hash"
83
+ require_relative "cmdx/coercions/integer"
84
+ require_relative "cmdx/coercions/rational"
85
+ require_relative "cmdx/coercions/string"
86
+ require_relative "cmdx/coercions/symbol"
87
+ require_relative "cmdx/coercions/time"
88
+ require_relative "cmdx/coercions/coerce"
89
+ require_relative "cmdx/coercions"
90
+ require_relative "cmdx/validators/absence"
91
+ require_relative "cmdx/validators/exclusion"
92
+ require_relative "cmdx/validators/format"
93
+ require_relative "cmdx/validators/inclusion"
94
+ require_relative "cmdx/validators/length"
95
+ require_relative "cmdx/validators/numeric"
96
+ require_relative "cmdx/validators/presence"
97
+ require_relative "cmdx/validators/validate"
98
+ require_relative "cmdx/validators"
99
+ require_relative "cmdx/executors/thread"
100
+ require_relative "cmdx/executors/fiber"
101
+ require_relative "cmdx/executors"
102
+ require_relative "cmdx/mergers/last_write_wins"
103
+ require_relative "cmdx/mergers/deep_merge"
104
+ require_relative "cmdx/mergers/no_merge"
105
+ require_relative "cmdx/mergers"
106
+ require_relative "cmdx/retriers/exponential"
107
+ require_relative "cmdx/retriers/half_random"
108
+ require_relative "cmdx/retriers/full_random"
109
+ require_relative "cmdx/retriers/bounded_random"
110
+ require_relative "cmdx/retriers/linear"
111
+ require_relative "cmdx/retriers/fibonacci"
112
+ require_relative "cmdx/retriers/decorrelated_jitter"
113
+ require_relative "cmdx/retriers"
114
+ require_relative "cmdx/deprecators/log"
115
+ require_relative "cmdx/deprecators/warn"
116
+ require_relative "cmdx/deprecators/error"
117
+ require_relative "cmdx/deprecators"
118
+ require_relative "cmdx/input"
119
+ require_relative "cmdx/inputs"
120
+ require_relative "cmdx/output"
121
+ require_relative "cmdx/outputs"
122
+ require_relative "cmdx/callbacks"
123
+ require_relative "cmdx/middlewares"
124
+ require_relative "cmdx/telemetry"
125
+ require_relative "cmdx/settings"
126
+ require_relative "cmdx/retry"
127
+ require_relative "cmdx/deprecation"
128
+ require_relative "cmdx/context"
129
+ require_relative "cmdx/chain"
130
+ require_relative "cmdx/signal"
131
+ require_relative "cmdx/result"
132
+ require_relative "cmdx/pipeline"
133
+ require_relative "cmdx/runtime"
134
+ require_relative "cmdx/errors"
135
+ require_relative "cmdx/task"
136
+ require_relative "cmdx/workflow"
137
+ require_relative "cmdx/configuration"
68
138
 
69
139
  # Conditionally load Rails components if Rails is available
70
140
  if defined?(Rails::Generators)
71
141
  require_relative "generators/cmdx/install_generator"
72
- require_relative "generators/cmdx/locale_generator"
73
142
  require_relative "generators/cmdx/task_generator"
74
143
  require_relative "generators/cmdx/workflow_generator"
75
144
  end
76
145
 
77
- # Load the Railtie last after everything else is required so we don't
78
- # need to load any CMDx components when we use this Railtie.
146
+ # Load the Railtie last after everything else is required so
147
+ # we don't load any CMDx components when we use this Railtie.
79
148
  require_relative "cmdx/railtie" if defined?(Rails::Railtie)