cmdx 2.0.1 → 2.1.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 +53 -8
- data/lib/cmdx/callbacks.rb +31 -11
- data/lib/cmdx/chain.rb +29 -10
- data/lib/cmdx/coercions/big_decimal.rb +1 -1
- data/lib/cmdx/coercions/boolean.rb +3 -9
- data/lib/cmdx/coercions/coerce.rb +4 -1
- data/lib/cmdx/coercions/date_time.rb +1 -1
- data/lib/cmdx/coercions/integer.rb +11 -2
- data/lib/cmdx/coercions/symbol.rb +23 -4
- data/lib/cmdx/coercions.rb +25 -10
- data/lib/cmdx/configuration.rb +31 -16
- data/lib/cmdx/context.rb +36 -52
- data/lib/cmdx/deprecation.rb +4 -7
- data/lib/cmdx/deprecators/error.rb +4 -1
- data/lib/cmdx/deprecators.rb +17 -8
- data/lib/cmdx/errors.rb +11 -10
- data/lib/cmdx/executors/fiber.rb +16 -4
- data/lib/cmdx/executors/thread.rb +18 -4
- data/lib/cmdx/executors.rb +22 -7
- data/lib/cmdx/fault.rb +15 -3
- data/lib/cmdx/i18n_proxy.rb +9 -5
- data/lib/cmdx/input.rb +23 -21
- data/lib/cmdx/inputs.rb +14 -26
- data/lib/cmdx/log_formatters/json.rb +8 -1
- data/lib/cmdx/log_formatters/logstash.rb +7 -1
- data/lib/cmdx/mergers.rb +22 -7
- data/lib/cmdx/middlewares.rb +40 -24
- data/lib/cmdx/output.rb +5 -2
- data/lib/cmdx/pipeline.rb +18 -3
- data/lib/cmdx/railtie.rb +1 -0
- data/lib/cmdx/result.rb +22 -6
- data/lib/cmdx/retriers/decorrelated_jitter.rb +10 -5
- data/lib/cmdx/retriers/exponential.rb +10 -2
- data/lib/cmdx/retriers/fibonacci.rb +29 -12
- data/lib/cmdx/retriers.rb +17 -8
- data/lib/cmdx/retry.rb +20 -13
- data/lib/cmdx/runtime.rb +18 -17
- data/lib/cmdx/settings.rb +9 -9
- data/lib/cmdx/signal.rb +1 -1
- data/lib/cmdx/task.rb +90 -45
- data/lib/cmdx/telemetry.rb +37 -10
- data/lib/cmdx/util.rb +50 -4
- data/lib/cmdx/validators/absence.rb +1 -1
- data/lib/cmdx/validators/exclusion.rb +15 -15
- data/lib/cmdx/validators/format.rb +12 -4
- data/lib/cmdx/validators/inclusion.rb +15 -15
- data/lib/cmdx/validators/length.rb +5 -49
- data/lib/cmdx/validators/numeric.rb +5 -49
- data/lib/cmdx/validators/presence.rb +1 -1
- data/lib/cmdx/validators/validate.rb +7 -1
- data/lib/cmdx/validators.rb +21 -9
- data/lib/cmdx/version.rb +1 -1
- data/lib/cmdx/workflow.rb +28 -14
- data/lib/cmdx.rb +24 -0
- data/lib/generators/cmdx/templates/install.rb +80 -39
- data/mkdocs.yml +1 -0
- metadata +1 -1
data/lib/cmdx/context.rb
CHANGED
|
@@ -47,10 +47,25 @@ module CMDx
|
|
|
47
47
|
elsif context.respond_to?(:to_h)
|
|
48
48
|
context.to_h
|
|
49
49
|
else
|
|
50
|
-
raise ArgumentError,
|
|
50
|
+
raise ArgumentError, <<~MSG.chomp
|
|
51
|
+
Context.build expected a Hash or an object responding to #to_h/#to_hash (got #{context.class}).
|
|
52
|
+
See https://drexed.github.io/cmdx/basics/context/#assigning-data
|
|
53
|
+
MSG
|
|
51
54
|
end.transform_keys(&:to_sym)
|
|
52
55
|
end
|
|
53
56
|
|
|
57
|
+
# Ensures `#dup` / `#clone` produce a context with an independent backing
|
|
58
|
+
# hash so mutations on the copy do not leak into the original. `@strict`
|
|
59
|
+
# is preserved on the copy.
|
|
60
|
+
#
|
|
61
|
+
# @param source [Context]
|
|
62
|
+
# @return [void]
|
|
63
|
+
def initialize_copy(source)
|
|
64
|
+
super
|
|
65
|
+
@table = source.instance_variable_get(:@table).dup
|
|
66
|
+
@strict = source.strict?
|
|
67
|
+
end
|
|
68
|
+
|
|
54
69
|
# @return [Boolean] whether dynamic reads for unknown keys raise instead
|
|
55
70
|
# of returning `nil`
|
|
56
71
|
def strict?
|
|
@@ -87,7 +102,7 @@ module CMDx
|
|
|
87
102
|
# @return [Context] self for chaining
|
|
88
103
|
def deep_merge(context = EMPTY_HASH)
|
|
89
104
|
other = self.class.build(context)
|
|
90
|
-
@table =
|
|
105
|
+
@table = Util.deep_merge(@table, other.to_h)
|
|
91
106
|
self
|
|
92
107
|
end
|
|
93
108
|
|
|
@@ -202,9 +217,11 @@ module CMDx
|
|
|
202
217
|
@table.hash
|
|
203
218
|
end
|
|
204
219
|
|
|
205
|
-
# @return [Hash{Symbol => Object}] the underlying table
|
|
220
|
+
# @return [Hash{Symbol => Object}] a shallow copy of the underlying table.
|
|
221
|
+
# Frozen contexts return the frozen table directly to preserve
|
|
222
|
+
# `Hash#frozen?` semantics for serialization callers.
|
|
206
223
|
def to_h
|
|
207
|
-
@table
|
|
224
|
+
@table.frozen? ? @table : @table.dup
|
|
208
225
|
end
|
|
209
226
|
|
|
210
227
|
# JSON-friendly hash view. Aliases {#to_h} for conventional `as_json`
|
|
@@ -227,7 +244,14 @@ module CMDx
|
|
|
227
244
|
|
|
228
245
|
# @return [String] space-separated `key=value.inspect` pairs
|
|
229
246
|
def to_s
|
|
230
|
-
|
|
247
|
+
buf = String.new(capacity: 256)
|
|
248
|
+
|
|
249
|
+
@table.each do |k, v|
|
|
250
|
+
buf << " " unless buf.empty?
|
|
251
|
+
buf << k.to_s << "=" << v.inspect
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
buf
|
|
231
255
|
end
|
|
232
256
|
|
|
233
257
|
# Pattern-matching support for `case context in {...}`.
|
|
@@ -247,12 +271,13 @@ module CMDx
|
|
|
247
271
|
|
|
248
272
|
# Returns a deep copy. Non-mutable scalars are shared; Hashes/Arrays are
|
|
249
273
|
# recursively duplicated; other objects fall back to `#dup` (and then
|
|
250
|
-
# to the original on `StandardError`).
|
|
274
|
+
# to the original on `StandardError`). `@strict` is preserved on the copy.
|
|
251
275
|
#
|
|
252
276
|
# @return [Context]
|
|
253
277
|
def deep_dup
|
|
254
278
|
ctx = self.class.allocate
|
|
255
|
-
ctx.instance_variable_set(:@table,
|
|
279
|
+
ctx.instance_variable_set(:@table, Util.deep_dup(@table))
|
|
280
|
+
ctx.instance_variable_set(:@strict, @strict)
|
|
256
281
|
ctx
|
|
257
282
|
end
|
|
258
283
|
|
|
@@ -267,19 +292,6 @@ module CMDx
|
|
|
267
292
|
|
|
268
293
|
private
|
|
269
294
|
|
|
270
|
-
# Provides dynamic read/write/predicate access to context keys.
|
|
271
|
-
#
|
|
272
|
-
# - `ctx.name` — reads `@table[name]`, `nil` when absent (raises
|
|
273
|
-
# `NoMethodError` when {#strict?} is true and the key is absent).
|
|
274
|
-
# - `ctx.name = val` — stores `val` under `:name`.
|
|
275
|
-
# - `ctx.name?` — truthy check for `@table[:name]`.
|
|
276
|
-
#
|
|
277
|
-
# @param method_name [Symbol] dynamic reader/writer/predicate name
|
|
278
|
-
# @param args [Array<Object>] stores RHS for writers (`name=` → `[value]`)
|
|
279
|
-
# @param _kwargs [Hash{Symbol => Object}] ignored (accepted for Ruby keyword forwarding)
|
|
280
|
-
# @option _kwargs [Object] ignored
|
|
281
|
-
# @raise [NoMethodError] when {#strict?} is true and the key is missing
|
|
282
|
-
# @api private
|
|
283
295
|
def method_missing(method_name, *args, **_kwargs, &)
|
|
284
296
|
if @table.key?(method_name)
|
|
285
297
|
@table[method_name]
|
|
@@ -288,44 +300,16 @@ module CMDx
|
|
|
288
300
|
elsif method_name.end_with?("?")
|
|
289
301
|
!!@table[method_name[..-2].to_sym]
|
|
290
302
|
elsif strict?
|
|
291
|
-
raise
|
|
303
|
+
raise UnknownAccessorError, <<~MSG.chomp
|
|
304
|
+
unknown context key #{method_name.inspect} (strict mode); declared keys: #{@table.keys.inspect}.
|
|
305
|
+
See https://drexed.github.io/cmdx/basics/context/#strict-mode
|
|
306
|
+
MSG
|
|
292
307
|
end
|
|
293
308
|
end
|
|
294
309
|
|
|
295
|
-
# @param method_name [Symbol]
|
|
296
|
-
# @param include_private [Boolean] forwarded to Ruby's `respond_to?` lookup
|
|
297
|
-
# @return [Boolean]
|
|
298
310
|
def respond_to_missing?(method_name, include_private = false)
|
|
299
311
|
@table.key?(method_name) || method_name.end_with?("=", "?") || super
|
|
300
312
|
end
|
|
301
313
|
|
|
302
|
-
# @param value [Object] nested value from the context table
|
|
303
|
-
# @return [Object] recursively duplicated scalar/collection snapshot
|
|
304
|
-
def compute_deep_dup(value)
|
|
305
|
-
case value
|
|
306
|
-
when Numeric, Symbol, TrueClass, FalseClass, NilClass
|
|
307
|
-
value
|
|
308
|
-
when Hash
|
|
309
|
-
value.each_with_object({}) { |(k, v), acc| acc[k] = compute_deep_dup(v) }
|
|
310
|
-
when Array
|
|
311
|
-
value.map { |e| compute_deep_dup(e) }
|
|
312
|
-
else
|
|
313
|
-
begin
|
|
314
|
-
value.dup
|
|
315
|
-
rescue StandardError
|
|
316
|
-
value
|
|
317
|
-
end
|
|
318
|
-
end
|
|
319
|
-
end
|
|
320
|
-
|
|
321
|
-
# @param lhs [Hash]
|
|
322
|
-
# @param rhs [Hash]
|
|
323
|
-
# @return [Hash] merged hash (recursive for nested `{Hash => Hash}` pairs)
|
|
324
|
-
def compute_deep_merge(lhs, rhs)
|
|
325
|
-
lhs.merge(rhs) do |_key, l, r|
|
|
326
|
-
l.is_a?(Hash) && r.is_a?(Hash) ? compute_deep_merge(l, r) : r
|
|
327
|
-
end
|
|
328
|
-
end
|
|
329
|
-
|
|
330
314
|
end
|
|
331
315
|
end
|
data/lib/cmdx/deprecation.rb
CHANGED
|
@@ -43,18 +43,15 @@ module CMDx
|
|
|
43
43
|
else
|
|
44
44
|
return @value.call(task) if @value.respond_to?(:call)
|
|
45
45
|
|
|
46
|
-
raise ArgumentError,
|
|
46
|
+
raise ArgumentError, <<~MSG.chomp
|
|
47
|
+
deprecation must be a Symbol, Proc, or respond to #call (got #{@value.class}).
|
|
48
|
+
See https://drexed.github.io/cmdx/deprecation/#declarations
|
|
49
|
+
MSG
|
|
47
50
|
end
|
|
48
51
|
end
|
|
49
52
|
|
|
50
53
|
private
|
|
51
54
|
|
|
52
|
-
# Resolves the deprecators registry to consult for built-in actions.
|
|
53
|
-
# Prefers the task class's registry (so per-task `register(:deprecator, ...)`
|
|
54
|
-
# overrides take effect) and falls back to the global configuration.
|
|
55
|
-
#
|
|
56
|
-
# @param task [Task]
|
|
57
|
-
# @return [Deprecators]
|
|
58
55
|
def deprecators_registry(task)
|
|
59
56
|
if task.class.respond_to?(:deprecators)
|
|
60
57
|
task.class.deprecators
|
|
@@ -14,7 +14,10 @@ module CMDx
|
|
|
14
14
|
# @return [void]
|
|
15
15
|
# @raise [DeprecationError]
|
|
16
16
|
def call(task)
|
|
17
|
-
raise DeprecationError,
|
|
17
|
+
raise DeprecationError, <<~MSG.chomp
|
|
18
|
+
#{task.class} is deprecated and prohibited from execution.
|
|
19
|
+
See https://drexed.github.io/cmdx/deprecation/#error
|
|
20
|
+
MSG
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
end
|
data/lib/cmdx/deprecators.rb
CHANGED
|
@@ -36,9 +36,12 @@ module CMDx
|
|
|
36
36
|
deprecator = callable || block
|
|
37
37
|
|
|
38
38
|
if callable && block
|
|
39
|
-
raise ArgumentError, "provide either a callable or a block, not both"
|
|
39
|
+
raise ArgumentError, "deprecator: provide either a callable or a block, not both"
|
|
40
40
|
elsif !deprecator.respond_to?(:call)
|
|
41
|
-
raise ArgumentError,
|
|
41
|
+
raise ArgumentError, <<~MSG.chomp
|
|
42
|
+
deprecator must respond to #call (got #{deprecator.class}).
|
|
43
|
+
See https://drexed.github.io/cmdx/deprecation/#custom-actions-via-the-deprecators-registry
|
|
44
|
+
MSG
|
|
42
45
|
end
|
|
43
46
|
|
|
44
47
|
registry[name.to_sym] = deprecator
|
|
@@ -48,22 +51,25 @@ module CMDx
|
|
|
48
51
|
# @param name [Symbol]
|
|
49
52
|
# @return [Deprecators] self for chaining
|
|
50
53
|
def deregister(name)
|
|
51
|
-
registry.delete(name
|
|
54
|
+
registry.delete(name)
|
|
52
55
|
self
|
|
53
56
|
end
|
|
54
57
|
|
|
55
58
|
# @param name [Symbol]
|
|
56
59
|
# @return [Boolean] whether a deprecator is registered under `name`
|
|
57
60
|
def key?(name)
|
|
58
|
-
registry.key?(name
|
|
61
|
+
registry.key?(name)
|
|
59
62
|
end
|
|
60
63
|
|
|
61
64
|
# @param name [Symbol]
|
|
62
65
|
# @return [#call] the registered deprecator
|
|
63
|
-
# @raise [
|
|
66
|
+
# @raise [UnknownEntryError] when `name` isn't registered
|
|
64
67
|
def lookup(name)
|
|
65
68
|
registry[name] || begin
|
|
66
|
-
raise
|
|
69
|
+
raise UnknownEntryError, <<~MSG.chomp
|
|
70
|
+
unknown deprecator #{name.inspect}; registered: #{registry.keys.inspect}.
|
|
71
|
+
See https://drexed.github.io/cmdx/deprecation/#custom-actions-via-the-deprecators-registry
|
|
72
|
+
MSG
|
|
67
73
|
end
|
|
68
74
|
end
|
|
69
75
|
|
|
@@ -73,7 +79,7 @@ module CMDx
|
|
|
73
79
|
#
|
|
74
80
|
# @param spec [Symbol, #call, nil]
|
|
75
81
|
# @return [#call, nil]
|
|
76
|
-
# @raise [
|
|
82
|
+
# @raise [UnknownEntryError] when `spec` is an unknown symbol or not callable
|
|
77
83
|
def resolve(spec)
|
|
78
84
|
case spec
|
|
79
85
|
when NilClass
|
|
@@ -83,7 +89,10 @@ module CMDx
|
|
|
83
89
|
else
|
|
84
90
|
return spec if spec.respond_to?(:call)
|
|
85
91
|
|
|
86
|
-
raise
|
|
92
|
+
raise UnknownEntryError, <<~MSG.chomp
|
|
93
|
+
unknown deprecator #{spec.inspect}; expected a Symbol from #{registry.keys.inspect} or a callable.
|
|
94
|
+
See https://drexed.github.io/cmdx/deprecation/#custom-actions-via-the-deprecators-registry
|
|
95
|
+
MSG
|
|
87
96
|
end
|
|
88
97
|
end
|
|
89
98
|
|
data/lib/cmdx/errors.rb
CHANGED
|
@@ -15,12 +15,13 @@ module CMDx
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
# Adds `message` under `key`. Duplicate messages are silently dropped.
|
|
18
|
+
# `key` is coerced to a Symbol to match {Context}'s key normalization.
|
|
18
19
|
#
|
|
19
|
-
# @param key [Symbol]
|
|
20
|
+
# @param key [Symbol, String]
|
|
20
21
|
# @param message [String]
|
|
21
22
|
# @return [Set<String>] the set of messages now stored under `key`
|
|
22
23
|
def add(key, message)
|
|
23
|
-
(messages[key] ||= Set.new) << message
|
|
24
|
+
(messages[key.to_sym] ||= Set.new) << message
|
|
24
25
|
end
|
|
25
26
|
alias []= add
|
|
26
27
|
|
|
@@ -40,23 +41,23 @@ module CMDx
|
|
|
40
41
|
end
|
|
41
42
|
end
|
|
42
43
|
|
|
43
|
-
# @param key [Symbol]
|
|
44
|
+
# @param key [Symbol, String]
|
|
44
45
|
# @return [Array<String>] messages for `key`, or a frozen empty array
|
|
45
46
|
def [](key)
|
|
46
|
-
messages[key]&.to_a || EMPTY_ARRAY
|
|
47
|
+
messages[key.to_sym]&.to_a || EMPTY_ARRAY
|
|
47
48
|
end
|
|
48
49
|
|
|
49
|
-
# @param key [Symbol]
|
|
50
|
+
# @param key [Symbol, String]
|
|
50
51
|
# @param message [String]
|
|
51
52
|
# @return [Boolean] true when `message` is recorded under `key`
|
|
52
53
|
def added?(key, message)
|
|
53
|
-
!!messages[key]&.include?(message)
|
|
54
|
+
!!messages[key.to_sym]&.include?(message)
|
|
54
55
|
end
|
|
55
56
|
|
|
56
|
-
# @param key [Symbol]
|
|
57
|
+
# @param key [Symbol, String]
|
|
57
58
|
# @return [Boolean]
|
|
58
59
|
def key?(key)
|
|
59
|
-
messages.key?(key)
|
|
60
|
+
messages.key?(key.to_sym)
|
|
60
61
|
end
|
|
61
62
|
alias for? key?
|
|
62
63
|
|
|
@@ -98,10 +99,10 @@ module CMDx
|
|
|
98
99
|
messages.each_value(&)
|
|
99
100
|
end
|
|
100
101
|
|
|
101
|
-
# @param key [Symbol]
|
|
102
|
+
# @param key [Symbol, String]
|
|
102
103
|
# @return [Set<String>, nil] the removed set, or nil when absent
|
|
103
104
|
def delete(key)
|
|
104
|
-
messages.delete(key)
|
|
105
|
+
messages.delete(key.to_sym)
|
|
105
106
|
end
|
|
106
107
|
|
|
107
108
|
# @return [Hash{Symbol => Set<String>}] empties the container
|
data/lib/cmdx/executors/fiber.rb
CHANGED
|
@@ -5,7 +5,8 @@ module CMDx
|
|
|
5
5
|
# Fiber-scheduler backed executor. Spawns one fiber per job, bounded by
|
|
6
6
|
# `concurrency` via a `SizedQueue` semaphore. Requires a Fiber scheduler to
|
|
7
7
|
# be installed on the current thread (e.g. inside `Async { ... }` from the
|
|
8
|
-
# `async` gem). `pool_size` caps in-flight fibers.
|
|
8
|
+
# `async` gem). `pool_size` caps in-flight fibers. Exceptions raised inside
|
|
9
|
+
# `on_job` are captured and re-raised once every fiber has completed.
|
|
9
10
|
#
|
|
10
11
|
# @api private
|
|
11
12
|
module Fiber
|
|
@@ -13,21 +14,30 @@ module CMDx
|
|
|
13
14
|
extend self
|
|
14
15
|
|
|
15
16
|
# @param jobs [Array]
|
|
16
|
-
# @param concurrency [Integer] max in-flight fibers
|
|
17
|
+
# @param concurrency [Integer] max in-flight fibers (must be >= 1)
|
|
17
18
|
# @param on_job [#call]
|
|
18
19
|
# @return [void]
|
|
20
|
+
# @raise [ArgumentError] when `concurrency` is not a positive Integer
|
|
19
21
|
# @raise [RuntimeError] when no `Fiber.scheduler` is installed
|
|
22
|
+
# @raise [StandardError] re-raises the first exception captured from any fiber
|
|
20
23
|
def call(jobs:, concurrency:, on_job:)
|
|
24
|
+
raise ArgumentError, "executor concurrency must be a positive Integer (got #{concurrency.inspect})" unless concurrency.is_a?(Integer) && concurrency.positive?
|
|
25
|
+
|
|
21
26
|
raise "executor: :fibers requires Fiber.scheduler; run the workflow inside a scheduler block (e.g. Async { ... })" unless ::Fiber.scheduler
|
|
22
27
|
|
|
23
28
|
slots = SizedQueue.new(concurrency)
|
|
24
29
|
concurrency.times { slots << :slot }
|
|
25
|
-
done
|
|
30
|
+
done = Queue.new
|
|
31
|
+
errors = Queue.new
|
|
26
32
|
|
|
27
33
|
jobs.each do |job|
|
|
28
34
|
slots.pop
|
|
29
35
|
::Fiber.schedule do
|
|
30
|
-
|
|
36
|
+
begin
|
|
37
|
+
on_job.call(job)
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
errors << e
|
|
40
|
+
end
|
|
31
41
|
ensure
|
|
32
42
|
slots << :slot
|
|
33
43
|
done << true
|
|
@@ -35,6 +45,8 @@ module CMDx
|
|
|
35
45
|
end
|
|
36
46
|
|
|
37
47
|
jobs.size.times { done.pop }
|
|
48
|
+
|
|
49
|
+
raise errors.pop unless errors.empty?
|
|
38
50
|
end
|
|
39
51
|
|
|
40
52
|
end
|
|
@@ -4,7 +4,9 @@ module CMDx
|
|
|
4
4
|
class Executors
|
|
5
5
|
# Default executor. Uses a fixed-size `Thread` pool drained via a `Queue`;
|
|
6
6
|
# sentinel `nil`s terminate workers. Workers inherit the parent's chain via
|
|
7
|
-
# fiber-local storage.
|
|
7
|
+
# fiber-local storage. Exceptions raised inside `on_job` are captured per
|
|
8
|
+
# worker and re-raised on the main thread once every worker has joined, so
|
|
9
|
+
# callers see failures instead of silently dropped jobs.
|
|
8
10
|
#
|
|
9
11
|
# @api private
|
|
10
12
|
module Thread
|
|
@@ -12,23 +14,35 @@ module CMDx
|
|
|
12
14
|
extend self
|
|
13
15
|
|
|
14
16
|
# @param jobs [Array] opaque job objects forwarded to `on_job`
|
|
15
|
-
# @param concurrency [Integer] worker count
|
|
17
|
+
# @param concurrency [Integer] worker count (must be >= 1)
|
|
16
18
|
# @param on_job [#call] unary callable invoked per job
|
|
17
19
|
# @return [void]
|
|
20
|
+
# @raise [ArgumentError] when `concurrency` is not a positive Integer
|
|
21
|
+
# @raise [StandardError] re-raises the first exception captured from any worker
|
|
18
22
|
def call(jobs:, concurrency:, on_job:)
|
|
19
|
-
|
|
23
|
+
raise ArgumentError, "executor concurrency must be a positive Integer (got #{concurrency.inspect})" unless concurrency.is_a?(Integer) && concurrency.positive?
|
|
24
|
+
|
|
25
|
+
queue = Queue.new
|
|
26
|
+
errors = Queue.new
|
|
27
|
+
|
|
20
28
|
jobs.each { |job| queue << job }
|
|
21
29
|
concurrency.times { queue << nil }
|
|
22
30
|
|
|
23
31
|
workers = Array.new(concurrency) do
|
|
24
32
|
::Thread.new do
|
|
25
33
|
while (job = queue.pop)
|
|
26
|
-
|
|
34
|
+
begin
|
|
35
|
+
on_job.call(job)
|
|
36
|
+
rescue StandardError => e
|
|
37
|
+
errors << e
|
|
38
|
+
end
|
|
27
39
|
end
|
|
28
40
|
end
|
|
29
41
|
end
|
|
30
42
|
|
|
31
43
|
workers.each(&:join)
|
|
44
|
+
|
|
45
|
+
raise errors.pop unless errors.empty?
|
|
32
46
|
end
|
|
33
47
|
|
|
34
48
|
end
|
data/lib/cmdx/executors.rb
CHANGED
|
@@ -36,9 +36,12 @@ module CMDx
|
|
|
36
36
|
executor = callable || block
|
|
37
37
|
|
|
38
38
|
if callable && block
|
|
39
|
-
raise ArgumentError, "provide either a callable or a block, not both"
|
|
39
|
+
raise ArgumentError, "executor: provide either a callable or a block, not both"
|
|
40
40
|
elsif !executor.respond_to?(:call)
|
|
41
|
-
raise ArgumentError,
|
|
41
|
+
raise ArgumentError, <<~MSG.chomp
|
|
42
|
+
executor must respond to #call (got #{executor.class}).
|
|
43
|
+
See https://drexed.github.io/cmdx/workflows/#executors
|
|
44
|
+
MSG
|
|
42
45
|
end
|
|
43
46
|
|
|
44
47
|
registry[name.to_sym] = executor
|
|
@@ -48,16 +51,25 @@ module CMDx
|
|
|
48
51
|
# @param name [Symbol]
|
|
49
52
|
# @return [Executors] self for chaining
|
|
50
53
|
def deregister(name)
|
|
51
|
-
registry.delete(name
|
|
54
|
+
registry.delete(name)
|
|
52
55
|
self
|
|
53
56
|
end
|
|
54
57
|
|
|
58
|
+
# @param name [Symbol]
|
|
59
|
+
# @return [Boolean] whether an executor is registered under `name`
|
|
60
|
+
def key?(name)
|
|
61
|
+
registry.key?(name)
|
|
62
|
+
end
|
|
63
|
+
|
|
55
64
|
# @param name [Symbol]
|
|
56
65
|
# @return [#call] the registered executor
|
|
57
|
-
# @raise [
|
|
66
|
+
# @raise [UnknownEntryError] when `name` isn't registered
|
|
58
67
|
def lookup(name)
|
|
59
68
|
registry[name] || begin
|
|
60
|
-
raise
|
|
69
|
+
raise UnknownEntryError, <<~MSG.chomp
|
|
70
|
+
unknown executor #{name.inspect}; registered: #{registry.keys.inspect}.
|
|
71
|
+
See https://drexed.github.io/cmdx/workflows/#executors
|
|
72
|
+
MSG
|
|
61
73
|
end
|
|
62
74
|
end
|
|
63
75
|
|
|
@@ -67,7 +79,7 @@ module CMDx
|
|
|
67
79
|
#
|
|
68
80
|
# @param spec [Symbol, #call, nil]
|
|
69
81
|
# @return [#call]
|
|
70
|
-
# @raise [
|
|
82
|
+
# @raise [UnknownEntryError] when `spec` is an unknown symbol or not callable
|
|
71
83
|
def resolve(spec)
|
|
72
84
|
case spec
|
|
73
85
|
when NilClass
|
|
@@ -77,7 +89,10 @@ module CMDx
|
|
|
77
89
|
else
|
|
78
90
|
return spec if spec.respond_to?(:call)
|
|
79
91
|
|
|
80
|
-
raise
|
|
92
|
+
raise UnknownEntryError, <<~MSG.chomp
|
|
93
|
+
unknown executor #{spec.inspect}; expected a Symbol from #{registry.keys.inspect} or a callable.
|
|
94
|
+
See https://drexed.github.io/cmdx/workflows/#executors
|
|
95
|
+
MSG
|
|
81
96
|
end
|
|
82
97
|
end
|
|
83
98
|
|
data/lib/cmdx/fault.rb
CHANGED
|
@@ -16,6 +16,10 @@ module CMDx
|
|
|
16
16
|
# inherits from) any of the given task classes. Suitable for use in
|
|
17
17
|
# `rescue`.
|
|
18
18
|
#
|
|
19
|
+
# @note Each call allocates a new anonymous subclass. Hoist the matcher
|
|
20
|
+
# to module scope (`MY_FAULT = CMDx::Fault.for?(MyTask)`) when used
|
|
21
|
+
# on hot paths to avoid class allocation churn over time.
|
|
22
|
+
#
|
|
19
23
|
# @param tasks [Array<Class>] one or more Task classes
|
|
20
24
|
# @return [Class<Fault>] anonymous matcher subclass
|
|
21
25
|
# @raise [ArgumentError] when no tasks are given
|
|
@@ -28,7 +32,7 @@ module CMDx
|
|
|
28
32
|
# end
|
|
29
33
|
def for?(*tasks)
|
|
30
34
|
tasks = tasks.flatten
|
|
31
|
-
raise ArgumentError, "at least one
|
|
35
|
+
raise ArgumentError, "Fault.for? requires at least one Task class" if tasks.empty?
|
|
32
36
|
|
|
33
37
|
matcher do |other|
|
|
34
38
|
tasks.any? { |task| other.task <= task }
|
|
@@ -38,6 +42,10 @@ module CMDx
|
|
|
38
42
|
# Returns a matcher subclass that matches Faults whose `result.reason`
|
|
39
43
|
# is equal to the given string. Suitable for use in `rescue`.
|
|
40
44
|
#
|
|
45
|
+
# @note Each call allocates a new anonymous subclass. Hoist the matcher
|
|
46
|
+
# to module scope (`PAYMENT_FAULT = CMDx::Fault.reason?("Payment failed")`)
|
|
47
|
+
# when used on hot paths to avoid class allocation churn.
|
|
48
|
+
#
|
|
41
49
|
# @param reason [String] the reason to match
|
|
42
50
|
# @return [Class<Fault>] anonymous matcher subclass
|
|
43
51
|
# @raise [ArgumentError] when no reason is given
|
|
@@ -49,7 +57,7 @@ module CMDx
|
|
|
49
57
|
# Alert.for_fault(fault)
|
|
50
58
|
# end
|
|
51
59
|
def reason?(reason)
|
|
52
|
-
raise ArgumentError, "reason
|
|
60
|
+
raise ArgumentError, "Fault.reason? requires a reason" unless reason
|
|
53
61
|
|
|
54
62
|
matcher do |other|
|
|
55
63
|
other.result.reason == reason
|
|
@@ -58,13 +66,17 @@ module CMDx
|
|
|
58
66
|
|
|
59
67
|
# Returns a matcher subclass whose `===` runs `block` against the fault.
|
|
60
68
|
#
|
|
69
|
+
# @note Each call allocates a new anonymous subclass. Hoist the matcher
|
|
70
|
+
# to module scope when used on hot paths to avoid class allocation
|
|
71
|
+
# churn.
|
|
72
|
+
#
|
|
61
73
|
# @param block [#call] `(fault) -> Boolean` matcher body
|
|
62
74
|
# @yieldparam fault [Fault]
|
|
63
75
|
# @yieldreturn [Boolean]
|
|
64
76
|
# @return [Class<Fault>] anonymous matcher subclass
|
|
65
77
|
# @raise [ArgumentError] when no block is given
|
|
66
78
|
def matches?(&block)
|
|
67
|
-
raise ArgumentError, "block
|
|
79
|
+
raise ArgumentError, "Fault.matches? requires a block" unless block
|
|
68
80
|
|
|
69
81
|
matcher(&block)
|
|
70
82
|
end
|
data/lib/cmdx/i18n_proxy.rb
CHANGED
|
@@ -79,8 +79,6 @@ module CMDx
|
|
|
79
79
|
|
|
80
80
|
private
|
|
81
81
|
|
|
82
|
-
# @param key [String, Symbol] lookup key (without locale prefix)
|
|
83
|
-
# @return [Object, nil] message template from bundled YAML, when present
|
|
84
82
|
def translation_default(key)
|
|
85
83
|
default_locale = CMDx.configuration.default_locale || "en"
|
|
86
84
|
translation_key = "#{default_locale}.#{key}"
|
|
@@ -92,9 +90,15 @@ module CMDx
|
|
|
92
90
|
@translations[default_locale] ||= begin
|
|
93
91
|
file = "#{default_locale}.yml"
|
|
94
92
|
paths = self.class.locale_paths.map { |dir| File.join(dir, file) }.select { |p| File.exist?(p) }
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
if paths.empty?
|
|
94
|
+
raise UnknownLocaleError, <<~MSG.chomp
|
|
95
|
+
unable to load #{default_locale.inspect} translations;
|
|
96
|
+
searched: #{self.class.locale_paths.map { |dir| File.join(dir, file) }.inspect}.
|
|
97
|
+
See https://drexed.github.io/cmdx/internationalization/#custom-locale-paths
|
|
98
|
+
MSG
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
paths.reduce({}) { |acc, path| Util.deep_merge(acc, YAML.safe_load_file(path)) }.freeze
|
|
98
102
|
end
|
|
99
103
|
|
|
100
104
|
@defaults[translation_key] = @translations[default_locale].dig(*translation_key.split("."))
|
data/lib/cmdx/input.rb
CHANGED
|
@@ -128,6 +128,11 @@ module CMDx
|
|
|
128
128
|
# validation error to `task.errors`. Returns `nil` when coercion or any
|
|
129
129
|
# validator fails (the failure message is recorded on `task.errors`).
|
|
130
130
|
#
|
|
131
|
+
# @note "Required" here means "the key is present in the source"; an
|
|
132
|
+
# explicit `nil` under an existing key satisfies the required check
|
|
133
|
+
# and is then routed through `:default`. Combine with `:presence` /
|
|
134
|
+
# `:validate` to reject explicit `nil` values.
|
|
135
|
+
#
|
|
131
136
|
# @param task [Task]
|
|
132
137
|
# @return [Object, nil] the resolved value (`nil` on failure)
|
|
133
138
|
def resolve(task)
|
|
@@ -177,10 +182,6 @@ module CMDx
|
|
|
177
182
|
|
|
178
183
|
private
|
|
179
184
|
|
|
180
|
-
# @param value [Object] candidate after source resolution
|
|
181
|
-
# @param key_provided [Boolean] whether the source reported an explicit key/value pair
|
|
182
|
-
# @param task [Task]
|
|
183
|
-
# @return [Object, nil]
|
|
184
185
|
def run_pipeline(value, key_provided, task)
|
|
185
186
|
if required?(task) && !key_provided
|
|
186
187
|
task.errors.add(accessor_name, I18nProxy.t("cmdx.attributes.required"))
|
|
@@ -202,15 +203,13 @@ module CMDx
|
|
|
202
203
|
value
|
|
203
204
|
end
|
|
204
205
|
|
|
205
|
-
# @param task [Task]
|
|
206
|
-
# @return [Array(Object, Boolean)] `[value, key_provided]`
|
|
207
206
|
def resolve_with_key(task)
|
|
208
207
|
case source
|
|
209
208
|
when :context
|
|
210
209
|
[task.context[name], task.context.key?(name)]
|
|
211
210
|
when Symbol
|
|
212
211
|
obj = task.send(source)
|
|
213
|
-
return [nil, false]
|
|
212
|
+
return [nil, false] unless obj
|
|
214
213
|
|
|
215
214
|
fetch_by_name(obj)
|
|
216
215
|
when Proc
|
|
@@ -218,20 +217,19 @@ module CMDx
|
|
|
218
217
|
else
|
|
219
218
|
return [source.call(task), true] if source.respond_to?(:call)
|
|
220
219
|
|
|
221
|
-
raise ArgumentError,
|
|
220
|
+
raise ArgumentError, <<~MSG.chomp
|
|
221
|
+
input source must be a Symbol, Proc, or respond to #call (got #{source.class}).
|
|
222
|
+
See https://drexed.github.io/cmdx/inputs/definitions/#sources
|
|
223
|
+
MSG
|
|
222
224
|
end
|
|
223
225
|
end
|
|
224
226
|
|
|
225
|
-
# @param parent_value [#[], #key?, Object]
|
|
226
|
-
# @return [Array(Object, Boolean)] `[value, key_provided]`
|
|
227
227
|
def resolve_from_parent_with_key(parent_value)
|
|
228
|
-
return [nil, false] unless parent_value.respond_to?(:[])
|
|
228
|
+
return [nil, false] unless parent_value.respond_to?(:[]) || parent_value.respond_to?(:fetch)
|
|
229
229
|
|
|
230
230
|
fetch_by_name(parent_value)
|
|
231
231
|
end
|
|
232
232
|
|
|
233
|
-
# @param obj [Object] source object (`Hash`, duck-typed reader, etc.)
|
|
234
|
-
# @return [Array(Object, Boolean)] `[value, key_provided]`
|
|
235
233
|
def fetch_by_name(obj)
|
|
236
234
|
if obj.respond_to?(name, true)
|
|
237
235
|
[obj.send(name), true]
|
|
@@ -243,9 +241,15 @@ module CMDx
|
|
|
243
241
|
else
|
|
244
242
|
[nil, false]
|
|
245
243
|
end
|
|
244
|
+
elsif obj.respond_to?(:fetch)
|
|
245
|
+
# Prefer #fetch with a sentinel so an explicit `nil` value is
|
|
246
|
+
# distinguishable from a missing key (Hash, Array, etc.).
|
|
247
|
+
value = obj.fetch(name) { obj.fetch(name.to_s) { EMPTY_SENTINEL } }
|
|
248
|
+
value.equal?(EMPTY_SENTINEL) ? [nil, false] : [value, true]
|
|
246
249
|
elsif obj.respond_to?(:[])
|
|
247
|
-
# Without #key? we cannot distinguish "key absent" from
|
|
248
|
-
# so an explicit nil is treated as not provided
|
|
250
|
+
# Without #key? or #fetch we cannot distinguish "key absent" from
|
|
251
|
+
# "value is nil", so an explicit nil is treated as not provided
|
|
252
|
+
# (triggers default/required).
|
|
249
253
|
value = obj[name] || obj[name.to_s]
|
|
250
254
|
[value, !value.nil?]
|
|
251
255
|
else
|
|
@@ -253,8 +257,6 @@ module CMDx
|
|
|
253
257
|
end
|
|
254
258
|
end
|
|
255
259
|
|
|
256
|
-
# @param task [Task]
|
|
257
|
-
# @return [Object, nil]
|
|
258
260
|
def apply_default(task)
|
|
259
261
|
return if default.nil?
|
|
260
262
|
|
|
@@ -270,9 +272,6 @@ module CMDx
|
|
|
270
272
|
end
|
|
271
273
|
end
|
|
272
274
|
|
|
273
|
-
# @param value [Object]
|
|
274
|
-
# @param task [Task]
|
|
275
|
-
# @return [Object]
|
|
276
275
|
def apply_transform(value, task)
|
|
277
276
|
case transform
|
|
278
277
|
when Symbol
|
|
@@ -286,7 +285,10 @@ module CMDx
|
|
|
286
285
|
else
|
|
287
286
|
return transform.call(value, task) if transform.respond_to?(:call)
|
|
288
287
|
|
|
289
|
-
raise ArgumentError,
|
|
288
|
+
raise ArgumentError, <<~MSG.chomp
|
|
289
|
+
input transform must be a Symbol, Proc, or respond to #call (got #{transform.class}).
|
|
290
|
+
See https://drexed.github.io/cmdx/inputs/transformations/#declarations
|
|
291
|
+
MSG
|
|
290
292
|
end
|
|
291
293
|
end
|
|
292
294
|
|