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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +131 -1
- data/README.md +37 -24
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/callbacks.rb +179 -0
- data/lib/cmdx/chain.rb +78 -175
- data/lib/cmdx/coercions/array.rb +19 -33
- data/lib/cmdx/coercions/big_decimal.rb +12 -29
- data/lib/cmdx/coercions/boolean.rb +25 -45
- data/lib/cmdx/coercions/coerce.rb +32 -0
- data/lib/cmdx/coercions/complex.rb +12 -27
- data/lib/cmdx/coercions/date.rb +29 -33
- data/lib/cmdx/coercions/date_time.rb +29 -33
- data/lib/cmdx/coercions/float.rb +8 -29
- data/lib/cmdx/coercions/hash.rb +17 -43
- data/lib/cmdx/coercions/integer.rb +8 -32
- data/lib/cmdx/coercions/rational.rb +12 -33
- data/lib/cmdx/coercions/string.rb +6 -24
- data/lib/cmdx/coercions/symbol.rb +12 -26
- data/lib/cmdx/coercions/time.rb +31 -35
- data/lib/cmdx/coercions.rb +174 -0
- data/lib/cmdx/configuration.rb +45 -225
- data/lib/cmdx/context.rb +263 -242
- data/lib/cmdx/deprecation.rb +67 -0
- data/lib/cmdx/deprecators/error.rb +22 -0
- data/lib/cmdx/deprecators/log.rb +22 -0
- data/lib/cmdx/deprecators/warn.rb +21 -0
- data/lib/cmdx/deprecators.rb +101 -0
- data/lib/cmdx/errors.rb +145 -79
- data/lib/cmdx/executors/fiber.rb +42 -0
- data/lib/cmdx/executors/thread.rb +36 -0
- data/lib/cmdx/executors.rb +95 -0
- data/lib/cmdx/fault.rb +85 -78
- data/lib/cmdx/i18n_proxy.rb +104 -0
- data/lib/cmdx/input.rb +294 -0
- data/lib/cmdx/inputs.rb +218 -0
- data/lib/cmdx/log_formatters/json.rb +9 -20
- data/lib/cmdx/log_formatters/key_value.rb +10 -21
- data/lib/cmdx/log_formatters/line.rb +7 -19
- data/lib/cmdx/log_formatters/logstash.rb +8 -21
- data/lib/cmdx/log_formatters/raw.rb +8 -20
- data/lib/cmdx/logger_proxy.rb +30 -0
- data/lib/cmdx/mergers/deep_merge.rb +23 -0
- data/lib/cmdx/mergers/last_write_wins.rb +23 -0
- data/lib/cmdx/mergers/no_merge.rb +20 -0
- data/lib/cmdx/mergers.rb +95 -0
- data/lib/cmdx/middlewares.rb +128 -0
- data/lib/cmdx/output.rb +115 -0
- data/lib/cmdx/outputs.rb +66 -0
- data/lib/cmdx/pipeline.rb +144 -131
- data/lib/cmdx/railtie.rb +10 -36
- data/lib/cmdx/result.rb +252 -473
- data/lib/cmdx/retriers/bounded_random.rb +24 -0
- data/lib/cmdx/retriers/decorrelated_jitter.rb +28 -0
- data/lib/cmdx/retriers/exponential.rb +23 -0
- data/lib/cmdx/retriers/fibonacci.rb +39 -0
- data/lib/cmdx/retriers/full_random.rb +23 -0
- data/lib/cmdx/retriers/half_random.rb +24 -0
- data/lib/cmdx/retriers/linear.rb +23 -0
- data/lib/cmdx/retriers.rb +106 -0
- data/lib/cmdx/retry.rb +117 -138
- data/lib/cmdx/runtime.rb +251 -0
- data/lib/cmdx/settings.rb +68 -196
- data/lib/cmdx/signal.rb +165 -0
- data/lib/cmdx/task.rb +443 -336
- data/lib/cmdx/telemetry.rb +108 -0
- data/lib/cmdx/util.rb +73 -0
- data/lib/cmdx/validators/absence.rb +10 -39
- data/lib/cmdx/validators/exclusion.rb +33 -52
- data/lib/cmdx/validators/format.rb +19 -49
- data/lib/cmdx/validators/inclusion.rb +33 -54
- data/lib/cmdx/validators/length.rb +125 -127
- data/lib/cmdx/validators/numeric.rb +123 -123
- data/lib/cmdx/validators/presence.rb +10 -39
- data/lib/cmdx/validators/validate.rb +31 -0
- data/lib/cmdx/validators.rb +161 -0
- data/lib/cmdx/version.rb +2 -4
- data/lib/cmdx/workflow.rb +74 -82
- data/lib/cmdx.rb +111 -42
- data/lib/generators/cmdx/install_generator.rb +7 -17
- data/lib/generators/cmdx/task_generator.rb +12 -29
- data/lib/generators/cmdx/templates/install.rb +128 -52
- data/lib/generators/cmdx/templates/task.rb.tt +1 -1
- data/lib/generators/cmdx/templates/workflow.rb.tt +1 -2
- data/lib/generators/cmdx/workflow_generator.rb +12 -29
- data/lib/locales/en.yml +9 -6
- data/mkdocs.yml +25 -23
- metadata +39 -138
- data/lib/cmdx/attribute.rb +0 -440
- data/lib/cmdx/attribute_registry.rb +0 -185
- data/lib/cmdx/attribute_value.rb +0 -252
- data/lib/cmdx/callback_registry.rb +0 -169
- data/lib/cmdx/coercion_registry.rb +0 -138
- data/lib/cmdx/deprecator.rb +0 -77
- data/lib/cmdx/exception.rb +0 -46
- data/lib/cmdx/executor.rb +0 -374
- data/lib/cmdx/identifier.rb +0 -30
- data/lib/cmdx/locale.rb +0 -78
- data/lib/cmdx/middleware_registry.rb +0 -148
- data/lib/cmdx/middlewares/correlate.rb +0 -140
- data/lib/cmdx/middlewares/runtime.rb +0 -62
- data/lib/cmdx/middlewares/timeout.rb +0 -78
- data/lib/cmdx/parallelizer.rb +0 -100
- data/lib/cmdx/utils/call.rb +0 -53
- data/lib/cmdx/utils/condition.rb +0 -71
- data/lib/cmdx/utils/format.rb +0 -82
- data/lib/cmdx/utils/normalize.rb +0 -52
- data/lib/cmdx/utils/wrap.rb +0 -38
- data/lib/cmdx/validator_registry.rb +0 -143
- data/lib/generators/cmdx/locale_generator.rb +0 -39
- data/lib/locales/af.yml +0 -53
- data/lib/locales/ar.yml +0 -53
- data/lib/locales/az.yml +0 -53
- data/lib/locales/be.yml +0 -53
- data/lib/locales/bg.yml +0 -53
- data/lib/locales/bn.yml +0 -53
- data/lib/locales/bs.yml +0 -53
- data/lib/locales/ca.yml +0 -53
- data/lib/locales/cnr.yml +0 -53
- data/lib/locales/cs.yml +0 -53
- data/lib/locales/cy.yml +0 -53
- data/lib/locales/da.yml +0 -53
- data/lib/locales/de.yml +0 -53
- data/lib/locales/dz.yml +0 -53
- data/lib/locales/el.yml +0 -53
- data/lib/locales/eo.yml +0 -53
- data/lib/locales/es.yml +0 -53
- data/lib/locales/et.yml +0 -53
- data/lib/locales/eu.yml +0 -53
- data/lib/locales/fa.yml +0 -53
- data/lib/locales/fi.yml +0 -53
- data/lib/locales/fr.yml +0 -53
- data/lib/locales/fy.yml +0 -53
- data/lib/locales/gd.yml +0 -53
- data/lib/locales/gl.yml +0 -53
- data/lib/locales/he.yml +0 -53
- data/lib/locales/hi.yml +0 -53
- data/lib/locales/hr.yml +0 -53
- data/lib/locales/hu.yml +0 -53
- data/lib/locales/hy.yml +0 -53
- data/lib/locales/id.yml +0 -53
- data/lib/locales/is.yml +0 -53
- data/lib/locales/it.yml +0 -53
- data/lib/locales/ja.yml +0 -53
- data/lib/locales/ka.yml +0 -53
- data/lib/locales/kk.yml +0 -53
- data/lib/locales/km.yml +0 -53
- data/lib/locales/kn.yml +0 -53
- data/lib/locales/ko.yml +0 -53
- data/lib/locales/lb.yml +0 -53
- data/lib/locales/lo.yml +0 -53
- data/lib/locales/lt.yml +0 -53
- data/lib/locales/lv.yml +0 -53
- data/lib/locales/mg.yml +0 -53
- data/lib/locales/mk.yml +0 -53
- data/lib/locales/ml.yml +0 -53
- data/lib/locales/mn.yml +0 -53
- data/lib/locales/mr-IN.yml +0 -53
- data/lib/locales/ms.yml +0 -53
- data/lib/locales/nb.yml +0 -53
- data/lib/locales/ne.yml +0 -53
- data/lib/locales/nl.yml +0 -53
- data/lib/locales/nn.yml +0 -53
- data/lib/locales/oc.yml +0 -53
- data/lib/locales/or.yml +0 -53
- data/lib/locales/pa.yml +0 -53
- data/lib/locales/pl.yml +0 -53
- data/lib/locales/pt.yml +0 -53
- data/lib/locales/rm.yml +0 -53
- data/lib/locales/ro.yml +0 -53
- data/lib/locales/ru.yml +0 -53
- data/lib/locales/sc.yml +0 -53
- data/lib/locales/sk.yml +0 -53
- data/lib/locales/sl.yml +0 -53
- data/lib/locales/sq.yml +0 -53
- data/lib/locales/sr.yml +0 -53
- data/lib/locales/st.yml +0 -53
- data/lib/locales/sv.yml +0 -53
- data/lib/locales/sw.yml +0 -53
- data/lib/locales/ta.yml +0 -53
- data/lib/locales/te.yml +0 -53
- data/lib/locales/th.yml +0 -53
- data/lib/locales/tl.yml +0 -53
- data/lib/locales/tr.yml +0 -53
- data/lib/locales/tt.yml +0 -53
- data/lib/locales/ug.yml +0 -53
- data/lib/locales/uk.yml +0 -53
- data/lib/locales/ur.yml +0 -53
- data/lib/locales/uz.yml +0 -53
- data/lib/locales/vi.yml +0 -53
- data/lib/locales/wo.yml +0 -53
- data/lib/locales/zh-CN.yml +0 -53
- data/lib/locales/zh-HK.yml +0 -53
- data/lib/locales/zh-TW.yml +0 -53
- 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
|
-
|
|
5
|
-
# Validates that a value is present and
|
|
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
|
-
#
|
|
17
|
-
#
|
|
18
|
-
# @
|
|
19
|
-
# @
|
|
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
|
-
|
|
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
|
|
25
|
+
return if present
|
|
54
26
|
|
|
55
|
-
|
|
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
data/lib/cmdx/workflow.rb
CHANGED
|
@@ -1,115 +1,107 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
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
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# @
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
# @
|
|
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
|
|
67
|
-
|
|
65
|
+
tasks:
|
|
66
|
+
tasks.map do |task|
|
|
67
|
+
next task if task.is_a?(Class) && (task <= Task)
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
#
|
|
79
|
-
|
|
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
|
-
#
|
|
84
|
-
#
|
|
85
|
-
# @
|
|
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
|
-
#
|
|
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
|
-
# @
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
#
|
|
33
|
-
#
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
#
|
|
58
|
-
#
|
|
59
|
-
|
|
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
|
-
#
|
|
62
|
-
#
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
|
78
|
-
#
|
|
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)
|