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
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CMDx
|
|
4
|
+
class Deprecators
|
|
5
|
+
# Writes a `warn`-level entry to the task's logger noting the deprecation.
|
|
6
|
+
# Execution proceeds; useful for gradual migration where you want
|
|
7
|
+
# observability without breaking callers.
|
|
8
|
+
#
|
|
9
|
+
# @api private
|
|
10
|
+
module Log
|
|
11
|
+
|
|
12
|
+
extend self
|
|
13
|
+
|
|
14
|
+
# @param task [Task]
|
|
15
|
+
# @return [void]
|
|
16
|
+
def call(task)
|
|
17
|
+
task.logger.warn { "DEPRECATED: #{task.class} - migrate to a replacement or discontinue use" }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CMDx
|
|
4
|
+
class Deprecators
|
|
5
|
+
# Emits a Ruby warning to stderr via `Kernel.warn`. Visible during
|
|
6
|
+
# development and testing without polluting structured production logs.
|
|
7
|
+
#
|
|
8
|
+
# @api private
|
|
9
|
+
module Warn
|
|
10
|
+
|
|
11
|
+
extend self
|
|
12
|
+
|
|
13
|
+
# @param task [Task]
|
|
14
|
+
# @return [void]
|
|
15
|
+
def call(task)
|
|
16
|
+
Kernel.warn("[#{task.class}] DEPRECATED: migrate to a replacement or discontinue use")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CMDx
|
|
4
|
+
# Registry of named deprecation actions consulted by `Deprecation#execute`
|
|
5
|
+
# to dispatch a task class's deprecation. Ships with built-ins for
|
|
6
|
+
# `:log`, `:warn`, and `:error`. A deprecator is any callable accepting
|
|
7
|
+
# `call(task)`; the return value is discarded.
|
|
8
|
+
class Deprecators
|
|
9
|
+
|
|
10
|
+
attr_reader :registry
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@registry = {
|
|
14
|
+
log: Deprecators::Log,
|
|
15
|
+
warn: Deprecators::Warn,
|
|
16
|
+
error: Deprecators::Error
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param source [Deprecators] registry to duplicate
|
|
21
|
+
# @return [void]
|
|
22
|
+
def initialize_copy(source)
|
|
23
|
+
@registry = source.registry.dup
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Registers a named deprecator, overwriting any existing entry.
|
|
27
|
+
#
|
|
28
|
+
# @param name [Symbol]
|
|
29
|
+
# @param callable [#call, nil] pass either this or a block
|
|
30
|
+
# @param block [#call, nil] deprecator callable when `callable` is omitted
|
|
31
|
+
# @yield deprecator body — `call(task)`
|
|
32
|
+
# @return [Deprecators] self for chaining
|
|
33
|
+
# @raise [ArgumentError] when both `callable` and a block are given, or
|
|
34
|
+
# when the resolved deprecator isn't callable
|
|
35
|
+
def register(name, callable = nil, &block)
|
|
36
|
+
deprecator = callable || block
|
|
37
|
+
|
|
38
|
+
if callable && block
|
|
39
|
+
raise ArgumentError, "provide either a callable or a block, not both"
|
|
40
|
+
elsif !deprecator.respond_to?(:call)
|
|
41
|
+
raise ArgumentError, "deprecator must respond to #call"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
registry[name.to_sym] = deprecator
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @param name [Symbol]
|
|
49
|
+
# @return [Deprecators] self for chaining
|
|
50
|
+
def deregister(name)
|
|
51
|
+
registry.delete(name.to_sym)
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @param name [Symbol]
|
|
56
|
+
# @return [Boolean] whether a deprecator is registered under `name`
|
|
57
|
+
def key?(name)
|
|
58
|
+
registry.key?(name.to_sym)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @param name [Symbol]
|
|
62
|
+
# @return [#call] the registered deprecator
|
|
63
|
+
# @raise [ArgumentError] when `name` isn't registered
|
|
64
|
+
def lookup(name)
|
|
65
|
+
registry[name] || begin
|
|
66
|
+
raise ArgumentError, "unknown deprecator: #{name.inspect}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Resolves a `deprecation` declaration's value to a concrete callable.
|
|
71
|
+
# Accepts a Symbol (registry lookup) or any object responding to `#call`.
|
|
72
|
+
# `nil` resolves to `nil` so callers can short-circuit.
|
|
73
|
+
#
|
|
74
|
+
# @param spec [Symbol, #call, nil]
|
|
75
|
+
# @return [#call, nil]
|
|
76
|
+
# @raise [ArgumentError] when `spec` is an unknown symbol or not callable
|
|
77
|
+
def resolve(spec)
|
|
78
|
+
case spec
|
|
79
|
+
when NilClass
|
|
80
|
+
nil
|
|
81
|
+
when Symbol
|
|
82
|
+
lookup(spec)
|
|
83
|
+
else
|
|
84
|
+
return spec if spec.respond_to?(:call)
|
|
85
|
+
|
|
86
|
+
raise ArgumentError, "unknown deprecator: #{spec.inspect}"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @return [Boolean]
|
|
91
|
+
def empty?
|
|
92
|
+
registry.empty?
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @return [Integer]
|
|
96
|
+
def size
|
|
97
|
+
registry.size
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
end
|
|
101
|
+
end
|
data/lib/cmdx/errors.rb
CHANGED
|
@@ -1,117 +1,183 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
4
|
+
# Per-task container of validation / coercion / output errors. Each key maps
|
|
5
|
+
# to a deduplicating Set of messages. A non-empty Errors forces Runtime to
|
|
6
|
+
# throw a failed signal (`signal_errors!`). Frozen on teardown by Runtime.
|
|
7
7
|
class Errors
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
include Enumerable
|
|
10
10
|
|
|
11
|
-
# Returns the internal hash of error messages by attribute.
|
|
12
|
-
#
|
|
13
|
-
# @return [Hash{Symbol => Set<String>}] Hash mapping attribute names to error message sets
|
|
14
|
-
#
|
|
15
|
-
# @example
|
|
16
|
-
# errors.messages # => { email: #<Set: ["must be valid", "is required"]> }
|
|
17
|
-
#
|
|
18
|
-
# @rbs @messages: Hash[Symbol, Set[String]]
|
|
19
11
|
attr_reader :messages
|
|
20
12
|
|
|
21
|
-
def_delegators :messages, :any?, :clear, :empty?, :size
|
|
22
|
-
|
|
23
|
-
# Initialize a new error collection.
|
|
24
|
-
#
|
|
25
|
-
# @rbs () -> void
|
|
26
13
|
def initialize
|
|
27
14
|
@messages = {}
|
|
28
15
|
end
|
|
29
16
|
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
# @param attribute [Symbol] The attribute name associated with the error
|
|
33
|
-
# @param message [String] The error message to add
|
|
34
|
-
#
|
|
35
|
-
# @example
|
|
36
|
-
# errors = CMDx::Errors.new
|
|
37
|
-
# errors.add(:email, "must be valid format")
|
|
38
|
-
# errors.add(:email, "cannot be blank")
|
|
17
|
+
# Adds `message` under `key`. Duplicate messages are silently dropped.
|
|
39
18
|
#
|
|
40
|
-
# @
|
|
41
|
-
|
|
42
|
-
|
|
19
|
+
# @param key [Symbol]
|
|
20
|
+
# @param message [String]
|
|
21
|
+
# @return [Set<String>] the set of messages now stored under `key`
|
|
22
|
+
def add(key, message)
|
|
23
|
+
(messages[key] ||= Set.new) << message
|
|
24
|
+
end
|
|
25
|
+
alias []= add
|
|
43
26
|
|
|
44
|
-
|
|
45
|
-
|
|
27
|
+
# Copies every message from `other` into self. Existing messages are
|
|
28
|
+
# preserved and duplicates (same key + message) are silently dropped by
|
|
29
|
+
# the underlying Set. Accepts any object that responds to `#to_hash`
|
|
30
|
+
# returning `Hash{Symbol => Enumerable<String>}` — typically another
|
|
31
|
+
# {Errors} instance.
|
|
32
|
+
#
|
|
33
|
+
# @param other [Errors, #to_hash]
|
|
34
|
+
# @return [void]
|
|
35
|
+
# @example Combine validation errors from a nested task
|
|
36
|
+
# parent.errors.merge!(child.result.errors)
|
|
37
|
+
def merge!(other)
|
|
38
|
+
other.to_hash.each do |key, messages|
|
|
39
|
+
messages.each { |message| add(key, message) }
|
|
40
|
+
end
|
|
46
41
|
end
|
|
47
42
|
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
# @return [Boolean] true if the attribute has errors, false otherwise
|
|
53
|
-
#
|
|
54
|
-
# @example
|
|
55
|
-
# errors.for?(:email) # => true
|
|
56
|
-
# errors.for?(:name) # => false
|
|
57
|
-
#
|
|
58
|
-
# @rbs (Symbol attribute) -> bool
|
|
59
|
-
def for?(attribute)
|
|
60
|
-
set = messages[attribute]
|
|
61
|
-
!set.nil? && !set.empty?
|
|
43
|
+
# @param key [Symbol]
|
|
44
|
+
# @return [Array<String>] messages for `key`, or a frozen empty array
|
|
45
|
+
def [](key)
|
|
46
|
+
messages[key]&.to_a || EMPTY_ARRAY
|
|
62
47
|
end
|
|
63
48
|
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
# @return [
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
# @
|
|
49
|
+
# @param key [Symbol]
|
|
50
|
+
# @param message [String]
|
|
51
|
+
# @return [Boolean] true when `message` is recorded under `key`
|
|
52
|
+
def added?(key, message)
|
|
53
|
+
!!messages[key]&.include?(message)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @param key [Symbol]
|
|
57
|
+
# @return [Boolean]
|
|
58
|
+
def key?(key)
|
|
59
|
+
messages.key?(key)
|
|
60
|
+
end
|
|
61
|
+
alias for? key?
|
|
62
|
+
|
|
63
|
+
# @return [Array<Symbol>] keys with at least one message
|
|
64
|
+
def keys
|
|
65
|
+
messages.keys
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @return [Boolean]
|
|
69
|
+
def empty?
|
|
70
|
+
messages.empty?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @return [Integer] number of keyed entries
|
|
74
|
+
def size
|
|
75
|
+
messages.size
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @return [Integer] total messages across all keys
|
|
79
|
+
def count
|
|
80
|
+
messages.each_value.sum(&:size)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @yield [key, set] each `[key, Set<String>]` pair
|
|
84
|
+
# @return [Errors, Enumerator]
|
|
85
|
+
def each(&)
|
|
86
|
+
messages.each(&)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @yield [Symbol]
|
|
90
|
+
# @return [Errors, Enumerator]
|
|
91
|
+
def each_key(&)
|
|
92
|
+
messages.each_key(&)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @yield [Set<String>]
|
|
96
|
+
# @return [Errors, Enumerator]
|
|
97
|
+
def each_value(&)
|
|
98
|
+
messages.each_value(&)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# @param key [Symbol]
|
|
102
|
+
# @return [Set<String>, nil] the removed set, or nil when absent
|
|
103
|
+
def delete(key)
|
|
104
|
+
messages.delete(key)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @return [Hash{Symbol => Set<String>}] empties the container
|
|
108
|
+
def clear
|
|
109
|
+
messages.clear
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @return [Hash{Symbol => Array<String>}] messages prefixed with their key
|
|
113
|
+
# (e.g. `{ name: ["name is required"] }`)
|
|
72
114
|
def full_messages
|
|
73
|
-
messages.each_with_object({}) do |(
|
|
74
|
-
hash[
|
|
115
|
+
messages.each_with_object({}) do |(key, set), hash|
|
|
116
|
+
hash[key] = set.map { |message| "#{key} #{message}" }
|
|
75
117
|
end
|
|
76
118
|
end
|
|
77
119
|
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
# @return [Hash{Symbol => Array<String>}] Hash with attribute keys and message arrays
|
|
81
|
-
#
|
|
82
|
-
# @example
|
|
83
|
-
# errors.to_h # => { email: ["must be valid format", "cannot be blank"] }
|
|
84
|
-
#
|
|
85
|
-
# @rbs () -> Hash[Symbol, Array[String]]
|
|
120
|
+
# @return [Hash{Symbol => Array<String>}] raw messages as arrays
|
|
86
121
|
def to_h
|
|
87
122
|
messages.transform_values(&:to_a)
|
|
88
123
|
end
|
|
89
124
|
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
# @param full [Boolean] Whether to include full messages with attribute names
|
|
93
|
-
# @return [Hash{Symbol => Array<String>}] Hash with attribute keys and message arrays
|
|
94
|
-
#
|
|
95
|
-
# @example
|
|
96
|
-
# errors.to_hash # => { email: ["must be valid format", "cannot be blank"] }
|
|
97
|
-
# errors.to_hash(true) # => { email: ["email must be valid format", "email cannot be blank"] }
|
|
98
|
-
#
|
|
99
|
-
# @rbs (?bool full) -> Hash[Symbol, Array[String]]
|
|
125
|
+
# @param full [Boolean] when true return {#full_messages}, otherwise {#to_h}
|
|
126
|
+
# @return [Hash{Symbol => Array<String>}]
|
|
100
127
|
def to_hash(full = false)
|
|
101
128
|
full ? full_messages : to_h
|
|
102
129
|
end
|
|
103
130
|
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
# @return [String] Formatted error messages joined with periods
|
|
131
|
+
# JSON-friendly hash view. Aliases {#to_h} for conventional `as_json`
|
|
132
|
+
# callers (e.g. Rails).
|
|
107
133
|
#
|
|
108
|
-
# @
|
|
109
|
-
|
|
134
|
+
# @return [Hash{Symbol => Array<String>}]
|
|
135
|
+
def as_json(*)
|
|
136
|
+
to_h
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Serializes the error messages to a JSON string. Symbol keys are
|
|
140
|
+
# emitted as strings by the `json` stdlib.
|
|
110
141
|
#
|
|
111
|
-
# @
|
|
142
|
+
# @param args [Array] forwarded to `Hash#to_json`
|
|
143
|
+
# @return [String]
|
|
144
|
+
def to_json(*args)
|
|
145
|
+
to_h.to_json(*args)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# @return [String] all full messages joined with `". "`, suitable as a
|
|
149
|
+
# fail reason
|
|
112
150
|
def to_s
|
|
113
151
|
full_messages.values.flatten.join(". ")
|
|
114
152
|
end
|
|
115
153
|
|
|
154
|
+
# Pattern-matching support for `case errors in {...}`.
|
|
155
|
+
#
|
|
156
|
+
# @param keys [Array<Symbol>, nil] restrict the returned hash to these keys
|
|
157
|
+
# @return [Hash{Symbol => Array<String>}]
|
|
158
|
+
#
|
|
159
|
+
# @example
|
|
160
|
+
# case task.errors
|
|
161
|
+
# in { name: [_, *] } then handle_name_errors(task)
|
|
162
|
+
# end
|
|
163
|
+
def deconstruct_keys(keys)
|
|
164
|
+
keys.nil? ? to_h : to_h.slice(*keys)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Pattern-matching support for `case errors in [...]`.
|
|
168
|
+
#
|
|
169
|
+
# @return [Array<Array(Symbol, Array<String>)>]
|
|
170
|
+
def deconstruct
|
|
171
|
+
to_h.to_a
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Freezes the container and every message set. Called by Runtime teardown.
|
|
175
|
+
#
|
|
176
|
+
# @return [Errors] self
|
|
177
|
+
def freeze
|
|
178
|
+
messages.each_value(&:freeze).freeze
|
|
179
|
+
super
|
|
180
|
+
end
|
|
181
|
+
|
|
116
182
|
end
|
|
117
183
|
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CMDx
|
|
4
|
+
class Executors
|
|
5
|
+
# Fiber-scheduler backed executor. Spawns one fiber per job, bounded by
|
|
6
|
+
# `concurrency` via a `SizedQueue` semaphore. Requires a Fiber scheduler to
|
|
7
|
+
# be installed on the current thread (e.g. inside `Async { ... }` from the
|
|
8
|
+
# `async` gem). `pool_size` caps in-flight fibers.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module Fiber
|
|
12
|
+
|
|
13
|
+
extend self
|
|
14
|
+
|
|
15
|
+
# @param jobs [Array]
|
|
16
|
+
# @param concurrency [Integer] max in-flight fibers
|
|
17
|
+
# @param on_job [#call]
|
|
18
|
+
# @return [void]
|
|
19
|
+
# @raise [RuntimeError] when no `Fiber.scheduler` is installed
|
|
20
|
+
def call(jobs:, concurrency:, on_job:)
|
|
21
|
+
raise "executor: :fibers requires Fiber.scheduler; run the workflow inside a scheduler block (e.g. Async { ... })" unless ::Fiber.scheduler
|
|
22
|
+
|
|
23
|
+
slots = SizedQueue.new(concurrency)
|
|
24
|
+
concurrency.times { slots << :slot }
|
|
25
|
+
done = Queue.new
|
|
26
|
+
|
|
27
|
+
jobs.each do |job|
|
|
28
|
+
slots.pop
|
|
29
|
+
::Fiber.schedule do
|
|
30
|
+
on_job.call(job)
|
|
31
|
+
ensure
|
|
32
|
+
slots << :slot
|
|
33
|
+
done << true
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
jobs.size.times { done.pop }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CMDx
|
|
4
|
+
class Executors
|
|
5
|
+
# Default executor. Uses a fixed-size `Thread` pool drained via a `Queue`;
|
|
6
|
+
# sentinel `nil`s terminate workers. Workers inherit the parent's chain via
|
|
7
|
+
# fiber-local storage.
|
|
8
|
+
#
|
|
9
|
+
# @api private
|
|
10
|
+
module Thread
|
|
11
|
+
|
|
12
|
+
extend self
|
|
13
|
+
|
|
14
|
+
# @param jobs [Array] opaque job objects forwarded to `on_job`
|
|
15
|
+
# @param concurrency [Integer] worker count
|
|
16
|
+
# @param on_job [#call] unary callable invoked per job
|
|
17
|
+
# @return [void]
|
|
18
|
+
def call(jobs:, concurrency:, on_job:)
|
|
19
|
+
queue = Queue.new
|
|
20
|
+
jobs.each { |job| queue << job }
|
|
21
|
+
concurrency.times { queue << nil }
|
|
22
|
+
|
|
23
|
+
workers = Array.new(concurrency) do
|
|
24
|
+
::Thread.new do
|
|
25
|
+
while (job = queue.pop)
|
|
26
|
+
on_job.call(job)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
workers.each(&:join)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CMDx
|
|
4
|
+
# Registry of named executors used by `:parallel` workflow groups to
|
|
5
|
+
# dispatch tasks concurrently. Ships with built-ins for `:threads` and
|
|
6
|
+
# `:fibers`. Executors are any callable accepting
|
|
7
|
+
# `call(jobs:, concurrency:, on_job:)` and must invoke `on_job.call(job)`
|
|
8
|
+
# for each job, blocking until every job is done.
|
|
9
|
+
class Executors
|
|
10
|
+
|
|
11
|
+
attr_reader :registry
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@registry = {
|
|
15
|
+
threads: Executors::Thread,
|
|
16
|
+
fibers: Executors::Fiber
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param source [Executors] registry to duplicate
|
|
21
|
+
# @return [void]
|
|
22
|
+
def initialize_copy(source)
|
|
23
|
+
@registry = source.registry.dup
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Registers a named executor, overwriting any existing entry.
|
|
27
|
+
#
|
|
28
|
+
# @param name [Symbol]
|
|
29
|
+
# @param callable [#call, nil] pass either this or a block
|
|
30
|
+
# @param block [#call, nil] executor callable when `callable` is omitted
|
|
31
|
+
# @yield executor body — `call(jobs:, concurrency:, on_job:)`
|
|
32
|
+
# @return [Executors] self for chaining
|
|
33
|
+
# @raise [ArgumentError] when both `callable` and a block are given, or
|
|
34
|
+
# when the resolved executor isn't callable
|
|
35
|
+
def register(name, callable = nil, &block)
|
|
36
|
+
executor = callable || block
|
|
37
|
+
|
|
38
|
+
if callable && block
|
|
39
|
+
raise ArgumentError, "provide either a callable or a block, not both"
|
|
40
|
+
elsif !executor.respond_to?(:call)
|
|
41
|
+
raise ArgumentError, "executor must respond to #call"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
registry[name.to_sym] = executor
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @param name [Symbol]
|
|
49
|
+
# @return [Executors] self for chaining
|
|
50
|
+
def deregister(name)
|
|
51
|
+
registry.delete(name.to_sym)
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @param name [Symbol]
|
|
56
|
+
# @return [#call] the registered executor
|
|
57
|
+
# @raise [ArgumentError] when `name` isn't registered
|
|
58
|
+
def lookup(name)
|
|
59
|
+
registry[name] || begin
|
|
60
|
+
raise ArgumentError, "unknown executor: #{name.inspect}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Resolves a declaration's `:executor` option to a concrete callable.
|
|
65
|
+
# Accepts `nil` (default `:threads`), a Symbol (registry lookup), or any
|
|
66
|
+
# object responding to `#call`.
|
|
67
|
+
#
|
|
68
|
+
# @param spec [Symbol, #call, nil]
|
|
69
|
+
# @return [#call]
|
|
70
|
+
# @raise [ArgumentError] when `spec` is an unknown symbol or not callable
|
|
71
|
+
def resolve(spec)
|
|
72
|
+
case spec
|
|
73
|
+
when NilClass
|
|
74
|
+
lookup(:threads)
|
|
75
|
+
when Symbol
|
|
76
|
+
lookup(spec)
|
|
77
|
+
else
|
|
78
|
+
return spec if spec.respond_to?(:call)
|
|
79
|
+
|
|
80
|
+
raise ArgumentError, "unknown executor: #{spec.inspect}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @return [Boolean]
|
|
85
|
+
def empty?
|
|
86
|
+
registry.empty?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @return [Integer]
|
|
90
|
+
def size
|
|
91
|
+
registry.size
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
end
|
|
95
|
+
end
|