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
data/lib/cmdx/context.rb
CHANGED
|
@@ -1,309 +1,330 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# The Context class extends Forwardable to delegate common hash methods and
|
|
9
|
-
# provides additional convenience methods for working with context data.
|
|
4
|
+
# Shared data object passed through task execution. Wraps a symbol-keyed
|
|
5
|
+
# hash; supports `ctx.foo`/`ctx.foo = 1`/`ctx.foo?` dynamic accessors via
|
|
6
|
+
# {#method_missing}. Runtime freezes the root context during teardown so
|
|
7
|
+
# nested subtasks can't mutate the outer task's state after completion.
|
|
10
8
|
class Context
|
|
11
9
|
|
|
12
|
-
|
|
10
|
+
include Enumerable
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
#
|
|
16
|
-
# @return [Hash{Symbol => Object}] The internal hash table
|
|
17
|
-
#
|
|
18
|
-
# @example
|
|
19
|
-
# context.table # => { name: "John", age: 30 }
|
|
20
|
-
#
|
|
21
|
-
# @rbs @table: Hash[Symbol, untyped]
|
|
22
|
-
attr_reader :table
|
|
23
|
-
alias to_h table
|
|
12
|
+
class << self
|
|
24
13
|
|
|
25
|
-
|
|
14
|
+
# Normalizes `context` into a Context instance. Passes through an
|
|
15
|
+
# unfrozen Context unchanged (so nested tasks share state); unwraps
|
|
16
|
+
# anything with `#context` (e.g. a Task); wraps hashes/hash-likes into
|
|
17
|
+
# a new Context with symbolized keys.
|
|
18
|
+
#
|
|
19
|
+
# @param context [Context, #context, Hash, #to_h, #to_hash]
|
|
20
|
+
# @return [Context]
|
|
21
|
+
# @raise [ArgumentError] when `context` doesn't respond to `#to_h`/`#to_hash`
|
|
22
|
+
def build(context = EMPTY_HASH)
|
|
23
|
+
if context.is_a?(self) && !context.frozen?
|
|
24
|
+
context
|
|
25
|
+
elsif context.respond_to?(:context)
|
|
26
|
+
build(context.context)
|
|
27
|
+
else
|
|
28
|
+
new(context)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
# @return [Context] a new Context instance
|
|
33
|
-
#
|
|
34
|
-
# @raise [ArgumentError] when args doesn't respond to `to_h` or `to_hash`
|
|
35
|
-
#
|
|
36
|
-
# @example
|
|
37
|
-
# context = Context.new(name: "John", age: 30)
|
|
38
|
-
# context[:name] # => "John"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Enables strict mode — when true, dynamic readers via {#method_missing}
|
|
35
|
+
# raise `NoMethodError` for unknown keys instead of returning `nil`.
|
|
36
|
+
# Set by `Task#initialize` from `Task.settings.strict_context`.
|
|
39
37
|
#
|
|
40
|
-
# @
|
|
41
|
-
|
|
38
|
+
# @return [Boolean]
|
|
39
|
+
attr_accessor :strict
|
|
40
|
+
|
|
41
|
+
# @param context [Hash, #to_h, #to_hash] source hash, keys are symbolized
|
|
42
|
+
# @raise [ArgumentError] when `context` doesn't respond to `#to_h`/`#to_hash`
|
|
43
|
+
def initialize(context = EMPTY_HASH)
|
|
42
44
|
@table =
|
|
43
|
-
if
|
|
44
|
-
|
|
45
|
-
elsif
|
|
46
|
-
|
|
45
|
+
if context.respond_to?(:to_hash)
|
|
46
|
+
context.to_hash
|
|
47
|
+
elsif context.respond_to?(:to_h)
|
|
48
|
+
context.to_h
|
|
47
49
|
else
|
|
48
50
|
raise ArgumentError, "must respond to `to_h` or `to_hash`"
|
|
49
51
|
end.transform_keys(&:to_sym)
|
|
50
52
|
end
|
|
51
53
|
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
#
|
|
57
|
-
# @return [Context] a Context instance, either new or reused
|
|
58
|
-
#
|
|
59
|
-
# @example
|
|
60
|
-
# existing = Context.new(name: "John")
|
|
61
|
-
# built = Context.build(existing) # reuses existing context
|
|
62
|
-
# built.object_id == existing.object_id # => true
|
|
63
|
-
#
|
|
64
|
-
# @rbs (untyped context) -> Context
|
|
65
|
-
def self.build(context = {})
|
|
66
|
-
if context.is_a?(self) && !context.frozen?
|
|
67
|
-
context
|
|
68
|
-
elsif context.respond_to?(:context)
|
|
69
|
-
build(context.context)
|
|
70
|
-
else
|
|
71
|
-
new(context)
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# Retrieves a value from the context by key.
|
|
76
|
-
#
|
|
77
|
-
# @param key [String, Symbol] the key to retrieve
|
|
78
|
-
#
|
|
79
|
-
# @return [Object, nil] the value associated with the key, or nil if not found
|
|
80
|
-
#
|
|
81
|
-
# @example
|
|
82
|
-
# context = Context.new(name: "John")
|
|
83
|
-
# context[:name] # => "John"
|
|
84
|
-
# context["name"] # => "John" (automatically converted to symbol)
|
|
85
|
-
#
|
|
86
|
-
# @rbs ((String | Symbol) key) -> untyped
|
|
87
|
-
def [](key)
|
|
88
|
-
table[key.to_sym]
|
|
54
|
+
# @return [Boolean] whether dynamic reads for unknown keys raise instead
|
|
55
|
+
# of returning `nil`
|
|
56
|
+
def strict?
|
|
57
|
+
!!@strict
|
|
89
58
|
end
|
|
90
59
|
|
|
91
|
-
# Stores
|
|
92
|
-
#
|
|
93
|
-
# @param key [String, Symbol] the key to store
|
|
94
|
-
# @param value [Object] the value to store
|
|
60
|
+
# Stores `value` under `key`, symbolizing the key. Overwrites any
|
|
61
|
+
# existing entry.
|
|
95
62
|
#
|
|
63
|
+
# @param key [Symbol, String]
|
|
64
|
+
# @param value [Object]
|
|
96
65
|
# @return [Object] the stored value
|
|
97
|
-
#
|
|
98
|
-
# @example
|
|
99
|
-
# context = Context.new
|
|
100
|
-
# context.store(:name, "John")
|
|
101
|
-
# context[:name] # => "John"
|
|
102
|
-
#
|
|
103
|
-
# @rbs ((String | Symbol) key, untyped value) -> untyped
|
|
104
66
|
def store(key, value)
|
|
105
|
-
table[key.to_sym] = value
|
|
67
|
+
@table[key.to_sym] = value
|
|
106
68
|
end
|
|
107
69
|
alias []= store
|
|
108
70
|
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
# @
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
#
|
|
121
|
-
#
|
|
71
|
+
# Merges another context/hash-like into this one in place. Keys from
|
|
72
|
+
# `context` win on conflict.
|
|
73
|
+
#
|
|
74
|
+
# @param context [Context, Hash, #to_h, #to_hash]
|
|
75
|
+
# @return [Context] self for chaining
|
|
76
|
+
def merge(context = EMPTY_HASH)
|
|
77
|
+
other = self.class.build(context)
|
|
78
|
+
@table.merge!(other.to_h)
|
|
79
|
+
self
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Like {#merge} but recursive into Hash values: a nested Hash key collision
|
|
83
|
+
# merges the two Hashes instead of replacing the left with the right.
|
|
84
|
+
# Non-Hash values follow last-write-wins (`context` wins).
|
|
85
|
+
#
|
|
86
|
+
# @param context [Context, Hash, #to_h, #to_hash]
|
|
87
|
+
# @return [Context] self for chaining
|
|
88
|
+
def deep_merge(context = EMPTY_HASH)
|
|
89
|
+
other = self.class.build(context)
|
|
90
|
+
@table = compute_deep_merge(@table, other.to_h)
|
|
91
|
+
self
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @param key [Symbol, String]
|
|
95
|
+
# @return [Object, nil]
|
|
96
|
+
def [](key)
|
|
97
|
+
@table[key.to_sym]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Hash-like fetch. Supports a default value, default block, or raises
|
|
101
|
+
# `KeyError` just like `Hash#fetch`.
|
|
122
102
|
#
|
|
123
|
-
# @
|
|
103
|
+
# @param key [Symbol, String]
|
|
104
|
+
# @return [Object]
|
|
124
105
|
def fetch(key, ...)
|
|
125
|
-
table.fetch(key.to_sym, ...)
|
|
106
|
+
@table.fetch(key.to_sym, ...)
|
|
126
107
|
end
|
|
127
108
|
|
|
128
|
-
#
|
|
129
|
-
#
|
|
130
|
-
# @
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
#
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
109
|
+
# @param key [Symbol, String] top-level key (symbolized)
|
|
110
|
+
# @param keys [Array<Object>] nested keys passed through untouched
|
|
111
|
+
# @return [Object, nil]
|
|
112
|
+
def dig(key, *keys)
|
|
113
|
+
@table.dig(key.to_sym, *keys)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Fetch-or-store. Returns the existing value, or stores and returns the
|
|
117
|
+
# default (from block if given, else `value`).
|
|
118
|
+
#
|
|
119
|
+
# @param key [Symbol, String]
|
|
120
|
+
# @param value [Object] fallback when no block is given
|
|
121
|
+
# @yield [] invoked only when `key` is absent
|
|
122
|
+
# @yieldreturn [Object] value to store
|
|
123
|
+
# @return [Object]
|
|
124
|
+
def retrieve(key, value = nil)
|
|
125
|
+
nk = key.to_sym
|
|
126
|
+
|
|
127
|
+
@table.fetch(nk) do
|
|
128
|
+
@table[nk] = block_given? ? yield : value
|
|
147
129
|
end
|
|
148
130
|
end
|
|
149
131
|
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
#
|
|
155
|
-
# @return [Context] self for method chaining
|
|
156
|
-
#
|
|
157
|
-
# @example
|
|
158
|
-
# context = Context.new(name: "John")
|
|
159
|
-
# context.merge!(age: 30, city: "NYC")
|
|
160
|
-
# context.to_h # => {name: "John", age: 30, city: "NYC"}
|
|
161
|
-
#
|
|
162
|
-
# @rbs (?untyped args) -> self
|
|
163
|
-
def merge!(args = EMPTY_HASH)
|
|
164
|
-
table.merge!(args.to_h.transform_keys(&:to_sym))
|
|
165
|
-
self
|
|
132
|
+
# @param key [Symbol, String]
|
|
133
|
+
# @return [Boolean]
|
|
134
|
+
def key?(key)
|
|
135
|
+
@table.key?(key.to_sym)
|
|
166
136
|
end
|
|
167
|
-
alias merge merge!
|
|
168
137
|
|
|
169
|
-
#
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
#
|
|
173
|
-
# @yield [key] a block to handle the case when key is not found
|
|
174
|
-
#
|
|
175
|
-
# @return [Object, nil] the deleted value, or the block result if key not found
|
|
176
|
-
#
|
|
177
|
-
# @example
|
|
178
|
-
# context = Context.new(name: "John", age: 30)
|
|
179
|
-
# context.delete!(:age) # => 30
|
|
180
|
-
# context.delete!(:city) { |key| "Key #{key} not found" } # => "Key city not found"
|
|
181
|
-
#
|
|
182
|
-
# @rbs ((String | Symbol) key) ?{ ((String | Symbol)) -> untyped } -> untyped
|
|
183
|
-
def delete!(key, &)
|
|
184
|
-
table.delete(key.to_sym, &)
|
|
138
|
+
# @return [Array<Symbol>]
|
|
139
|
+
def keys
|
|
140
|
+
@table.keys
|
|
185
141
|
end
|
|
186
|
-
alias delete delete!
|
|
187
142
|
|
|
188
|
-
#
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
#
|
|
194
|
-
|
|
195
|
-
|
|
143
|
+
# @return [Array<Object>]
|
|
144
|
+
def values
|
|
145
|
+
@table.values
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# @return [Boolean]
|
|
149
|
+
def empty?
|
|
150
|
+
@table.empty?
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# @return [Integer]
|
|
154
|
+
def size
|
|
155
|
+
@table.size
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# @yield [key, value]
|
|
159
|
+
# @return [Context, Enumerator]
|
|
160
|
+
def each(&)
|
|
161
|
+
@table.each(&)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# @yield [Symbol]
|
|
165
|
+
# @return [Context, Enumerator]
|
|
166
|
+
def each_key(&)
|
|
167
|
+
@table.each_key(&)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# @yield [Object]
|
|
171
|
+
# @return [Context, Enumerator]
|
|
172
|
+
def each_value(&)
|
|
173
|
+
@table.each_value(&)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# @param key [Symbol, String]
|
|
177
|
+
# @yield [Symbol] optional default block, receives the symbolized key
|
|
178
|
+
# @return [Object, nil] removed value
|
|
179
|
+
def delete(key, &)
|
|
180
|
+
@table.delete(key.to_sym, &)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Removes every entry.
|
|
196
184
|
#
|
|
197
|
-
# @
|
|
198
|
-
def clear
|
|
199
|
-
table.clear
|
|
185
|
+
# @return [Context] self
|
|
186
|
+
def clear
|
|
187
|
+
@table.clear
|
|
200
188
|
self
|
|
201
189
|
end
|
|
202
|
-
alias clear clear!
|
|
203
190
|
|
|
204
|
-
#
|
|
205
|
-
#
|
|
206
|
-
# @param other [Object] the object to compare with
|
|
191
|
+
# Equal when `other` is a Context with the same underlying hash.
|
|
207
192
|
#
|
|
208
|
-
# @
|
|
209
|
-
#
|
|
210
|
-
# @example
|
|
211
|
-
# context1 = Context.new(name: "John")
|
|
212
|
-
# context2 = Context.new(name: "John")
|
|
213
|
-
# context1 == context2 # => true
|
|
214
|
-
#
|
|
215
|
-
# @rbs (untyped other) -> bool
|
|
193
|
+
# @param other [Object]
|
|
194
|
+
# @return [Boolean]
|
|
216
195
|
def eql?(other)
|
|
217
196
|
other.is_a?(self.class) && (to_h == other.to_h)
|
|
218
197
|
end
|
|
219
198
|
alias == eql?
|
|
220
199
|
|
|
221
|
-
#
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
#
|
|
225
|
-
# @return [Boolean] true if the key exists in the context
|
|
226
|
-
#
|
|
227
|
-
# @example
|
|
228
|
-
# context = Context.new(name: "John")
|
|
229
|
-
# context.key?(:name) # => true
|
|
230
|
-
# context.key?(:age) # => false
|
|
231
|
-
#
|
|
232
|
-
# @rbs ((String | Symbol) key) -> bool
|
|
233
|
-
def key?(key)
|
|
234
|
-
table.key?(key.to_sym)
|
|
200
|
+
# @return [Integer]
|
|
201
|
+
def hash
|
|
202
|
+
@table.hash
|
|
235
203
|
end
|
|
236
204
|
|
|
237
|
-
#
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
#
|
|
205
|
+
# @return [Hash{Symbol => Object}] the underlying table (not a copy)
|
|
206
|
+
def to_h
|
|
207
|
+
@table
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# JSON-friendly hash view. Aliases {#to_h} for conventional `as_json`
|
|
211
|
+
# callers (e.g. Rails); values pass through unchanged — non-primitive
|
|
212
|
+
# entries rely on their own `as_json` / `to_json`.
|
|
243
213
|
#
|
|
244
|
-
# @
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
214
|
+
# @return [Hash{Symbol => Object}]
|
|
215
|
+
def as_json(*)
|
|
216
|
+
to_h
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Serializes the context to a JSON string. Symbol keys are emitted as
|
|
220
|
+
# strings by the `json` stdlib.
|
|
248
221
|
#
|
|
249
|
-
# @
|
|
250
|
-
|
|
251
|
-
|
|
222
|
+
# @param args [Array] forwarded to `Hash#to_json`
|
|
223
|
+
# @return [String]
|
|
224
|
+
def to_json(*args)
|
|
225
|
+
to_h.to_json(*args)
|
|
252
226
|
end
|
|
253
227
|
|
|
254
|
-
#
|
|
228
|
+
# @return [String] space-separated `key=value.inspect` pairs
|
|
229
|
+
def to_s
|
|
230
|
+
@table.map { |k, v| "#{k}=#{v.inspect}" }.join(" ")
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Pattern-matching support for `case context in {...}`.
|
|
255
234
|
#
|
|
256
|
-
# @
|
|
235
|
+
# @param keys [Array<Symbol>, nil] restrict the returned hash to these keys
|
|
236
|
+
# @return [Hash{Symbol => Object}]
|
|
237
|
+
def deconstruct_keys(keys)
|
|
238
|
+
keys.nil? ? @table : @table.slice(*keys)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Pattern-matching support for `case context in [...]`.
|
|
257
242
|
#
|
|
258
|
-
# @
|
|
259
|
-
|
|
260
|
-
|
|
243
|
+
# @return [Array<Array(Symbol, Object)>]
|
|
244
|
+
def deconstruct
|
|
245
|
+
@table.to_a
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Returns a deep copy. Non-mutable scalars are shared; Hashes/Arrays are
|
|
249
|
+
# recursively duplicated; other objects fall back to `#dup` (and then
|
|
250
|
+
# to the original on `StandardError`).
|
|
251
|
+
#
|
|
252
|
+
# @return [Context]
|
|
253
|
+
def deep_dup
|
|
254
|
+
ctx = self.class.allocate
|
|
255
|
+
ctx.instance_variable_set(:@table, compute_deep_dup(@table))
|
|
256
|
+
ctx
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Freezes the context and its backing hash. Runtime calls this on the
|
|
260
|
+
# root task's context during teardown.
|
|
261
261
|
#
|
|
262
|
-
# @
|
|
263
|
-
def
|
|
264
|
-
|
|
262
|
+
# @return [Context] self
|
|
263
|
+
def freeze
|
|
264
|
+
@table.freeze
|
|
265
|
+
super
|
|
265
266
|
end
|
|
266
267
|
|
|
267
268
|
private
|
|
268
269
|
|
|
269
|
-
#
|
|
270
|
-
#
|
|
271
|
-
#
|
|
272
|
-
#
|
|
273
|
-
#
|
|
274
|
-
#
|
|
275
|
-
#
|
|
276
|
-
#
|
|
277
|
-
# @
|
|
278
|
-
#
|
|
279
|
-
# @
|
|
280
|
-
#
|
|
281
|
-
# @
|
|
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
|
|
282
283
|
def method_missing(method_name, *args, **_kwargs, &)
|
|
283
284
|
if method_name.end_with?("=")
|
|
284
|
-
|
|
285
|
+
@table[method_name[..-2].to_sym] = args.first
|
|
286
|
+
elsif method_name.end_with?("?")
|
|
287
|
+
!!@table[method_name[..-2].to_sym]
|
|
288
|
+
elsif strict? && !@table.key?(method_name)
|
|
289
|
+
raise NoMethodError, "unknown context key #{method_name.inspect} (strict mode)"
|
|
285
290
|
else
|
|
286
|
-
table[method_name]
|
|
291
|
+
@table[method_name]
|
|
287
292
|
end
|
|
288
293
|
end
|
|
289
294
|
|
|
290
|
-
#
|
|
291
|
-
#
|
|
292
|
-
#
|
|
293
|
-
# @param method_name [Symbol] the method name to check
|
|
294
|
-
# @param include_private [Boolean] whether to include private methods
|
|
295
|
-
#
|
|
296
|
-
# @return [Boolean] true if the method can be called
|
|
297
|
-
#
|
|
298
|
-
# @example
|
|
299
|
-
# context = Context.new(name: "John")
|
|
300
|
-
# context.respond_to?(:name) # => true
|
|
301
|
-
# context.respond_to?(:name=) # => true
|
|
302
|
-
# context.respond_to?(:age) # => false
|
|
303
|
-
#
|
|
304
|
-
# @rbs (Symbol method_name, ?bool include_private) -> bool
|
|
295
|
+
# @param method_name [Symbol]
|
|
296
|
+
# @param include_private [Boolean] forwarded to Ruby's `respond_to?` lookup
|
|
297
|
+
# @return [Boolean]
|
|
305
298
|
def respond_to_missing?(method_name, include_private = false)
|
|
306
|
-
key?(method_name) || method_name.end_with?("=") || super
|
|
299
|
+
@table.key?(method_name) || method_name.end_with?("=", "?") || super
|
|
300
|
+
end
|
|
301
|
+
|
|
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
|
|
307
328
|
end
|
|
308
329
|
|
|
309
330
|
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CMDx
|
|
4
|
+
# Declared via `Task.deprecation`. Runs before a task's lifecycle to warn,
|
|
5
|
+
# log, raise, or delegate when a task class has been marked deprecated.
|
|
6
|
+
# Supports conditional `:if` / `:unless` gating via {Util.satisfied?}.
|
|
7
|
+
class Deprecation
|
|
8
|
+
|
|
9
|
+
# @param value [:log, :warn, :error, Symbol, Proc, #call, nil] action to take;
|
|
10
|
+
# `nil` disables
|
|
11
|
+
# @param options [Hash{Symbol => Object}]
|
|
12
|
+
# @option options [Symbol, Proc, #call] :if
|
|
13
|
+
# @option options [Symbol, Proc, #call] :unless
|
|
14
|
+
def initialize(value, options = EMPTY_HASH)
|
|
15
|
+
@value = value
|
|
16
|
+
@options = options.freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Runs the configured deprecation action, yielding first so Runtime can
|
|
20
|
+
# mark the result as deprecated for telemetry.
|
|
21
|
+
#
|
|
22
|
+
# @param task [Task]
|
|
23
|
+
# @yield invoked immediately before the action runs, only when conditions pass
|
|
24
|
+
# @return [void]
|
|
25
|
+
# @raise [DeprecationError] when `value` is `:error`
|
|
26
|
+
# @raise [ArgumentError] when `value` is an unsupported type
|
|
27
|
+
def execute(task)
|
|
28
|
+
return if @value.nil?
|
|
29
|
+
return unless Util.satisfied?(@options[:if], @options[:unless], task)
|
|
30
|
+
|
|
31
|
+
yield
|
|
32
|
+
|
|
33
|
+
case @value
|
|
34
|
+
when Symbol
|
|
35
|
+
registry = deprecators_registry(task)
|
|
36
|
+
if registry.key?(@value)
|
|
37
|
+
registry.lookup(@value).call(task)
|
|
38
|
+
else
|
|
39
|
+
task.send(@value)
|
|
40
|
+
end
|
|
41
|
+
when Proc
|
|
42
|
+
task.instance_exec(task, &@value)
|
|
43
|
+
else
|
|
44
|
+
return @value.call(task) if @value.respond_to?(:call)
|
|
45
|
+
|
|
46
|
+
raise ArgumentError, "deprecation must be a Symbol, Proc, or respond to #call"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
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
|
+
def deprecators_registry(task)
|
|
59
|
+
if task.class.respond_to?(:deprecators)
|
|
60
|
+
task.class.deprecators
|
|
61
|
+
else
|
|
62
|
+
CMDx.configuration.deprecators
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CMDx
|
|
4
|
+
class Deprecators
|
|
5
|
+
# Raises {DeprecationError} to prevent the task from executing. Use for
|
|
6
|
+
# tasks that must no longer run.
|
|
7
|
+
#
|
|
8
|
+
# @api private
|
|
9
|
+
module Error
|
|
10
|
+
|
|
11
|
+
extend self
|
|
12
|
+
|
|
13
|
+
# @param task [Task]
|
|
14
|
+
# @return [void]
|
|
15
|
+
# @raise [DeprecationError]
|
|
16
|
+
def call(task)
|
|
17
|
+
raise DeprecationError, "#{task.class} usage prohibited"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|